封面画师:ツチヤ     封面ID:80414367

本文参考视频:小马哥教育(SEEMYGO) 2019年 恋上数据结构与算法(第一季)

源码仓库:mofan212/data-structure-and-algorithm (github.com)

辅助学习网址:数据结构和算法动态可视化

1. 栈

1.1 基本含义

栈(Stack)是一种特殊的线性表,只能在一端 进行操作。

往栈中添加元素的操作,一般叫做 入栈(push);从栈中移除元素的操作,一般叫做 出栈(pop),由于只能移除栈顶元素,因此也叫作“弹出栈顶元素”。

栈遵循 后进先出 的原则,Last In First Out,缩写为:LIFO。

出栈与入栈示意图

注意: 这里所说的“栈”与内存中的“栈空间”是两个不同的概念,但肯定也有相似处,不然为啥都带个栈。

2.2 接口设计

元素的数量: int size();

是否为空: boolean isEmpty();

入栈: void push(E element);

出栈: E pop();

获取栈顶元素: E top();

实现栈可以使用以前学习过的数据结构,比如:动态数组、链表。具体选用哪个都可以,因为栈只能在末尾添加元素,使用这两种数据结构在末尾添加元素的复杂度都是 O(1)

既然如此,选择使用动态数组实现栈。有两种方式,一种是继承,一种是引用。但是继承有坏处,如果继承动态数组 ArrayList,那么可以在栈中使用动态数组的一些方法(比如 get()remove()),这和预先设计相悖。因此选择使用引用的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Stack<E> {
    // 使用之前编写的 ArrayList,而非 JDK 的
    private List<E> list = new ArrayList<>();

    public int size(){
        return list.size();
    }

    public boolean isEmpty(){
        return list.isEmpty();
    }

    public void push(E element){
        list.add(element);
    }

    public E pop(){
        return list.remove(list.size() - 1);
    }

    public E top(){
        return list.get(list.size() - 1);
    }
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(11);
        stack.push(22);
        stack.push(33);
        stack.push(44);
        while (!stack.isEmpty()){
            System.out.println(stack.pop());
        }
    }
}

JDK 官方也提供了栈的实现,名字也叫 Stack,它选择的是继承:

1
public class Stack<E> extends Vector<E> { ... }

它继承了 VectorVector 和动态数组类似,但它是线程安全的,因此使用官方 JDK 的 Stack 可以使用动态数组的方法。

2.3 应用举例

浏览器的前进、后退就使用了栈,只不过是两个栈的结合使用。

应用说明

假设现在有两个栈,分别叫 A、B。

先访问网址 aa 的信息存入栈 A;然后又访问网址 b,将 b 的信息存入栈 A;最后访问网址 c,将 c 的信息存入栈 A。这时栈 A 的栈顶是网址 c,浏览器上显示的也是网址 c 的信息。

如果点击浏览器上的“后退”,此时会将栈 A 的栈顶元素弹出并放入到栈 B 中。这种情况下,栈 A 的栈顶元素是 b,浏览器上也将显示网址 b 的信息。再点击“后退”,将栈 A 的栈顶元素弹出并放入到栈 B 中。此刻,栈 A 的栈顶元素是 a,浏览器上显示的也是网址 a 的信息。

如果点击“前进”,那么会将栈 B 的栈顶元素弹出并放入到栈 A 中。此刻,栈 A 的栈顶元素是 b,浏览器上显示的也是网址 b 的信息。

如果又访问了网址 d,那么就会将 d 的信息存入栈 A,而栈 B 会被清空。

此时,栈 A 中的信息为 d - b - a ,而栈 B 是空栈,这代表无法使用“前进”或“后退”访问网址 c

再比如,软件的撤销(Undo)、恢复(Redo)功能,也都使用了栈。

2. 队列

2.1 基本含义

队列(Queue)是一种特殊的线性表,只能在 头尾两端 进行操作。

队尾(rear):只能从队尾添加元素,一般叫做 enQueue,入队

队头(front):只能从队头移除元素,一般叫做 deQueue,出队

队列遵循 先进先出 的原则,First In First Out,缩写为:FIFO。

