Thinking logic of computer programs (48) and thinking 48
We have introduced two implementation classes of Queue: Queue list and PriorityQueue. The Queue list also implements the dual-end Queue interface Deque. In the Java container class, there is also an implementation class of double-end queues, ArrayDeque, it is implemented based on arrays.
We know that, in general, because elements need to be moved, the efficiency of array insertion and deletion is relatively low, but the efficiency of ArrayDeque is very high. How does it implement it? In this section, we will discuss in detail.
Let's first look at the usage of ArrayDeque, then analyze its implementation principles, and finally summarize and analyze its features.
Usage
ArrayDeque implements the Deque interface. Like the queue list, its queue length is unlimited. In the queue list section, we have introduced the Deque interface. Here we will briefly review it.
Deque extends the Queue and has all the methods of the Queue. It can also be seen as a stack. It has the basic method of pushing/pop/peek, as well as clear methods at both ends of the operation, such as addFirst/removeLast.
ArrayDeque has the following constructor:
public ArrayDeque()public ArrayDeque(int numElements)public ArrayDeque(Collection<? extends E> c)
NumElements indicates the number of elements. The initial allocated space will contain at least so many elements, but the space is not as big as numElements. We will check its implementation details later.
ArrayDeque can be considered as an FIFO queue, for example:
Queue<String> queue = new ArrayDeque<>();queue.offer("a");queue.offer("b");queue.offer("c");while(queue.peek()!=null){ System.out.print(queue.poll() +" "); }
Output:
a b c
You can also think of ArrayDeque as an advanced, backward, and FIFO stack, for example:
Deque<String> stack = new ArrayDeque<>();stack.push("a");stack.push("b");stack.push("c");while(stack.peek()!=null){ System.out.print(stack.pop()+" "); }
Output:
c b a
You can also use the common methods at both ends of the operation, such:
Deque<String> deque = new ArrayDeque<>();deque.addFirst("a");deque.offerLast("b");deque.addLast("c");deque.addFirst("d");System.out.println(deque.getFirst()); //dSystem.out.println(deque.peekLast()); //cSystem.out.println(deque.removeFirst()); //dSystem.out.println(deque.pollLast()); //c
ArrayDeque is easy to use. Let's look at its implementation principle.
Implementation Principle
Internal components
ArrayDeque mainly has the following instance variables:
private transient E[] elements;private transient int head;private transient int tail;
Elements is an array of elements. ArrayDeque's efficiency comes from the head and tail variables. They make the physical simple array from start to end to a logical loop array, avoiding moving at the beginning and end. Let's explain the concept of loop array.
Loop Array
For general arrays, such as arr, the first element is arr [0], and the last element is arr [arr. length-1]. However, for an array in ArrayDeque, it is a logical loop array. The so-called loop means that the elements can start from the array header after they reach the end of the array, the length, first, and last elements of the array are related to the head and tail variables. Specifically:
Let's look at some icons.
In the first case, the array is empty, and the head and tail are the same, as shown below:
In the second case, tail is greater than head, as shown below, and contains three elements:
In the third case, tail is 0, as shown below:
In the fourth case, tail is not 0 and smaller than head, as shown below:
After understanding the concept of loop array, let's look at the code of some main operations of ArrayDeque, and first look at the constructor method.
Constructor
The default constructor code is:
public ArrayDeque() { elements = (E[]) new Object[16];}
An array of 16 characters is allocated.
If the numElements parameter is available, the code is:
public ArrayDeque(int numElements) { allocateElements(numElements);}
Instead of simply allocating a given length, allocateElements is called and the code is:
private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } elements = (E[]) new Object[initialCapacity];}
This code looks complicated, but it is mainly used to calculate the length of the array to be allocated initialCapacity. The computing logic is as follows:
- If numElements is smaller than MIN_INITIAL_CAPACITY, the allocated array length is MIN_INITIAL_CAPACITY, which is a static constant with a value of 8.
- When numElements is greater than or equal to 8, the actual length of the allocation is the minimum number of an integer power that is strictly greater than numElements and 2. For example, if numElements is 10, 16 is actually allocated. If numElements is 32, 64 is allocated.
Why is it a power of 2? We will see later that this will make many operations very efficient.
Why is it more rigorous than numElements? Because the cyclic array must have at least one vacancy at a time, and the tail variable points to the next vacancy, at least one numElements position is required to accommodate numElements elements.
The obscure part of this Code is:
initialCapacity |= (initialCapacity >>> 1);initialCapacity |= (initialCapacity >>> 2);initialCapacity |= (initialCapacity >>> 4);initialCapacity |= (initialCapacity >>> 8);initialCapacity |= (initialCapacity >>> 16);initialCapacity++;
What is this? In fact, it is copying 1 of the highest bit on the left of initialCapacity to every bit on the right. This replication is similar to virus replication, it is an exponential copy of 1, 2, 4, and 4, and then executes initialCapacity ++ to obtain the smallest number of power-powers that are greater than initialCapacity and is 2. We have introduced some Integer binary operations in the analysis packaging class (in) section, where there is a very similar code:
public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1);}
Algorithm descriptions are all in Hacker's Delight.
See the last constructor:
public ArrayDeque(Collection<? extends E> c) { allocateElements(c.size()); addAll(c);}
Similarly, allocateElements is called to allocate an array, and addAll is then called, while addAll is called cyclically. Let's look at the implementation of add.
Add from the end
The code for the add method is:
public boolean add(E e) { addLast(e); return true;}
The addLast code is:
public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity();}
Add the element to the tail and point the tail to the next position. If the queue is full, the doubleCapacity extension array is called. The next position of tail is: (tail + 1) & (elements. length-1). If it is the same as head, the queue is full.
To ensure that the index is in the correct range, and (elements. length-1) and you can get the next correct position because of elements. length is the power of 2, (elements. the last few digits of length-1 are all 1, whether positive or negative, and (elements. length-1) and get the expected next correct position.
For example, if elements. if the length is 8, then (elements. length-1) is 7, binary is 0111, for negative-1, and 7, the result is 7, for positive 8, and 7, the result is 0, to find the next correct position in the loop array.
This bit operation is a common operation in the loop array, which is highly efficient and will be seen in subsequent code.
DoubleCapacity doubles the array and the code is:
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 = (E[])a; head = 0; tail = n;}
Assign a new array a that doubles its length, copy the elements on the right of the head to the beginning of the new array, copy the elements on the left to the new array, and then reset the head and tail, set head to 0 and tail to n.
Let's look at an example. Suppose the original length is 8, the head and tail are 4, and now the array is expanded, as shown in the following figure:
Add is added at the end. Let's look at the code added in the header.
Add From Header
The code of the addFirst method is:
public void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity();}
To add a header, first let the head point to the forward position, and then assign the value to the head position. The first position of the head is: (head-1) & (elements. length-1 ). At the beginning, head is 0. If elements. length is 8, (head-1) & (elements. length-1) returns 7. For example, run the following code:
Deque<String> queue = new ArrayDeque<>(7);queue.addFirst("a");queue.addFirst("b");
After execution, the internal structure is shown in:
After the introduction, let's see how to delete it.
Delete from Header
The code of the removeFirst method is:
public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x;}
The pollFirst code is:
public E pollFirst() { int h = head; E result = elements[h]; // Element is null if deque empty if (result == null) return null; elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); return result;}
The code is relatively simple. Set the original header position to null, and the head to the next position. The next position is: (h + 1) & (elements. length-1 ).
Delete from the end
The code of the removeLast method is:
public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x;}
The pollLast code is:
public E pollLast() { int t = (tail - 1) & (elements.length - 1); E result = elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result;}
T is the last position, result is the last element, set this position to null, then modify the tail to the previous position, and finally return the original last element.
View Length
ArrayDeque does not have a separate field to maintain the length. The code of its size method is:
public int size() { return (tail - head) & (elements.length - 1);}
This method can be used to calculate the size.
Check whether a given element exists
The contains method code is:
public boolean contains(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; E x; while ( (x = elements[i]) != null) { if (o.equals(x)) return true; i = (i + 1) & mask; } return false;}
It means that the head is traversed and compared. The tail is not used during the loop, but the element ends when it is null. This is because in ArrayDeque, the valid element cannot be null.
ToArray Method
The code of the toArray method is:
public Object[] toArray() { return copyElements(new Object[size()]);}
The code for copyElements is:
private <T> T[] copyElements(T[] a) { if (head < tail) { System.arraycopy(elements, head, a, 0, size()); } else if (head > tail) { int headPortionLen = elements.length - head; System.arraycopy(elements, head, a, 0, headPortionLen); System.arraycopy(elements, 0, a, headPortionLen, tail); } return a;}
If the head is smaller than the tail, the size () is copied from the head. Otherwise, the copy logic is similar to that in the doubleCapacity method. First, the part from the head to the end is copied, copy the part from 0 to tail.
Principles
The above is the basic principle of ArrayDeque, which is a dynamically scalable loop array. It maintains the start and end of the array through the head and tail variables, and the length of the array is the power of 2, use efficient bit operations for various judgment and head and tail maintenance.
ArrayDeque features
ArrayDeque implements a dual-end queue and uses a circular array internally. This determines that it has the following features:
- It is highly efficient to add or delete elements at both ends. The memory allocation and array copy overhead required for dynamic expansion can be shared. Specifically, the efficiency of adding N elements is O (N ).
- The efficiency of searching and deleting elements is relatively low, which is O (N ).
- Unlike ArrayList and sorted list, the index location does not exist and cannot be operated based on the index location.
ArrayDeque and revoke list both implement the Deque interface. Which one should be used? If you only need the Deque interface to perform operations from both ends, generally, ArrayDeque is more efficient and should be used first. However, if you need to perform operations based on the index location at the same time, or you often need to insert or delete data in the middle, you should select the sequence list.
Summary
This section describes the usage and implementation principles of ArrayDeque. In terms of usage, ArrayDeque implements a dual-end queue interface, which can be used as a queue, stack, or dual-end queue. It is more efficient than a shard list, in principle, it uses a dynamically scalable array of loops and efficient bit operations.
Now, we have finished the introduction of the queue-related container classes. We have introduced the queue list, PriorityQueue, and ArrayDeque. PriorityQueue and ArrayDeque are both array-based, but they are not simple arrays. Through some special constraints, auxiliary members and algorithms, they can effectively solve some specific problems, this is probably an art of using data structures and algorithms in computer programs.
For Map and Set, we introduce two implementation methods: HashMap/HashSet and TreeMap/TreeSet, let's introduce the two implementations. What are their features?
----------------
For more information, see the latest article. Please pay attention to the Public Account "lauma says programming" (scan the QR code below), from entry to advanced, ma and you explore the essence of Java programming and computer technology. Retain All copyrights with original intent.