1. 栈
栈是一种遵循先入后出逻辑的数据结构,。只能在栈顶添加或者删除元素,因此栈可以看作一种受限制的数组或链表。
1.1. 栈的常用操作
一般而言,编程语言都会内置栈类 stack
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push() | 入栈至栈顶 | O(1) |
| pop() | 栈顶元素出栈 | O(1) |
| peek() | 访问栈顶元素 | O(1) |
# 初始化栈
# Python 没有内置的栈类,可以把 list 当作栈来使用
stack: list[int] = []
# 元素入栈
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)
# 访问栈顶元素
peek: int = stack[-1]
# 元素出栈
pop: int = stack.pop()
# 获取栈的长度
size: int = len(stack)
# 判断是否为空
is_empty: bool = len(stack) == 0
/* 初始化栈 */
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. 栈的其他实现
根据栈的定义可以得知,栈的实现可以基于链表或者数组实现
1.2.1. 基于链表的实现
在此实现栈时,将链表的头节点视为栈顶,尾节点视为栈底。入栈操作视作在链表的头节点插入元素,出栈视作删除头节点,入栈和出栈方法称为头插法。
class LinkedListStack:
"""基于链表实现的栈"""
def __init__(self):
"""构造方法"""
self._peek: ListNode | None = None # 初始化栈顶为空
self._size: int = 0 # 初始化栈为空栈
def size(self) -> int:
"""获取栈的长度"""
return self._size
def is_empty(self) -> bool:
"""判断栈是否为空"""
return self._size == 0
def push(self, val: int):
"""入栈"""
node = ListNode(val)
node.next = self._peek #连接栈顶与新元素,头插法方法
self._peek = node
self._size += 1
def pop(self) -> int:
"""出栈"""
num = self.peek()
self._peek = self._peek.next
self._size -= 1
return num
def peek(self) -> int:
"""访问栈顶元素"""
if self.is_empty():
raise IndexError("栈为空")
return self._peek.val
def to_list(self) -> list[int]:
"""转化为列表用于打印"""
arr = []
node = self._peek
while node: #当前节点元素不为空
arr.append(node.val)
node = node.next
arr.reverse()
return arr
/* 基于链表实现的栈 */
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;
}
/* 将 List 转化为 Array 并返回 */
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;
}
};
1.2.2. 基于数组的实现
使用数组实现栈时,将数组的尾部作为栈顶。入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 O(1) 。
然而普通的数组会有容量的限制,因此在实现栈时,主要通过动态数组进行实现,以满足入栈元素的增加。
class ArrayStack:
"""基于数组实现的栈"""
def __init__(self):
"""构造方法"""
self._stack: list[int] = [] #初始化动态数组并作为一个栈,实际上,这里是直接初始化的一个列表
def size(self) -> int:
"""获取栈的长度"""
return len(self._stack)
def is_empty(self) -> bool:
"""判断栈是否为空"""
return self.size() == 0
def push(self, item: int):
"""入栈"""
self._stack.append(item)
def pop(self) -> int:
"""出栈"""
if self.is_empty():
raise IndexError("栈为空")
return self._stack.pop()
def peek(self) -> int:
"""访问栈顶元素"""
if self.is_empty():
raise IndexError("栈为空")
return self._stack[-1]
def to_list(self) -> list[int]:
"""返回列表用于打印"""
return self._stack
/* 基于数组实现的栈 */
class ArrayStack {
private:
vector<int> stack;
public:
/* 获取栈的长度 */
int size() {
return stack.size();
}
/* 判断栈是否为空 */
bool isEmpty() {
return stack.size() == 0;
}
/* 入栈 */
void push(int num) {
stack.push_back(num);
}
/* 出栈 */
int pop() {
int num = top();
stack.pop_back();
return num;
}
/* 访问栈顶元素 */
int top() {
if (isEmpty())
throw out_of_range("栈为空");
return stack.back();
}
/* 返回 Vector */
vector<int> toVector() {
return stack;
}
};
1.2.3. 实现方法的对比
| 类型 | 支持操作 | 时间效率 | 空间效率 |
|---|---|---|---|
| 基于链表 | 支持栈的操作与其余操作 | 速度更快,具有更加稳定的效率 | 由于需要额外存储指针存在可能造成一定的空间浪费 |
| 基于数组 | 支持栈的操作与其余操作 | 在不超出数组容量时,较快,超出后需扩容,较慢。平均效率较快 | 由于扩容的存在可能造成一定的空间浪费 |
1.3. 栈的应用
- 浏览器中的后退与前进、软件中的撤销与反撤销。
- 程序内存管理
2. 队列
队列(queue)是一种遵循先入先出规则的线性数据结构。队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。先入队和先出队的都是队首。
2.1. 队列的常见操作
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push() | 入队至队尾 | O(1) |
| pop() | 队首元素出队 | O(1) |
| peek() | 访问队首元素 | O(1) |
| 一般编程语言中都有现成的队列类 queue |
from collections import deque
# 初始化队列
# 在 Python 中,我们一般将双向队列类 deque 当作队列使用
# 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不推荐
que: deque[int] = deque()
# 元素入队
que.append(1)
que.append(3)
que.append(2)
que.append(5)
que.append(4)
'output = 13254'
# 访问队首元素
front: int = que[0]
'output = 1'
# 元素出队
pop: int = que.popleft()
'output = 1'
# 获取队列的长度
size: int = len(que)
# 判断队列是否为空
is_empty: bool = len(que) == 0
/* 初始化队列 */
queue<int> queue;
/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 访问队首元素 */
int front = queue.front();
'output = 1'
/* 元素出队 */
queue.pop();
'output = 1'
/* 获取队列的长度 */
int size = queue.size();
/* 判断队列是否为空 */
bool empty = queue.empty();
2.2. 队列的其他实现
队列和栈相同,也可以使用链表和数组实现,但是,数组依旧是使用动态数组来实现。
2.2.1. 基于链表的实现
将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。这样可以保证添加和删除时的时间复杂度均为 O(1)
class LinkedListQueue:
"""基于链表实现的队列"""
def __init__(self):
"""构造方法"""
self._front: ListNode | None = None # 头节点 front
self._rear: ListNode | None = None # 尾节点 rear
self._size: int = 0
def size(self) -> int:
"""获取队列的长度"""
return self._size
def is_empty(self) -> bool:
"""判断队列是否为空"""
return self._size == 0
def push(self, num: int):
"""入队"""
# 在尾节点后添加 num
node = ListNode(num)
# 如果队列为空,则令头、尾节点都指向该节点
if self._front is None:
self._front = node
self._rear = node
# 如果队列不为空,则将该节点添加到尾节点后
else:
self._rear.next = node
self._rear = node
self._size += 1
def pop(self) -> int:
num = self.peek()
if self.size() == 1:
self._front = None
self._rear = None
else:
self._front = self._front.next
self._size -= 1
return num
def peek(self) -> int:
"""访问队首元素"""
if self.is_empty():
raise IndexError("队列为空")
return self._front.val
def to_list(self) -> list[int]:
"""转化为列表用于打印"""
queue = []
temp = self._front
while temp:
queue.append(temp.val)
temp = temp.next
return queue
/* 基于链表实现的队列 */
class LinkedListQueue {
private:
ListNode *front, *rear; // 头节点 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) {
// 在尾节点后添加 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 并返回 */
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;
}
};
2.2.2. 基于数组的实现
在使用数组来实现栈的时候,由于数组的结构,因此在删除首元素的时候的时间复杂度为 O(n)
我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。
基于此设计,数组中包含元素的有效区间为 [front, rear - 1]
- 入队操作:将输入元素赋值给
rear索引处,并将size增加 1 。 - 出队操作:只需将
front增加 1 ,并将size减少 1 。 可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 O(1) 。
由于不断的入队和出队的过程,front 和 rear 都在向右移动,当它们到达数组尾部时(到达或要超过数组的长度限制)就无法继续移动了,此时为解决该问题,则将数组视为首尾相连的“环形数组”
环形数组:让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现
那么公式就变成了:rear = (front + size) % capacity
class ArrayQueue:
"""基于环形数组实现的队列"""
def __init__(self, size: int):
"""构造方法"""
self._nums: list[int] = [0] * size # 用于存储队列元素的数组
self._front: int = 0 # 队首指针,指向队首元素
self._size: int = 0 # 队列长度
def capacity(self) -> int:
"""获取队列的容量"""
return len(self._nums)
def size(self) -> int:
"""获取队列的长度"""
return self._size
def is_empty(self) -> bool:
"""判断队列是否为空"""
return self._size == 0
def push(self, num: int):
"""入队"""
if self._size == self.capacity():#到达数组最大容量
raise IndexError("队列已满")
# 计算队尾指针,指向队尾索引 + 1
# 通过取余操作实现 rear 越过数组尾部后回到头部
rear: int = (self._front + self._size) % self.capacity()
# 将 num 添加至队尾
self._nums[rear] = num
self._size += 1
def pop(self) -> int:
"""出队"""
num: int = self.peek()
# 队首指针向后移动一位,若越过尾部,则返回到数组头部
self._front = (self._front + 1) % self.capacity()
self._size -= 1
return num
def peek(self) -> int:
"""访问队首元素"""
if self.is_empty():
raise IndexError("队列为空")
return self._nums[self._front]
def to_list(self) -> list[int]:
"""返回列表用于打印"""
res = [0] * self.size()
j: int = self._front
for i in range(self.size()):
res[i] = self._nums[(j % self.capacity())]
j += 1
return res
"""Driver Code"""
if __name__ == "__main__":
# 初始化队列|
queue = ArrayQueue(10)
/* 基于环形数组实现的队列 */
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;
}
// 计算队尾指针,指向队尾索引 + 1
// 通过取余操作实现 rear 越过数组尾部后回到头部
int rear = (front + queSize) % queCapacity;
// 将 num 添加至队尾
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 并返回 */
vector<int> toVector() {
// 仅转换有效长度范围内的列表元素
vector<int> arr(queSize);
for (int i = 0, j = front; i < queSize; i++, j++) {
arr[i] = nums[j % queCapacity];
}
return arr;
}
};
2.3. 队列的应用
- 淘宝订单:按照顺序进行处理
- 代办事项:事项先来先办
3. 双向队列
双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
3.1. 双向队列常用操作
可以直接使用编程语言中已实现的双向队列类:deque
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push_first() | 元素添到队首 | O(1) |
| push_last() | 元素添到队尾 | O(1) |
| pop_first() | 删除队首元素 | O(1) |
| pop_last() | 删除队尾元素 | O(1) |
| peek_first() | 访问队首元素 | O(1) |
| peek_last() | 访问队尾元素 | O(1) |
from collections import deque
# 初始化双向队列
deq: deque[int] = deque()
# 元素入队
deq.append(2) # 添加至队尾
deq.append(5)
deq.append(4)
deq.appendleft(3) # 添加至队首
deq.appendleft(1)
# 访问元素
front: int = deq[0] # 队首元素
rear: int = deq[-1] # 队尾元素
# 元素出队
pop_front: int = deq.popleft() # 队首元素出队
pop_rear: int = deq.pop() # 队尾元素出队
# 获取双向队列的长度
size: int = len(deq)
# 判断双向队列是否为空
is_empty: bool = len(deq) == 0
/* 初始化双向队列 */
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();
3.2. 双向队列的实现:
www.hello-algo.com/chapter_sta…
3.3. 双向队列的应用
代替栈来实现软件中的撤销功能
4. 小结
4.1. 栈:
- 栈是一种遵循先入后出逻辑的数据结构,栈顶元素称为栈顶,元素加入栈称为入栈,删除称为出栈。只能在栈顶添加或者删除元素。
- 一般而言,编程语言都会内置栈类 stack
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push() | 入栈至栈顶 | O(1) |
| pop() | 栈顶元素出栈 | O(1) |
| peek() | 访问栈顶元素 | O(1) |
4.2. 队列:
- 队列(queue)是一种遵循先入先出规则的线性数据结构。队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。先入队和先出队的都是队首。
- 一般编程语言中都有现成的队列类 queue
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push() | 入队至队尾 | O(1) |
| pop() | 队首元素出队 | O(1) |
| peek() | 访问队首元素 | O(1) |
4.3. 双向队列:
- 双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
- 可以直接使用编程语言中已实现的双向队列类:deque
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| push_first() | 元素添到队首 | O(1) |
| push_last() | 元素添到队尾 | O(1) |
| pop_first() | 删除队首元素 | O(1) |
| pop_last() | 删除队尾元素 | O(1) |
| peek_first() | 访问队首元素 | O(1) |
| peek_last() | 访问队尾元素 | O(1) |