队列

2.2 接口设计

元素的数量: int size();

是否为空: boolean isEmpty();

入队: void enQueue(E element);

出队: E deQueue();

获取队列的头元素: E font();

队列的内部实现是否可以直接使用以前学过的数据结构呢?

答案是肯定的,可以选择使用动态数组、链表。 优先选择 双向链表,因为队列只在 头尾 操作元素。

在队列中引用 双向链表,实现其功能:

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
public class Queue<E> {
    private List<E> list = new LinkedList<>();

    public int size(){
        return list.size();
    }

    public boolean isEmpty(){
        return list.isEmpty();
    }
    
    public void clear(){
        list.clear();
    }

    public void enQueue(E element){
        list.add(element);
    }

    public E deQueue(){
        return list.remove(0);
    }

    public E front(){
        return list.get(0);
    }
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

    public static void main(String[] args) {
        Queue<Integer> queue = new Queue<>();
        queue.enQueue(11);
        queue.enQueue(22);
        queue.enQueue(33);
        queue.enQueue(44);
        while (!queue.isEmpty()){
            System.out.println(queue.deQueue());
        }
    }

}

2.3 JDK 源码

JDK 官方也提供了队列(Queue),只不过这是一个接口。

1
public interface Queue<E> extends Collection<E> { ... }

接口区别

入队 : boolean offer(E e);

出队: E poll();

获取队列的头元素: E peek();

源码实现

在 JDK 中,队列是一个接口,查看 Queue 的实现类,可以看到有个熟悉的家伙 LinkedListLinkedList 还实现了队列的接口?

1
2
3
4
5
6
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{ 
    // ...
}

查看 Deque 的源码可知,接口 Deque 继承了 Queue 接口:

1
public interface Deque<E> extends Queue<E> { ... }

Deque 表示双端队列,接着往下看👇

3. 双端队列

双端队列是指能在 头尾 两端 添加删除 的队列。

英文 Deque 是 double ended queue 的简称。

接口设计

元素的数量: int size();

是否为空: boolean isEmpty();

从队尾入队: void enQueueRear(E element);

从队头出队: E deQueueFront();

从队头入队: void enQueueFront(E element);

从队尾出队: E deQueueRear();

获取队列的头元素: E front();

获取队列的尾元素: E rear();

3.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
/**
 * @author 默烦
 * @date 2020/7/2
 */
public class Deque<E> {
    private List<E> list = new LinkedList<>();

    public int size(){
        return list.size();
    }

    public boolean isEmpty(){
        return list.isEmpty();
    }
    
    public void clear(){
        list.clear();
    }

    public void enQueueRear(E element){
        list.add(element);
    }

    public E deQueueFront(){
        return list.remove(0);
    }

    public void enQueueFront(E element){
        list.add(0, element);
    }

    public E deQueueRear(){
        return list.remove(list.size() - 1);
    }

    public E front(){
        return list.get(0);
    }

    public E rear(){
        return list.get(list.size() - 1);
    }
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        Deque<Integer> deque = new Deque<>();
        deque.enQueueFront(11);
        deque.enQueueFront(22);
        deque.enQueueRear(33);
        deque.enQueueRear(44);
        /* 尾 44 33 11 22 首*/
        while (!deque.isEmpty()){
            System.out.println(deque.deQueueRear());
        }
    }
}

4. 循环队列

4.1 基本含义

循环队列 (Circle Queue)底层是用数组实现的。

循环队列

当需要删除一个元素(出队)时,就将队头指针指向的数据删除,之后使对头指针指向下一个元素。如:上图所示队列需要出队,那么删除元素 11,然后将 front 指向索引 1。

如果需要添加一个元素(入队),直接在末位索引后添加元素即可。如:上图所示队列需要入队元素 66,直接在索引 5 位置添加元素 66 即可。

似乎并没有循环?并不是!

还是针对上图所示队列,先出队两个元素(11、22),这时对头指针 front 指向索引 2 的元素,之后进行入队操作,先入队两个元素(66、77),这时数组似乎已满?

