Collection in Java (3) inherits the Queue interface of Collection

Collection in Java (3) inherits the Queue interface of Collection

1. Introduction to Queue

The Queue interface inherits from the Collection interface and is a queue data structure defined in Java. The elements are ordered (sorted in order of insertion) and the first-in first-out (FIFO) principle. Random access to data is not supported, new elements are inserted at the end of the queue, and the operation of accessing elements (poll) returns the elements at the head of the queue. Generally, queues do not allow random access to elements in the queue.

Queue: It is a data structure in the computer. The data stored in it has the characteristics of "First In First Out (FIFO, First In First Out)" . The new element is inserted at the end of the queue and the element is accessed. The element at the head of the queue will be returned. Generally, queues do not allow random access to elements in the queue.

In Java, the queue is divided into two forms, one is a single queue, one is a circular queue;

  Usually, arrays are used to implement queues, assuming that the length of the array is 5, that is, the length of the queue is 5;

  (1) Single queue:

1. Create an empty array of length 5 and define two properties front and rear, which represent the head pointer and the tail pointer, respectively.

2. Insert data into the array

 

3. Remove the head element: element 1, element 2

 

4. Insert data into the array again, and at this time, real points to a non-existent subscript

 

At this time, the array will appear "false overflow" phenomenon, the tail pointer points to a non-existent subscript, if you want to solve this situation, there are generally two methods:

1. Unlimited expansion of array capacity;

2. Use a circular queue.

(2) Circular queue

When the tail pointer points to a non-existing subscript, that is, exceeds the size of the array, at this time we determine whether there is free space at the head of the array, and if so, point the tail pointer to the free space at the head, as shown below:

 

The circular queue is to connect the head and tail of the single queue to form a circle, so that there will be no subscript overflow (distruptor implementation). 

Second, the Queue class diagram

1. The Queue interface inherits from the Collection interface;

2. The Queue interface has Deque sub-interface and AbstractQueue abstract class respectively;

3. Deque sub-interfaces have LinkedList class and ArrayDeque class respectively;

4. The AbstractQueue abstract class has PriorityQueue implementation class

3. Deque (Double-ended queue) interface

The Deque interface is a sub-interface of the Queue interface. It creates a double-ended queue structure with greater flexibility. It can iterate forward or backward, and can insert or delete linear collections of elements at the head and tail of the queue. Its two main implementation classes are ArrayDeque and LinkedList .

The Deque interface supports double-ended queues with fixed capacity and double-ended queues with variable capacity. In general, the capacity of double-ended queues is not fixed.

(1) Features

1. Insert, delete, and get operations support two forms: fast failure and return null or true / false;

2. It has both FIFO (First in, First out) and LIFO (Last in, First out) features.

3. It is not recommended to insert null, because null as the return value indicates that the queue is empty;

4. Undefined element-based equals and hashCode;

5. Does not support index access to elements;

6. Deque is not only a double-ended queue, but can also be used as a stack, because this class defines pop (outbound), push (push) and other methods.

(2) Relationship between Deque interface, Queue interface and Stack

As can be seen from the above description, Deque can be used not only as a double-ended queue, but also as a stack.

1. When Deque is used as a double-ended queue, the relationship between the Deque interface and the Queue interface

When Deque is used as a Queue (FIFO), the added element is added to the tail of the queue, and the deleted element is the head element. The methods inherited from the Queue interface correspond to the methods of Deque as shown in the figure:

2. When Deque is used as a stack, the relationship between the Deque interface and the Stack

When Deque is used as a stack (LIFO). At this time, the stacking and unstacking elements are performed at the head of the double-ended queue. The method corresponding to Stack in Deque is shown in the figure:


Note: Because Stack is relatively old and the function implementation is very unfriendly, it is basically not suitable for program development now, so you can choose the Deque interface instead of Stack for stack operation. 

Four, ArrayDeque implementation class

There are two implementation classes under the Deque interface, namely ArrayDeque and LinkedList. LinkedList will not be described in this section, this section mainly talks about ArrayDeque.

