六、链表实现栈和队列

1,075 阅读3分钟

关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!

承接上文,今天我们来用链表实现栈和队列。

先看栈,我们使用链表的头部作为栈顶,链表的尾部作为栈底。实现比较简单,直接上代码:

    @Override
    public void push(E e){
        list.addFirst(e);
    }

    @Override
    public E pop(){
        return list.removeFirst();
    }

    @Override
    public E peek(){
        return list.getFirst();
    }

代码基于LinkedList来实现,那么getSize(),isEmpty()等的操作就不提了。

接下来,我们来测试下基于数组实现的栈和基于链表实现的栈的区别:

    public class CompareStack {

        // 测试使用stack运行opCount个push和pop操作所需的时间,单位:秒
        private static double testStack(Stack<Integer> stack, int opCount){

            long startTime = System.nanoTime();

            Random random = new Random();
            for (int i = 0; i< opCount; i++){
                stack.push(random.nextInt(Integer.MAX_VALUE));
            }
            for (int i = 0; i<opCount; i++){
                stack.pop();
            }

            long endTime = System.nanoTime();

            return (endTime - startTime) / 1000000000.0;
        }
        public static void main(String[] args) {

            int opCount = 10000000;

            ArrayStack<Integer> arrayStack = new ArrayStack<>();
            double time1 = testStack(arrayStack, opCount);
            System.out.println("arrayStack, time: " + time1 + "s");

            LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
            double time2 = testStack(linkedListStack, opCount);
            System.out.println("linkedListStack, time: " + time2 + "s");
        }
    }

我们随机在栈中插入1000000个数字,并取出元素,那么测试结果如下:

arrayStack, time: 0.4852778s2
linkedListStack, time: 2.5215067s

其实这个时间比较很复杂,因为LinkedListStack中包含更多的new操作(node),需要不停的在内存中寻找地方开辟空间,当数量越多,耗时越长。实际上不管是arrayStack还是linkedListStack在我们的测试中,他们的时间复杂度都是一样的。

现在我们来看用链表来实现队列。

对于链表来说,头部节点head的操作是O(1),如果想实现队列,那么我们需要定义个尾部节点tail,记录最后一个元素的位置。同时,上篇文章我们说过链表的头部节点添加元素还是删除元素都是O(1),那么我们如何定义队首队尾的位置呢?

如上图所示,我们使用tail来记录链表尾部元素位置,此时我们在tail端添加元素就变为O(1),但是当我们删除元素时,因为必须知道前一个元素的节点,所以需要遍历整个链表,此时就变的比较复杂。而我们在head端,无论是添加元素还是删除元素,时间复杂度都是O(1),那么我们就可以把head端作为队首出队,把tail端作为队尾入队,也就是从head端删除元素,从tail端插入元素。

同时呢,由于没有dummyhead,那么需要注意链表为空时的情况,此时head和tail都指向同一位置。

下面我们来用链表实现队列。

    private Node head, tail;

    @Override
    public void enqueue(E e){
        // 当tail为空时,也就意味着head也为空
        if(tail == null){
            tail = new Node(e);
            head = tail;
        }
        else {
            tail.next = new Node(e);
            tail = tail.next;
        }
        size++;
    }

    @Override
    public E dequeue(){
        if (isEmpty()){
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        }

        Node retNode = head;
        head = head.next;
        retNode.next = null;

        // 特殊情况:链表中只有一个元素,若删除,则为空
        if (head == null){
            tail = null;
        }
        size--;
        return retNode.e;
    }

    @Override
    public E getFront(){
        if (isEmpty()){
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        }
        return head.e;
    }

队列实现完毕,我们来测试一波arrayQueue,loopQueue和linkedListQueue的运行效率。测试代码如下:

    // 测试使用q运行opCount个enqueue和dequeue操作所需的时间,单位:秒
    private static double testQueue(Queue<Integer> q, int opCount){

        long startTime = System.nanoTime();

        Random random = new Random();
        for (int i = 0; i< opCount; i++){
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        }
        for (int i = 0; i<opCount; i++){
            q.dequeue();
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }

我们在队列中插入100000个数字,并取出。测试时间如下:

ArrayQueue, time: 2.8499096s
LoopQueue, time: 0.0099262s
linkedListQueue, time: 0.0128844s

由于arrayQueue出队操作时间复杂度是O(n),而loopQueue和linkedListQueue时间复杂度都是O(1),所以arrayQueue的时间消耗最多,然后linkedListQueue是基于链表来实现的,在操作的过程中,需要new出很多节点,加之各种原因(系统因素,元素数量等)的影响,所以时间会略有不同。

好了,今天我们用链表来实现了栈和队列,也算是巩固了之前学的知识。经过这几天的总结,相信大家对于数组、栈、队列、链表的认识更加深刻。后续呢,我们会尽可能每天刷点题,提升自己的代码能力。

OK,拜了个拜~