自己动手实现java数据结构九 跳表

1. 跳表介绍

  在之前关于数据结构的博客中已经介绍过两种最基础的数据结构:基于连续内存空间的向量(线性表)和基于链式节点结构的链表。

  有序的向量可以通过二分查找以logn对数复杂度完成随机查找,但由于插入/删除元素时可能导致内部数组内整体数据的平移复制,导致随机插入/删除的效率较低。而普通的一维链表结构虽然可以做到高效的插入/删除元素(只是关联的节点拓扑结构改变),但是在随机查找时却效率较低,因为其只能从头/尾节点顺序的进行遍历才能找到对应节点。

  计算机科学家发明了能够兼具向量与链表优点的平衡二叉搜索树(Balance Binary Search Tree BBST),这其中红黑树是平均性能最高,也最复杂的一种BBST。

  正是因为高性能的平衡二叉树过于复杂,使得计算机科学家另辟蹊径,发明了被称为跳表(Skip List)的数据结构。跳表通过建立具有层次结构的索引节点,解决了普通链表无法进行二分查找的缺陷。跳表是基于链表的,因此其插入和删除效率和链表一样优秀;而由于索引节点的引入,也使得跳表可以以类似二分查找的形式进行特定元素的搜索,其查找性能也达到了O(logn)的对数复杂度,和有序向量以及平衡二叉树查询渐进时间复杂度一致。

  总的来说,跳表是一个平均性能很优秀,结构相对简单的数据结构,在redis以及LevelDB、RocksDB等KV键值对数据库中被广泛使用。

2. 跳表工作原理

跳表查询:

  跳表是一个拥有多层索引节点的链表,最低层是一个链表,保存着全部的原始数据节点。而索引节点是建立在最底层链表节点之上的,且从下到上索引节点的数量逐渐稀疏。

  在查询时,从最高层开始以类似二分查找的方式跳跃着的逐步向下层逼近查找最终的目标节点。

跳表结构示意图:

跳表插入:

  了解了跳表的结构,以及其能够高效随机查询的原理之后。很自然的会想到一个问题,跳表的索引节点是如何维护的?换句话说,当插入/删除节点时跳表的索引结构是如何变化的?

  要想保证跳表高效的查询效率,需要令跳表相邻的上下层节点的数量之比大致为1:2,且同一层索引节点的分布尽量均匀(二分查找)。

  一种自然的想法是每次插入新节点时,详细的检查每一层的索引节点,精心维护相邻水平层索引节点1:2的数量,并控制节点排布的稀疏程度(必要时甚至可以重建整个索引)。但这样使得跳表的插入性能大大降低,所以实际上跳表并没有选择这种容易想到但低效方式维护索引。

  在跳表中,通过类似丢硬币的方式,以概率来决定索引节点是否需要被创建。具体的说,每当插入一个新节点时,根据某种概率算法计算是否需要为其建立上一层的索引节点。如果判断需要建立,那么再接着进行一次基于概率的判断,如果为真则在更高一层也建立索引节点,并循环往复。

  假设概率算法为真的数学期望为1/2,则插入新节点时有50%(1/2)的概率建立第一层的索引节点,25%(1/2^2)的概率建立第两层的索引节点,12.5%(1/2^3)的概率建立第三层的索引节点,以此类推。这种基于概率的索引节点建立方式,从宏观的数学期望上也能保证相邻上下层d的索引节点个数之比为1:2。同时由于插入节点数值的大小和插入顺序都是完全随机的,因此从期望上来说,同一水平层索引节点的分布也是大致均匀的。

  总的来说,插入新节点时基于概率的索引建立算法插入效率相对来说非常高,虽然在极端情况下会导致索引节点上下、水平的分布不均,但依然是非常优秀的实现。同时,可以通过控制概率算法的数学期望,灵活的调整跳表的空间占用与查询效率的取舍(概率算法为真的数学期望从1/2降低到1/4时,建立上级索引的概率降低,索引的密度下降,因此其随机查询效率降低,但其索引节点将会大大减少以节约空间,跳表的这一特性在对空间占用敏感的内存数据库应用中是很有价值的)。

跳表插入节点示意图:

跳表删除:

  在理解了跳表插入的原理后,跳表的删除就很好理解了。当最底层的数据节点被删除时,只需要将其之上的所有索引节点一并删除即可。

跳表删除节点示意图: 

 

