Data Structure - Stack & Queue & Deque Implementation Comparison

stack

Stack: A linear list that restricts insertion and deletion operations only at the end of the list;

  • Last In First Out (LIFO).
  • The operation is performed at the end of the list, which is the top of the stack; the latest element pushed onto the stack is at the bottom of the stack.

stack ADT

Stack_ADT

Push & Pop

stack

stack storage structure

  • sequential stack

The stack is also a linear table, but the insertion and deletion positions of elements in the table are limited, so it is easy to think of using a one-dimensional array to realize the storage structure of the stack. The Stack class in Java inherits from Vector, which is implemented with an array.

Stack.java

public class Stack<E> extends Vector<E> {
    
    

    public Stack() {
    }

    public E push(E item) {
        addElement(item);

        return item;
    }

    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    private static final long serialVersionUID = 1224463164541339165L;
}
  • Two stacks share storage space

    If we have two stacks of the same type, and we open up array space for each of them, it is very likely that the first stack is full, and then it overflows, while the other stack still has a lot of free storage space. At this time, we can make full use of the unidirectional extension of sequential stacks, use an array to store two stacks, let the bottom of one stack be the beginning of the array, and the bottom of the other stack is the end of the array. Extend from each end to the middle.

share_stack

ShareStack.java


/**
 * Created by engineer on 2017/10/22.
 */

public class ShareStack<T> {
    
    
    private Object[] element; //存放元素的数组

    private int stackSize;  // 栈大小

    private int top1; //栈1的栈顶指针

    private int top2; //栈2的栈顶指针


    /**
     * 初始化栈
     * @param size
     */
    public ShareStack(int size){
        element = new Object[size];
        stackSize = size;
        top1 = -1;
        top2 = stackSize;
    }


    /**
     * 压栈
     * @param i 第几个栈
     * @param o 入栈元素
     * @return
     */
    public boolean push(int i , Object o){

        if(top1 == top2 - 1)
            throw new RuntimeException("栈满!");
        else if(i == 1){
            top1++;
            element[top1] = o;
        }else if(i == 2){
            top2--;
            element[top2] = o;
        }else
            throw new RuntimeException("输入错误!");

        return true;
    }

    /**
     * 出栈
     * @param i
     * @return
     */
    @SuppressWarnings("unchecked")
    public T pop(int i){

        if(i == 1){
            if(top1 == -1)
                throw new RuntimeException("栈1为空");
            return (T)element[top1--];
        } else if(i == 2){
            if(top2 == stackSize)
                throw new RuntimeException("栈2为空");
            return (T)element[top2++];
        } else
            throw new RuntimeException("输入错误!");

    }


    /**
     * 获取栈顶元素
     * @param i
     * @return
     */
    @SuppressWarnings("unchecked")
    public T get(int i){

        if(i == 1){
            if(top1 == -1)
                throw new RuntimeException("栈1为空");
            return (T)element[top1];
        } else if(i == 2){
            if(top2 == stackSize)
                throw new RuntimeException("栈2为空");
            return (T)element[top2];
        } else
            throw new RuntimeException("输入错误!");
    }


    /**
     * 判断栈是否为空
     * @param i
     * @return
     */
    public boolean isEmpty(int i){

        if(i == 1){
            if(top1 == -1)
                return true;
            else
                return false;
        } else if(i == 2){
            if(top2 == stackSize)
                return true;
            else
                return false;
        } else
            throw new RuntimeException("输入错误!");
    }

}

Of course, considering that the size of the array needs to be limited at the time of initialization, the problem of expansion should also be considered. Therefore, the stack can also be implemented using a linked list; this will be discussed later, and will not be expanded here.

The data structure of stack is very practical; the fallback stack of Activity in Android is the best example. In normal mode, we push an Activity into the fallback stack through startActivity, and the finish() method is from the fallback stack. Pop up the Activity at the top; of course, there are many other operations in the actual process, and this is just the general process; the recursive idea also uses the structure of the stack.

queue

Queue: A linear list that allows only inserts at one end and deletes at the other.

  • First in first out (FIFO)
  • Insert at the end of the queue, delete from the head of the queue

ADT of the queue

Queue_ADT

Enqueue & Dequeue

Deque

stack storage structure

  • sequential storage structure

When using an array to implement the storage structure of the queue, in order to avoid moving each element behind each time an element is deleted from the head of the queue, two pointers front and rear are added, pointing to the head of the queue and the tail of the queue respectively; When deleting an element, you can move the front pointer without moving a large number of elements, but this will inevitably cause a false overflow problem, and the storage space will not be fully utilized, so it is necessary to use a circular queue to implement the sequential storage structure of the queue .

  • circular queue

Assuming that in the circular queue, QueueSize is the size of the circular queue, that is, the length of the array, the following conclusions can be drawn:

  1. Circular queue empty condition: front==rear;
  2. The condition for the circular queue to be full: (rear+1)%QueueSize=front;
  3. Circular queue length: (rear-front*QueueSize)%QueueSize;

In general, the use of sequential storage structure still needs to consider the problem of capacity. Therefore, in the case where we cannot estimate the queue length, we need to pay attention to the chain storage structure.

  • chain storage structure

As we have said above , LinkList implements the Deque interface, so it is a queue implemented with a linked list. Here is a brief analysis of the implementation of the enqueue push and dequeue pop operations.