(1) ArrayDeque: A linear double-ended queue based on circular array, variable size, and null is not allowed.

1. The bottom layer is realized through an array. The size of the array is 16 by default. You can specify the length or not. The capacity of the array is dynamically expanded according to the number of added elements. In order to satisfy that you can insert and delete elements at the same time, the array must be a circular array, that is, each point in the array can be regarded as a starting point or an end point.

2. ArrayDeque is not thread-safe. In a multi-threaded environment, manual synchronization is required; in addition, ArrayDeque does not allow the insertion of null elements.

3. Since ArrayDeque implements Deque based on head and tail pointers, it cannot directly access the first and last elements. If you want to traverse the elements, you need to use Iterator iterator, you can use positive and negative iteration to traverse the elements.

4. ArrayDeque is generally superior to linked list queues / double-ended queues. A limited amount of garbage is generated (old arrays will be discarded for expansion). It is recommended to use deque, ArrayDeque takes precedence.

(B), ArrayDeque operation diagram

Assume that the length of the array is 6, that is , the length of the ArrayDeque queue is 6;


 

It can be seen from the above figure: front always points to the position of the first valid element in the array, and rear always points to the first spatial position where the element can be inserted, so front must not be equal to 0, and will not always compare Rear is big, and rear is not always smaller than front.

(3) In ArrayDeque, the realization of circular array

ArrayDeque maintains two properties, which point to the head pointer and the tail pointer:

 transient int head; // Point to head pointer 

 transient int tail; // 指向尾指针 

假定数组的长度为10,也就是ArrayDeque队列的长度为10;

1、ArrayDeque刚创建时;

2、当向尾部插入时,直接在tail下标的位置插入元素,所以tail下标 - 1;


3、当从头部插入时,head下标 - 1,然后插入元素,tail下标为当前数组末尾元素的下标 + 1;

通过上面的步骤可以知道,将ArrayDeque看出成是一个首尾相接的圆形数组更好理解循环数组的含义。 

通过addFrist(E e)代码,看看ArrayQueue是如何实现的:

1 public void addFirst(E e) {
2     if (e == null)
3         throw new NullPointerException();
4 
5     elements[head = (head - 1) & (elements.length - 1)] = e;
6     if (head == tail)
7         doubleCapacity();
8 }        
    • 当加入元素时,先看是否为空(ArrayDeque不可以存取null元素,因为系统根据某个位置是否为null来判断元素的存在)。然后head-1,插入元素。
    • head = (head - 1) & (elements.length - 1)很好的解决了下标越界的问题。这段代码相当于取模,同时解决了head为负值的情况。因为elements.length必需是2的指数倍(代码中有具体操作),elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用。如果head - 1为负数,其实只可能是-1,当为-1时,和elements.length - 1进行与操作,这时结果为elements.length - 1。其他情况则不变,等于它本身。
    • 当插入元素后,在进行判断是否还有余量。因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。

(四)、扩容函数doubleCapacity()

扩容函数doubleCapacity()的逻辑是:申请一个更大容量的数组,将原数组原样复制到新数组中。


从上图可以看出,复制分为两次进行:先复制head右边的元素,然后再复制head左边的元素。

 1 private void doubleCapacity() {
 2     assert head == tail;
 3     int p = head;
 4     int n = elements.length;
 5     int r = n - p; // head右边元素的个数
 6     int newCapacity = n << 1;//原空间的2倍
 7     if (newCapacity < 0)
 8         throw new IllegalStateException("Sorry, deque too big");
 9     Object[] a = new Object[newCapacity];
10     System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
11     System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
12     elements = (E[])a;
13     head = 0;
14     tail = n;
15 }

(五)、小结

通过上面描述,我们便理解了ArrayDeque循环数组添加以及扩容的过程,另外需要注意的是:ArrayDeque不是线程安全的。 当作为栈使用时,性能比Stack好;当作为队列使用时,性能比LinkedList好。

五、PriorityQueue实现类