3. 跳表实现细节

  下面介绍跳表的实现细节。本篇博客的跳表SkipListMap是用java实现的,为了令代码更容易理解,在一些地方选择了效率相对较低,但更容易理解的实现。

跳表节点实现

  为了令整个跳表的实现更加简单,区别于jdk的ConcurrentSkipListMap。当前版本跳表的定义的节点结构既用于最底层的数据节点,也用于上层的索引节点;且节点持有上、下、左、右关联的四个节点的引用。在每一水平层引入了左右两个哨兵节点,通过节点中的NodeType枚举区分哨兵节点与普通的索引/数据节点。

  为了能够在后续介绍的插入/删除等操作中,更加简单的定位到临近的节点,简化代码的理解难度。相比jdk、redis等工程化的高性能跳表实现,当前版本实现的跳表节点冗余了一些不必要的字段属性以及额外的哨兵节点,额外的浪费了一些空间,但跳表实现的核心思路是一致的。

跳表Node节点定义:

  private static class Node<K,V> implements EntryNode<K,V>{
        K key;
        V value;
        Node<K,1)"> left;
        Node<K,1)"> right;
        Node<K,1)"> up;
        Node<K,1)"> down;

        NodeType nodeType;

        private Node(K key,V value) {
            this.key = key;
            this.value = value;
            this.nodeType = NodeType.NORMAL;
        }

         Node() {
        }

         Node(NodeType nodeType) {
             nodeType;
        }

        /**
         * 将一个节点作为"当前节点"的"右节点" 插入链表
         * @param node  需要插入的节点
         * */
        private void linkAsRight(Node<K,1)"> node){
            // 先设置新增节点的 左右节点
            node.left = this;
            node.right = .right;

            将新增节点插入 当前节点和当前节点的左节点之间
            this.right.left = node;
            this.right = node;
        }

        
         * 将"当前节点"从当前水平链表移除(令其左右节点直接牵手)
         * void unlinkSelfHorizontal(){
            // 令当前链表的 左节点和右节点建立关联
            this.left.right = .right;
             令当前链表的 右节点和左节点建立关联
            this.right.left = .left;
        }

        
         * 将"当前节点"从当前垂直链表移除(令其上下节点直接牵手)
         *  unlinkSelfVertical(){
            this.up.down = .down;
            this.down.up = .up;
        }

        @Override
        public String toString() {
            if(this.key != null){
                return "{" +
                        "key=" + key +
                        ",value=" + value +
                        '}';
            }else{
                return "{" +
                        "nodeType=" + nodeType +
                        '}';
            }
        }

        @Override
         K getKey() {
            return .key;
        }

        @Override
         V getValue() {
            .value;
        }

        @Override
        public  setValue(V value) {
             value;
        }
    }

NodeType枚举:

enum NodeType{
        
         * 普通节点
         * */
        NORMAL,
         * 左侧哨兵节点
         * 
        LEFT_SENTINEL,1)">
         * 右侧哨兵节点
         * 
        RIGHT_SENTINEL,;
    }

跳表的基础结构

  跳表是一个能够支持高效增删改查、平均性能很高的数据结构,对标的是红黑树为首的平衡二叉搜索树。因此在我们参考jdk的实现,跳表和之前系列博客中的TreeMap一样也实现了Map接口。

  跳表的每一个水平层是从左到右,有小到大进行排序的,具体的比较逻辑由compare函数来完成。

跳表定义:

class SkipListMap<K,1)">extends AbstractMap<K,1)">{

    private Node<K,1)"> head;
     tail;
    private Comparator<K> comparator;
    int maxLevel;
     size;

    
     * 插入新节点时,提升的概率为0.5,期望保证上一层和下一层元素的个数之比为 1:2
     * 以达到查询节点时,log(n)的对数时间复杂度
     * */
    final double PROMOTE_RATE = 1.0/2.0;
    int INIT_MAX_LEVEL = 1;

     SkipListMap() {
         初始化整个跳表结构
        initialize();
    }

    public SkipListMap(Comparator<K> comparator) {
        ();
         设置比较器
        this.comparator = comparator;
    }

     initialize(){
        this.size = 0;
        this.maxLevel = INIT_MAX_LEVEL;

         构造左右哨兵节点
        Node<K,V> headNode = new Node<>();
        headNode.nodeType = NodeType.LEFT_SENTINEL;

        Node<K,V> tailNode = ();
        tailNode.nodeType = NodeType.RIGHT_SENTINEL;

         跳表初始化时只有一层,包含左右哨兵两个节点
        this.head = headNode;
        this.tail = tailNode;
         左右哨兵牵手
        this.head.right = .tail;
        this.tail.left = .head;
    }

    。。。。。。 
}