其实也不是,索引 1、2 的位置并没有元素。如果要进一步入队元素 88,可以将元素 88 添加至索引 0 的位置。等到整个数组空间全部用完,再进行动态扩容。(这个原理很像动态数组的扩容)

其实队列底层也可以使用 动态数组 实现,并且各项接口也可以优化到 O(1) 的时间复杂度,这个用数组实现并且优化之后的队列就叫作:循环队列

4.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
public class CircleQueue<E> {
    private int front;  // 队头指针
    private int size;
    private E[] elements;
    // 默认容量
    private static final int DEFAULT_CAPACITY = 10;

    public CircleQueue() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
    
    public void clear(){
        for (int i = 0; i < size; i++) {
            elements[(front + i) % elements.length] = null;
        }
        size = 0;
        front = 0;
    }

    public void enQueue(E element) {
        // 进行模运算 体现循环
        elements[(front + size) % elements.length] = element;
        size++;
    }

    public E deQueue() {
        E frontElement = elements[front];
        elements[front] = null;
        // 进行模运算 体现循环
        front = (front + 1) % elements.length;
        size--;
        return frontElement;
    }

    public E front() {
        return elements[front];
    }
    
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("capacity=").append(elements.length)
                .append(" size=").append(size)
                .append(" front=").append(front)
                .append(", [");
        for (int i = 0; i < elements.length; i++) {
            if (i != 0) {
                builder.append(", ");
            }
            builder.append(elements[i]);

        }
        builder.append("]");
        return builder.toString();
    }
}

注意: 添加元素(入队)和删除元素(出队)时,需要进行模运算,其主要原因就是在于循环二字。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void test2(){
        CircleQueue<Integer> queue = new CircleQueue<>();
        // 0 1 2 3 4 5 6 7 8 9
        for (int i = 0; i < 10; i++) {
            queue.enQueue(i);
        }
        // null null null null null 5 6 7 8 9
        for (int i = 0; i < 5; i++) {
            queue.deQueue();
        }
        // 15 16 17 18 19 5 6 7 8 9
        for (int i = 15; i < 20; i++) {
            queue.enQueue(i);
        }
        System.out.println(queue);
        while (!queue.isEmpty()){
            System.out.println(queue.deQueue());
        }
    }

4.3 动态扩容

现在队列的默认容量是 10 ,如果插入超过 10 个元素就会报错,因此需要进行动态扩容。循环队列扩容方式与动态数组的扩容方式类似,但也有所区别,区别也是在 循环 二字。

只有添加数据(入队)时才需要扩容:

1
2
3
4
5
6
7
public void enQueue(E element) {
    // 动态扩容
    ensureCapacity(size + 1);
    // 进行模运算 体现循环
    elements[(front + size) % elements.length] = element;
    size++;
}

扩容步骤:

  • 比较所需队列(数组)长度 B 与当前队列(数组)长度 A;
  • 如果 A >= B ,直接返回,不进行扩容
  • 反之,进行扩容:
    • 设置新容量 C 为当前长度(容量)A 的 1.5 倍
    • 创建新数组,其长度为 C
    • 将原数组(队列)的元素复制到新数组中。 注意头指针 front!
    • 将原数组赋值为新数组
    • 重置 front

扩容的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 保证要有capacity的容量(不考虑线程安全)
private void ensureCapacity(int capacity){
    // 当前elements.length 为 10,因为默认容量为10
    int oldCapacity = elements.length;
    if (oldCapacity >= capacity) return;
    // 新容量为新容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    E[] newElements = (E[]) new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        // 设置元素
        newElements[i] = elements[(i + front) % elements.length];
    }
    elements = newElements;
    // 重置front
    front = 0;
}

设置好动态扩容后,可以尝试插入超过默认容量的数据来进行测试。

4.4 索引映射封装

在代码编写中,多次编写了求循环队列索引的代码,可以将其封装一下,便于复用。

1
2
3
private int index(int index){
    return (front + index) % elements.length;
}

5. 循环双端队列

5.1 接口设计

循环双端队列 :可以进行 两端 添加、删除操作的循环队列。

