数据结构与算法之三(栈和队列的java实现)

栈在java中的实现方式是:

public class Stack<E> extends Vector<E> 

Vector向量可以理解为高级的数组,因为它里面通过一个Object[]数组来维护所有元素,并且实现了该数组的管理以及动态增长。所以Stack本质上就是一个数组,通过继承Vector,同时提供Stack特性的几个方法push,pop,peek等,原理比较简单。

队列

》一般队列
队列的特点是先进先出(FIFO),java中已经封装好Queue类。

队列的效率:添加和移除数据项时间为O(1)。注意这里的添加是往队尾添加数据项,移除的是队头的数据项,不是同一个。

队列在java中的实现方式是:

public interface Queue<E> extends Collection<E> public interface Collection<E> extends Iterable<E> 

注意这里是一个接口,也就是说可以通过实现该接口得到自己的队列。Collection接口中提供了诸如add,remove,isEmpty等方法,同时继承了Iterable可以获取迭代器。Queue中则新增了一些针对队列的一些方法。

》双端队列
双端队列顾名思义,就是可以在两端同时进行添加和移除操作的队列。在java中的实现如下

public interface Deque<E> extends Queue<E> 

也是一个接口,通过继承Queue,并提供对双端的额外操作,addFirst,addLast,removeFirst,removeLast等等。这是一个综合体,因为可以操作一端,你也可以当成栈来使用,同时还是个队列。

》优先级队列
优先级队列中的元素按照关键字的值有序。数据项在插入的时候会插入到合适的位置维持队列的顺序。在java中的实现如下:

//PriorityQueue继承了AbstractQueue
class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable //抽象类AbstractQueue 继承自AbstractCollection并实现Queue接口 public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> //抽象类AbstractCollection实现了Collection接口 public abstract class AbstractCollection<E> implements Collection<E> 

看了源码你会发现PriorityQueue是一个具体的类,之前的Queue等接口我们是看不到队列里的元素是什么的。只有实现接口的子类中才有。比如这里PriorityQueue中也使用Object[]数组来保存队列元素。

同时,抽象类AbstractQueue中还是只有add, remove等操作。跟优先级相关的内容肯定是在PriorityQueue中的,它是如何实现的呢?我们知道优先级队列在添加元素的时候维持顺序,所以直接看add方法。源码如下:

 public boolean add(E e) {
        return offer(e);
    }

 //offer源码如下,我们具体看一下
 public boolean offer(E e) {
        //排除空指针
        if (e == null)
            throw new NullPointerException();
       /**modCount的解释是The number of times this priority queue has been structurally modified,不用管*/
        modCount++;
        //当数量超出数组大小时,通过grow动态新增数组空间
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        //size++
        size = i + 1;
        //如果是第一次插入数据则直接放入
        if (i == 0)
            queue[0] = e;
        else //如果已经存在数据则调用siftup,这里面是某种排序
            siftUp(i, e);
        return true;
    }

 //siftUp源码如下:
  private void siftUp(int k, E x) {
        //comparator不会陌生,如果构造器里提供了比较器就用比较器,没有就默认处理。siftUpUsingComparator与siftUpComparable算法一样就是比较器不一样
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

//就看siftUpComparable。x是要插入的元素,k是插入前队列的长度
 private void siftUpComparable(int k, E x) {
        //获取默认比较器,这里注意一定是可排序的元素才能使用优先级队列
        Comparable<? super E> key = (Comparable<? super E>) x;
        //下面是一段神祕代码,但是我们知道它一定实现了某种排序
        while (k > 0) {
            //其实就是(k-1)/2
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

下面我们分析一下这段神祕代码,对于我们这些算法战五渣只知道插入排序什么的,这里明显不是啊。如果我们自己按照它的程序模拟插入会发现一个奇怪的结果,有点不对劲啊。我写了个简单的例子,记录了它的储存过程。

PriorityQueue<Integer> queue = new PriorityQueue<>();
         queue.add(1);
         System.out.println(queue.toString());
         queue.add(5);
         System.out.println(queue.toString());
         queue.add(2);
         System.out.println(queue.toString());
         queue.add(3);
         System.out.println(queue.toString());

然后运行,结果如下:

[1]
[1, 5]
[1, 5, 2]
[1, 3, 2, 5]

wtf,这排了序?兄台莫慌,会有这样的疑问很正常,因为我们的惯性思维就是排好序的就是1,2,3,4。。。然而,这里的数据结构是一个队列,不是数组,应该以队列的方式验证它的排序,这叫在其位谋其政。队列的方式就是进去的时候无序,出来的时候是有序的。怎么出来?

while(!queue.isEmpty()){
     System.out.print(queue.poll()+" ");
}

结果是:1 2 3 5 。完全正确

看来这段神祕代码确实有效,只不过排序的过程比较特别,刚才我们解锁的姿势不对。那么这段神祕代码是什么?其实从parent,siftUp这些字眼能看出来这是一个堆排序,建立在二叉树的基础上。

相关知识我们后面了解,今天的话题是栈和队列在java中是如何实现的。我们可以看到存储可以用数组,在存储的基础上实现各中对应操作。优先级队列底层用了堆排等等。

上一节:数据结构与算法之二(栈常见案例)
下一节:数据结构与算法之四(链表)

点赞