compare比较逻辑实现:

 doCompare(K key1,K key2){
        this.comparator != ){
             如果跳表被设置了比较器,则使用比较器进行比较
            .comparator.compare(key1,key2);
        }{
             否则强制转换为Comparable比较(对于没有实现Comparable的key会抛出强制类型转换异常)
            return ((Comparable)key1).compareTo(key2);
        }
    }

跳表查询实现

  跳表实现的一个关键就是如何进行快速的随机查找。

  对于指定key的查找,首先从最上层的跳表head节点开始,从左到右的进行比对,当找到一个节点比key小,而且其相邻的右节点比key大时,则沿着找到的节点进入下一层继续查找。(每一个水平层的左哨兵节点视为无穷小,而右哨兵节点视为无穷大)

  由于跳表的相邻上下两层的节点稀疏程度不同,进入下一水平层更有可能逼近指定key对应的数据节点。通过在水平层大跨步的跳跃,并在对应的节点处进入下一层,循环往复的如此操作直到最底层。跳跃式的进行链表节点的查找方式,也是跳表名称SkipList的来源。

  从代码实现中可以看到,跳表通过建立在其上的索引节点进行查找,比起原始的一维链表,能够更快的定位到所要查找的节点。且如果按照概率算法构建的索引节点分布比较平均的话,跳表的查找效率将能够媲美有序向量、平衡二叉树的查找效率。

跳表查找方法实现:

   
     * 找到最逼近参数key的前驱数据节点
     * (返回的节点的key并不一定等于参数key,也有可能是最逼近的)
     *  findPredecessorNode(K key){
         从跳表头结点开始,从上层到下层逐步逼近
        Node<K,V> currentNode = head;

        while(true 当前遍历节点的右节点不是右哨兵,且data >= 右节点data
            while (currentNode.right.nodeType != NodeType.RIGHT_SENTINEL && doCompare(key,currentNode.right.key) >= 0 指向同一层的右节点
                currentNode = currentNode.right;
            }

             跳出了上面循环,说明找到了同层最接近的一个节点
            if(currentNode.down !=  currentNode.down != null,未到最底层,进入下一层中继续查找、逼近
                currentNode = currentNode.down;
            } currentNode.down == null,说明到了最下层保留实际节点的,直接返回
                 (currentNode.key并不一定等于参数key,可能是最逼近的前缀节点)
                 currentNode;
            }
        }
    }

   
     * 找到key对应的数据节点
     * 如果没有找到,返回null
     *  searchNode(K key){
        Node<K,V> preNode = findPredecessorNode(key);
        if(preNode.key != null && Objects.equals(preNode.key,key)){
             preNode;
        };
        }
    }
 

跳表插入实现

  跳表在插入节点的过程中,首先通过findProdecessorNode查询到最逼近key的前驱数据节点,如果发现当前key并不存在,则在最底层的数据节点链表中插入新的数据节点。

  在新的数据节点插入完成后,根据random生成一个0-1之间的随机数,与定义的PROMOTE_RATE常量进行比对,判断是否需要为当前新插入的节点创建更上一层的索引节点。这一比对可能会进行多次,相对应的也会为新插入节点在垂直方向上创建更多的索引节点。

