2.4 队列

1) 概述

计算机科学中,queue 是以顺序的方式维护的一组数据集合,在一端添加数据,从另一端移除数据。习惯来说,添加的一端称为,移除的一端称为,就如同生活中的排队买商品

In computer science, a queue is a collection of entities that are maintained in a sequence and can be modified by the addition of entities at one end of the sequence and the removal of entities from the other end of the sequence

先定义一个简化的队列接口

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
public interface Queue<E> {

/**
* 向队列尾插入值
* @param value 待插入值
* @return 插入成功返回 true, 插入失败返回 false
*/
boolean offer(E value);

/**
* 从对列头获取值, 并移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E poll();

/**
* 从对列头获取值, 不移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E peek();

/**
* 检查队列是否为空
* @return 空返回 true, 否则返回 false
*/
boolean isEmpty();

/**
* 检查队列是否已满
* @return 满返回 true, 否则返回 false
*/
boolean isFull();
}

2) 链表实现

下面以单向环形带哨兵链表方式来实现队列

image-20250326154818811.png

代码

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
public class LinkedListQueue<E>
implements Queue<E>, Iterable<E> {

private static class Node<E> {
E value;
Node<E> next;

public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}

private Node<E> head = new Node<>(null, null);
private Node<E> tail = head;
private int size = 0;
private int capacity = Integer.MAX_VALUE;

{
tail.next = head;
}

public LinkedListQueue() {
}

public LinkedListQueue(int capacity) {
this.capacity = capacity;
}

@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
Node<E> added = new Node<>(value, head);
tail.next = added;
tail = added;
size++;
return true;
}

@Override
public E poll() {
if (isEmpty()) {
return null;
}
Node<E> first = head.next;
head.next = first.next;
if (first == tail) {
tail = head;
}
size--;
return first.value;
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return head.next.value;
}

@Override
public boolean isEmpty() {
return head == tail;
}

@Override
public boolean isFull() {
return size == capacity;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = head.next;
@Override
public boolean hasNext() {
return p != head;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
}

3) 环形数组实现

好处

  1. 对比普通数组,起点和终点更为自由,不用考虑数据移动
  2. “环”意味着不会存在【越界】问题
  3. 数组性能更佳
  4. 环形数组比较适合实现有界队列、RingBuffer 等

image-20250326155001369.png

下标计算

例如,数组长度是 5,当前位置是 3 ,向前走 2 步,此时下标为 $(3 + 2)%5 = 0$

image-20250326155106849.png

  • cur 当前指针位置
  • step 前进步数
  • length 数组长度

注意:

  • 如果 step = 1,也就是一次走一步,可以在 >= length 时重置为 0 即可

判断空

image-20250326155125221.png

判断满

image-20250326155142950.png

满之后的策略可以根据业务需求决定

  • 例如我们要实现的环形队列,满之后就拒绝入队

代码

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
public class ArrayQueue<E> implements Queue<E>, Iterable<E>{

private int head = 0;
private int tail = 0;
private final E[] array;
private final int length;

@SuppressWarnings("all")
public ArrayQueue(int capacity) {
length = capacity + 1;
array = (E[]) new Object[length];
}

@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % length;
return true;
}

@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % length;
return value;
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}

@Override
public boolean isEmpty() {
return tail == head;
}

@Override
public boolean isFull() {
return (tail + 1) % length == head;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}

@Override
public E next() {
E value = array[p];
p = (p + 1) % array.length;
return value;
}
};
}
}

判断空、满方法2

引入 size

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
public class ArrayQueue2<E> implements Queue<E>, Iterable<E> {

private int head = 0;
private int tail = 0;
private final E[] array;
private final int capacity;
private int size = 0;

@SuppressWarnings("all")
public ArrayQueue2(int capacity) {
this.capacity = capacity;
array = (E[]) new Object[capacity];
}

@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % capacity;
size++;
return true;
}

@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % capacity;
size--;
return value;
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}

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

@Override
public boolean isFull() {
return size == capacity;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;

@Override
public boolean hasNext() {
return p != tail;
}

@Override
public E next() {
E value = array[p];
p = (p + 1) % capacity;
return value;
}
};
}
}

判断空、满方法3

  • head 和 tail 不断递增,用到索引时,再用它们进行计算,两个问题

    • 如何保证 head 和 tail 自增超过正整数最大值的正确性

    • 如何让取模运算性能更高

  • 答案:让 capacity 为 2 的幂

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
public class ArrayQueue3<E> implements Queue<E>, Iterable<E> {

private int head = 0;
private int tail = 0;
private final E[] array;
private final int capacity;

@SuppressWarnings("all")
public ArrayQueue3(int capacity) {
if ((capacity & capacity - 1) != 0) {
throw new IllegalArgumentException("capacity 必须为 2 的幂");
}
this.capacity = capacity;
array = (E[]) new Object[this.capacity];
}

@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail & capacity - 1] = value;
tail++;
return true;
}

@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head & capacity - 1];
head++;
return value;
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head & capacity - 1];
}

@Override
public boolean isEmpty() {
return tail - head == 0;
}

@Override
public boolean isFull() {
return tail - head == capacity;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;

@Override
public boolean hasNext() {
return p != tail;
}

@Override
public E next() {
E value = array[p & capacity - 1];
p++;
return value;
}
};
}
}

为什么循环队列实现中要求 capacity 必须为2的幂,这涉及到两个关键问题的优化:索引越界处理取模运算性能。我们通过一个超市排队叫号系统的类比来逐步理解其原理。

🌰 生活场景类比

假设某超市有 8个服务窗口(对应数组长度 capacity=8),顾客取号后按顺序到窗口办理业务:

  • 初始状态:叫号器 tail=0,当前服务号 head=0,窗口空闲。
  • 顾客取号:每来一个顾客,tail 递增(如 tail=1,2,3...)。
  • 业务办理:每完成一个顾客,head 递增(如 head=1,2,3...)。

当叫号器达到最大值(如 Integer.MAX_VALUE)后继续增加时,如何保证窗口序号正确?如何快速计算窗口位置?

🔑 关键问题与解决方案

1. 索引越界处理

headtail 持续递增到超过 Integer.MAX_VALUE 时,会溢出变成负数(如 Integer.MAX_VALUE + 1 = -2147483648)。此时直接取模运算 tail % capacity 会产生负数索引,导致错误。

解决方案: 利用 按位与运算 替代取模运算,要求 capacity 必须是2的幂:

1
2
3
Java// 计算窗口索引的两种方式
int index = tail % 8; // 传统取模(可能产生负数)
int index = tail & (8 - 1); // 按位与(高效且安全)

为什么必须是2的幂?capacity 是2的幂时,capacity-1 的二进制形式为全1。例如:

  • capacity=88-1=70111
  • capacity=1616-1=151111

此时 tail & (capacity-1) 等效于 tail % capacity,但避免了负数问题:

1
2
3
Java// 示例:tail = -2147483648 (假设 capacity=8)
int index1 = (-2147483648) % 8; // 结果为 0(Java取模特性)
int index2 = (-2147483648) & (8-1); // 结果为 0

2. 取模运算性能优化

传统取模运算 % 需要除法操作,而按位与运算 & 是位运算,速度更快。当 capacity 是2的幂时,tail & (capacity-1) 的性能优势明显。

性能对比

1
2
3
4
5
6
7
8
9
Java// 传统取模(较慢)
for (int i = 0; i < 1e9; i++) {
int mod = i % 8;
}

// 按位与(更快)
for (int i = 0; i < 1e9; i++) {
int mod = i & 7;
}

📐 数学证明与特性

特性1:按位与等效取模的条件

capacity 是2的幂(即 capacity = 2^n),则: 任意整数 k⇒k & (2n−1)=k % 2n任意整数 kk & (2n−1)=k % 2n 证明

  • 2^n - 1 的二进制是 n 个1(如 8=2^37=0111)。
  • k & (2^n - 1) 会保留 k 的最低 n 位,等效于对 2^n 取模。

特性2:溢出安全

headtail 溢出为负数时,按位与运算仍能得到正确索引:

1
2
3
Java// 示例:capacity=8
int tail = Integer.MAX_VALUE + 1; // -2147483648
int index = tail & 7; // 0(正确对应窗口0)

🛠️ 代码实现分析

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
Javapublic class ArrayQueue3<E> implements Queue<E>, Iterable<E> {
private int head = 0;
private int tail = 0;
private final E[] array;
private final int capacity;

public ArrayQueue3(int capacity) {
// 强制要求 capacity 是2的幂
if ((capacity & capacity - 1) != 0) {
throw new IllegalArgumentException("capacity 必须为2的幂");
}
this.capacity = capacity;
array = (E[]) new Object[this.capacity];
}

@Override
public boolean offer(E value) {
if (isFull()) return false;
array[tail & (capacity - 1)] = value; // 按位与计算索引
tail++;
return true;
}

@Override
public E poll() {
if (isEmpty()) return null;
E value = array[head & (capacity - 1)]; // 按位与计算索引
head++;
return value;
}

// 其他方法(isEmpty/isFull等)与普通循环队列相同
}

🌟 总结

  • 为何要求 capacity 是2的幂: 保证按位与运算 tail & (capacity-1) 等效于取模运算,且避免负数索引问题。
  • 为何要使用按位与: 提升索引计算性能,解决传统取模运算在溢出时的潜在错误。
  • 溢出安全性: 即使 headtail 超过 Integer.MAX_VALUE,按位与运算仍能正确计算索引。

习题

E01. 二叉树层序遍历-Leetcode 102

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
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) {
return result;
}//二叉树没数据
LinkedListQueue<TreeNode> queue = new LinkedListQueue<>();
queue.offer(root);
int c1 = 1; // 本层节点个数
while (!queue.isEmpty()) {
int c2 = 0; // 下层节点个数
List<Integer> level = new ArrayList<>();// 保存每一层的结点结果
for (int i = 0; i < c1; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
c2++;
}
if (node.right != null) {
queue.offer(node.right);
c2++;
}
}
c1 = c2;
result.add(level);
}
return result;
}
/*
1
/ \
2 3
/ \ / \
4 5 6 7

头【】 尾
1 2 3 4 5 6 7

*/

// 自定义队列
static class LinkedListQueue<E> {

private static class Node<E> {
E value;
Node<E> next;

public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}

private final Node<E> head = new Node<>(null, null);
private Node<E> tail = head;
int size = 0;
private int capacity = Integer.MAX_VALUE;

{
tail.next = head;
}

public LinkedListQueue() {
}

public LinkedListQueue(int capacity) {
this.capacity = capacity;
}

public boolean offer(E value) {
if (isFull()) {
return false;
}
Node<E> added = new Node<>(value, head);
tail.next = added;
tail = added;
size++;
return true;
}

public E poll() {
if (isEmpty()) {
return null;
}
Node<E> first = head.next;
head.next = first.next;
if (first == tail) {
tail = head;
}
size--;
return first.value;
}

public E peek() {
if (isEmpty()) {
return null;
}
return head.next.value;
}

public boolean isEmpty() {
return head == tail;
}

public boolean isFull() {
return size == capacity;
}
}
}
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
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int count_of_level = 1;
while (!queue.isEmpty()) {
int count_of_nextLevel = 0;
List<Integer> level = new ArrayList<>(count_of_level);
for (int i = 0; i < count_of_level; i++) {
TreeNode node = queue.poll();
level.add(node.val);
// 修正:使用当前节点node的左右子节点
if (node.left != null) {
queue.offer(node.left);
count_of_nextLevel++;
}
if (node.right != null) {
queue.offer(node.right); // 修正:添加右子节点
count_of_nextLevel++;
}
}
count_of_level = count_of_nextLevel;
result.add(level);
}
return result;
}
}