(一)、PriorityQueue:底层基于数组实现的堆结构的优先队列。

 PriorityQueue是AbstractQueue的子类,AbstractQueue又实现了Queue接口,所以PriorityQueue具有Queue接口的优先队列。

 优先队列与普通队列不同,普通队列遵循“FIFO”的特性,获取元素时根据元素的插入顺序获取,优先队列获取元素时根据元素的优先级,获取优先级最高的数据。

(二)、PriorityQueue的排序方式

PriorityQueue保存队列元素时不是按照插入队列顺序进行排序,而是按照插入元素的大小进行排序的。因此当调用peek()、pop()方法时,不是取出最先插入的元素,而是取出队列当中最小元素。

1、排序的方式

PriorityQueue队列当中的元素是可以默认自然排序(数值型元素默认是最小的在队列头部,字符串则按字典序排序),或者通过Comparator(比较器)在队列实例化指定排序方式。

注意:当PriorityQueue中没有指定Comparator时,加入PriorityQueue的元素必须实现了Comparable接口(即元素是可比较的),否则会导致 ClassCastException。

(三)、PriorityQueue的本质

PriorityQueue本质是一个动态数组,默认实现由三种构造方法:

  1.  public PriorityQueue() 调用无参构造方法时,使用默认的初始容量(DEFAULT_INITIAL_CAPACITY=11)来创建PriorityQueue,并根据其自然顺序排序其中的元素(排序方式使用的是元素中实现的Comparable接口);
  2.  public PriorityQueue(int initialCapacity) 调用指定容量构造方法时,使用initialCapacity定义初始容量创建PriorityQueue,并根据其自然顺序排序其中的元素(排序方式使用的是元素中实现的Comparable接口);
  3.  PriorityQueue(int initialCapacity,Comparator<? super E> comparator) 当使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器comparator来排序其元素。

从上面的三个构造函数可以得出:PriorityQueue内部维护了一个动态数组,

除此之外,还要注意:

    • PriorityQueue不是线程安全的。如果多个线程中的任意线程从结构上修改了列表, 则这些线程不应同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。
    • 不允许插入 null 元素。
    • PriorityQueue实现插入方法(offer、poll、remove() 和 add 方法) 的时间复杂度是O(log(n)) ;实现 remove(Object) 和 contains(Object) 方法的时间复杂度是O(n) ;实现检索方法(peek、element 和 size)的时间复杂度是O(1)。所以在遍历时,若不需要删除元素,则以peek的方式遍历每个元素。
    • 方法iterator()中提供的迭代器并不保证以有序的方式遍历优PriorityQueue中的元素。

(四)、自然排序和Comparator比较器

1、Java中的两种比较器:Conparator和Comparable

Comparable和Comparator接口都是为了对类进行比较,众所周知,诸如int,double等基本数据类型,Java可以对他们进行比较,而对于对象即类的比较,需要人工定义比较用到的字段比较逻辑。可以把Comparable理解为内部比较器,而Comparator是外部比较器

(1)、Comparable接口:内部比较器,实现了Comparable接口的类需要实现compareTo(T o)方法,传入一个外部参数进行比对;

当一个对象调用该compareTo(T o)方法与另一个对象比较时,例如o1.compareTo(o2)

  • 如果该方法返回0,则表明两个对象相等;
  • 如果该方法返回一个整数,则表明o1大于o2;
  • 如果该方法返回一个负整数,则表明o1小于o2。

(2)、Conparator接口:外部比较器,实现了Comparator接口的方法需要实现compare(T o1,T o2)方法,对外部传入的两个类进行比较,从而让外部方法在比较时调用;

该compare(T o1,T o2)方法用于比较o1,o2的大小:

  • 如果该方法返回正整数,则表明o1大于o2;
  • 如果该方法返回0,则表明o1等于o2;
  • 如果该方法返回负整数,则表明o1小于o2。

2、自然排序

自然排序是调用元素所属类的compareTo(T o)方法来比较元素之间的大小关系,然后将集合元素按升序排列,即把通过compareTo(T o)方法比较后比较大的的往后排。这种方式就是自然排序。 

Guess you like

Origin www.cnblogs.com/lingq/p/12729471.html