跳表插入代码:

  private Node<K,1)"> putNode(K key,V value){
        if(key == throw new RuntimeException("key required");
        }

         从最底层中,找到其直接最接近的前驱节点
        Node<K,V> predecessorNode = findPredecessorNode(key);

        if(Objects.equals(key,predecessorNode.key)){
             data匹配,已经存在,直接返回false代表未插入成功
             predecessorNode;
        }

         当前跳表元素个数+1
        this.size++;

         之前不存在,需要新插入节点
        Node<K,V> newNode = (key,value);
         将新节点挂载至前驱节点之后
        predecessorNode.linkAsRight(newNode);
        int currentLevel = INIT_MAX_LEVEL;

        Random random = new Random();

        Node<K,V> hasUpNodePredecessorNode = predecessorNode;
        Node<K,V> newNodeUpperNode = newNode;

        boolean doPromoteLevel = falsewhile (random.nextDouble() < PROMOTE_RATE && !doPromoteLevel) {
             当前插入的节点需要提升等级,在更高层插入索引节点
            if(currentLevel == .maxLevel){
                promoteLevel();
                 保证一次插入节点,做多只会提升一层(否则将会有小概率出现高位的许多层中只有极少数(甚至只有1个)元素的情况)
                doPromoteLevel = ;
            }

             找到上一层的前置节点
            while (hasUpNodePredecessorNode.up == ) {
                 向左查询,直到找到最近的一个有上层节点的前驱节点
                hasUpNodePredecessorNode = hasUpNodePredecessorNode.left;
            }
             指向上一层的node
            hasUpNodePredecessorNode = hasUpNodePredecessorNode.up;

            Node<K,V> upperNode =  将当前data的up节点和上一层最接近的左上的node建立连接
            hasUpNodePredecessorNode.linkAsRight(upperNode);

             当前data这一列的上下节点建立关联
            upperNode.down = newNodeUpperNode;
            newNodeUpperNode.up = upperNode;

             由于当前data节点可能需要在更上一层建立索引节点,所以令newNodeUpperNode指向更上层的up节点
            newNodeUpperNode = newNodeUpperNode.up;
             当前迭代层次++
            currentLevel++;
        }

        ;
    }

  在通过概率算法决定是否建立更高层索引节点的过程中,有可能需要额外的再升高一层。这时需要通过promoteLevel方法将整个跳表的水平层抬高一层,并令跳表的head作为新增水平层的左哨兵节点。

promoteLevel方法实现:

   
     * 提升当前跳表的层次(在当前最高层上建立一个只包含左右哨兵的一层,并令跳表的head指向左哨兵)
     *  promoteLevel(){
         最大层数+1
        this.maxLevel++ 当前最高曾左、右哨兵节点
        Node<K,V> upperLeftSentinel = (NodeType.LEFT_SENTINEL);
        Node<K,V> upperRightSentinel = (NodeType.RIGHT_SENTINEL);

         最高层左右哨兵牵手
        upperLeftSentinel.right = upperRightSentinel;
        upperRightSentinel.left = upperLeftSentinel;

         最高层的左右哨兵,和当前第一层的head/right建立上下连接
        upperLeftSentinel.down = .head;
        upperRightSentinel.down = .tail;

        this.head.up = upperLeftSentinel;
        this.tail.up = upperRightSentinel;

         令跳表的head/tail指向最高层的左右哨兵
         upperRightSentinel;
    }

跳表删除实现

  跳表的删除相对简单,在找到需要被删除的最底层数据节点之后,通过up引用找到其对应的所有索引节点删除即可。

  当删除某一索引节点后,如果发现对应水平层只剩下左/右哨兵时,还需要通过destoryLevel方法将对应的水平层删除。

跳表删除节点:

 needRemoveNode){
        if (needRemoveNode ==  如果没有找到对应的节点,不需要删除,直接返回
            ;
        }
         当前跳表元素个数-1
        this.size-- 保留需要返回的最底层节点Node
        Node<K,V> returnCache = needRemoveNode;

         找到了对应节点,则当前节点以及其所有层的up节点都需要被删除
         INIT_MAX_LEVEL;
        while (needRemoveNode !=  将当前节点从该水平层的链表中移除(令其左右节点直接牵手)
            needRemoveNode.unlinkSelfHorizontal();

             当该节点的左右都是哨兵节点时,说明当前层只剩一个普通节点
            boolean onlyOneNormalData =
                    needRemoveNode.left.nodeType == NodeType.LEFT_SENTINEL &&
                    needRemoveNode.right.nodeType == NodeType.RIGHT_SENTINEL;
            boolean isLowestLevel = currentLevel == INIT_MAX_LEVEL;

            if(!isLowestLevel && onlyOneNormalData){
                 不是最底层,且只剩当前一个普通节点了,需要删掉这一层(将该层的左哨兵节点传入)
                destroyLevel(needRemoveNode.left);
            } 不需要删除该节点
                currentLevel++;
            }
             指向该节点的上一点
            needRemoveNode = needRemoveNode.up;
        }

         returnCache;
    }

跳表删除水平层destoryLevel实现:

