栈和队列的实现
一、数据结构概述
栈(Stack) 和 队列(Queue) 是两种基础且重要的线性数据结构:
-
栈:后进先出(LIFO),支持入栈(Push)、出栈(Pop)、获取栈顶元素(Top)等操作。典型应用场景包括函数调用栈、表达式求值、括号匹配等。
-
队列:先进先出(FIFO),支持入队(Push)、出队(Pop)、获取队首/队尾元素(Front/Back)等操作。典型应用场景包括任务调度、缓冲区管理等。
二、队列的单链表实现
1. 核心数据结构
// 队列节点(单链表)
typedef struct QListNode {int val; // 存储数据struct QListNode* next; // 指向下一个节点
} QNode;// 队列结构
typedef struct Queue {QNode* front; // 队首指针(出队位置)QNode* tail; // 队尾指针(入队位置)int size; // 队列当前元素数量
} Queue;
2. 关键操作实现
入队(QueuePush
)
-
逻辑:在队尾添加新节点,维护
tail
指针。 -
时间复杂度:O(1)
void QueuePush(Queue* q, int x) {QNode* NewNode = (QNode*)malloc(sizeof(QNode));NewNode->val = x;NewNode->next = NULL;if (q->front == NULL) { // 队列为空q->front = q->tail = NewNode;} else { // 队列非空q->tail->next = NewNode;q->tail = NewNode;}q->size++;
}
出队(QueuePop
)
-
逻辑:删除队首节点,维护
front
指针。 -
时间复杂度:O(1)
void QueuePop(Queue* q) {if (q->front == q->tail) { // 只有一个元素free(q->front);q->front = q->tail = NULL;} else { // 多个元素QNode* tmp = q->front;q->front = q->front->next;free(tmp);}q->size--;
}
判空:检查front
指针是否为NULL
(时间复杂度O(1))
bool QueueEmpty(Queue* q)
{assert(q); // 确保队列指针非空return q->front == NULL; // 队首为空表示队列为空
}
销毁队列:循环释放所有节点内存,避免内存泄漏。
//销毁队列
void QueueDestroy(Queue* q)
{assert(q);assert(q->front);while (q->size != 0){if (q->front == q->tail){free(q->front);}else{QNode* tmp = q->front->next;free(q->front);q->front = tmp;}q->size--;}q->front = NULL;q->tail = NULL;q->size = 0;
}
三、栈的动态数组实现
1. 核心数据结构
typedef int STDataType;typedef struct Stack {STDataType* arr; // 动态数组int top; // 栈顶指针(指向下一个可用位置)int capacity; // 当前容量
} ST;
2. 关键操作实现
入栈(STPush
)
-
逻辑:动态扩容后插入元素到栈顶。
-
时间复杂度:均摊O(1)
void STPush(ST* ps, STDataType x) {CheckCapacity(ps); // 检查并扩容ps->arr[ps->top] = x;ps->top++;
}// 动态扩容函数
void CheckCapacity(ST* ps) {if (ps->capacity == ps->top) {int newcapacity = (ps->capacity == 0) ? 4 : 2 * ps->capacity;STDataType* tmp = realloc(ps->arr, newcapacity * sizeof(STDataType));ps->arr = tmp;ps->capacity = newcapacity;}
}
出栈(STPop
)
-
逻辑:仅移动栈顶指针,无需实际删除数据。
-
时间复杂度:O(1)
void STPop(ST* ps) {assert(!STEmpty(ps));ps->top--;
}
-
获取栈顶元素:返回
arr[top - 1]
(时间复杂度O(1))。
STDataType STTop(ST* ps)//输出栈顶元素
{assert(ps);assert(!STEmpty(ps));return ps->arr[ps->top - 1];
}
-
判空:检查
top
是否为0(时间复杂度O(1))。
bool STEmpty(ST* ps)
{assert(ps); // 确保栈指针非空return ps->top == 0; // 栈顶指针为0表示空栈
}
四、实现对比与性能分析
1. 数据结构选择与内存管理
特性 | 队列(单链表) | 栈(动态数组) |
---|---|---|
数据结构 | 单链表(离散内存) | 动态数组(连续内存) |
内存分配 | 动态分配节点,按需增长 | 预先分配连续内存,按需扩容 |
内存开销 | 每个节点需额外存储指针(空间开销大) | 无额外指针,仅存储数据(空间紧凑) |
扩容/缩容 | 无需扩容,直接插入新节点 | 需动态扩容(通常翻倍),可能内存复制 |
2. 核心操作复杂度
操作 | 队列(单链表) | 栈(动态数组) |
---|---|---|
插入(Push) | O(1) | 均摊O(1)(扩容时可能O(n)) |
删除(Pop) | O(1) | O(1) |
访问头部 | O(1)(直接通过front 指针) | 不支持(栈仅允许访问栈顶) |
随机访问 | 不支持 | 支持(但受栈操作限制) |
3. 优点与缺点
1. 队列(单链表)
-
优点:
-
动态大小:无需预先分配内存,适合元素数量不固定的场景。
-
高效操作:入队(尾部插入)和出队(头部删除)均为O(1)。
-
灵活性:适合频繁插入和删除的场景(如任务调度)。
-
-
缺点:
-
内存碎片:节点离散存储,可能导致内存碎片。
-
额外指针开销:每个节点需存储
next
指针,空间利用率较低。 -
遍历成本高:若需遍历队列,时间复杂度为O(n)。
-
2 栈(动态数组)
-
优点:
-
内存紧凑:连续存储,缓存友好,访问速度快。
-
高效扩容:均摊时间复杂度为O(1)(扩容策略优化)。
-
快速操作:入栈和出栈均为O(1),适合高频操作场景。
-
-
缺点:
-
扩容开销:扩容时需复制数据,可能引发短暂性能下降。
-
容量限制:初始容量需合理设置,否则频繁扩容影响效率。
-
内存浪费:若容量远大于实际需求,可能造成内存浪费。
-
3. 关键设计差异
-
操作逻辑:
-
队列是先进先出(FIFO),操作分别在队尾(入队)和队首(出队)。
-
栈是后进先出(LIFO),所有操作集中在栈顶。
-
-
内存管理:
-
队列通过指针维护动态链表,内存灵活但碎片化。
-
栈通过动态数组管理内存,连续存储但需处理扩容。
-
-
错误处理:
-
队列需处理空队列的
Pop
操作(代码中使用assert
强制检查)。 -
栈需处理空栈的
Pop
和Top
操作,以及扩容失败时的内存分配问题。
-
4. 总结
队列和栈在实现上的核心差异源于其操作特性(FIFO vs LIFO)和底层数据结构的选择(链表 vs 数组)。
-
队列的单链表实现牺牲空间效率换取了操作的灵活性和动态扩展能力。
-
栈的动态数组实现以潜在扩容开销为代价,换取了内存紧凑性和高速访问性能。
选择队列(链表)的情况:
-
需要频繁插入和删除,且元素数量不可预测。
-
对内存碎片不敏感,但要求严格O(1)操作时间。
选择栈(数组)的情况:
-
元素数量相对稳定或可预测。
-
需要高频访问栈顶元素,且追求内存连续性带来的性能优势。