LinkedList-add queue enqueue

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        //创建新的结点,其前驱指向last,后继为null
        final Node<E> newNode = new Node<>(l, e, null);
        //last 指针指向新的结点
        last = newNode;
        if (l == null)
            first = newNode;  //如果链表为空,frist指针指向新的结点
        else
            l.next = newNode; //链表不为空,新的结点连接到原来最后一个结点之后
        size++; //链表长度+1
        modCount++;
    }

LinkList is a doubly linked list, where first is the pointer to execute the first node, and last is the pointer to the last node.

LinkList-pop queue dequeue

    public E pop() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //获取要删除结点的值
        final E element = f.item;
        //得到f的下一个结点,也就是第二个结点
        final Node<E> next = f.next;
        // f 释放
        f.item = null;
        f.next = null; // help GC
        // first 指针指向f的下个结点,
        first = next;
        // f 后面已经没有结点了
        if (next == null)
            last = null; 
        else
            next.prev = null; // 第二个结点(也就是现在的第一个结点)前驱为null,因为LinkList 是双端链表,非循环。
        size--;
        modCount++;
        return element;
    }

Here is a typical implementation of deleting the head node of a singly linked list. So far, we have mastered the respective characteristics of the two data structures stack and queue; let's take a look at the implementation of stack and queue officially provided by Java.

Deque

Here we mainly talk about the Deque class.

/**
 * A linear collection that supports element insertion and removal at
 * both ends.  The name <i>deque</i> is short for "double ended queue"
 * and is usually pronounced "deck".  Most {@code Deque}
 * implementations place no fixed limits on the number of elements
 * they may contain, but this interface supports capacity-restricted
 * deques as well as those with no fixed size limit.
 * /
public interface Deque<E> extends Queue<E> {
    void addFirst(E var1);

    void addLast(E var1);

    boolean offerFirst(E var1);

    boolean offerLast(E var1);

    E removeFirst();

    E removeLast();

    E pollFirst();

    E pollLast();

    E getFirst();

    E getLast();

    E peekFirst();

    E peekLast();

    boolean add(E var1);

    boolean offer(E var1);

    E remove();

    E poll();

    E element();

    E peek();

    void push(E var1);

    E pop();

    ........
}

The Deque interface is the abbreviation of "double ended queue" (usually pronounced "deck"), that is, a double-ended queue, which supports insertion and deletion of elements at both ends of a linear list, and inherits the Queue interface. Most implementations have no limit on the number of elements, but this interface supports both capacity-limited deques and ones without a fixed-size limit.

We know that the Queue interface defines the operation set of the queue, and the Deque interface is extended on its basis to define the operation of inserting and deleting on both ends. Therefore, we can think that the Deque interface can be used as both a queue and a stack.

Therefore, looking back, we can find that LinkList is a linked list structure and implements both a queue and a stack . The operation of LinkList as a queue has been analyzed earlier. Below we can see how he implements the chain structure to implement the queue.

push onto the stack

    public void addLast(E e) {
        linkLast(e);
    }

It can be seen that for push operations and queues, elements are inserted at the end of the linked list, and the linkLast() method is used like the queue.

pop

    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }

The unlinkLast method is also used to pop the stack, but the element to be popped is last. instead of first in the queue.

Deque's sequential storage implements ArrayDeque

ArrayDeque implements all the operations required for stacks and queues with a dynamic array.

add element

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }

    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

    private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = a;
        head = 0;
        tail = n;
    }

It can be seen here that whether new elements are added at the head or at the tail, when the capacity needs to be expanded, it will directly change to twice the original size. At the same time need to copy and move a lot of elements.

remove element

public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            elements[h] = null; // Must null out slot
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

    public E pollLast() {
        final Object[] elements = this.elements;
        final int t = (tail - 1) & (elements.length - 1);
        @SuppressWarnings("unchecked")
        E result = (E) elements[t];
        if (result != null) {
            elements[t] = null;
            tail = t;
        }
        return result;
    }

It is more convenient to delete (get) elements from the head and tail, just modify the head and tail positions. head is the position of the first element in the current array, tail is the first empty position in the array.

BlockingDeque

/**
 * A {@link Deque} that additionally supports blocking operations that wait
 * for the deque to become non-empty when retrieving an element, and wait for
 * space to become available in the deque when storing an element.
 * /

public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
}

Regarding the last point of Deque, BlockingDeque implements the blocking function on the basis of Deque. When the stack or queue is empty, it is not allowed to pop out of the stack or out of the queue, and it will remain blocked until there is an element that can be popped from the stack. Similarly, the queue is full is not allowed to enqueue unless an element is popped to make room. The commonly used concrete implementation class is LinkedBlockingDeque, which uses a chain structure to implement its blocking function. The thread pool queue inside AsyncTask that everyone is very familiar with in Android is implemented using LinkedBlockingDeque, with a length of 128, which ensures the serial execution of AsyncTask.

Comparing here, it can be found that for the two special data structures of stack and queue, since the position of obtaining (finding) elements has been limited, the sequential storage structure does not have a great advantage, but is adding elements due to the capacity of the array. It also brings additional consumption; therefore, when the data capacity cannot be known in advance, implementing stacks and queues using chained structures should be a better choice.


Well, stacks and queues are here first.

Guess you like

Origin blog.csdn.net/TOYOTA11/article/details/78314109