void destroyLevel(Node<K,1)"> levelLeftSentinelNode){
         最大层数减1
        this.maxLevel-- 当前层的右哨兵节点
        Node<K,V> levelRightSentinelNode = levelLeftSentinelNode.right;

        if(levelLeftSentinelNode == .head){
             需要删除的是当前最高层(levelLeftSentinelNode是跳表的头结点)

             令下一层的左右哨兵节点的up节点清空
            levelLeftSentinelNode.down.up = ;
            levelRightSentinelNode.down.up = ;

             令跳表的head/tail指向最高层的左右哨兵
             levelLeftSentinelNode.down;
             levelRightSentinelNode.down;
        } 需要删除的是中间层

             移除当前水平层左哨兵,令其上下节点建立连接
            levelLeftSentinelNode.unlinkSelfVertical();
             移除当前水平层右哨兵,令其上下节点建立连接
            levelRightSentinelNode.unlinkSelfHorizontal();
        }
    }

4. 跳表性能分析

跳表空间效率分析

  高效的跳表实现(例如jdk的ConcurrentSkipListMap)相对于本篇博客的简易版实现,上层的索引节点只需要持有down和right两个关联节点的引用即可(K/V引用也可以简化为对底层数据节点的引用),而最底层的数据节点则仅维护关联的right节点即可。同时,通过边界条件的判断,也并不需要水平层的左右哨兵节点。

  可以看到,高效跳表的空间效率其实很高,其空间占用正比于数据节点的数目,渐进的空间复杂度为O(n)。在redis的zset实现中,就是使用跳表作为其底层实现的。redis的zset跳表实现中,建立上一级索引节点的概率被设置为1/4,综合来看每个节点所持有的平均引用数量大约为1.33,比红黑树节点2个引用(左右孩子节点,都不考虑value的引用)的空间效率要高。

跳表时间效率分析

跳表的查询性能

  跳表通过概率算法建立起了均匀分布的索引节点层(从数学期望上来看是均匀分布的,但存在一定波动),能够以正比于跳表层数的O(logn)对数时间复杂度完成随机查询。

  跳表的查询操作效率与跳表的层数有关,因此跳表查询操作的渐进时间复杂度为O(logn)。

  跳表和哈希表在对空间/时间的取舍上类似,哈希表可以通过调整负载因子进行空间效率与查询时间效率的取舍;而跳表也可以通过设置增加上一层索引节点的概率来调节查询效率与空间效率。

跳表的插入性能

  跳表的插入依赖于跳表的查询(logn),且需要根据概率决定是否创建对应的上一层索引节点。在最坏情况下,可能需要创建n+1个索引节点(n为跳表当前层数,1表示可能会增加新的一层);最好情况下不需要创建任何索引节点。

  跳表的插入操作效率与跳表的层数有关,因此跳表插入操作的渐进时间复杂度为O(logn)。

跳表的删除性能

  跳表的删除同样依赖于跳表的查询,删除最底层数据节点时也需要将被删除节点对应的索引节点一并删除。在最坏情况下,可能需要删除至多n个索引节点(n为跳表层数),最好情况下不需要删除任何索引节点。

  跳表的删除操作效率与跳表的层数有关,因此跳表删除操作的渐进时间复杂度为O(logn)。

为什么redis使用跳表而不是红黑树实现ZSET?

下面是redis作者给出的回答:

  1) They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

  2) A sorted set is often target of many ZRANGE or ZREVRANGE operations,that is,traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

  3) They are simpler to implement,debug,and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

大致的翻译:

  1) 跳表是否很消耗内存,这取决于你。通过改变提升跳表节点索引等级的概率参数可以令跳表的内存消耗少于B树。

  2) 一个有序集合通常被作为ZRANGE或是ZREVERANGE操作的目标。也就是说,通常是以链表的形式来遍历跳表的,在这种遍历操作下,缓存了相邻节点位置的跳表性能将至少和其它类型的自平衡树一样优秀。

  3) 跳表更容易实现和调试,等等。得益于跳表的简单性,我收到了一个能够在跳表中以O(logN)效率实现ZRANK的补丁(已经在redis的master分支中了),而这只需要对代码稍作修改。

  经过前面博客中对跳表原理的介绍,是否对redis作者的回答有了更深的体会呢?

5.总结

  通过自己的思路实现了一个简易版的跳表之后,理解了跳表的设计思想,也使得我有能力更进一步的去理解jdk、redis中更为高效的跳表实现。同时也加深了对跳表、平衡二叉树、哈希表等不同数据结构的理解,以及如何在不同场景下应该如何选择更高效、更符合实际需求的数据结构。

  本系列博客的代码在我的 github上:https://github.com/1399852153/DataStructures (SkipListMap类),存在许多不足之处,还请多多指教。

原文地址:https://www.cnblogs.com/xiaoxiongcanguan

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设