封面:
列队/栈
列队-栈的基本原理
计算机的两种存储方式,顺序存储(数组)和链式存储(链表)在之前的文章有详细介绍,可以去主页查看。
这部分讲解列队和栈的基本原理。
先说概念吧,其实队列和栈都是「操作受限」的数据结构。说它操作受限,主要是和基本的数组和链表相比,它们提供的 API 是不完整的。
前面实现的数组和链表,增删查改的 API 都实现过了,你可以对任意一个索引元素进行增删查改,只要索引不越界,就可以进行任意操作。
但是对于队列和栈,它们的操作是受限的:队列只能在一端插入元素,另一端删除元素;栈只能在某一端插入和删除元素。
形象地说,队列只允许在队尾插入元素,在队头删除元素,栈只允许在栈顶插入元素,从栈顶删除元素:
图中把栈竖着画,队列横着画,只是为了更形象,但实际上它们底层都是数组和链表实现的。
这两种输出结构的基本API如下(cpp):
# 创建列队
std::queue<int> myQueue;
// 入队
myQueue.push(10);
// 访问队首元素
std::cout << "Front element: " << myQueue.front() << std::endl;
// 出队
myQueue.pop();
// 再次访问队首元素
std::cout << "Front element after pop: " << myQueue.front() << std::endl;
// 遍历队列并输出
std::cout << "Queue elements: ";
while (!myQueue.empty()) {
std::cout << myQueue.front() << " ";
myQueue.pop();
}
//创建栈
std::stack<int> myStack;
// 入栈
myStack.push(10);
myStack.push(20);
myStack.push(30);
// 访问栈顶元素
std::cout << "Top element: " << myStack.top() << std::endl;
// 出栈
myStack.pop();
// 再次访问栈顶元素
std::cout << "Top element after pop: " << myStack.top() << std::endl;
// 遍历栈并输出
std::cout << "Stack elements: ";
while (!myStack.empty()) {
std::cout << myStack.top() << " ";
myStack.pop(); // 每次迭代后出栈
}
std::cout << std::endl;
不同编程语言中,队列和栈提供的方法名称可能不一样,但每个方法的效果肯定是一样的。
用链表实现列队和栈
链表作为底层实现链表和栈是比较简单的,直接调用双链表的API即可。
template <typename T>
class MyLinkedStack {
private:
std::list<T> list;
public:
// 向栈顶加入元素,时间复杂度 O(1)
void push(const T& element) {
list.push_back(element);
}
// 从栈顶弹出元素,时间复杂度 O(1)
T pop() {
T element = list.back();
list.pop_back();
return element;
}
// 查看栈顶元素,时间复杂度 O(1)
T& peek() {
return list.back();
}
// 返回栈中的元素个数,时间复杂度 O(1)
int size() {
return list.size();
}
};
用链表实现队列也是一样的,也直接调用双链表的 API 就可以了:
template <typename T>
class MyLinkedQueue {
private:
std::list<T> list;
public:
// 向队尾插入元素,时间复杂度 O(1)
void push(const T& element) {
list.push_back(element);
}
// 从队头删除元素,时间复杂度 O(1)
T pop() {
T element = list.front();
list.pop_front();
return element;
}
// 查看队头元素,时间复杂度 O(1)
T& peek() {
return list.front();
}
// 返回队列中的元素个数,时间复杂度 O(1)
int size() {
return list.size();
}
};
环形数组技巧
数组是没有环形的说法的,但是我们可以通过逻辑将数组变成环形的,比如:
int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]);
int i = 0;
// 模拟环形数组,这个循环永远不会结束
while (true) {
std::cout << arr[i] << std::endl;
i = (i + 1) % length;
}
这个技巧如何帮助我们在 O(1)
的时间在数组头部增删元素呢?
*核心原理
上面只是让大家对环形数组有一个直观地印象,环形数组的关键在于,它维护了两个指针 start
和 end
,start
指向第一个有效元素的索引,end
指向最后一个有效元素的下一个位置索引。这样,当我们在数组头部添加或删除元素时,只需要移动 start
索引,而在数组尾部添加或删除元素时,只需要移动 end
索引。当 start, end
移动超出数组边界(< 0
或 >= arr.length
)时,我们可以通过求模运算 %
让它们转一圈到数组头部或尾部继续工作,这样就实现了环形数组的效果。
代码实现:
#include <iostream>
template<typename T>
class CycleArray {
private:
T *arr;
int start;
int end;
int count;
int size;
public:
CycleArray() : CycleArray(1) {}
CycleArray(int size) {
this->size = size;
this->arr = new T[size];
this->start = 0;
this->end = 0;
this->count = 0;
}
~CycleArray() {
delete[] arr;
}
void resize(int newSize) {
T *newArr = new T[newSize];
for (int i = 0; i < count; ++i) {
newArr[i] = arr[(start + i) % size];
}
delete[] arr;
arr = newArr;
start = 0;
end = count;
size = newSize;
}
void addFirst(T val) {
if (isFull()) {
resize(size * 2);
}
start = (start - 1 + size) % size;
arr[start] = val;
count++;
}
void removeFirst() {
if (isEmpty()) {
throw std::runtime_error("Array is empty");
}
arr[start] = T(); // Clear the element
start = (start + 1) % size;
count--;
if (count > 0 && count == size / 4) {
resize(size / 2);
}
}
void addLast(T val) {
if (isFull()) {
resize(size * 2);
}
arr[end] = val;
end = (end + 1) % size;
count++;
}
void removeLast() {
if (isEmpty()) {
throw std::runtime_error("Array is empty");
}
end = (end - 1 + size) % size;
arr[end] = T(); // Clear the element
count--;
if (count > 0 && count == size / 4) {
resize(size / 2);
}
}
T getFirst() {
if (isEmpty()) {
throw std::runtime_error("Array is empty");
}
return arr[start];
}
T getLast() {
if (isEmpty()) {
throw std::runtime_error("Array is empty");
}
return arr[(end - 1 + size) % size];
}
bool isFull() {
return count == size;
}
int getSize() {
return count;
}
bool isEmpty() {
return count == 0;
}
};
在数组增删头部元素的时间复杂度是 O(N)
,因为需要搬移元素。但是,如果我们使用环形数组,其实是可以实现在 O(1)
的时间复杂度内增删头部元素的。