0%

数据结构学习笔记 - 04 队列

介绍

队列的的操作也是数组的子集,不同的是,队列只能从一端(队尾)添加元素,只能从另一端(队首)取出元素。

队列实现

将队列定义为一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public interface Queue<E> {

/**
* 入队。
*
* @param e 入队的元素。
*/
void enqueue(E e);

/**
* 出队。
*
* @return 出队的元素。
*/
E dequeue();

/**
* 获得队首元素。
*
* @return 队首的元素。
*/
E getFront();

/**
* 获得队列中元素的个数。
*
* @return 元素的个数。
*/
int getSize();

/**
* 查看队列是否为空。
*
* @return 队列是否为空。
*/
boolean isEmpty();
}

使用动态数组实现队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* ArrayQueue
*
* 类似栈的实现,这里也使用动态数组对队列进行一个实现。
*
* 这个队列中的五个方法复杂度:
* void enqueue(E) O(1) 均摊
* E dequeue() O(n)
* E getFront() O(1)
* int getSize() O(1)
* boolean isEmpty() O(1)
*
* 可以看到,数组实现的队列最大问题就是出队的复杂度是 O(n)
*/
public class ArrayQueue<E> implements Queue<E> {

private Array<E> array;

public ArrayQueue(int capacity) {
array = new Array<>(capacity);
}

public ArrayQueue() {
array = new Array<>();
}

/**
* 队列的元素个数。
*/
@Override
public int getSize() {
return array.getSize();
}

/**
* 队列元素是否为空。
*/
@Override
public boolean isEmpty() {
return array.isEmpty();
}

/**
* 查看实现这个队列的这个动态数组当前的容积。
*/
public int getCapacity() {
return array.getCapacity();
}

/**
* 入队。
*/
@Override
public void enqueue(E e) {
array.addLast(e);
}

/**
* 出队。
*/
@Override
public E dequeue() {
return array.removeFirst();
}

/**
* 获取队首元素。
*/
@Override
public E getFront() {
return array.getFirst();
}

@Override
public String toString() {

StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("front [");

for (int i = 0; i < array.getSize(); i++) {
res.append(array.get(i));
if (i != array.getSize() - 1) {
res.append(", ");
}
}

res.append("] tail");

return res.toString();
}
}

使用循环队列解决数组队列出队复杂度高的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* LoopQueue
* 数组队列的问题在于,出队的时间复杂度过高,导致如果队列元素特别多的情况下,出队就比较耗时。
* 循环队列可以解决这一问题。
*
* 这个队列中的五个方法复杂度:
* void enqueue(E) O(1) 均摊
* E dequeue() O(1) 均摊
* E getFront() O(1)
* int getSize() O(1)
* boolean isEmpty() O(1)
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front, tail;
private int size;

@SuppressWarnings("unchecked")
public LoopQueue(int capacity) {

// 创建的数组容量比用户期望的多一个,因为循环队列中,我们会有意识的浪费掉一个空间。
// 因为我们通常认为 front == tail 的时候是队列为空,对于循环队列,我们如果不让出一个空间,那么这个表达式也有可能是队列元素占满的情况。
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}

public LoopQueue() {
this(10);
}

/**
* 循环队列创建的数组中,我们会有意识地浪费掉一个空间。
*
* @return
*/
public int getCapacity() {
return data.length - 1;
}

/**
* 循环队列使用 front == tail 来判断元素为空,这也是为什么创建数组需要多一个空间。
*/
@Override
public boolean isEmpty() {
return front == tail;
}

/**
* 获得队列元素的个数。
*/
@Override
public int getSize() {
return size;
}

/**
* 入队。
*/
@Override
public void enqueue(E e) {

// 判断队列是否满了
if ((tail + 1) % data.length == front) {
resize(getCapacity() * 2);
}

data[tail] = e;
tail = (tail + 1) % data.length;
size++;
}

/**
* 出队。
*/
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size--;

// 缩容。
if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}

return ret;
}

@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalArgumentException("Queue is empty.");
}
return data[front];
}

@SuppressWarnings("unchecked")
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity + 1];

// 第一种遍历循环队列的所有元素的方式。
for (int i = 0; i < size; i++) {
// 当扩容或者缩容的时候,原本 front 不为 0 的队列被重新规整为 front 为 0 的队列了
newData[i] = data[(i + front) % data.length];
}

data = newData;
front = 0;
tail = size;
}

/**
* 打印该类的显示字符串。
*/
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("QUeue: size = %d, capacity = %d\n", size, getCapacity()));
res.append("front [");

// 第二种遍历循环队列中所有元素的方式。
for (int i = front; i != tail; i = (i + 1) % data.length) {
res.append(data[i]);
if ((i + 1) % data.length != tail) {
res.append(", ");
}
}
res.append("] tail");
return res.toString();
}
}

使用链表实现队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* LinkedListQueue
*
* 这里我们使用链表实现队列。
* 由于队列需要从一端添加,另一端删除,对于链表来说,我们需要给它先添加一个 tail 标记,类似 head 标记那样,这样我们从末尾添加元素就也是 O(1)
* 级别的了。
* 但是有一个问题是,即使添加了 tail 标记,在从尾部删除一个元素的时候,我们无法快速找到 tail 之前的那个节点。
*
* 所以合理的设计队列的方式就是,从尾部只负责添加元素,在链表的头部负责删除元素,也就是出队。
*
* 这里我们设计的链表就不使用 dummyHead 了,但是要注意链表为空的情况。
*/
public class LinkedListQueue<E> implements Queue<E> {

private class Node {
public E e;
public Node next;

public Node(E e, Node next) {
this.e = e;
this.next = next;
}

public Node(E e) {
this(e, null);
}

// public Node() {
// this(null);
// }

@Override
public String toString() {
return e.toString();
}
}

private Node head, tail;
private int size;

public LinkedListQueue() {
head = null;
tail = null;
size = 0;
}

@Override
public int getSize() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
public void enqueue(E e) {
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("Queue is empty.");
}

return head.e;
}

@Override
public String toString() {

StringBuilder res = new StringBuilder();
res.append("Queue: front ");

Node cur = head;
while (cur != null) {
res.append(cur + "->");
cur = cur.next;
}

res.append("NULL tail");

return res.toString();
}
}