Ex1. 设计队列-Leetcode 622

由于与课堂例题差别不大,这里只给出参考解答

基于链表的实现

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
public class Ex1Leetcode622 {

private static class Node {
int value;
Node next;
Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
private final Node head = new Node(-1, null);
private Node tail = head;
private int size = 0;
private int capacity = 0;

{
tail.next = head;
}

public Ex1Leetcode622(int capacity) {
this.capacity = capacity;
}

public boolean enQueue(int value) {
if(isFull()) {
return false;
}
Node added = new Node(value, head);
tail.next = added;
tail = added;
size++;
return true;
}

public boolean deQueue() {
if(isEmpty()) {
return false;
}
Node first = head.next;
head.next = first.next;
if (first == tail) {
tail = head;
}
size--;
return true;
}

public int Front() {
if(isEmpty()) {
return -1;
}
return head.next.value;
}

public int Rear() {
if(isEmpty()) {
return -1;
}
return tail.value;
}

public boolean isEmpty() {
return head == tail;
}

public boolean isFull() {
return size == capacity;
}
}

注意:

  • Leetcode 的实现里 deQueue(出队)返回值是布尔值,并不会返回队头元素
  • 它期望用法是先用 Front 返回对头元素,再 deQueue 出队

622. 设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

示例:

1
2
3
4
5
6
7
8
9
10
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1); // 返回 true
circularQueue.enQueue(2); // 返回 true
circularQueue.enQueue(3); // 返回 true
circularQueue.enQueue(4); // 返回 false,队列已满
circularQueue.Rear(); // 返回 3
circularQueue.isFull(); // 返回 true
circularQueue.deQueue(); // 返回 true
circularQueue.enQueue(4); // 返回 true
circularQueue.Rear(); // 返回 4

提示:

  • 所有的值都在 0 至 1000 的范围内;
  • 操作数将在 1 至 1000 的范围内;
  • 请不要使用内置的队列库。

这道题说「循环」的意思是要求我们在数组里实现。使用链表的实现,创建结点和删除结点都是动态的,也就不存在需要循环利用的问题了。

在数组里的操作,我们参考「动态数组」的实现,是为了让每一步的操作复杂度都最低。

「MyCircularQueue(k): 构造器,设置队列长度为 k 」,这句话说明:题目 不要求我们实现动态扩容与动态缩容。

需要注意的地方:

定义循环变量 front 和 rear 。一直保持这个定义,到底是先赋值还是先移动指针就很容易想清楚了。
front:指向队列头部第 1 个有效数据的位置;
rear:指向队列尾部(即最后 1 个有效数据)的下一个位置,即下一个从队尾入队元素的位置。

(说明:这个定义是依据“动态数组”的定义模仿而来。)

为了避免「队列为空」和「队列为满」的判别条件冲突,我们有意浪费了一个位置。
浪费一个位置是指:循环数组中任何时刻一定至少有一个位置不存放有效元素。

判别队列为空的条件是:front == rear;
判别队列为满的条件是:(rear + 1) % capacity == front;。可以这样理解,当 rear 循环到数组的前面,要从后面追上 front,还差一格的时候,判定队列为满。
因为有循环的出现,要特别注意处理数组下标可能越界的情况。指针后移的时候,下标 +1,并且为了防止数组下标越界要取模。

来源:liweiwei1419

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
class MyCircularQueue {

private int front;
private int rear;
private int capacity;
private int[] arr;

public MyCircularQueue(int k) {
capacity = k+1;
arr = new int[capacity];
front = 0;
rear = 0;

}

public boolean enQueue(int value) {
if(isFull()){
return false;
}
arr[rear] = value;
rear = (rear+1)%capacity;
return true;
}

public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front+1)%capacity;
return true;
}

public int Front() {
if(isEmpty()){
return -1;
}
return arr[front];
}

public int Rear() {
if(isEmpty()){
return -1;
}
return arr[(rear-1+capacity) % capacity];
}

public boolean isEmpty() {
return front==rear;

}

public boolean isFull() {
return (rear+1)%capacity == front;
}
}

/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* boolean param_1 = obj.enQueue(value);
* boolean param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* boolean param_5 = obj.isEmpty();
* boolean param_6 = obj.isFull();
*/

641. 设计循环双端队列

设计实现双端队列。

实现 MyCircularDeque 类:

  • MyCircularDeque(int k) :构造函数,双端队列最大为 k
  • boolean insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true ,否则返回 false
  • boolean insertLast() :将一个元素添加到双端队列尾部。如果操作成功返回 true ,否则返回 false
  • boolean deleteFront() :从双端队列头部删除一个元素。 如果操作成功返回 true ,否则返回 false
  • boolean deleteLast() :从双端队列尾部删除一个元素。如果操作成功返回 true ,否则返回 false
  • int getFront() ):从双端队列头部获得一个元素。如果双端队列为空,返回 -1
  • int getRear() :获得双端队列的最后一个元素。 如果双端队列为空,返回 -1
  • boolean isEmpty() :若双端队列为空,则返回 true ,否则返回 false
  • boolean isFull() :若双端队列满了,则返回 true ,否则返回 false

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出
[null, true, true, true, false, 2, true, true, true, 4]

解释
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已经满了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4

提示:

  • 1 <= k <= 1000
  • 0 <= value <= 1000
  • insertFront, insertLast, deleteFront, deleteLast, getFront, getRear, isEmpty, isFull 调用次数不大于 2000
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
class MyCircularDeque {
private int capacity;
private int[] arr;
private int front;
private int rear;

public MyCircularDeque(int k) {
capacity = k+1;
arr = new int[capacity];
// 头部指向第 1 个存放元素的位置
// 插入时,先减,再赋值
// 删除时,索引 +1(注意取模)
front = 0;
// 尾部指向下一个插入元素的位置
// 插入时,先赋值,再加
// 删除时,索引 -1(注意取模)
rear = 0;

}

public boolean insertFront(int value) {
if(isFull()){
return false;
}
front = (front-1+capacity) % capacity;
arr[front] = value;
return true;
}

public boolean insertLast(int value) {
if(isFull()){
return false;
}
arr[rear] = value;
rear=(rear+1)%capacity;
return true;
}

public boolean deleteFront() {
if(isEmpty()){
return false;
}
front = (front+1)%capacity;
return true;
}



public boolean deleteLast() {
if(isEmpty()){
return false;
}
rear = (rear-1+capacity)%capacity;
return true;
}

public int getFront() {
if(isEmpty()){
return -1;
}
return arr[front];
}

public int getRear() {
if(isEmpty()){
return -1;
}
//当rear 为 0 的时候防止越界
return arr[(rear-1+capacity) % capacity];
}

public boolean isEmpty() {
return front == rear;
}

public boolean isFull() {
return (rear+1)%capacity == front;
}
}

/**
* Your MyCircularDeque object will be instantiated and called as such:
* MyCircularDeque obj = new MyCircularDeque(k);
* boolean param_1 = obj.insertFront(value);
* boolean param_2 = obj.insertLast(value);
* boolean param_3 = obj.deleteFront();
* boolean param_4 = obj.deleteLast();
* int param_5 = obj.getFront();
* int param_6 = obj.getRear();
* boolean param_7 = obj.isEmpty();
* boolean param_8 = obj.isFull();
*/

2.5 栈

1) 概述

计算机科学中,stack 是一种线性的数据结构,只能在其一端添加数据和移除数据。习惯来说,这一端称之为栈顶,另一端不能操作数据的称之为栈底,就如同生活中的一摞书

先提供一个栈接口

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
public interface Stack<E> {
/**
* 向栈顶压入元素
* @param value 待压入值
* @return 压入成功返回 true, 否则返回 false
*/
boolean push(E value);

/**
* 从栈顶弹出元素
* @return 栈非空返回栈顶元素, 栈为空返回 null
*/
E pop();

/**
* 返回栈顶元素, 不弹出
* @return 栈非空返回栈顶元素, 栈为空返回 null
*/
E peek();

/**
* 判断栈是否为空
* @return 空返回 true, 否则返回 false
*/
boolean isEmpty();

/**
* 判断栈是否已满
* @return 满返回 true, 否则返回 false
*/
boolean isFull();
}

可视化示例

  • 初始状态head -> null
  • push(A)head -> A -> null
  • push(B)head -> B -> A -> null
  • pop()head -> A -> null
  • head 的物理位置从未改变,只是 next 指针在变化
  1. 数组实现
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
public class ArrayStack<E> implements Stack<E>, Iterable<E>{
private final E[] array;
private int top = 0;

@SuppressWarnings("all")
public ArrayStack(int capacity) {
this.array = (E[]) new Object[capacity];
}

@Override
public boolean push(E value) {
if (isFull()) {
return false;
}
array[top++] = value;
return true;
}

@Override
public E pop() {
if (isEmpty()) {
return null;
}
return array[--top];
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[top-1];
}

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

@Override
public boolean isFull() {
return top == array.length;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = top;
@Override
public boolean hasNext() {
return p > 0;
}

@Override
public E next() {
return array[--p];
}
};
}
}

4) 应用

模拟如下方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
System.out.println("main1");
System.out.println("main2");
method1();
method2();
System.out.println("main3");
}

public static void method1() {
System.out.println("method1");
method3();
}

public static void method2() {
System.out.println("method2");
}

public static void method3() {
System.out.println("method3");
}

模拟代码

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
public class CPU {
static class Frame {
int exit;

public Frame(int exit) {
this.exit = exit;
}
}
static int pc = 1; // 模拟程序计数器 Program counter
static ArrayStack<Frame> stack = new ArrayStack<>(100); // 模拟方法调用栈

public static void main(String[] args) {
stack.push(new Frame(-1));
while (!stack.isEmpty()) {
switch (pc) {
case 1 -> {
System.out.println("main1");
pc++;
}
case 2 -> {
System.out.println("main2");
pc++;
}
case 3 -> {
stack.push(new Frame(pc + 1));
pc = 100;
}
case 4 -> {
stack.push(new Frame(pc + 1));
pc = 200;
}
case 5 -> {
System.out.println("main3");
pc = stack.pop().exit;
}
case 100 -> {
System.out.println("method1");
stack.push(new Frame(pc + 1));
pc = 300;
}
case 101 -> {
pc = stack.pop().exit;
}
case 200 -> {
System.out.println("method2");
pc = stack.pop().exit;
}
case 300 -> {
System.out.println("method3");
pc = stack.pop().exit;
}
}
}
}
}

习题

E01. 有效的括号-Leetcode 20

一个字符串中可能出现 [] (){} 三种括号,判断该括号是否有效

有效的例子

1
2
3
4
5
()[]{}

([{}])

()

无效的例子

1
2
3
4
5
[)

([)]

