Understanding java collections from shallow to deep (4) - Collection Queue

Queue is used to simulate the data structure of a queue, which usually refers to a "first in first out" (FIFO) container. The new element is inserted (offer) to the tail of the queue, and the access element (poll) operation returns the element at the head of the queue. In general, queues do not allow random access to elements in the queue.

insert image description here

This structure is like queuing in our lives.

Let's introduce PriorityQueue, an important implementation class in Queue.

PriorityQueue
The order in which PriorityQueue saves queue elements is not in the order in which they were added to the queue, but is reordered by the size of the queue elements. Therefore, when the peek() or pool() method is called to remove the element at the head of the queue, it is not the element that entered the queue first, but the smallest element in the queue.

The sorting method of PriorityQueue
The elements in PriorityQueue can be sorted naturally by default (that is, the numbers are small by default at the head of the queue, and the strings are arranged in lexicographical order) or the sorting method specified when the queue is instantiated through the provided Comparator (comparator) . For natural sorting and Comparator (comparator), you can refer to my explanation when introducing the collection Set.
Note: The head of the queue is the smallest element in the specified ordering. If multiple elements are the minimum value, the head is one of the elements - the method of choice is arbitrary.

Note: When the Comparator is not specified in the PriorityQueue, the elements added to the PriorityQueue must implement the Comparable interface (that is, the elements are comparable), otherwise a ClassCastException will result.
Let's write an example to show the sorting method in PriorityQueue:

PriorityQueue<Integer> qi = new PriorityQueue<Integer>();
        qi.add(5);
        qi.add(2);
        qi.add(1);
        qi.add(10);
        qi.add(3);
        while (!qi.isEmpty()){
    
    
          System.out.print(qi.poll() + ",");
        }
        System.out.println();
        //采用降序排列的方式,越小的越排在队尾
        Comparator<Integer> cmp = new Comparator<Integer>() {
    
    
          public int compare(Integer e1, Integer e2) {
    
    
            return e2 - e1;
          }
        };
        PriorityQueue<Integer> q2 = new PriorityQueue<Integer>(5,cmp);
        q2.add(2);
        q2.add(8);
        q2.add(9);
        q2.add(1);
        while (!q2.isEmpty()){
    
    
              System.out.print(q2.poll() + ",");
            }





输出结果:

1,2,3,5,10,
9,8,2,1,

It can be seen from this that PriorityQueue uses natural sorting by default. When Comparator is specified, PriorityQueue adopts the specified sorting method.

The method of PriorityQueue
PriorityQueue implements the Queue interface, and the methods of PriorityQueue are listed below.

insert image description here

