自己动手实现java数据结构四双端队列

1.双端队列介绍

  在介绍双端队列之前,我们需要先介绍队列的概念。和栈相对应,在许多算法设计中,需要一种"先进先出(First Input First Output)"的数据结构,因而一种被称为"队列(Queue)"的数据结构被抽象了出来(因为现实中的队列,就是先进先出的)。

  队列是一种线性表,将线性表的一端作为队列的头部,而另一端作为队列的尾部。队列元素从尾部入队,从头部出队(尾进头出,先进先出)。

  双端队列(Double end Queue)是一种特殊的队列结构,和普通队列不同的是,双端队列的线性表两端都可以进行出队和入队操作。当只允许使用一端进行出队、入队操作时,双端队列等价于一个栈;当限制一端只能出队,另一端只能入队时,双端队列等价于一个普通队列。

  简洁起见,下述内容的"队列"默认代表的就是"双端队列"。

2.双端队列ADT接口

/**
 * 双端队列 ADT接口
 * */
public interface Deque<E>{

    
     * 头部元素插入
     * */
    void addHead(E e);

    
     * 尾部元素插入
     *  addTail(E e);

    
     * 头部元素删除
     * */
    E removeHead();

    
     * 尾部元素删除
     * 
    E removeTail();

    
     * 窥视头部元素(不删除)
     * 
    E peekHead();

    
     * 窥视尾部元素(不删除)
     * 
    E peekTail();

    
     * @return 返回当前队列中元素的个数
     int size();

    
     * 判断当前队列是否为空
     *  如果当前队列中元素个数为0,返回true;否则,返回false
     boolean isEmpty();

    
     * 清除队列中所有元素
     *  clear();

    
     * 获得迭代器
     * 
    Iterator<E> iterator();
}

3.双端队列实现细节

3.1 双端队列基于数组的实现(ArrayDeque)

  双端队列作为一个线性表,一开始也许会考虑能否像栈一样,使用向量作为双端队列的底层实现。

  但是仔细思考就会发现:在向量中,头部元素的插入、删除会导致内部元素的整体批量的移动,效率很差。而队列具有"先进先出"的特性,对于频繁入队,出队的队列容器来说,O(n)时间复杂度的单位操作效率是无法容忍的。因此我们必须更进一步,从更为基础的数组结构出发,实现我们的双端队列。

3.1.1 数组双端队列实现思路:

  在进行代码细节的展开之前,让我们先来理解以下基本思路:

  1.和向量一样,双端队列在内部数组容量不足时,能和向量一样动态的扩容。

  2.双端队列内部维护着"头部下标"、"尾部下标"。头部下标指向的是队列中第一位元素尾部下标指向的是下一个尾部元素插入的位置

     从头部下标起始,到尾部下标截止(左闭右开区间),连续保存着队列中的全部元素。在元素出队,入队时,通过移动头尾下标,进行队列中元素的插入、删除,从而避免了类似向量中大量内部元素的整体移动。

     当头部元素入队时,头部下标向左移动一位头部元素出队时,头部下标向右移动一位。

     当尾部元素入队时,尾部下标向右移动一位尾部元素出队时,尾部下标向左移动一位。

  3.当元素下标的移动达到了边界时,需要将数组从逻辑上看成一个环,其头尾是相邻的:

    下标从数组第0位时,向左移动一位,会跳转到数组的最后一位。

    下标从数组最后一位时,向右移动一位,会跳转到数组的第0位。

   下标越界时的跳转操作,在细节上是通过下标取模实现的。

   

3.1.2 队列的基本属性:

  只有当队列为空时,头部节点和尾部节点的下标才会相等。


 * 基于数组的 双端队列
 * class ArrayDeque<E> implements Deque<E>
     * 内部封装的数组
     * private Object[] elements;

    
     * 队列默认的容量大小
     * private static final int DEFAULT_CAPACITY = 16;

    
     * 扩容翻倍的基数
     * int EXPAND_BASE = 2
     * 队列头部下标
     *  head;

    
     * 队列尾部下标
     *  tail;


    
     * 默认构造方法
     * public ArrayDeque() {
        //:::设置数组大小为默认
        this.elements = new Object[DEFAULT_CAPACITY];

        :::初始化队列 头部,尾部下标
        this.head = 0;
        this.tail = 0;
    }
}