([]

思路

  • 遇到左括号, 把要配对的右括号放入栈顶
  • 遇到右括号, 若此时栈为空, 返回 false,否则把它与栈顶元素对比
    • 若相等, 栈顶元素弹出, 继续对比下一组
    • 若不等, 无效括号直接返回 false
  • 循环结束
    • 若栈为空, 表示所有括号都配上对, 返回 true
    • 若栈不为空, 表示右没配对的括号, 应返回 false

答案(用到了课堂案例中的 ArrayStack 类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean isValid(String s) {
ArrayStack<Character> stack = new ArrayStack<>(s.length() / 2 + 1);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop();
} else {
return false;
}
}
}
return stack.isEmpty();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public boolean isValid(String s) {
LinkedList<Character> stack = new LinkedList<>();
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 先检查栈是否为空,再比较栈顶
if (stack.isEmpty() || c != stack.peek()) {
return false;
}
stack.pop(); // 匹配成功才弹栈
}
}
return stack.isEmpty();
}
}

LinkedList 可以完全替代 ArrayStack 实现栈的功能。栈的核心操作(push/pop/peek)都能在 O(1) 时间复杂度内完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean isValid(String s) {
Deque<Character> stack = new LinkedList<>(); // 用 LinkedList 模拟栈
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')'); // 压入对应右括号
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 遇到右括号时检查栈顶
if (stack.isEmpty() || stack.pop() != c) {
return false;
}
}
}
return stack.isEmpty(); // 所有左括号必须匹配完
}
  1. 为什么 LinkedList 能当栈用?

    • LinkedList实现了Deque(双端队列)接口,而
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      Deque提供了标准的栈操作方法:

      - `push(E e)`:压栈(等效 `addFirst`
      - `pop()`:弹栈(等效 `removeFirst`
      - `peek()`:查看栈顶(等效 `peekFirst`



      #### E02. 后缀表达式求值-Leetcode 120

      后缀表达式也称为逆波兰表达式,即运算符写在后面

      * 从左向右进行计算
      * 不必考虑运算符优先级,即不用包含括号

      示例

输入:tokens = [“2”,“1”,“+”,“3”,“*”]
输出:9
即:(2 + 1) * 3

输入:tokens = [“4”,“13”,“5”,“/”,“+”]
输出:6
即:4 + (13 / 5)

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

题目假设

* 数字都视为整数
* 数字和运算符个数给定正确,不会有除零发生

代码

```java
public int evalRPN(String[] tokens) {
LinkedList<Integer> numbers = new LinkedList<>();
for (String t : tokens) {
switch (t) {
case "+" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a + b);
}
case "-" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a - b);
}
case "*" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a * b);
}
case "/" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a / b);
}
default -> numbers.push(Integer.parseInt(t));
}
}
return numbers.pop();
}

E03. 中缀表达式转后缀

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
public class E03InfixToSuffix {
/*
思路
1. 遇到数字, 拼串
2. 遇到 + - * /
- 优先级高于栈顶运算符 入栈
- 否则将栈中高级或平级运算符出栈拼串, 本运算符入栈
3. 遍历完成, 栈中剩余运算符出栈拼串
- 先出栈,意味着优先运算
4. 带 ()
- 左括号直接入栈
- 右括号要将栈中直至左括号为止的运算符出栈拼串

| |
| |
| |
_____

a+b
a+b-c
a+b*c
a*b+c
(a+b)*c

*/
public static void main(String[] args) {
System.out.println(infixToSuffix("a+b"));
System.out.println(infixToSuffix("a+b-c"));
System.out.println(infixToSuffix("a+b*c"));
System.out.println(infixToSuffix("a*b-c"));
System.out.println(infixToSuffix("(a+b)*c"));
System.out.println(infixToSuffix("a+b*c+(d*e+f)*g"));
}

static String infixToSuffix(String exp) {
LinkedList<Character> stack = new LinkedList<>();
StringBuilder sb = new StringBuilder(exp.length());
for (int i = 0; i < exp.length(); i++) {
char c = exp.charAt(i);
switch (c) {
case '+', '-', '*', '/' -> {
if (stack.isEmpty()) {
stack.push(c);
} else {
if (priority(c) > priority(stack.peek())) {
stack.push(c);
} else {
while (!stack.isEmpty()
&& priority(stack.peek()) >= priority(c)) {
sb.append(stack.pop());
}
stack.push(c);
}
}
}
case '(' -> {
stack.push(c);
}
case ')' -> {
while (!stack.isEmpty() && stack.peek() != '(') {
sb.append(stack.pop());
}
stack.pop();
}
default -> {
sb.append(c);
}
}
}
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.toString();
}

static int priority(char c) {
return switch (c) {
case '(' -> 0;
case '*', '/' -> 2;
case '+', '-' -> 1;
default -> throw new IllegalArgumentException("不合法字符:" + c);
};
}
}

E04. 双栈模拟队列-Leetcode 232

给力扣题目用的自实现栈,可以定义为静态内部类

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
class ArrayStack<E> {

private E[] array;
private int top; // 栈顶指针

@SuppressWarnings("all")
public ArrayStack(int capacity) {
this.array = (E[]) new Object[capacity];
}

public boolean push(E value) {
if (isFull()) {
return false;
}
array[top++] = value;
return true;
}

public E pop() {
if (isEmpty()) {
return null;
}
return array[--top];
}

public E peek() {
if (isEmpty()) {
return null;
}
return array[top - 1];
}

public boolean isEmpty() {
return top == 0;
}

public boolean isFull() {
return top == array.length;
}
}

参考解答,注意:题目已说明

  • 调用 push、pop 等方法的次数最多 100
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
public class E04Leetcode232 {

/*
队列头 队列尾
s1 s2
顶 底 底 顶
abc

push(a)
push(b)
push(c)
pop()
*/
ArrayStack<Integer> s1 = new ArrayStack<>(100);
ArrayStack<Integer> s2 = new ArrayStack<>(100);

public void push(int x) {
s2.push(x);
}

public int pop() {
if (s1.isEmpty()) {
while (!s2.isEmpty()) {
s1.push(s2.pop());
}
}
return s1.pop();
}

public int peek() {
if (s1.isEmpty()) {
while (!s2.isEmpty()) {
s1.push(s2.pop());
}
}
return s1.peek();
}

public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}

}

双栈结构图示

1
2
3
4
5
6
 队列头 → |   | ← 栈s1顶部
| |
| |
队列尾 → | | ← 栈s2顶部
| |
| |
  • 栈s1:用于 出队操作,栈顶元素即为队列头
  • 栈s2:用于 入队操作,新元素始终压入s2栈顶
  • 队列内容:所有元素分布在 s1 和 s2 中,s1 的栈顶到栈底 + s2 的栈底到栈顶 构成完整队列顺序

push(1); push(2); push(3);

1
2
3
4
5
6
7
8
9
10
11
12
 s1:
s2: [3 ← 顶部]
2
1
队列顺序: 123

pop(); // 应返回1
步骤1:发现 s1 为空,将 s2 所有元素弹出压入 s1
s2弹出顺序:321
s1压入后:1 ← 顶部
2
3

E05. 单队列模拟栈-Leetcode 225

给力扣题目用的自实现队列,可以定义为静态内部类

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
public class ArrayQueue3<E> {

private final E[] array;
int head = 0;
int tail = 0;

@SuppressWarnings("all")
public ArrayQueue3(int c) {
c -= 1;
c |= c >> 1;
c |= c >> 2;
c |= c >> 4;
c |= c >> 8;
c |= c >> 16;
c += 1;
array = (E[]) new Object[c];
}

public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail & (array.length - 1)] = value;
tail++;
return true;
}

public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head & (array.length - 1)];
head++;
return value;
}

public E peek() {
if (isEmpty()) {
return null;
}
return array[head & (array.length - 1)];
}

public boolean isEmpty() {
return head == tail;
}

public boolean isFull() {
return tail - head == array.length;
}
}

参考解答,注意:题目已说明

  • 调用 push、pop 等方法的次数最多 100
  • 每次调用 pop 和 top 都能保证栈不为空
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
public class E05Leetcode225 {
/*
队列头 队列尾
cba
顶 底

queue.offer(a)
queue.offer(b)
queue.offer(c)
*/
ArrayQueue3<Integer> queue = new ArrayQueue3<>(100);
int size = 0;
public void push(int x) {
queue.offer(x);
for (int i = 0; i < size; i++) {
queue.offer(queue.poll());
}
size++;
}

public int pop() {
size--;
return queue.poll();
}

public int top() {
return queue.peek();
}

public boolean empty() {
return queue.isEmpty();
}
}

操作示例

假设连续入栈元素 abc

  1. push(a)
    • 队列:[a]
    • 无需移动元素
    • 栈状态:[a]
  2. push(b)
    • 队列先入队 b[a, b]
    • 循环移动旧元素 1 次:
      • a 出队 → 入队 → 队列变为 [b, a]
    • 栈状态:[b, a](实际栈顶是 b
  3. push©
    • 队列先入队 c[b, a, c]
    • 循环移动旧元素 2 次:
      • b → 出队入队 → [a, c, b]
      • a → 出队入队 → [c, b, a]
    • 栈状态:[c, b, a](栈顶 c
  4. pop()
    • 弹出队列头部 c
    • 队列变为 [b, a]
    • 栈状态:[b, a]

2.6 双端队列

1) 概述

双端队列、队列、栈对比

定义 特点
队列 一端删除(头)另一端添加(尾) First In First Out
一端删除和添加(顶) Last In First Out
双端队列 两端都可以删除、添加
优先级队列 优先级高者先出队
延时队列 根据延时时间确定优先级
并发非阻塞队列 队列空或满时不阻塞
并发阻塞队列 队列空时删除阻塞、队列满时添加阻塞

注1:

  • Java 中 LinkedList 即为典型双端队列实现,不过它同时实现了 Queue 接口,也提供了栈的 push pop 等方法

注2:

  • 不同语言,操作双端队列的方法命名有所不同,参见下表

    操作 Java JavaScript C++ leetCode 641
    尾部插入 offerLast push push_back insertLast
    头部插入 offerFirst unshift push_front insertFront
    尾部移除 pollLast pop pop_back deleteLast
    头部移除 pollFirst shift pop_front deleteFront
    尾部获取 peekLast at(-1) back getRear
    头部获取 peekFirst at(0) front getFront
  • 吐槽一下 leetCode 命名比较 low

  • 常见的单词还有 enqueue 入队、dequeue 出队

接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Deque<E> {

boolean offerFirst(E e);

boolean offerLast(E e);

E pollFirst();

E pollLast();

E peekFirst();

E peekLast();

boolean isEmpty();

boolean isFull();
}

2) 链表实现

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
/**
*基于双向环形链表
* 基于环形链表的双端队列
* @param <E> 元素类型
*/
public class LinkedListDeque<E> implements Deque<E>, Iterable<E> {

@Override
public boolean offerFirst(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel;
Node<E> b = sentinel.next;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
// a added b
@Override
public boolean offerLast(E e) {
if (isFull()) {
return false;
}
size++;
Node<E> a = sentinel.prev;
Node<E> b = sentinel;
Node<E> offered = new Node<>(a, e, b);
a.next = offered;
b.prev = offered;
return true;
}
// a removed b
@Override
public E pollFirst() {
if (isEmpty()) {
return null;
}
Node<E> a = sentinel;
Node<E> polled = sentinel.next;
Node<E> b = polled.next;
a.next = b;
b.prev = a;
size--;
return polled.value;
}

@Override
public E pollLast() {
if (isEmpty()) {
return null;
}
Node<E> polled = sentinel.prev;
Node<E> a = polled.prev;
Node<E> b = sentinel;
a.next = b;
b.prev = a;
size--;
return polled.value;
}

@Override
public E peekFirst() {
if (isEmpty()) {
return null;
}
return sentinel.next.value;
}

@Override
public E peekLast() {
if (isEmpty()) {
return null;
}
return sentinel.prev.value;
}

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

@Override
public boolean isFull() {
return size == capacity;
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = sentinel.next;
@Override
public boolean hasNext() {
return p != sentinel;
}

@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}

static class Node<E> {
Node<E> prev;
E value;
Node<E> next;

public Node(Node<E> prev, E value, Node<E> next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}

Node<E> sentinel = new Node<>(null, null, null);
int capacity;
int size;

public LinkedListDeque(int capacity) {
sentinel.next = sentinel;
sentinel.prev = sentinel;
this.capacity = capacity;
}
}

3) 数组实现

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* 基于循环数组实现, 特点
* <ul>
* <li>tail 停下来的位置不存储, 会浪费一个位置</li>
* </ul>
* @param <E>
*/
public class ArrayDeque1<E> implements Deque<E>, Iterable<E> {

/*
h
t
0 1 2 3
a b c

offerLast(a) 先添加元素 tail++
offerLast(b)
offerFirst(c) 先head-- 再添加元素

pollFirst() 先获取要移除的值 再head++
pollLast() 先tail-- 再获取要移除的值

head == tail 空
head ~ tail == 数组长度-1 满
必须浪费一个位置 除非你用size
*/
@Override
public boolean offerFirst(E e) {
if (isFull()) {
return false;
}
head = dec(head, array.length);
array[head] = e;
return true;
}

@Override
public boolean offerLast(E e) {
if (isFull()) {
return false;
}
array[tail] = e;
tail = inc(tail, array.length);
return true;
}

@Override
public E pollFirst() {
if (isEmpty()) {
return null;
}
E e = array[head];
array[head] = null;
head = inc(head, array.length);
return e;
}

@Override
public E pollLast() {
if (isEmpty()) {
return null;
}
tail = dec(tail, array.length);
E e = array[tail];
array[tail] = null;// help GC
return e;
}

@Override
public E peekFirst() {
if (isEmpty()) {
return null;
}
return array[head];
}

@Override
public E peekLast() {
if (isEmpty()) {
return null;
}
return array[dec(tail, array.length)];
}

@Override
public boolean isEmpty() {
return head == tail;
}
/**
h
t
0 1 2 3
a b c
tail > head
3-0 == array.length - 1

h
t
0 1 2 3
tail < head
head - tail == 1

*/

@Override
public boolean isFull() {
if (tail > head) {
return tail - head == array.length - 1;
} else if (tail < head) {
return head - tail == 1;
} else {
return false;
}
}

@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}

@Override
public E next() {
E e = array[p];
p = inc(p, array.length);
return e;
}
};
}

E[] array;
int head;
int tail;

@SuppressWarnings("unchecked")
public ArrayDeque1(int capacity) {
array = (E[]) new Object[capacity + 1];
}

//借鉴JDK的双端队列的实现
static int inc(int i, int length) {
if (i + 1 >= length) {
return 0;
}
return i + 1;
}

static int dec(int i, int length) {
if (i - 1 < 0) {
return length - 1;
}
return i - 1;
}
}

数组实现中,如果存储的是基本类型,那么无需考虑内存释放,例如

image-20230110084245095.png

但如果存储的是引用类型,应当设置该位置的引用为 null,以便内存及时释放

image-20230110084632543.png

习题

E01. 二叉树 Z 字层序遍历-Leetcode 103

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
/*
1
2 3
4 5 6 7
8 9
先复习一下 Leetcode102层序遍历
*/
public class E01Leetcode103 {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true;
int c1 = 1;//当前层节点数
while (!queue.isEmpty()) {
int c2 = 0;
LinkedList<Integer> deque = new LinkedList<>();//保存每一层结果
//这里为什么用LinkedList而不用ArrayList,因为ArrayList是基于一个普通数组添加,普通数组尾部添加效率还可以但是遇到这种偶数层要头部添加效率很低 用双端队列的实现更合适
//JDK 前面也不要用List类型 因为LinkedList有双端队列一些特有的方法
for (int i = 0; i < c1; i++) {
TreeNode n = queue.poll();
if (leftToRight) {//奇数层 直接尾部添加
deque.offerLast(n.val);
} else {
deque.offerFirst(n.val);//偶数层 往集合头部添加元素
}
if (n.left != null) {
queue.offer(n.left);
c2++;
}
if (n.right != null) {
queue.offer(n.right);
c2++;
}
}
c1 = c2;
leftToRight = !leftToRight;//取反
result.add(deque);
}

return result;
}

public static void main(String[] args) {
TreeNode root = new TreeNode(
new TreeNode(
new TreeNode(4),
2,
new TreeNode(5)
),
1,
new TreeNode(
new TreeNode(6),
3,
new TreeNode(7)
)
);
List<List<Integer>> lists = new E01Leetcode103().zigzagLevelOrder(root);
for (List<Integer> list : lists) {
System.out.println(list);
}
}
}
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
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;

LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true; // 控制遍历方向

while (!queue.isEmpty()) {
int levelSize = queue.size();
LinkedList<Integer> level = new LinkedList<>(); // 使用链表支持头插

for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();

// 根据方向决定插入顺序
if (leftToRight) {
level.addLast(node.val);
} else {
level.addFirst(node.val);
}

// 无论方向如何,子节点始终按左→右顺序加入队列
// 下一层的遍历方向由队列的读取顺序控制
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}

result.add(level);
leftToRight = !leftToRight; // 反转方向
}
return result;
}
}

Ex1. 设计双端队列-Leetcode 641

与课堂例题也是差别不大,略

2.7 优先级队列

1) 无序数组实现

要点

  1. 入队保持顺序
  2. 出队前找到优先级最高的出队,相当于一次选择排序
1
2
3
4
5
6
7
public interface Priority{
/**
* 返回对象的优先级 ,约定数字越大 优先级越高
* Returns: 优先级
*/
int priority();
}
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
/*
普通队列 FIFO
优先级队列 按照优先级出队
*/

public class PriorityQueue1<E extends Priority> implements Queue<E> {

Priority[] array;
int size;

public PriorityQueue1(int capacity) {
array = new Priority[capacity];//0~size-1
}

@Override // O(1)
public boolean offer(E e) {
if (isFull()) {
return false;
}
array[size++] = e;
return true;
}

// 返回优先级最高的索引值
private int selectMax() {
int max = 0;
for (int i = 1; i < size; i++) {
if (array[i].priority() > array[max].priority()) {
max = i;
}
}
return max;
}

@Override // O(n)
public E poll() {
if (isEmpty()) {
return null;
}
int max = selectMax();
E e = (E) array[max];
remove(max);
return e;
}

private void remove(int index) {
if (index < size - 1) {
System.arraycopy(array, index + 1,
array, index, size - 1 - index);
}
array[--size] = null; // help GC
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
int max = selectMax();
return (E) array[max];
}

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

@Override
public boolean isFull() {
return size == array.length;
}
}
  • 视频中忘记了 help GC,注意一下

2) 有序数组实现

要点

  1. 入队后排好序,优先级最高的排列在尾部
  2. 出队只需删除尾部元素即可
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
public class PriorityQueue2<E extends Priority> implements Queue<E> {

Priority[] array;
int size;

public PriorityQueue2(int capacity) {
array = new Priority[capacity];
}

// O(n)
@Override
public boolean offer(E e) {
if (isFull()) {
return false;
}
insert(e);
size++;
return true;
}

// 一轮插入排序
private void insert(E e) {
int i = size - 1;
while (i >= 0 && array[i].priority() > e.priority()) {
array[i + 1] = array[i];
i--;
}
array[i + 1] = e;
}

// O(1)
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E e = (E) array[size - 1];
array[--size] = null; // help GC
return e;
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[size - 1];
}

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

@Override
public boolean isFull() {
return size == array.length;
}
}

3) 堆实现

计算机科学中,堆是一种基于树的数据结构,通常用完全二叉树实现。堆的特性如下

  • 在大顶堆中,任意节点 C 与它的父节点 P 符合 $P.value \geq C.value$
  • 而小顶堆中,任意节点 C 与它的父节点 P 符合 $P.value \leq C.value$
  • 最顶层的节点(没有父亲)称之为 root 根节点

In computer science, a heap is a specialized tree-based data structure which is essentially an almost complete tree that satisfies the heap property: in a max heap, for any given node C, if P is a parent node of C, then the key (the value) of P is greater than or equal to the key of C. In a min heap, the key of P is less than or equal to the key of C. The node at the “top” of the heap (with no parents) is called the root node

例1 - 满二叉树(Full Binary Tree)特点:每一层都是填满的

image-20230112171444699.png

例2 - 完全二叉树(Complete Binary Tree)特点:最后一层可能未填满,靠左对齐

image-20230112171917135.png

例3 - 大顶堆

image-20230112170242265.png

例4 - 小顶堆

image-20230112171236067.png

完全二叉树可以使用数组来表示

image-20230112174351649.png

特征

  • 如果从索引 0 开始存储节点数据
    • 节点 $i$ 的父节点为 $floor((i-1)/2)$,当 $i>0$ 时
    • 节点 $i$ 的左子节点为 $2i+1$,右子节点为 $2i+2$,当然它们得 $< size$
  • 如果从索引 1 开始存储节点数据
    • 节点 $i$ 的父节点为 $floor(i/2)$,当 $i > 1$ 时
    • 节点 $i$ 的左子节点为 $2i$,右子节点为 $2i+1$,同样得 $< size$

代码

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
public class PriorityQueue4<E extends Priority> implements Queue<E> {

Priority[] array;
int size;

public PriorityQueue4(int capacity) {
array = new Priority[capacity];
}
/*
1.入堆新元素,加入到数组末尾(索引位置 child)
2.不断比较新加元素怒与它父节点(parent)优先级
- 如果父节点优先级低,则向下移动,并找到下一个parent
- 直至父节点优先级更高或child==0为止

*/
@Override
public boolean offer(E offered) {
if (isFull()) {
return false;
}
int child = size++;//末尾元素
int parent = (child - 1) / 2;//公式 找到父亲
while (child > 0 && offered.priority() > array[parent].priority()) {
array[child] = array[parent];//父亲下调
child = parent;
parent = (child - 1) / 2;
}
array[child] = offered;//插入子元素
return true;
}


private void swap(int i, int j) {
Priority t = array[i];
array[i] = array[j];
array[j] = t;
}

/*
把堆顶元素和最后一个元素交换 然后size--
重新调整堆 让它符合大顶堆的特性
从堆顶开始 将父元素与两个孩子的较大者交换
直到父元素大于两个孩子或者没有孩子为止
*/
@Override
public E poll() {
if (isEmpty()) {
return null;
}
swap(0, size - 1);
size--;
Priority e = array[size];
array[size] = null;//help GC

shiftDown(0);
return (E) e;
}

void shiftDown(int parent) {
int left = 2 * parent + 1;
int right = left + 1;
int max = parent;
if (left < size && array[left].priority() > array[max].priority()) {
max = left;
}
if (right < size && array[right].priority() > array[max].priority()) {
max = right;
}
if (max != parent) {
swap(max, parent);
shiftDown(max);
}
}

@Override
public E peek() {
if (isEmpty()) {
return null;
}
return (E) array[0];
}

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

@Override
public boolean isFull() {
return size == array.length;
}
}

习题

E01. 合并多个有序链表-Leetcode 23

这道题目之前解答过,现在用刚学的优先级队列来实现一下

题目中要从小到大排列,因此选择用小顶堆来实现,自定义小顶堆如下

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
/*
p
1->4->5->null

p
1->3->4->null

p
2->6->null

小顶堆
空链表 1->1->2->3->4->4->5->6


*/
public class MinHeap {

ListNode[] array;
int size;

public MinHeap(int capacity) {
array = new ListNode[capacity];
}

public void offer(ListNode offered) {
int child = size++;
int parent = (child - 1) / 2;
while (child > 0 && offered.val < array[parent].val) {
array[child] = array[parent];
child = parent;
parent = (child - 1) / 2;
}
array[child] = offered;
}

public ListNode poll() {
if (isEmpty()) {
return null;
}
swap(0, size - 1);
size--;
ListNode e = array[size];
array[size] = null; // help GC

down(0);

return e;
}

private void down(int parent) {
int left = 2 * parent + 1;
int right = left + 1;
int min = parent;
if (left < size && array[left].val < array[min].val) {
min = left;
}
if (right < size && array[right].val < array[min].val) {
min = right;
}
if (min != parent) {
swap(min, parent);
down(min);
}
}

private void swap(int i, int j) {
ListNode t = array[i];
array[i] = array[j];
array[j] = t;
}

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

代码

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
public class E01Leetcode23 {
public ListNode mergeKLists(ListNode[] lists) {
// 1. 使用 jdk 的优先级队列实现
// PriorityQueue<ListNode> queue = new PriorityQueue<>(Comparator.comparingInt(a -> a.val));
// 2. 使用自定义小顶堆实现
//1). 将链表的头节点加入小顶堆
MinHeap queue = new MinHeap(lists.length);
for (ListNode head : lists) {
if (head != null) {
queue.offer(head);
}
}
//2)不断从堆顶移除最小元素 加入新链表
ListNode s = new ListNode(-1, null);//哨兵
ListNode p = s;
while (!queue.isEmpty()) {
ListNode node = queue.poll();//最小元素 连接到链表尾部
p.next = node;
p = node;
if (node.next != null) {
queue.offer(node.next);
}
}
return s.next;
}
}

提问:

  • 能否将每个链表的所有元素全部加入堆,再一个个从堆顶移除?

回答:

  • 可以是可以,但对空间占用就高了,堆的一个优点就是用有限的空间做事情
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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
//使用小顶堆
Queue<ListNode>pq = new PriorityQueue<>((v1,v2)->v1.val-v2.val);
for(ListNode node:lists){
if(node!=null){
pq.offer(node);
}
}
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while(!pq.isEmpty()){
ListNode minNode = pq.poll();
tail.next = minNode;
tail = minNode;
if(minNode.next!=null){
pq.offer(minNode.next);
}
}
return dummyHead.next;
}
}

2.8 阻塞队列

之前的队列在很多场景下都不能很好地工作,例如

  1. 大部分场景要求分离向队列放入(生产者)、从队列拿出(消费者)两个角色、它们得由不同的线程来担当,而之前的实现根本没有考虑线程安全问题
  2. 队列为空,那么在之前的实现里会返回 null,如果就是硬要拿到一个元素呢?只能不断循环尝试
  3. 队列为满,那么再之前的实现里会返回 false,如果就是硬要塞入一个元素呢?只能不断循环尝试

因此我们需要解决的问题有

  1. 用锁保证线程安全
  2. 用条件变量让等待非空线程等待不满线程进入等待状态,而不是不断循环尝试,让 CPU 空转

有同学对线程安全还没有足够的认识,下面举一个反例,两个线程都要执行入队操作(几乎在同一时刻)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestThreadUnsafe {
private final String[] array = new String[10];
private int tail = 0;

public void offer(String e) {
array[tail] = e;
tail++;
}

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

public static void main(String[] args) {
TestThreadUnsafe queue = new TestThreadUnsafe();
new Thread(()-> queue.offer("e1"), "t1").start();
new Thread(()-> queue.offer("e2"), "t2").start();
}
}

执行的时间序列如下,假设初始状态 tail = 0,在执行过程中由于 CPU 在两个线程之间切换,造成了指令交错

线程1 线程2 说明
array[tail]=e1 线程1 向 tail 位置加入 e1 这个元素,但还没来得及执行 tail++
array[tail]=e2 线程2 向 tail 位置加入 e2 这个元素,覆盖掉了 e1
tail++ tail 自增为1
tail++ tail 自增为2
最后状态 tail 为 2,数组为 [e2, null, null …]

糟糕的是,由于指令交错的顺序不同,得到的结果不止以上一种,宏观上造成混乱的效果

1) 单锁实现

Java 中要防止代码段交错执行,需要使用锁,有两种选择

  • synchronized 代码块,属于关键字级别提供锁保护,功能少
  • ReentrantLock 类,功能丰富

以 ReentrantLock 为例

1
2
3
4
5
6
7
8
9
10
11
ReentrantLock lock = new ReentrantLock();

public void offer(String e) {
lock.lockInterruptibly();//可打断锁 如果是lock.lock()那如果中间出现bug那后面可能解不了锁要用finally reentranlock的使用套路 lockInterruptibly() 可以提前唤醒 可以随时打断
try {
array[tail] = e;
tail++;
} finally {
lock.unlock();//解锁
}
}

只要两个线程执行上段代码时,锁对象是同一个,就能保证 try 块内的代码的执行不会出现指令交错现象,即执行顺序只可能是下面两种情况之一

线程1 线程2 说明
lock.lockInterruptibly() t1对锁对象上锁
array[tail]=e1
lock.lockInterruptibly() 即使 CPU 切换到线程2,但由于t1已经对该对象上锁,因此线程2卡在这儿进不去
tail++ 切换回线程1 执行后续代码
lock.unlock() 线程1 解锁
array[tail]=e2 线程2 此时才能获得锁,执行它的代码
tail++
  • 另一种情况是线程2 先获得锁,线程1 被挡在外面
  • 要明白保护的本质,本例中是保护的是 tail 位置读写的安全

事情还没有完,上面的例子是队列还没有放满的情况,考虑下面的代码(这回锁同时保护了 tail 和 size 的读写安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ReentrantLock lock = new ReentrantLock();
int size = 0;

public void offer(String e) {
lock.lockInterruptibly();
try {
if(isFull()) {
// 满了怎么办?
}
array[tail] = e;
tail++;

size++;
} finally {
lock.unlock();
}
}

private boolean isFull() {
return size == array.length;
}

之前是返回 false 表示添加失败,前面分析过想达到这么一种效果:

  • 在队列满时,不是立刻返回,而是当前线程进入等待
  • 什么时候队列不满了,再唤醒这个等待的线程,从上次的代码处继续向下运行

ReentrantLock 可以配合条件变量来实现,代码进化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ReentrantLock lock = new ReentrantLock();
Condition tailWaits = lock.newCondition(); // 条件变量
int size = 0;

public void offer(String e) {
lock.lockInterruptibly();
try {
while (isFull()) {
tailWaits.await(); // 当队列满时, 当前线程进入 tailWaits 等待
}
array[tail] = e;
tail++;

size++;
} finally {
lock.unlock();
}
}

private boolean isFull() {
return size == array.length;
}
  • 条件变量底层也是个队列,用来存储这些需要等待的线程,当队列满了,就会将 offer 线程加入条件队列,并暂时释放锁
  • 将来我们的队列如果不满了(由 poll 线程那边得知)可以调用 tailWaits.signal() 来唤醒 tailWaits 中首个等待的线程,被唤醒的线程会再次抢到锁,从上次 await 处继续向下运行

思考为何要用 while 而不是 if,设队列容量是 3

操作前 offer(4) offer(5) poll() 操作后
[1 2 3] 队列满,进入tailWaits 等待 [1 2 3]
[1 2 3] 取走 1,队列不满,唤醒线程 [2 3]
[2 3] 抢先获得锁,发现不满,放入 5 [2 3 5]
[2 3 5] 从上次等待处直接向下执行 [2 3 5 ?]

关键点:

  • 从 tailWaits 中唤醒的线程,会与新来的 offer 的线程争抢锁,谁能抢到是不一定的,如果后者先抢到,就会导致条件又发生变化
  • 这种情况称之为虚假唤醒,唤醒后应该重新检查条件,看是不是得重新进入等待

最后的实现代码

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
/**
* 单锁实现
* @param <E> 元素类型
*/
public class BlockingQueue1<E> implements BlockingQueue<E> {
private final E[] array;
private int head = 0;
private int tail = 0;
private int size = 0; // 元素个数

@SuppressWarnings("all")
public BlockingQueue1(int capacity) {
array = (E[]) new Object[capacity];
}

ReentrantLock lock = new ReentrantLock();
Condition tailWaits = lock.newCondition();
Condition headWaits = lock.newCondition();

@Override
public void offer(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
while (isFull()) {
tailWaits.await();
}
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
size++;
headWaits.signal();
} finally {
lock.unlock();
}
}

@Override
public void offer(E e, long timeout) throws InterruptedException {
lock.lockInterruptibly();
try {
long t = TimeUnit.MILLISECONDS.toNanos(timeout);
while (isFull()) {
if (t <= 0) {
return;
}
t = tailWaits.awaitNanos(t);
}
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
size++;
headWaits.signal();
} finally {
lock.unlock();
}
}

@Override
public E poll() throws InterruptedException {
lock.lockInterruptibly();
try {
while (isEmpty()) {
headWaits.await();
}
E e = array[head];
array[head] = null; // help GC
if (++head == array.length) {
head = 0;
}
size--;
tailWaits.signal();
return e;
} finally {
lock.unlock();
}
}

private boolean isEmpty() {
return size == 0;
}

private boolean isFull() {
return size == array.length;
}
}
  • public void offer(E e, long timeout) throws InterruptedException 是带超时的版本,可以只等待一段时间,而不是永久等下去,类似的 poll 也可以做带超时的版本,这个留给大家了

注意

  • JDK 中 BlockingQueue 接口的方法命名与我的示例有些差异
    • 方法 offer(E e) 是非阻塞的实现,阻塞实现方法为 put(E e)
    • 方法 poll() 是非阻塞的实现,阻塞实现方法为 take()

2) 双锁实现

单锁的缺点在于:

  • 生产和消费几乎是不冲突的,唯一冲突的是生产者和消费者它们有可能同时修改 size
  • 冲突的主要是生产者之间:多个 offer 线程修改 tail
  • 冲突的还有消费者之间:多个 poll 线程修改 head

如果希望进一步提高性能,可以用两把锁

  • 一把锁保护 tail
  • 另一把锁保护 head
1
2
3
4
5
ReentrantLock headLock = new ReentrantLock();  // 保护 head 的锁
Condition headWaits = headLock.newCondition(); // 队列空时,需要等待的线程集合

ReentrantLock tailLock = new ReentrantLock(); // 保护 tail 的锁
Condition tailWaits = tailLock.newCondition(); // 队列满时,需要等待的线程集合

先看看 offer 方法的初步实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void offer(E e) throws InterruptedException {
tailLock.lockInterruptibly();
try {
// 队列满等待
while (isFull()) {
tailWaits.await();
}

// 不满则入队
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}

// 修改 size (有问题)
size++;

} finally {
tailLock.unlock();
}
}

上面代码的缺点是 size 并不受 tailLock 保护,tailLock 与 headLock 是两把不同的锁,并不能实现互斥的效果。因此,size 需要用下面的代码保证原子性

1
2
3
4
AtomicInteger size = new AtomicInteger(0);	   // 保护 size 的原子变量

size.getAndIncrement(); // 自增
size.getAndDecrement(); // 自减

代码修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void offer(E e) throws InterruptedException {
tailLock.lockInterruptibly();
try {
// 队列满等待
while (isFull()) {
tailWaits.await();
}

// 不满则入队
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}

// 修改 size
size.getAndIncrement();

} finally {
tailLock.unlock();
}
}

对称地,可以写出 poll 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public E poll() throws InterruptedException {
E e;
headLock.lockInterruptibly();
try {
// 队列空等待
while (isEmpty()) {
headWaits.await();
}

// 不空则出队
e = array[head];
if (++head == array.length) {
head = 0;
}

// 修改 size
size.getAndDecrement();

} finally {
headLock.unlock();
}
return e;
}

下面来看一个难题,就是如何通知 headWaits 和 tailWaits 中等待的线程,比如 poll 方法拿走一个元素,通知 tailWaits:我拿走一个,不满了噢,你们可以放了,因此代码改为

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
@Override
public E poll() throws InterruptedException {
E e;
headLock.lockInterruptibly();
try {
// 队列空等待
while (isEmpty()) {
headWaits.await();
}

// 不空则出队
e = array[head];
if (++head == array.length) {
head = 0;
}

// 修改 size
size.getAndDecrement();

// 通知 tailWaits 不满(有问题)
tailWaits.signal();

} finally {
headLock.unlock();
}
return e;
}

问题在于要使用这些条件变量的 await(), signal() 等方法需要先获得与之关联的锁,上面的代码若直接运行会出现以下错误

1
java.lang.IllegalMonitorStateException

那有同学说,加上锁不就行了吗,于是写出了下面的代码

image-20230208160143386.png

发现什么问题了?两把锁这么嵌套使用,非常容易出现死锁,如下所示

image-20230208160343493.png

因此得避免嵌套,两段加锁的代码变成了下面平级的样子

image-20230208162857435.png

性能还可以进一步提升

  1. 代码调整后 offer 并没有同时获取 tailLock 和 headLock 两把锁,因此两次加锁之间会有空隙,这个空隙内可能有其它的 offer 线程添加了更多的元素,那么这些线程都要执行 signal(),通知 poll 线程队列非空吗?

    • 每次调用 signal() 都需要这些 offer 线程先获得 headLock 锁,成本较高,要想法减少 offer 线程获得 headLock 锁的次数
    • 可以加一个条件:当 offer 增加前队列为空,即从 0 变化到不空,才由此 offer 线程来通知 headWaits,其它情况不归它管
  2. 队列从 0 变化到不空,会唤醒一个等待的 poll 线程,这个线程被唤醒后,肯定能拿到 headLock 锁,因此它具备了唤醒 headWaits 上其它 poll 线程的先决条件。如果检查出此时有其它 offer 线程新增了元素(不空,但不是从0变化而来),那么不妨由此 poll 线程来唤醒其它 poll 线程

这个技巧被称之为级联通知(cascading notifies),类似的原因

  1. 在 poll 时队列从满变化到不满,才由此 poll 线程来唤醒一个等待的 offer 线程,目的也是为了减少 poll 线程对 tailLock 上锁次数,剩下等待的 offer 线程由这个 offer 线程间接唤醒

最终的代码为

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
public class BlockingQueue2<E> implements BlockingQueue<E> {

private final E[] array;
private int head = 0;
private int tail = 0;
private final AtomicInteger size = new AtomicInteger(0);
ReentrantLock headLock = new ReentrantLock();
Condition headWaits = headLock.newCondition();
ReentrantLock tailLock = new ReentrantLock();
Condition tailWaits = tailLock.newCondition();

public BlockingQueue2(int capacity) {
this.array = (E[]) new Object[capacity];
}

@Override
public void offer(E e) throws InterruptedException {
int c;
tailLock.lockInterruptibly();
try {
while (isFull()) {
tailWaits.await();
}
array[tail] = e;
if (++tail == array.length) {
tail = 0;
}
c = size.getAndIncrement();
// a. 队列不满, 但不是从满->不满, 由此offer线程唤醒其它offer线程
if (c + 1 < array.length) {
tailWaits.signal();
}
} finally {
tailLock.unlock();
}
// b. 从0->不空, 由此offer线程唤醒等待的poll线程
if (c == 0) {
headLock.lock();
try {
headWaits.signal();
} finally {
headLock.unlock();
}
}
}

@Override
public E poll() throws InterruptedException {
E e;
int c;
headLock.lockInterruptibly();
try {
while (isEmpty()) {
headWaits.await();
}
e = array[head];
if (++head == array.length) {
head = 0;
}
c = size.getAndDecrement();
// b. 队列不空, 但不是从0变化到不空,由此poll线程通知其它poll线程
if (c > 1) {
headWaits.signal();
}
} finally {
headLock.unlock();
}
// a. 从满->不满, 由此poll线程唤醒等待的offer线程
if (c == array.length) {
tailLock.lock();
try {
tailWaits.signal();
} finally {
tailLock.unlock();
}
}
return e;
}

private boolean isEmpty() {
return size.get() == 0;
}

private boolean isFull() {
return size.get() == array.length;
}

}

双锁实现的非常精巧,据说作者 Doug Lea 花了一年的时间才完善了此段代码

2.9 堆

以大顶堆为例,相对于之前的优先级队列,增加了堆化等方法

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
public class MaxHeap {
int[] array;
int size;

public MaxHeap(int capacity) {
this.array = new int[capacity];
}

/**
* 获取堆顶元素
*
* @return 堆顶元素
*/
public int peek() {
return array[0];
}

/**
* 删除堆顶元素
*
* @return 堆顶元素
*/
public int poll() {
int top = array[0];
swap(0, size - 1);
size--;
down(0);
return top;
}

/**
* 删除指定索引处元素
*
* @param index 索引
* @return 被删除元素
*/
public int poll(int index) {
int deleted = array[index];
up(Integer.MAX_VALUE, index);
poll();
return deleted;
}

/**
* 替换堆顶元素
*
* @param replaced 新元素
*/
public void replace(int replaced) {
array[0] = replaced;
down(0);
}

/**
* 堆的尾部添加元素
*
* @param offered 新元素
* @return 是否添加成功
*/
public boolean offer(int offered) {
if (size == array.length) {
return false;
}
up(offered, size);
size++;
return true;
}

// 将 offered 元素上浮: 直至 offered 小于父元素或到堆顶
private void up(int offered, int index) {
int child = index;
while (child > 0) {
int parent = (child - 1) / 2;
if (offered > array[parent]) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}

public MaxHeap(int[] array) {
this.array = array;
this.size = array.length;
heapify();
}

// 建堆
private void heapify() {
// 如何找到最后这个非叶子节点 size / 2 - 1
for (int i = size / 2 - 1; i >= 0; i--) {
down(i);
}
}

// 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
private void down(int parent) {
int left = parent * 2 + 1;
int right = left + 1;
int max = parent;
if (left < size && array[left] > array[max]) {
max = left;
}
if (right < size && array[right] > array[max]) {
max = right;
}
if (max != parent) { // 找到了更大的孩子
swap(max, parent);
down(max);
}
}

// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}

public static void main(String[] args) {

int[] array = {2, 3, 1, 7, 6, 4, 5};
MaxHeap heap = new MaxHeap(array);
System.out.println(Arrays.toString(heap.array));

while (heap.size > 1) {
heap.swap(0, heap.size - 1);
heap.size--;
heap.down(0);
}

System.out.println(Arrays.toString(heap.array));
}
}

建堆

Floyd 建堆算法作者(也是之前龟兔赛跑判环作者):

image-20230213095110902.png

  1. 找到最后一个非叶子节点
  2. 从后向前,对每个节点执行下潜

一些规律

  • 一棵满二叉树节点个数为 2ℎ−1,如下例中高度 ℎ=3 节点数是 23−1=7
  • 非叶子节点范围为 [0,𝑠𝑖𝑧𝑒/2−1]

算法时间复杂度分析

image-20230213114024607.png

下面看交换次数的推导:设节点高度为 3

本层节点数 高度 下潜最多交换次数(高度-1)
4567 这层 4 1 0
23这层 2 2 1
1这层 1 3 2

每一层的交换次数为:节点个数此节点交换次数节点个数∗此节点交换次数,总的交换次数为

4∗0+2∗1+1∗282∗0+84∗1+88∗2821∗0+822∗1+823∗2

∑𝑖=1ℎ(2ℎ2𝑖∗(𝑖−1))

https://www.wolframalpha.com/ 输入

1
Sum[\(40)Divide[Power[2,x],Power[2,i]]*\(40)i-1\(41)\(41),{i,1,x}]

推导出

2ℎ−ℎ−1

其中 2ℎ≈𝑛,ℎ≈log2⁡𝑛,因此有时间复杂度 𝑂(𝑛)

习题

E01. 堆排序

算法描述

  1. heapify 建立大顶堆
  2. 将堆顶与堆底交换(最大元素被交换到堆底),缩小并下潜调整堆
  3. 重复第二步直至堆里剩一个元素

可以使用之前课堂例题的大顶堆来实现

1
2
3
4
5
6
7
8
9
10
int[] array = {1, 2, 3, 4, 5, 6, 7};
MaxHeap maxHeap = new MaxHeap(array);
System.out.println(Arrays.toString(maxHeap.array));

while (maxHeap.size > 1) {
maxHeap.swap(0, maxHeap.size - 1);
maxHeap.size--;
maxHeap.down(0);
}
System.out.println(Arrays.toString(maxHeap.array));

E02. 数组中第K大元素-Leetcode 215

小顶堆(可删去用不到代码)

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
class MinHeap {
int[] array;
int size;

public MinHeap(int capacity) {
array = new int[capacity];
}

private void heapify() {
for (int i = (size >> 1) - 1; i >= 0; i--) {
down(i);
}
}

public int poll() {
swap(0, size - 1);
size--;
down(0);
return array[size];
}

public int poll(int index) {
swap(index, size - 1);
size--;
down(index);
return array[size];
}

public int peek() {
return array[0];
}

public boolean offer(int offered) {
if (size == array.length) {
return false;
}
up(offered);
size++;
return true;
}

public void replace(int replaced) {
array[0] = replaced;
down(0);
}

private void up(int offered) {
int child = size;
while (child > 0) {
int parent = (child - 1) >> 1;
if (offered < array[parent]) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}

private void down(int parent) {
int left = (parent << 1) + 1;
int right = left + 1;
int min = parent;
if (left < size && array[left] < array[min]) {
min = left;
}
if (right < size && array[right] < array[min]) {
min = right;
}
if (min != parent) {
swap(min, parent);
down(min);
}
}

// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}

题解

1
2
3
4
5
6
7
8
9
10
11
12
public int findKthLargest(int[] numbers, int k) {
MinHeap heap = new MinHeap(k);
for (int i = 0; i < k; i++) {
heap.offer(numbers[i]);
}
for (int i = k; i < numbers.length; i++) {
if(numbers[i] > heap.peek()){
heap.replace(numbers[i]);
}
}
return heap.peek();
}

求数组中的第 K 大元素,使用堆并不是最佳选择,可以采用快速选择算法

优先队列的思路是很朴素的。由于找第 K 大元素,其实就是整个数组排序以后后半部分最小的那个元素。因此,我们可以维护一个有 K 个元素的最小堆:

如果当前堆不满,直接添加;
堆满的时候,如果新读到的数小于等于堆顶,肯定不是我们要找的元素,只有新遍历到的数大于堆顶的时候,才将堆顶拿出,然后放入新读到的数,进而让堆自己去调整内部结构。
说明:这里最合适的操作其实是 replace(),即直接把新读进来的元素放在堆顶,然后执行下沉(siftDown())操作。Java 当中的 PriorityQueue 没有提供这个操作,只好先 poll() 再 offer()。

优先队列的写法就很多了,这里只例举一个有代表性的,其它的写法大同小异,没有本质差别。

优先队列可以用一个 for 循环解决哈。就是在 for 循环里面判断小顶堆里面的 size() 是否大于 k 个数,是的话就 poll() 出去;整个 for 循环结束之后剩下来的就是 k 个数的小顶堆。堆顶即第 k 大的数。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer>heap = new PriorityQueue<>();
for(int num:nums){
heap.add(num);
if(heap.size()>k){
heap.poll();
}
}
return heap.peek();
}
}

E03. 数据流中第K大元素-Leetcode 703

上题的小顶堆加一个方法

1
2
3
4
5
6
class MinHeap {
// ...
public boolean isFull() {
return size == array.length;
}
}

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class KthLargest {

private MinHeap heap;

public KthLargest(int k, int[] nums) {
heap = new MinHeap(k);
for(int i = 0; i < nums.length; i++) {
add(nums[i]);
}
}

public int add(int val) {
if(!heap.isFull()){
heap.offer(val);
} else if(val > heap.peek()){
heap.replace(val);
}
return heap.peek();
}

}

求数据流中的第 K 大元素,使用堆最合适不过

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
class KthLargest {

private PriorityQueue<Integer>queue;
private int limit;

public KthLargest(int k, int[] nums) {
limit = k;
queue=new PriorityQueue<>(k);
for(int num:nums){
add(num);
}

}

public int add(int val) {
if(queue.size() < limit){
queue.add(val);
}else if(val > queue.peek()){
queue.poll();
queue.add(val);
}
return queue.peek();
}
}

/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/

E04. 数据流的中位数-Leetcode 295

可以扩容的 heap, max 用于指定是大顶堆还是小顶堆

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
public class Heap {
int[] array;
int size;
boolean max;

public int size() {
return size;
}

public Heap(int capacity, boolean max) {
this.array = new int[capacity];
this.max = max;
}

/**
* 获取堆顶元素
*
* @return 堆顶元素
*/
public int peek() {
return array[0];
}

/**
* 删除堆顶元素
*
* @return 堆顶元素
*/
public int poll() {
int top = array[0];
swap(0, size - 1);
size--;
down(0);
return top;
}

/**
* 删除指定索引处元素
*
* @param index 索引
* @return 被删除元素
*/
public int poll(int index) {
int deleted = array[index];
swap(index, size - 1);
size--;
down(index);
return deleted;
}

/**
* 替换堆顶元素
*
* @param replaced 新元素
*/
public void replace(int replaced) {
array[0] = replaced;
down(0);
}

/**
* 堆的尾部添加元素
*
* @param offered 新元素
*/
public void offer(int offered) {
if (size == array.length) {
grow();
}
up(offered);
size++;
}

private void grow() {
int capacity = size + (size >> 1);
int[] newArray = new int[capacity];
System.arraycopy(array, 0,
newArray, 0, size);
array = newArray;
}

// 将 offered 元素上浮: 直至 offered 小于父元素或到堆顶
private void up(int offered) {
int child = size;
while (child > 0) {
int parent = (child - 1) / 2;
boolean cmp = max ? offered > array[parent] : offered < array[parent];
if (cmp) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}

public Heap(int[] array, boolean max) {
this.array = array;
this.size = array.length;
this.max = max;
heapify();
}

// 建堆
private void heapify() {
// 如何找到最后这个非叶子节点 size / 2 - 1
for (int i = size / 2 - 1; i >= 0; i--) {
down(i);
}
}

// 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
private void down(int parent) {
int left = parent * 2 + 1;
int right = left + 1;
int min = parent;
if (left < size && (max ? array[left] > array[min] : array[left] < array[min])) {
min = left;
}
if (right < size && (max ? array[right] > array[min] : array[right] < array[min])) {
min = right;
}
if (min != parent) { // 找到了更大的孩子
swap(min, parent);
down(min);
}
}

// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}

题解

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
private Heap left = new Heap(10, false);
private Heap right = new Heap(10, true);

/**
为了保证两边数据量的平衡
<ul>
<li>两边数据一样时,加入左边</li>
<li>两边数据不一样时,加入右边</li>
</ul>
但是, 随便一个数能直接加入吗?
<ul>
<li>加入左边前, 应该挑右边最小的加入</li>
<li>加入右边前, 应该挑左边最大的加入</li>
</ul>
*/
public void addNum(int num) {
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
} else {
left.offer(num);
right.offer(left.poll());
}
}

/**
* <ul>
* <li>两边数据一致, 左右各取堆顶元素求平均</li>
* <li>左边多一个, 取左边元素</li>
* </ul>
*/
public double findMedian() {
if (left.size() == right.size()) {
return (left.peek() + right.peek()) / 2.0;
} else {
return left.peek();
}
}

本题还可以使用平衡二叉搜索树求解,不过代码比两个堆复杂

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
class MedianFinder {
Queue<Integer>A,B;
public MedianFinder() {
A = new PriorityQueue<>();//小顶堆 保存较大的一半
B = new PriorityQueue<>((x,y)->(y-x)); // 大顶堆,保存较小的一半

}

public void addNum(int num) {
if(A.size()!=B.size()){//N为计数
A.add(num);//如果你想给把一个东西加到A堆,那么先放到另一个堆B堆,由B堆自己维护出来一个合理的值上贡到A堆,B再pop
B.add(A.poll());
}else{
B.add(num);
A.add(B.poll());
}

}

public double findMedian() {
return A.size()!=B.size()?A.peek():(A.peek()+B.peek())/2.0;
}
}

/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/

2.10 二叉树

二叉树是这么一种树状结构:每个节点最多有两个孩子,左孩子和右孩子

重要的二叉树结构

  • 完全二叉树(complete binary tree)是一种二叉树结构,除最后一层以外,每一层都必须填满,填充时要遵从先左后右
  • 平衡二叉树(balance binary tree)是一种二叉树结构,其中每个节点的左右子树高度相差不超过 1

1) 存储

存储方式分为两种

  1. 定义树节点与左、右孩子引用(TreeNode)
  2. 使用数组,前面讲堆时用过,若以 0 作为树的根,索引可以通过如下方式计算
    • 父 = floor((子 - 1) / 2)
    • 左孩子 = 父 * 2 + 1
    • 右孩子 = 父 * 2 + 2

2) 遍历

遍历也分为两种

  1. 广度优先遍历(Breadth-first order):尽可能先访问距离根最近的节点,也称为层序遍历
  2. 深度优先遍历(Depth-first order):对于二叉树,可以进一步分成三种(要深入到叶子节点)
    1. pre-order 前序遍历,对于每一棵子树,先访问该节点,然后是左子树,最后是右子树
    2. in-order 中序遍历,对于每一棵子树,先访问左子树,然后是该节点,最后是右子树
    3. post-order 后序遍历,对于每一棵子树,先访问左子树,然后是右子树,最后是该节点

广度优先

image-20230216153607396.png

本轮开始时队列 本轮访问节点
[1] 1
[2, 3] 2
[3, 4] 3
[4, 5, 6] 4
[5, 6] 5
[6, 7, 8] 6
[7, 8] 7
[8] 8
[]
  1. 初始化,将根节点加入队列
  2. 循环处理队列中每个节点,直至队列为空
  3. 每次循环内处理节点后,将它的孩子节点(即下一层的节点)加入队列

注意

  • 以上用队列来层序遍历是针对 TreeNode 这种方式表示的二叉树
  • 对于数组表现的二叉树,则直接遍历数组即可,自然为层序遍历的顺序

深度优先

image-20230221110443230.png

栈暂存 已处理 前序遍历 中序遍历
[1] 1 ✔️ 左💤 右💤 1
[1, 2] 2✔️ 左💤 右💤
1✔️ 左💤 右💤
2
[1, 2, 4] 4✔️ 左✔️ 右✔️
2✔️ 左💤 右💤
1✔️ 左💤 右💤
4 4
[1, 2] 2✔️ 左✔️ 右✔️
1✔️ 左💤 右💤
2
[1] 1✔️ 左✔️ 右💤 1
[1, 3] 3✔️ 左💤 右💤
1✔️ 左✔️ 右💤
3
[1, 3, 5] 5✔️ 左✔️ 右✔️
3✔️ 左💤 右💤
1✔️ 左✔️ 右💤
5 5
[1, 3] 3✔️ 左✔️ 右💤
1✔️ 左✔️ 右💤
3
[1, 3, 6] 6✔️ 左✔️ 右✔️
3✔️ 左✔️ 右💤
1✔️ 左✔️ 右💤
6 6
[1, 3] 3✔️ 左✔️ 右✔️
1✔️ 左✔️ 右💤
[1] 1✔️ 左✔️ 右✔️
[]

递归实现

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
/**
* <h3>前序遍历</h3>
* @param node 节点
*/
static void preOrder(TreeNode node) {
if (node == null) {
return;
}
System.out.print(node.val + "\t"); // 值
preOrder(node.left); // 左
preOrder(node.right); // 右
}

/**
* <h3>中序遍历</h3>
* @param node 节点
*/
static void inOrder(TreeNode node) {
if (node == null) {
return;
}
inOrder(node.left); // 左
System.out.print(node.val + "\t"); // 值
inOrder(node.right); // 右
}

/**
* <h3>后序遍历</h3>
* @param node 节点
*/
static void postOrder(TreeNode node) {
if (node == null) {
return;
}
postOrder(node.left); // 左
postOrder(node.right); // 右
System.out.print(node.val + "\t"); // 值
}

非递归实现

前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;

while (!stack.isEmpty() || curr != null) {
if (curr != null) {
stack.push(curr);
System.out.println(curr);
curr = curr.left;
} else {
TreeNode pop = stack.pop();
curr = pop.right;
}

}

中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;

while (!stack.isEmpty() || curr != null) {
if (curr != null) {
stack.push(curr);
curr = curr.left;
} else {
TreeNode pop = stack.pop();
System.out.println(pop);
curr = pop.right;
}
}

后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;
TreeNode pop = null;

while (!stack.isEmpty() || curr != null) {
if (curr != null) {
stack.push(curr);
curr = curr.left;
} else {
TreeNode peek = stack.peek();
if (peek.right == null || peek.right == pop) {
pop = stack.pop();
System.out.println(pop);
} else {
curr = peek.right;
}
}
}

对于后序遍历,向回走时,需要处理完右子树才能 pop 出栈。如何知道右子树处理完成呢?

  • 如果栈顶元素的 𝑟𝑖𝑔ℎ𝑡≡𝑛𝑢𝑙𝑙 表示没啥可处理的,可以出栈
  • 如果栈顶元素的 𝑟𝑖𝑔ℎ𝑡≠𝑛𝑢𝑙𝑙,
    • 那么使用 lastPop 记录最近出栈的节点,即表示从这个节点向回走
    • 如果栈顶元素的 𝑟𝑖𝑔ℎ𝑡==𝑙𝑎𝑠𝑡𝑃𝑜𝑝 此时应当出栈

对于前、中两种遍历,实际以上代码从右子树向回走时,并未走完全程(stack 提前出栈了)后序遍历以上代码是走完全程了

统一写法

下面是一种统一的写法,依据后序遍历修改

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
LinkedList<TreeNode> stack = new LinkedList<>();

TreeNode curr = root; // 代表当前节点
TreeNode pop = null; // 最近一次弹栈的元素
while (curr != null || !stack.isEmpty()) {
if (curr != null) {
colorPrintln("前: " + curr.val, 31);
stack.push(curr); // 压入栈,为了记住回来的路
curr = curr.left;
} else {
TreeNode peek = stack.peek();
// 右子树可以不处理, 对中序来说, 要在右子树处理之前打印
if (peek.right == null) {
colorPrintln("中: " + peek.val, 36);
pop = stack.pop();
colorPrintln("后: " + pop.val, 34);
}
// 右子树处理完成, 对中序来说, 无需打印
else if (peek.right == pop) {
pop = stack.pop();
colorPrintln("后: " + pop.val, 34);
}
// 右子树待处理, 对中序来说, 要在右子树处理之前打印
else {
colorPrintln("中: " + peek.val, 36);
curr = peek.right;
}
}
}

public static void colorPrintln(String origin, int color) {
System.out.printf("\033[%dm%s\033[0m%n", color, origin);
}

一张图演示三种遍历

Sorted_binary_tree_ALL_RGB.svg.png

  • 红色:前序遍历顺序
  • 绿色:中序遍历顺序
  • 蓝色:后续遍历顺序

习题

E01. 前序遍历二叉树-Leetcode 144

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer>res = new ArrayList<Integer>();
preorder(root,res);
return res;
}

public void preorder(TreeNode root,List<Integer>res){
if(root==null){
return;
}
res.add(root.val);
preorder(root.left,res);
preorder(root.right,res);
}
}

E02. 中序遍历二叉树-Leetcode 94

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorderTraversalRecursion(root,res);
return res;
}
public void inorderTraversalRecursion(TreeNode root,List<Integer> res){
if(root==null){
return;
}
inorderTraversalRecursion(root.left,res);
res.add(root.val);
inorderTraversalRecursion(root.right,res);
}
}

E03. 后序遍历二叉树-Leetcode 145

E04. 对称二叉树-Leetcode 101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean isSymmetric(TreeNode root) {
return check(root.left, root.right);
}

public boolean check(TreeNode left, TreeNode right) {
// 若同时为 null
if (left == null && right == null) {
return true;
}
// 若有一个为 null (有上一轮筛选,另一个肯定不为 null)
if (left == null || right == null) {
return false;
}
if (left.val != right.val) {
return false;
}
return check(left.left, right.right) && check(left.right, right.left);
}

类似题目:Leetcode 100 题 - 相同的树

E05. 二叉树最大深度-Leetcode 104

后序遍历求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
思路:
1. 得到左子树深度, 得到右子树深度, 二者最大者加一, 就是本节点深度
2. 因为需要先得到左右子树深度, 很显然是后序遍历典型应用
3. 关于深度的定义:从根出发, 离根最远的节点总边数,
注意: 力扣里的深度定义要多一

深度2 深度3 深度1
1 1 1
/ \ / \
2 3 2 3
\
4
*/
public int maxDepth(TreeNode node) {
if (node == null) {
return 0; // 非力扣题目改为返回 -1
}
int d1 = maxDepth(node.left);
int d2 = maxDepth(node.right);
return Integer.max(d1, d2) + 1;
}

后序遍历求解-非递归

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
/*
思路:
1. 使用非递归后序遍历, 栈的最大高度即为最大深度
*/
public int maxDepth(TreeNode root) {
TreeNode curr = root;
LinkedList<TreeNode> stack = new LinkedList<>();
int max = 0;
TreeNode pop = null;
while (curr != null || !stack.isEmpty()) {
if (curr != null) {
stack.push(curr);
int size = stack.size();
if (size > max) {
max = size;
}
curr = curr.left;
} else {
TreeNode peek = stack.peek();
if(peek.right == null || peek.right == pop) {
pop = stack.pop();
} else {
curr = peek.right;
}
}
}
return max;
}

层序遍历求解

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
/*
思路:
1. 使用层序遍历, 层数即最大深度
*/
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0;
while (!queue.isEmpty()) {
level++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return level;
}

E06. 二叉树最小深度-Leetcode 111

后序遍历求解

1
2
3
4
5
6
7
8
9
10
11
public int minDepth(TreeNode node) {
if (node == null) {
return 0;
}
int d1 = minDepth(node.left);
int d2 = minDepth(node.right);
if (d1 == 0 || d2 == 0) {
return d1 + d2 + 1;
}
return Integer.min(d1, d2) + 1;
}

相较于求最大深度,应当考虑:

  • 当右子树为 null,应当返回左子树深度加一
  • 当左子树为 null,应当返回右子树深度加一

上面两种情况满足时,不应该再把为 null 子树的深度 0 参与最小值比较,例如这样

1
2
3
  1
/
2
  • 正确深度为 2,若把为 null 的右子树的深度 0 考虑进来,会得到错误结果 1
1
2
3
4
5
1
\
3
\
4
  • 正确深度为 3,若把为 null 的左子树的深度 0 考虑进来,会得到错误结果 1

层序遍历求解

遇到的第一个叶子节点所在层就是最小深度

例如,下面的树遇到的第一个叶子节点 3 所在的层就是最小深度,其他 4,7 等叶子节点深度更深,也更晚遇到

1
2
3
4
5
6
7
    1
/ \
2 3
/ \
4 5
/
7

代码

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
public int minDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0;
while (!queue.isEmpty()) {
level++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left == null && node.right == null) {
return level;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return level;
}

效率会高于之前后序遍历解法,因为找到第一个叶子节点后,就无需后续的层序遍历了

E07. 翻转二叉树-Leetcode 226

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public TreeNode invertTree(TreeNode root) {
fn(root);
return root;
}

private void fn(TreeNode node){
if (node == null) {
return;
}
TreeNode t = node.left;
node.left = node.right;
node.right = t;
fn(node.left);
fn(node.right);
}

先交换、再递归或是先递归、再交换都可以

其实就是交换一下左右节点,然后再递归的交换左节点,右节点
根据动画图我们可以总结出递归的两个条件如下:

终止条件:当前节点为 null 时返回
交换当前节点的左右节点,再递归的交换当前节点的左节点,递归的交换当前节点的右节点
时间复杂度:每个元素都必须访问一次,所以是 O(n)
空间复杂度:最坏的情况下,需要存放 O(h) 个函数调用(h是树的高度),所以是 O(h)

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
//交换左右子树
TreeNode tmp = root.right;
root.right = root.left;
root.left = tmp;
//递归交换当前节点的左子树
invertTree(root.left);
//递归交换当前节点的右子树
invertTree(root.right);
return root;
}
}

E08. 后缀表达式转二叉树

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
static class TreeNode {
public String val;
public TreeNode left;
public TreeNode right;

public TreeNode(String val) {
this.val = val;
}

public TreeNode(TreeNode left, String val, TreeNode right) {
this.left = left;
this.val = val;
this.right = right;
}

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

/*
中缀表达式 (2-1)*3
后缀(逆波兰)表达式 21-3*

1.遇到数字入栈
2.遇到运算符, 出栈两次, 与当前节点建立父子关系, 当前节点入栈


| |
| |
| |
_____

表达式树
*
/ \
- 3
/ \
2 1

21-3*
*/
public TreeNode constructExpressionTree(String[] tokens) {
LinkedList<TreeNode> stack = new LinkedList<>();
for (String t : tokens) {
switch (t) {
case "+", "-", "*", "/" -> { // 运算符
TreeNode right = stack.pop();
TreeNode left = stack.pop();
TreeNode parent = new TreeNode(t);
parent.left = left;
parent.right = right;
stack.push(parent);
}
default -> { // 数字
stack.push(new TreeNode(t));
}
}
}
return stack.peek();
}

E09. 根据前序与中序遍历结果构造二叉树-Leetcode 105

  • 先通过前序遍历结果定位根节点
  • 再结合中序遍历结果切分左右子树
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
public class E09Leetcode105 {

/*
preOrder = {1,2,4,3,6,7}
inOrder = {4,2,1,6,3,7}

根 1
pre in
左 2,4 4,2
右 3,6,7 6,3,7


根 2
左 4

根 3
左 6
右 7
*/

public TreeNode buildTree(int[] preOrder, int[] inOrder) {
if (preOrder.length == 0) {
return null;
}
// 创建根节点
int rootValue = preOrder[0];
TreeNode root = new TreeNode(rootValue);
// 区分左右子树
for (int i = 0; i < inOrder.length; i++) {
if (inOrder[i] == rootValue) {
// 0 ~ i-1 左子树
// i+1 ~ inOrder.length -1 右子树
int[] inLeft = Arrays.copyOfRange(inOrder, 0, i); // [4,2]
int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length); // [6,3,7]

int[] preLeft = Arrays.copyOfRange(preOrder, 1, i + 1); // [2,4]
int[] preRight = Arrays.copyOfRange(preOrder, i + 1, inOrder.length); // [3,6,7]

root.left = buildTree(preLeft, inLeft); // 2
root.right = buildTree(preRight, inRight); // 3
break;
}
}
return root;
}

}
  • 代码可以进一步优化,涉及新数据结构,以后实现

E10. 根据中序与后序遍历结果构造二叉树-Leetcode 106

  • 先通过后序遍历结果定位根节点
  • 再结合中序遍历结果切分左右子树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public TreeNode buildTree(int[] inOrder, int[] postOrder) {
if (inOrder.length == 0) {
return null;
}
// 根
int rootValue = postOrder[postOrder.length - 1];
TreeNode root = new TreeNode(rootValue);
// 切分左右子树
for (int i = 0; i < inOrder.length; i++) {
if (inOrder[i] == rootValue) {
int[] inLeft = Arrays.copyOfRange(inOrder, 0, i);
int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length);

int[] postLeft = Arrays.copyOfRange(postOrder, 0, i);
int[] postRight = Arrays.copyOfRange(postOrder, i, postOrder.length - 1);

root.left = buildTree(inLeft, postLeft);
root.right = buildTree(inRight, postRight);
break;
}
}
return root;
}
  • 代码可以进一步优化,涉及新数据结构,以后实现