Stack
栈(stack)是一种遵循先入后出(LIFO)逻辑的线性数据结构:我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。
Stack in STL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <stack>
stack<int> stack;
stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4);
int top = stack.top();
stack.pop();
int size = stack.size();
bool empty = stack.empty();
|
栈的实现
由于数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性,下面我们分别用数组和链表实现栈的功能!
基于数组的实现
使用数组实现栈时,我们可以将数组的尾部作为栈顶。由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| vector <int> stack;
int size() { return stack.size(); }
bool empty() { return stack.size() == 0; }
void push(int num){ stack.push_back(num); }
void pop() { stack.pop_back(); }
int top() { if (isempty()) { throw out_of_range("栈为空"); } return stack.back(); }
|
基于链表的实现
使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。
- 对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。
- 对于出栈操作,只需将头节点从链表中删除即可。
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
| class LinkedListStack { private: ListNode *stackTop; int stkSize;
public: LinkedListStack() { stackTop = nullptr; stkSize = 0; }
~LinkedListStack() { freeMemoryLinkedList(stackTop); }
int size() { return stkSize; }
bool isEmpty() { return size() == 0; }
void push(int num) { ListNode *node = new ListNode(num); node->next = stackTop; stackTop = node; stkSize++; }
int pop() { int num = top(); ListNode *tmp = stackTop; stackTop = stackTop->next; delete tmp; stkSize--; return num; }
int top() { if (isEmpty()) throw out_of_range("栈为空"); return stackTop->val; }
vector<int> toVector() { ListNode *node = stackTop; vector<int> res(size()); for (int i = res.size() - 1; i >= 0; i--) { res[i] = node->val; node = node->next; } return res; } };
|
Queue
队列(queue)是一种遵循先入先出(FIFO)规则的线性数据结构。队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。
Queue in STL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <queue>
queue<int> queue;
queue.push(1); queue.push(3); queue.push(2); queue.push(5); queue.push(4);
int front = queue.front();
queue.pop();
int size = queue.size();
bool empty = queue.empty();
|
队列的实现
基于数组的实现
在数组中删除首元素的时间复杂度为 O(n) ,这会导致出队操作效率较低。然而我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。
基于此设计,数组中包含元素的有效区间为 [front, rear - 1]。
- 入队操作:将输入元素赋值给
rear 索引处,并将 size 增加 1 。
- 出队操作:只需将
front 增加 1 ,并将 size 减少 1 。
可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 O(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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| class ArrayQueue { private: int *nums; int front; int queSize; int queCapacity;
public: ArrayQueue(int capacity) { nums = new int[capacity]; queCapacity = capacity; front = queSize = 0; }
~ArrayQueue() { delete[] nums; }
int capacity() { return queCapacity; }
int size() { return queSize; }
bool isEmpty() { return size() == 0; }
void push(int num) { if (queSize == queCapacity) { cout << "队列已满" << endl; return; } int rear = (front + queSize) % queCapacity; nums[rear] = num; queSize++; }
int pop() { int num = peek(); front = (front + 1) % queCapacity; queSize--; return num; }
int peek() { if (isEmpty()) throw out_of_range("队列为空"); return nums[front]; }
vector<int> toVector() { vector<int> arr(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { arr[i] = nums[j % queCapacity]; } return arr; } };
|
基于链表的实现
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
| class LinkedListQueue { private: ListNode *front, *rear; int queSize;
public: LinkedListQueue() { front = nullptr; rear = nullptr; queSize = 0; }
~LinkedListQueue() { freeMemoryLinkedList(front); }
int size() { return queSize; }
bool isEmpty() { return queSize == 0; }
void push(int num) { ListNode *node = new ListNode(num); if (front == nullptr) { front = node; rear = node; } else { rear->next = node; rear = node; } queSize++; }
int pop() { int num = peek(); ListNode *tmp = front; front = front->next; delete tmp; queSize--; return num; }
int peek() { if (size() == 0) throw out_of_range("队列为空"); return front->val; }
vector<int> toVector() { ListNode *node = front; vector<int> res(size()); for (int i = 0; i < res.size(); i++) { res[i] = node->val; node = node->next; } return res; } };
|
▶
在不断进行入队和出队的过程中,front 和 rear 都在向右移动,当它们到达数组尾部时就无法继续移动了,这时应该怎么办呢?
我们可以将数组视为首尾相接的“环形数组”。对于环形数组,我们需要让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现!
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
| class ArrayQueue { private: int *nums; int front; int queSize; int queCapacity;
public: ArrayQueue(int capacity) { nums = new int[capacity]; queCapacity = capacity; front = queSize = 0; }
~ArrayQueue() { delete[] nums; }
int capacity() { return queCapacity; }
int size() { return queSize; }
bool isEmpty() { return size() == 0; }
void push(int num) { if (queSize == queCapacity) { cout << "队列已满" << endl; return; } int rear = (front + queSize) % queCapacity; nums[rear] = num; queSize++; }
int pop() { int num = peek(); front = (front + 1) % queCapacity; queSize--; return num; }
int peek() { if (isEmpty()) throw out_of_range("队列为空"); return nums[front]; }
vector<int> toVector() { vector<int> arr(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { arr[i] = nums[j % queCapacity]; } return arr; } };
|
双向队列
在队列中,我们仅能删除头部元素或在尾部添加元素。双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
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
| #include <deque>
deque<int> deque;
deque.push_back(2); deque.push_back(5); deque.push_back(4);
deque.push_front(3); deque.push_front(1);
int front = deque.front(); int back = deque.back();
deque.pop_front(); deque.pop_back();
int size = deque.size();
bool empty = deque.empty();
|
双向队列的实现
基于数组的实现
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
| class ArrayDeque { private: vector<int> nums; int front; int queSize;
public: ArrayDeque(int capacity) { nums.resize(capacity); front = queSize = 0; }
int capacity() { return nums.size(); }
int size() { return queSize; }
bool isEmpty() { return queSize == 0; }
int index(int i) { return (i + capacity()) % capacity(); }
void pushFirst(int num) { if (queSize == capacity()) { cout << "双向队列已满" << endl; return; } front = index(front - 1); nums[front] = num; queSize++; }
void pushLast(int num) { if (queSize == capacity()) { cout << "双向队列已满" << endl; return; } int rear = index(front + queSize); nums[rear] = num; queSize++; }
int popFirst() { int num = peekFirst(); front = index(front + 1); queSize--; return num; }
int popLast() { int num = peekLast(); queSize--; return num; }
int peekFirst() { if (isEmpty()) throw out_of_range("双向队列为空"); return nums[front]; }
int peekLast() { if (isEmpty()) throw out_of_range("双向队列为空"); int last = index(front + queSize - 1); return nums[last]; }
vector<int> toVector() { vector<int> res(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[index(j)]; } return res; } };
|
基于链表的实现
对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。
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
| struct DoublyListNode { int val; DoublyListNode *next; DoublyListNode *prev; DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) { } };
class LinkedListDeque { private: DoublyListNode *front, *rear; int queSize = 0;
public: LinkedListDeque() : front(nullptr), rear(nullptr) { }
~LinkedListDeque() { DoublyListNode *pre, *cur = front; while (cur != nullptr) { pre = cur; cur = cur->next; delete pre; } }
int size() { return queSize; }
bool isEmpty() { return size() == 0; }
void push(int num, bool isFront) { DoublyListNode *node = new DoublyListNode(num); if (isEmpty()) front = rear = node; else if (isFront) { front->prev = node; node->next = front; front = node; } else { rear->next = node; node->prev = rear; rear = node; } queSize++; }
void pushFirst(int num) { push(num, true); }
void pushLast(int num) { push(num, false); }
int pop(bool isFront) { if (isEmpty()) throw out_of_range("队列为空"); int val; if (isFront) { val = front->val; DoublyListNode *fNext = front->next; if (fNext != nullptr) { fNext->prev = nullptr; front->next = nullptr; } delete front; front = fNext; } else { val = rear->val; DoublyListNode *rPrev = rear->prev; if (rPrev != nullptr) { rPrev->next = nullptr; rear->prev = nullptr; } delete rear; rear = rPrev; } queSize--; return val; }
int popFirst() { return pop(true); }
int popLast() { return pop(false); }
int peekFirst() { if (isEmpty()) throw out_of_range("双向队列为空"); return front->val; }
int peekLast() { if (isEmpty()) throw out_of_range("双向队列为空"); return rear->val; }
vector<int> toVector() { DoublyListNode *node = front; vector<int> res(size()); for (int i = 0; i < res.size(); i++) { res[i] = node->val; node = node->next; } return res; } };
|
▶
撤销(undo)和反撤销(redo)具体是如何实现的?
使用两个栈,栈 A 用于撤销,栈 B 用于反撤销。
每当用户执行一个操作,将这个操作压入栈 A ,并清空栈 B 。
当用户执行“撤销”时,从栈 A 中弹出最近的操作,并将其压入栈 B 。
当用户执行“反撤销”时,从栈 B 中弹出最近的操作,并将其压入栈 A 。