3.1.3 取模计算:

  在jdk基于数组的双端队列实现中,强制保持内部数组容量为2的平方(初始化时容量为2的平方,每次自动扩容容量 * 2),因此其取模运算可以通过按位与(&)运算来加快计算速度。

  取模运算在双端队列的基本接口实现中无处不在,相比jdk的双端队列实现,我们实现的双端队列实现更加原始,效率也较差。但相对的,我们的双端队列实现也较为简洁和易于理解。在理解了基础的实现思路之后,可以在这个初始版本的基础上进一步优化。

   
     * 取模运算
     * int getMod( logicIndex){
        int innerArrayLength = this.elements.length;

        :::由于队列下标逻辑上是循环的

        :::当逻辑下标小于零时
        if(logicIndex < 0){
            :::加上当前数组长度
            logicIndex += innerArrayLength;
        }
        :::当逻辑下标大于数组长度时
        if(logicIndex >= innerArrayLength){
            :::减去当前数组长度
            logicIndex -= innerArrayLength;
        }

        :::获得真实下标
        return logicIndex;
    }

  取模运算时间复杂度:

  取模运算中只是进行了简单的整数运算,时间复杂度为O(1),而在jdk的双端队列实现中,使用位运算的取模效率还要更高。

3.1.4 基于数组的双端队列常用操作接口实现:

  结合代码,我们再来回顾一下前面提到的基本思路:

  1. 头部下标指向的是队列中第一位元素尾部下标指向的是下一个尾部元素插入的位置

  2. 头部插入元素时,head下标左移一位头部删除元素时,head下标右移一位

      尾部插入元素时,tail下标右移一位尾部删除元素时,tail下标左移一位

  3. 内部数组被看成是一个环,下标移动到边界临界点时,通过取模运算来计算逻辑下标对应的真实下标。

    @Override
     addHead(E e) {
        :::头部插入元素 head下标左移一位
        this.head = getMod(this.head - 1);
        :::存放新插入的元素
        this.elements[this.head] = e;

        :::判断当前队列大小 是否到达临界点
        if(head == tail){
            :::内部数组扩容
            expand();
        }
    }

    @Override
     addTail(E e) {
        this.tail] = e;
        :::尾部插入元素 tail下标右移一位
        this.tail = getMod(this.tail + 1);

                    expand();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
     E removeHead() {
        :::暂存需要被删除的数据
        E dataNeedRemove = (E).head];
        :::将当前头部元素引用释放
        this.head] = null;

        :::头部下标 右移一位
        this.head + 1 dataNeedRemove;
    }

    @Override
    @SuppressWarnings("unchecked" E removeTail() {
        :::获得尾部元素下标(左移一位)
        int lastIndex = getMod(this.tail - 1.elements[lastIndex];

        :::设置尾部下标
        this.tail = lastIndex;

         E peekHead() {
        return (E).head];
    }

    @Override
    @SuppressWarnings("unchecked" E peekTail() {
        .elements[lastIndex];
    }

  队列常用接口时间复杂度:

  基于数组的队列在访问头尾元素时,进行了一次取模运算获得真实下标,由于数组的随机访问是常数时间复杂度(O(1)),因此队列常用接口的时间复杂度都为O(1),效率很高。

3.1.5 扩容操作:

  可以看到,在入队插入操作结束后,会判断当前队列容量是否已经到达了临界点。

  前面提到,只有在队列为空时,头部下标才会和尾部下标重合;而当插入新的入队元素之后,使得头部下标等于尾部下标时,说明内部数组的容量已经达到了极限,需要进行扩容才能容纳更多的元素。

我们举一个简单的例子来理解扩容操作:

  尾部下标为2.头部下标为3,队列内的元素为头部下标到尾部下标(左闭右开)中的元素排布为(1,2,3,4,5,6)。

  目前队列刚刚在下标为2处的尾部入队元素"7"。尾部下标从2向右移动一位和头部下标重合,此时队列中元素排布为(1,2,3,4,5,6,7),此时需要进行一次扩容操作。

  在扩容完成之后,我们希望让队列的元素在内部数组中排列的更加自然:

    1. 队列中元素的顺序不变,依然是(1,2,3,4,5,6,7),内部数组扩容一定的倍数(两倍)

    2. 队列中第一个元素将位于内部数组的第0位,队列中的元素按照头尾顺序依次排列下去

  扩容的大概思路:

    1. 将"头部下标"直至"当前内部数组尾部"的元素按照顺序整体复制到新扩容数组的起始位置(红色背景的元素)

    2. 将"当前内部数组头部"直至"尾部下标"的元素按照顺序整体复制到新扩容数组中(位于第一步操作复制的数据区间之后)(蓝色背景的元素)

扩容前:

  

扩容后:

扩容代码的实现:  

   
     * 内部数组扩容
     *  expand(){
        :::内部数组 扩容两倍
        int elementsLength = .elements.length;
        Object[] newElements = new Object[elementsLength * EXPAND_BASE];

        :::将"head -> 数组尾部"的元素 复制在新数组的前面 (tips:使用System.arraycopy效率更高)
        for(int i=this.head,j=0; i<elementsLength; i++,j++){
            newElements[j] = .elements[i];
        }

        :::将"0 -> head"的元素 复制在新数组的后面 (tips:使用System.arraycopy效率更高)
        int i=0,j=elementsLength-this.head; i<this.head; i++,1)">:::初始化head,tail下标
        this.tail = :::内部数组指向 新扩容的数组
        this.elements = newElements;
    }

  扩容操作时间复杂度:

  动态扩容的操作由于需要进行内部数组的整体copy,其时间复杂度是O(n)。

  但是站在全局的角度,动态扩容只会在入队操作导致空间不足时偶尔的被触发,整体来看,动态扩容的时间复杂度为O(1)

3.1.6 其它接口实现:

 size() {
        return getMod(tail - head);
    }

    @Override
     isEmpty() {
        :::当且仅当 头尾下标相等时 队列为空
        return (head == tail);
    }

    @Override
     clear() {
        int head = .head;
        int tail = .tail;

        while(head !=this.elements[head] = ;
            head = getMod(head + 1);
        }

        ;
    }

    @Override
    public Iterator<E> iterator() {
        return  Itr();
    }

3.1.7 基于数组的双端队列——迭代器实现:

  迭代器从头部元素开始迭代,直至尾部元素终止。

  值得一提的是,虽然队列的api接口中没有提供直接删除队列中间(非头部、尾部)的元素,但是迭代器的remove接口却依然允许这种操作。由于必须要时刻保持队列内元素排布的连续性,因此在删除队列中间的元素后,需要整体的移动其他元素。

  此时,有两种选择:

    方案一:将"头部下标"到"被删除元素下标"之间的元素整体向右平移一位

    方案二:将"被删除元素下标"到"尾部下标"之间的元素整体向左平移一位

  我们可以根据被删除元素所处的位置,计算出两种方案各自需要平移元素的数量,选择平移元素数量较少的方案,进行一定程度的优化。

队列迭代器的remove操作中存在一些细节值得注意,我们使用一个简单的例子来帮助理解:

  1. 当前队列在迭代时需要删除元素"7"(红色元素),采用方案一需要整体平移(1,2,3,4,5,6)六个元素,而方案二只需要整体平移(8,9,10,11,12)五个元素。因此采用平移元素更少的方案二,

  2. 这时由于(8,9,10,11,12)五个元素被物理上截断了,所以主要分三个步骤进行平移。

    第一步: 先将靠近尾部的 (8,9)两个元素整体向左平移一位(蓝色元素)

    第二步: 将内部数组头部的元素(10),复制到内部数组的尾部(粉色元素)

    第三部 :  将剩下的元素(11,12),整体向左平移一位(绿色元素)

remove操作执行前:

remove操作执行后:

迭代器代码实现:

  在remove操作中有多种可能的情况,由于思路相通,可以通过上面的举例说明帮助理解。

   /**
     * 双端队列 迭代器实现
     * private class Itr implements Iterator<E> {
        
         * 当前迭代下标 = head
         * 代表遍历从头部开始
         * */
        int currentIndex = ArrayDeque..head;

        
         * 目标终点下标 = tail
         * 代表遍历至尾部结束
         * int targetIndex = ArrayDeque.
         * 上一次返回的位置下标
         *  lastReturned;

        @Override
         hasNext() {
            :::当前迭代下标未到达终点,还存在下一个元素
            this.currentIndex != .targetIndex;
        }

        @Override
        @SuppressWarnings("unchecked")
         E next() {
            :::先暂存需要返回的元素
            E value = (E)ArrayDeque..currentIndex];

            :::最近一次返回元素下标 = 当前迭代下标
            this.lastReturned = .currentIndex;
            :::当前迭代下标 向后移动一位(需要取模)
            this.currentIndex = getMod(this.currentIndex + 1);

             value;
        }

        @Override
         remove() {
            if(this.lastReturned == -1){
                throw new IteratorStateErrorException("迭代器状态异常: 可能在一次迭代中进行了多次remove操作");
            }

            :::删除当前迭代下标的元素
            boolean deleteFromTail = delete(.currentIndex);
            :::如果从尾部进行收缩
            if(deleteFromTail){
                :::当前迭代下标前移一位
                this.currentIndex - 1:::为了防止用户在一次迭代(next调用)中多次使用remove方法,将lastReturned设置为-1
            this.lastReturned = -1;
        }

        
         * 删除队列内部数组特定下标处的元素
         * @param currentIndex 指定的下标
         *  true 被删除的元素靠近尾部
         *         false 被删除的元素靠近头部
         * boolean delete( currentIndex){
            Object[] elements = ArrayDeque..elements;
            int head = ArrayDeque..head;
            int tail = ArrayDeque..tail;

            :::当前下标 之前的元素个数
            int beforeCount = getMod(currentIndex - head);
            :::当前下标 之后的元素个数
            int afterCount = getMod(tail - currentIndex);

            :::判断哪一端的元素个数较少
            if(beforeCount < afterCount){
                :::距离头部元素较少,整体移动前半段元素

                :::判断头部下标 是否小于 当前下标
                if(head < currentIndex){
                    :::小于,正常状态  仅需要复制一批数据

                    :::将当前数组从"头部下标"开始,整体向右平移一位,移动的元素个数为"当前下标 之前的元素个数"
                    System.arraycopy(elements,head,elements,head+1,beforeCount);
                }else{
                    :::不小于,说明存在溢出环  需要复制两批数据

                    :::将数组从"0下标处"的元素整体向右平移一位,移动的元素个数为"从0到当前下标之间的元素个数"
                    System.arraycopy(elements,1:::将数组最尾部的数据设置到头部,防止被覆盖
                    elements[0] = elements[(elements.length-1)];
                    :::将数组尾部的数据整体向右平移一位
                    System.arraycopy(elements,head+1,(elements.length-head-1));
                }
                :::释放被删除元素的引用
                elements[currentIndex] = ;
                :::头部下标 向右移动一位
                ArrayDeque.this.head = getMod(ArrayDeque.);

                :::没有删除尾部元素 返回false
                false;
            }{
                :::距离尾部元素较少,整体移动后半段元素

                :::判断尾部下标 是否小于 当前下标
                if(currentIndex < tail){
                    :::将当前数组从"当前"开始,整体向左平移一位,移动的元素个数为"当前下标 之后的元素个数"
                    System.arraycopy(elements,currentIndex+1:::将数组从"当前下标处"的元素整体向左平移一位,移动的元素个数为"从当前下标到数组末尾的元素个数-1 ps:因为要去除掉被删除的元素"
                    System.arraycopy(elements,currentIndex+1,(elements.length-currentIndex-1));
                    :::将数组头部的元素设置到末尾
                    elements[elements.length-1] = elements[0];
                    :::将数组头部的数据整体向左平移一位,移动的元素个数为"从0到尾部下标之间的元素个数"
                    System.arraycopy(elements,1,0:::尾部下标 向左移动一位
                ArrayDeque.this.tail = getMod(ArrayDeque.);
:::删除了尾部元素 返回true true; } } }

3.2 基于链表的链式双端队列

  和向量不同,双向链表在头尾部进行插入、删除操作时,不需要额外的操作,效率极高。

  因此,我们可以使用之前已经封装好的的双向链表作为基础,轻松的实现一个链式结构的双端队列。限于篇幅,就不继续展开了,有兴趣的读者可以尝试自己完成这个任务。

4.双端队列性能

  空间效率:

    基于数组的双端队列:数组空间结构非常紧凑,效率很高。

    基于链表的双端队列:由于链式结构的节点存储了相关联的引用,空间效率比数组结构稍低。

  时间效率:

    对于双端队列常用的出队入队操作,由于都是在头尾处进行操作,数组队列和链表队列的执行效率都非常高(时间复杂度(O(1)))。

    需要注意的是,由于双端队列的迭代器remove接口允许删除队列中间部位的元素,而删除中间队列元素的效率很低(时间复杂度O(n)),所以在使用迭代器remove接口时需要谨慎。

5.双端队列总结

  至此,我们实现了一个基础的、基于数组的双端队列。要想更近一步的学习双端队列,可以尝试着阅读jdk的java.util.ArrayDeque类并且按照自己的思路尝试着动手实现一个双端队列。我个人认为,如果事先没有一个明确的思路,直接去硬看源代码,很容易就陷入细节之中无法自拔,"不识庐山真面目,只缘生在此山中"。

  希望这篇博客能够让读者更好的理解双端队列,更好的理解自己所使用的数据结构,写出更高效,易维护的程序。

  博客的完整代码在我的 github上:https://github.com/1399852153/DataStructures ,存在许多不足之处,请多多指教。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 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 的计算机程序设