循环双端队列 就是 循环队列双端队列 的结合。想要实现循环双端队列,需要使用到循环队列和双端队列。在实现循环双端队列时,也是采用与循环队列一样的 数组方式来实现 ,而接口则是 选择双端队列的接口

不多 BB,直接看实现:😎

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
public class CircleDeque<E> {
    private int front;  // 队头指针
    private int size;
    private E[] elements;
    // 默认容量
    private static final int DEFAULT_CAPACITY = 10;

    public CircleDeque() {
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }
    
    public void clear(){
        for (int i = 0; i < size; i++) {
            elements[index(i)] = null;
        }
        size = 0;
        front = 0;
    }

    /**
     * 从尾部入队
     */
    public void enQueueRear(E element) {
        ensureCapacity(size + 1);
        elements[index(size)] = element;
        size++;
    }

    /**
     * 从头部出队
     */
    public E deQueueFront() {
        E frontElement = elements[front];
        elements[front] = null;
        front = index(1);
        size--;
        return frontElement;
    }

    /**
     * 从头部入队
     */
    public void enQueueFront(E element) {
        ensureCapacity(size + 1);
        front = index(-1);
        elements[front] = element;
        size++;
    }

    /**
     * 从尾部出队
     */
    public E deQueueRear() {
        int rearIndex = index(size - 1);
        E rear = elements[rearIndex];
        elements[rearIndex] = null;
        size--;
        return rear;
    }

    public E front() {
        return elements[front];
    }

    public E rear() {
        return elements[index(size - 1)];
    }

    private int index(int index) {
        index += front;
        if (index < 0) {
            return index + elements.length;
        }
        return index % elements.length;
    }

    // 保证要有capacity的容量(不考虑线程安全)
    private void ensureCapacity(int capacity) {
        // 当前elements.length 为 10,因为默认容量为10
        int oldCapacity = elements.length;
        if (oldCapacity >= capacity) return;
        // 新容量为新容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[index(i)];
        }
        elements = newElements;
        // 重置front
        front = 0;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("capacity=").append(elements.length)
                .append(" size=").append(size)
                .append(" front=").append(front)
                .append(", [");
        for (int i = 0; i < elements.length; i++) {
            if (i != 0) {
                builder.append(", ");
            }
            builder.append(elements[i]);

        }
        builder.append("]");
        return builder.toString();
    }
}

动态扩容、toString() 与循环队列一样,可以直接沿用逻辑。

但是因为循环双端队列可以从队首、队尾操作,因此将队列指定索引转换为底层数组索引的 int index(int index) 方法要发生改变。

当传入的 index 是负数,且队首元素位于数组索引 0 的位置时,调用 index() 方法进行计算后,得到的是负数,需要规避这种情况:

1
2
3
4
5
6
7
private int index(int index) {
    index += front;
    if (index < 0) {
        return index + elements.length;
    }
    return index % elements.length;
}

还需要注意 从队首入队从队首出队从队尾入队从队尾出队 四个方法的实现。

5.2 模运算优化

应尽量避免乘、除、模、浮点数运算,因为它们的效率很低下。前文的实现中使用的大量的模运算,可以对这些模运算进行优化。

优化方式可以根据下列代码得出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
    int n = 19;
    int m = 10;
    
    // m > 0, n >= 0, n < 2m
    if (n >= m) {
        System.out.println(n - m);
    } else {
        System.out.println(n);
    }

    // m > 0, n >= 0, n < 2m
    // 上述运算等价于:
    System.out.println(n - (n >= m ? m : 0));
    
    System.out.println(n % m);
}

按照上述代码给出的思路,可以对 循环队列int index(int index) 方法进行优化:

1
2
3
4
5
private int index(int index){
    index += front;
    // 未考虑负数情况 循环队列中不会出现负数
    return index - (index >= elements.length ? elements.length : 0);
}

还可以对 循环双端队列 进行优化:

1
2
3
4
5
6
7
private int index(int index) {
    index += front;
    if (index < 0) {
        return index + elements.length;
    }
    return index - (index >= elements.length ? elements.length : 0);
}

已知 n >= 0m > 0,那么 n % m 等价于 n - (m > n ? 0 : m) 的前提条件是:n < 2m