The essence of PriorityQueue
The essence of PriorityQueue is also a dynamic array, which is consistent with ArrayList in this respect.
When PriorityQueue calls the default constructor, a PriorityQueue is created with the default initial capacity (DEFAULT_INITIAL_CAPACITY=11), and its elements are sorted according to their natural order (Comparable implemented using the collection elements added to it).

 public PriorityQueue() {
    
    
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

When using the capacity-specified constructor, creates a PriorityQueue with the specified initial capacity and sorts its elements according to their natural order (Comparable implemented using the collection elements added to it).

 public PriorityQueue(int initialCapacity) {
    
    
        this(initialCapacity, null);
    }

Creates a PriorityQueue with the specified initial capacity and sorts its elements according to the specified comparator.

public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
    
    
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

As can be seen from the third construction method, a dynamic array is maintained internally. When adding elements to the collection, it will first check whether the array has a margin, and if there is a surplus, add the new element to the collection. If there is no surplus, call the grow() method to increase the capacity, and then call siftUp to sort and insert the newly added elements into the corresponding array Location.

 public boolean offer(E e) {
    
    
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

In addition, note:
①PriorityQueue is not thread-safe. If any of multiple threads modify the list structurally, those threads should not concurrently access the PriorityQueue instance, in which case use the thread-safe PriorityBlockingQueue class.

② It is not allowed to insert null elements.

③PriorityQueue implements the time complexity of inserting methods (offer, poll, remove() and add methods) is O(log(n)); the time complexity of implementing remove(Object) and contains(Object) methods is O(n); The time complexity of implementing the retrieval methods (peek, element, and size) is O(1). So when traversing, if there is no need to delete elements, each element is traversed by peek.

④ The iterator provided in the method iterator() does not guarantee to traverse the elements in PriorityQueue in an orderly manner.

Dueue interface and ArrayDeque implementation class
Dueue interface
Deque interface is a sub-interface of Queue interface, which represents a double-ended queue. LinkedList also implements the Deque interface, so it can also be used as a double-ended queue. You can also see the previous introduction to LinkedList to understand the Deque interface.
Therefore, the Deque interface adds some methods for double-ended queue operations.

void addFirst(E e):将指定元素插入此列表的开头。
void addLast(E e): 将指定元素添加到此列表的结尾。
E getFirst(E e): 返回此列表的第一个元素。
E getLast(E e): 返回此列表的最后一个元素。
boolean offerFirst(E e): 在此列表的开头插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 获取但不移除此列表的第一个元素;如果此列表为空,则返回 nullE peekLast(E e): 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 nullE pollFirst(E e): 获取并移除此列表的第一个元素;如果此列表为空,则返回 nullE pollLast(E e): 获取并移除此列表的最后一个元素;如果此列表为空,则返回 nullE removeFirst(E e): 移除并返回此列表的第一个元素。
boolean removeFirstOccurrence(Objcet o): 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
E removeLast(E e): 移除并返回此列表的最后一个元素。
boolean removeLastOccurrence(Objcet o): 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

As can be seen from the above method, Deque can be used not only as a double-ended queue, but also as a stack, because this class also includes two methods: pop (out of the stack) and push (into the stack).

The relationship between Deque, Queue, and Stack
When Deque is used as a Queue queue (FIFO), the added element is added to the tail of the queue, and the head element is deleted when deleted. The method inherited from the Queue interface corresponds to the method of Deque as shown in the figure:insert image description here

Deque can also be used as a Stack (LIFO). At this time, the push and pop elements are all performed at the head of the double-ended queue. The method corresponding to Stack in Deque is shown in the figure:
insert image description here

Note: Stack is too old and implemented very poorly, so it is basically not used now, and Deque can be used directly instead of Stack for stack operations.
As the name suggests, ArrayDeque
is a Deque implemented with an array; since the bottom layer is an array, its capacity can be specified or not. The default length is 16, and then dynamically expanded according to the number of added elements. Because ArrayDeque is a queue at both ends, its order is generated according to the corresponding position of the element inserted in the array (details will be explained below).
Due to the limitations of its own data structure, ArrayDeque does not have the trimToSize method in ArrayList to slim down for itself. The method of using ArrayDeque is the method of using Deque above, and there is basically no way to expand Deque.

The essence of ArrayDeque
In
order to meet the requirement of inserting or deleting elements at both ends of the array, the circular array ArrayDeque must also have a circular internal dynamic array, that is, a circular array (circular array), which means that any point of the array may be as a starting point or an ending point.
ArrayDeque maintains two variables, representing the head and tail of ArrayDeque

 transient int head;
 transient int tail;

When inserting an element to the head, the head subscript is reduced by one and the element is inserted. The index represented by tail is the index value represented by the current end element plus one. If when inserting an element to the tail, insert it directly to the position indicated by tail, and then subtract one from tail.
Take the picture below as an example to explain.
insert image description here

In the image above: The image on the left indicates that 4 elements are inserted from the head and 2 from the tail. Initially, head=0, tail=0. When inserting element 5 from the head, head-1, since the array is a circular array, move to the last position of the array and insert 5. When inserting element 34 from the head, head-1 is then inserted at the corresponding position. And so on, and finally insert 4 elements in the head. When inserting 12 at the tail, insert directly at the position of 0, then tail=tail+1=1, when inserting 7 from the tail, insert directly at the position of 1, then tail = tail +1=2. The output order in the final queue is 8, 3, 34, 5, 12, 7.
Think of the array as a circular array end-to-end to better understand the meaning of a circular array.

Let's take a look at how ArrayDeque actually applies the circular array?
Take addFirst(E e) as an example to study

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

When adding elements, first check whether it is empty (ArrayDeque cannot access null elements, because the system judges the existence of elements based on whether a certain position is null). Then head-1 inserts the element. head = (head - 1) & (elements.length - 1) solves the problem of subscript out of bounds very well. This code is equivalent to taking a modulus, and at the same time solves the situation where the head is a negative value. Because elements.length must be an exponential multiple of 2 (there are specific operations in the code), elements - 1 is all 1s in the binary low order, and after being ANDed with head - 1, it plays the role of modulo. If head - 1 is a negative number, it can only be -1. When it is -1, AND with elements.length - 1, the result is elements.length - 1. Other things being equal, it is equal to itself.

After inserting an element, it is judged whether there is room left. Because tail always points to the next slot that can be inserted, it means that the elements array has at least one slot, so there is no need to consider space issues when inserting elements.

Next, let’s talk about the expansion function doubleCapacity(). Its logic is to apply for a larger array (twice the original array), and then copy the original array over. The process is shown in the figure below:

insert image description here

In the figure, we can see that the copying is performed twice, the first copying the elements on the right of the head, and the second copying the elements on the left of the head.

//doubleCapacity()
private void doubleCapacity() {
    
    
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // head右边元素的个数
    int newCapacity = n << 1;//原空间的2倍
    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 = (E[])a;
    head = 0;
    tail = n;
}

From this, we understand the process of adding and expanding the ArrayDeque loop array, and other operations are similar.
Note: ArrayDeque is not thread safe. When used as a stack, the performance is better than Stack; when used as a queue, the performance is better than LinkedList.

Guess you like

Origin blog.csdn.net/weixin_45817985/article/details/130685217