一、栈的核心知识点
1、栈的核心定义与特性
- 核心操作限制
栈仅允许在表的一端进行插入(入栈)和删除(出栈)操作,这一端被称为栈顶(Top);而表的另一端是固定的,无法进行任何操作,被称为栈底(Bottom)。
栈底始终是第一个入栈的元素所在位置,栈顶则随元素的入栈、出栈动态移动。
- 核心原则:后进先出(LIFO,Last In First Out)
最后入栈的元素,一定会最先出栈;最先入栈的元素,只能最后出栈。
举例:元素按 1→2→3→4 顺序入栈,出栈顺序只能是 4→3→2→1;若入栈过程中穿插出栈(如1入栈→1出栈→2、3入栈→3出栈→4入栈),则出栈顺序为 1→3→4→2,仍符合"后进先出"。
- 栈的空/满状态
• 空栈:栈中没有任何元素,此时栈顶与栈底重合,是栈的初始状态。
• 满栈:针对静态栈(数组实现),栈的存储空间被元素占满,无法再执行入栈操作;动态栈(链表实现)无"满栈"概念,只要内存足够,可一直入栈。
2、栈的基本术语与操作
所有操作的时间复杂度均为O(1)(无循环/遍历),这是栈的高效性核心,以下是栈的基础操作(所有操作均围绕栈顶展开,无随机访问),且操作前需做合法性校验(如出栈/取栈顶前需判空,入栈前需判满(静态栈)):
|---------|-----------|------------------------|-----------------------------------------------------|-----------|
| 操作名称 | 英文标识 | 操作含义 | 核心实现逻辑 | 时间复杂度 |
| 初始化栈 | STInit | 创建一个空栈,初始化动态数组、栈顶指针和容量 | 将数组指针 a 置为 NULL,top(指向栈顶下一个位置)和 capacity 置为 0 | O(1) |
| 销毁栈 | STDestroy | 释放栈的动态数组内存,恢复为空栈 | 释放数组内存,重置指针、top 和 capacity | O(1) |
| 入栈 | STPush | 在栈顶插入新元素 | 检查容量,若已满则扩容(初始容量为4,后续扩容为2倍),将元素存入 a[top] 并将 top++ | O(1) (均摊) |
| 出栈 | STPop | 删除栈顶元素 | 校验栈非空,直接将 top--(无需销毁元素,覆盖即可) | O(1) |
| 获取栈顶元素 | STTop | 返回栈顶元素的值 | 校验栈非空,返回 a[top - 1] | O(1) |
| 判断栈是否为空 | STEmpty | 检查栈中是否无元素 | 返回 top == 0 的布尔值 | O(1) |
| 获取栈大小 | STSize | 返回栈中有效元素的个数 | 直接返回 top(因 top 指向栈顶下一个位置,其值即为元素个数) | O(1) |
3. 实现方式
操作合法性原则:空栈不可出栈、不可取栈顶;满栈不可入栈,违反则会出现"栈下溢"(空栈出栈)或"栈上溢"(满栈入栈)的错误。
二、栈的实现方式
栈的实现基于线性表的两种基础结构(数组、链表),两种方式各有优劣,适用于不同开发场景,核心要求均为保证入栈、出栈操作在栈顶完成,且时间复杂度O(1)。
数组实现(静态/动态数组栈)
核心设计
• 用数组存储栈的元素,数组的一端(通常为末尾)作为栈顶,另一端(数组起始位置[0])作为栈底。
• 用整型变量top作为栈顶指针,表示栈顶元素的下一个位置(常用设计),或直接指向栈顶元素;栈底指针固定为0,无需额外变量。
👉 推荐设计:top初始值为0(空栈,栈顶=栈底),入栈时arr[top] = 元素,再top++;出栈时top--,栈顶元素为arr[top-1]。
栈可以用数组或链表实现,数组实现更高效,因为栈顶操作对应数组的尾插尾删,时间复杂度为O(1)。
栈的完整C语言实现(动态数组版)
stack.h
cpp
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct stack{
STDataType* a;
int top;
int capacity;
}ST;
//初始化和销毁
void STInit(ST* pst);
void STDestroy(ST* pst);
//入栈
void STPush(ST* pst, STDataType x);
//出栈
void STPop(ST* pst);
//获取栈顶元素
STDataType STTop(ST* pst);
//判断栈是否为空
bool STEmpty(ST* pst);
//获取栈中元素个数
int STSize(ST* pst);
//打印栈中元素
void STPrint(ST* pst);
stack.c
cpp
#include "stack.h"
// 初始化和销毁
void STInit(ST *pst)
{
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下一个位置,相当于size
pst->top = 0;
// top指向栈顶数据
// pst->top = -1;
pst->capacity = 0;
}
void STDestroy(ST *pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
// 入栈和出栈
void STPush(ST *pst, STDataType x)
{
assert(pst);
//扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType *tmp = (STDataType *)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST *pst)
{
assert(pst);
assert(pst->top > 0);//top=0时,栈为空
pst->top--;
}
// 获取栈顶元素
STDataType STTop(ST *pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判断栈是否为空
bool STEmpty(ST *pst)
{
assert(pst);
return pst->top == 0;
}
// 获取栈中元素个数
int STSize(ST *pst)
{
assert(pst);
return pst->top;
}
test.c
cpp
#include "stack.h"
int main()
{
ST st;
STInit(&st);
STPush(&st, 1);
STPush(&st, 2);
STPush(&st, 3);
STPush(&st, 4);
STPush(&st, 5);
printf("top=%d\n", STTop(&st));
STPop(&st);
printf("top=%d\n", STTop(&st));
STPop(&st);
printf("top=%d\n", STTop(&st));
int size = STSize(&st);
printf("size=%d\n", size);
while(!STEmpty(&st))
{
printf("%d ", STTop(&st));
STPop(&st);
}
if (STEmpty(&st))
{
printf("stack is empty\n");
}
STDestroy(&st);
return 0;
}
三、栈的核心特性延伸:无随机访问
栈作为受限线性表,与普通数组/链表的核心区别是不支持随机访问:
• 普通数组/链表可通过下标/指针直接访问任意位置的元素(如arr[5]、node->next->next);
• 栈只能访问栈顶元素,若要访问栈中其他位置的元素,必须先将其上方的所有元素依次出栈,直到目标元素成为栈顶,且访问后无法恢复原栈结构(除非重新入栈)。
✅ 例:栈中元素为1(底)→2→3→4(顶),要访问元素2,需先出栈4、3,使2成为栈顶;访问后,栈中仅剩1、2,若要恢复原栈,需重新入栈3、4。
四、栈的关键注意事项
-
数组栈的扩容策略:动态数组栈的初始容量不宜过大/过小,通常初始容量为4/8,满栈时扩容为原容量的2倍(平衡扩容开销和内存浪费)。
-
链表栈的节点释放:出栈时必须释放节点的内存,避免内存泄漏;销毁栈时需逐个释放所有节点,不能仅释放栈顶指针。
-
栈顶指针的设计:数组栈的top指针有两种设计(指向栈顶元素/栈顶下一个位置),项目中需保持统一,避免逻辑混乱。
-
空栈校验:所有对栈顶的操作(出栈、取栈顶)前,必须先判断栈是否为空,否则会导致野指针访问(链表栈)或数组越界(数组栈)。
-
元素类型的通用性:实际开发中,可通过C语言的typedef或C++的模板定义栈的元素类型,使栈支持任意数据类型(如int、char、结构体),提高复用性。
三、队列的核心知识点
1. 队列的核心定义与特性
- 核心操作限制
• 队列仅允许在队尾(Rear)进行插入操作(入队),在队头(Front)进行删除操作(出队)。
• 队头是第一个入队的元素所在位置,队尾是最后一个入队的元素所在位置。
- 核心原则:先进先出(FIFO,First In First Out)
最先入队的元素,一定会最先出队;最后入队的元素,只能最后出队。
举例:元素按 1→2→3→4 顺序入队,出队顺序只能是 1→2→3→4;若入队过程中穿插出队(如1入队→1出队→2、3入队→2出队→4入队),则出队顺序为 1→2→4→3,仍符合"先进先出"。
- 队列的空/满状态
• 空队列:队列中没有任何元素,此时队头与队尾重合,是队列的初始状态。
• 满队列:针对静态队列(数组实现),队列的存储空间被元素占满,无法再执行入队操作;动态队列(链表实现)无"满队列"概念,只要内存足够,可一直入队。
2. 队列的基本术语与操作
所有操作的时间复杂度均为O(1)(无循环/遍历),这是队列的高效性核心。以下是队列的基础操作(所有操作均围绕队头和队尾展开,无随机访问),且操作前需做合法性校验(如出队/取队头前需判空,入队前需判满(静态队列)):
|----------|--------------|-------------------------|-----------------------------------------------------|-------|
| 操作名称 | 英文标识 | 操作含义 | 核心实现逻辑 | 时间复杂度 |
| 初始化队列 | QueueInit | 创建一个空队列,初始化队头、队尾指针和元素个数 | 将 phead 和 ptail 置为 NULL,size 置为 0 | O(1) |
| 销毁队列 | QueueDestroy | 释放队列所有节点的内存,恢复为空队列 | 遍历链表,逐个释放节点,最后重置指针和 size | O(n) |
| 入队 | QueuePush | 在队尾插入新元素 | 为新节点分配内存,若队列为空则同时更新队头和队尾,否则链接到队尾并更新队尾指针,size++ | O(1) |
| 出队 | QueuePop | 删除队头元素 | 若队列只有一个节点,直接释放并重置指针;否则保存队头的下一个节点,释放队头并更新队头指针,size-- | O(1) |
| 获取队头元素 | QueueFront | 返回队头元素的值 | 直接返回队头节点的 val | O(1) |
| 获取队尾元素 | QueueBack | 返回队尾元素的值 | 直接返回队尾节点的 val | O(1) |
| 判断队列是否为空 | QueueEmpty | 检查队列是否无元素 | 返回 size == 0 的布尔值 | O(1) |
| 获取队列大小 | QueueSize | 返回队列中有效元素的个数 | 直接返回 size | O(1) |
| 遍历打印队列 | QueuePrint | 从队头到队尾打印所有元素 | 遍历链表,依次打印每个节点的 val | O(n) |
操作合法性原则:空队列不可出队、不可取队头/队尾;满队列不可入队,违反则会出现"队列下溢"(空队列出队)或"队列上溢"(满队列入队)的错误。
3. 队列的两种经典实现方式
队列的实现基于线性表的两种基础结构(数组、链表),两种方式各有优劣,适用于不同开发场景,核心要求均为保证入队、出队操作在队尾/队头完成,且时间复杂度O(1)。
链表实现(链表队列)
核心设计
• 用单链表存储队列的元素,链表的头节点作为队头,链表的尾节点作为队尾。
• 入队:尾插法(在链表尾部插入新节点),新节点成为新队尾;出队:头删法(删除链表头部节点),原第二个节点成为新队头。
• 为了保证尾插操作的时间复杂度为O(1),需要维护一个队尾指针(ptail),直接指向链表的尾节点。
队列用链表实现更高效,因为链表的头删和尾插操作时间复杂度为O(1),而数组实现的队列在出队时需要移动元素,效率较低。
数组实现(循环队列)
核心设计
• 用数组存储队列的元素,通过取模运算让数组首尾相连,解决普通数组队列的"假溢出"问题。
• 用两个指针 front(队头)和 rear(队尾)表示队列的范围,front 指向队头元素,rear 指向队尾元素的下一个位置。
• 队空条件:front == rear
• 队满条件:(rear + 1) % capacity == front(预留一个空位区分空/满)
• 有效元素个数:(rear - front + capacity) % capacity
4. 队列的核心特性延伸:无随机访问
队列作为受限线性表,与普通数组/链表的核心区别是不支持随机访问:
• 普通数组/链表可通过下标/指针直接访问任意位置的元素(如 arr[5]、node->next->next);
• 队列只能访问队头和队尾元素,若要访问队列中其他位置的元素,必须先将其前方的所有元素依次出队,直到目标元素成为队头,且访问后无法恢复原队列结构(除非重新入队)。
✅ 例:队列中元素为 1(头)→2→3→4(尾),要访问元素3,需先出队1、2,使3成为队头;访问后,队列中仅剩3、4,若要恢复原队列,需重新入队1、2。
四、队列的完整C语言实现(链表版)
Queue.h
cpp
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode *next;
QDataType val;
} QNode;
typedef struct Queue
{
QNode *phead;
QNode *ptail;
int size;
} Queue;
void QueueInit(Queue *pq); // 初始化
void QueueDestroy(Queue *pq); // 销毁
void QueuePush(Queue *pq, QDataType x); // 入队,队尾插入
void QueuePop(Queue *pq); // 出队,队头删除
QDataType QueueFront(Queue *pq); // 获取队头元素
QDataType QueueBack(Queue *pq); // 获取队尾元素
bool QueueEmpty(Queue *pq); // 判断队列是否为空
void QueuePrint(Queue *pq); // 打印队列
int QueueSize(Queue *pq); // 返回队列中元素个数
Queue.c
cpp
#include "Queue.h"
// QueueCreate 创建队列
void QueueInit(Queue *pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// QueueDestroy 销毁队列
void QueueDestroy(Queue *pq)
{
assert(pq);
QNode *cur = pq->phead;
while (cur)
{
QNode *next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// QueuePush 队尾插入
void QueuePush(Queue *pq, QDataType x)
{
assert(pq);
QNode *newnode = (QNode *)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
// QueuePop 队头删除
void QueuePop(Queue *pq)
{
assert(pq);
assert(pq->size != 0);
// QNode *next=pq->phead->next;
// free(pq->phead);
// pq->phead=next;
// if(pq->phead==NULL)
// {
// pq->ptail=NULL;
// }
if (pq->phead->next == NULL) // 如果只有一个节点
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else // 多个节点
{
QNode *next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
// QueueFront 返回队头元素
QDataType QueueFront(Queue *pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
// QueueBack 返回队尾元素
QDataType QueueBack(Queue *pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
// QueueEmpty 判断队列是否为空
bool QueueEmpty(Queue *pq)
{
assert(pq);
return pq->size == 0;
}
// 返回元素个数
int QueueSize(Queue *pq)
{
assert(pq);
return pq->size;
}
// 遍历队列
void QueuePrint(Queue *pq)
{
assert(pq);
QNode *cur = pq->phead;
while (cur)
{
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
test.c
cpp
#include "Queue.h"
//gcc test.c Queue.c -o queue_test
//./queue_test
int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePrint(&q); // 1 2 3 4
QueuePop(&q);
QueuePop(&q);
QueuePrint(&q); // 3 4
QDataType front = QueueFront(&q);
printf("%d\n", front); // 3
QDataType back = QueueBack(&q);
printf("%d\n", back); // 4
printf("%d\n", QueueSize(&q)); // 2
printf("%d\n", QueueEmpty(&q)); // 0
QueueDestroy(&q);
return 0;
}
五、循环队列核心知识点
- 核心原理
• 循环队列是数组实现队列的优化方案,通过取模运算让数组首尾相连,解决"假溢出"问题。
• 队空条件:front == rear
• 队满条件:(rear + 1) % N == front(预留一个空位区分空/满)
• 有效元素个数:(rear - front + N) % N
六、经典例题
1. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
--左括号必须用相同类型的右括号闭合。
--左括号必须以正确的顺序闭合。
--每个右括号都有一个对应的相同类型的左括号。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack {
STDataType* _a;
int _top; // 栈顶:指向栈顶下一个位置
int _capacity; // 容量
} Stack;
// 初始化栈
void StackInit(Stack* ps) {
assert(ps);
ps->_a = NULL;
ps->_top = ps->_capacity = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data) {
assert(ps);
if (ps->_top == ps->_capacity) {
int newcapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->_a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
printf("realloc fail\n");
exit(-1);
}
ps->_a = tmp;
ps->_capacity = newcapacity;
}
ps->_a[ps->_top++] = data;
}
// 出栈
void StackPop(Stack* ps) {
assert(ps);
assert(ps->_top > 0);
ps->_top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps) {
assert(ps);
assert(ps->_top > 0);
return ps->_a[ps->_top - 1];
}
// 获取有效元素个数
int StackSize(Stack* ps) {
assert(ps);
return ps->_top;
}
// 检测栈是否为空
int StackEmpty(Stack* ps) {
assert(ps);
return ps->_top == 0;
}
// 销毁栈
void StackDestroy(Stack* ps) {
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_top = ps->_capacity = 0;
}
// 有效括号判断
bool isValid(char* s) {
Stack st;
StackInit(&st);
while (*s) { // 遍历字符串直到结束符'\0'
if (*s == '(' || *s == '[' || *s == '{') {
StackPush(&st, *s); // 左括号入栈
} else {
// 右括号匹配时栈空,直接不合法
if (StackEmpty(&st)) {
StackDestroy(&st);
return false;
}
STDataType top = StackTop(&st);
// 括号类型匹配则出栈
if (*s == ')' && top == '(' || *s == ']' && top == '[' || *s == '}' && top == '{') {
StackPop(&st);
} else {
// 类型不匹配,直接不合法
StackDestroy(&st);
return false;
}
}
s++; // 移动指针,遍历下一个字符
}
// 将栈判空/销毁/返回移到循环外部,遍历完所有字符再判断
bool ret = StackEmpty(&st); // 栈空=所有括号匹配,非空=有左括号残留
StackDestroy(&st); // 无论结果如何,最终都销毁栈
return ret;
}
// 测试主函数:验证不同用例
int main() {
char s1[] = "()[]{}"; // 合法 → 1
char s2[] = "([)]"; // 不合法 → 0
char s3[] = "((()))"; // 合法 → 1
char s4[] = "{"; // 不合法 → 0
char s5[] = "}{"; // 不合法 → 0
printf("s1: %d\n", isValid(s1));
printf("s2: %d\n", isValid(s2));
printf("s3: %d\n", isValid(s3));
printf("s4: %d\n", isValid(s4));
printf("s5: %d\n", isValid(s5));
return 0;
}
有效括号问题的核心逻辑(栈的经典应用)
利用栈后进先出的特性,完美匹配括号的嵌套/并列规则:
• 左括号:无脑入栈,等待后续右括号匹配;
• 右括号:先判栈空(无左括号匹配→不合法),再判类型(类型不匹配→不合法),匹配则出栈;
• 遍历结束:栈空=所有左括号都有对应右括号,栈非空=有左括号残留→不合法。
2. 用队列实现栈
实现一个栈,该栈的所有操作(入栈、出栈、获取栈顶、判空)仅通过两个队列的基本操作完成(队列仅支持:队尾插入、队头删除、获取队头元素、判空、获取大小等基础操作)。
需实现的栈结构与接口
定义栈结构 MyStack,并实现以下核心函数,函数签名固定:
- 创建栈:MyStack* myStackCreate();
◦ 功能:初始化栈的结构,创建并初始化两个底层队列,返回栈的指针。
- 入栈:void myStackPush(MyStack* obj, int x);
◦ 功能:将元素 x 压入栈顶,仅通过队列操作实现。
- 出栈:int myStackPop(MyStack* obj);
◦ 功能:删除并返回栈顶元素,仅通过队列操作实现;栈非空时调用。
- 获取栈顶:int myStackTop(MyStack* obj);
◦ 功能:仅返回栈顶元素(不删除),仅通过队列操作实现;栈非空时调用。
- 判空:bool myStackEmpty(MyStack* obj);
◦ 功能:判断栈是否为空,返回布尔值(空:true,非空:false)。
- 销毁栈:void myStackFree(MyStack* obj);
◦ 功能:释放栈及底层两个队列的所有内存,避免内存泄漏。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
// 定义队列存储的数据类型
typedef int QDataType;
// 定义队列节点结构
typedef struct QueueNode
{
struct QueueNode *next; // 指向下一个节点的指针
QDataType val; // 节点存储的数据
} QNode;
// 定义队列结构(带头尾指针+大小,方便操作)
typedef struct Queue
{
QNode *phead; // 队列头指针
QNode *ptail; // 队列尾指针
int size; // 队列当前元素个数
} Queue;
// 初始化队列:传入队列地址,初始化头尾指针和大小
void QueueInit(Queue *pq)
{
assert(pq); // 断言保证队列地址非空
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// 销毁队列:释放所有节点内存,重置队列状态
void QueueDestroy(Queue *pq)
{
assert(pq); // 断言保证队列地址非空
QNode *cur = pq->phead; // 从队头开始遍历
while (cur)
{
QNode *next = cur->next; // 保存下一个节点地址,避免释放后丢失
free(cur); // 释放当前节点
cur = next; // 遍历下一个节点
}
pq->phead = pq->ptail = NULL; // 重置头尾指针
pq->size = 0; // 重置元素个数
}
// 队尾入队:在队列尾部插入新元素
void QueuePush(Queue *pq, QDataType x)
{
assert(pq); // 断言保证队列地址非空
// 开辟新节点内存
QNode *newnode = (QNode *)malloc(sizeof(QNode));
if (newnode == NULL) // 检查内存开辟是否成功
{
perror("malloc fail");
exit(-1);
}
newnode->val = x; // 给新节点赋值
newnode->next = NULL; // 新节点作为队尾,next置空
if (pq->ptail == NULL) // 队列空的情况:头尾指针都指向新节点
{
pq->phead = pq->ptail = newnode;
}
else // 队列非空:尾节点next指向新节点,更新尾指针
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++; // 队列元素个数+1
}
// 队头出队:删除队列头部的元素
void QueuePop(Queue *pq)
{
assert(pq);
assert(pq->size != 0); // 断言保证队列非空,避免空队列删除
if (pq->phead->next == NULL) // 队列只有一个节点的情况
{
free(pq->phead); // 释放唯一节点
pq->phead = pq->ptail = NULL; // 头尾指针置空
}
else // 队列有多个节点的情况
{
QNode *next = pq->phead->next; // 保存第二个节点地址
free(pq->phead); // 释放队头节点
pq->phead = next; // 头指针指向新的队头
}
pq->size--; // 队列元素个数-1
}
// 获取队头元素:返回队列头部的元素值(不删除)
QDataType QueueFront(Queue *pq)
{
assert(pq);
assert(pq->phead); // 断言保证队列非空
return pq->phead->val;
}
// 获取队尾元素:返回队列尾部的元素值(不删除)
QDataType QueueBack(Queue *pq)
{
assert(pq);
assert(pq->ptail); // 断言保证队列非空
return pq->ptail->val;
}
// 判断队列是否为空:空返回true,非空返回false
bool QueueEmpty(Queue *pq)
{
assert(pq);
return pq->size == 0; // 通过元素个数判断,更直观
}
// 获取队列元素个数:返回当前队列的节点数
int QueueSize(Queue *pq)
{
assert(pq);
return pq->size;
}
// 遍历打印队列:从队头到队尾输出所有元素
void QueuePrint(Queue *pq)
{
assert(pq);
QNode *cur = pq->phead; // 从队头开始遍历
while (cur)
{
printf("%d ", cur->val); // 打印当前节点值
cur = cur->next; // 遍历下一个节点
}
printf("\n"); // 换行,美化输出
}
// 用两个队列实现栈的结构定义
typedef struct {
Queue *q1; // 主队列:始终存储栈的所有元素,队头=栈顶
Queue *q2; // 辅助队列:入栈时临时存储,完成后置空
} MyStack;
// 创建并初始化栈:开辟栈内存,初始化两个底层队列
MyStack* myStackCreate() {
// 开辟栈的内存空间
MyStack* stack = (MyStack*)malloc(sizeof(MyStack));
// 为两个队列分别开辟内存
stack->q1 = (Queue*)malloc(sizeof(Queue));
stack->q2 = (Queue*)malloc(sizeof(Queue));
// 初始化两个队列的头尾指针和大小
QueueInit(stack->q1);
QueueInit(stack->q2);
return stack; // 返回栈的指针
}
// 栈的入栈操作:将元素x压入栈顶(核心:利用辅助队列实现队列转栈)
void myStackPush(MyStack* obj, int x) {
// 第一步:新元素先入队到辅助队列q2(队尾入)
QueuePush(obj->q2, x);
// 第二步:将主队列q1的所有元素,依次出队并入队到q2
while(!QueueEmpty(obj->q1))
{
QueuePush(obj->q2, QueueFront(obj->q1)); // q1队头元素入q2队尾
QueuePop(obj->q1); // 弹出q1的队头元素,避免数据残留
}
// 第三步:交换q1和q2的指向,让q2成为新的空辅助队列,q1成为主队列
// 此时q1的队头就是刚入栈的元素(栈顶),实现队列→栈的特性转换
Queue* t = obj->q1;
obj->q1 = obj->q2;
obj->q2 = t;
}
// 栈的出栈操作:删除并返回栈顶元素(q1队头=栈顶,直接操作q1即可)
int myStackPop(MyStack* obj) {
// 第一步:先获取q1队头元素(即栈顶元素),保存到临时变量
int top = QueueFront(obj->q1);
// 第二步:删除q1的队头元素(完成栈顶的删除操作)
QueuePop(obj->q1);
// 第三步:返回保存的栈顶元素
return top;
}
// 获取栈顶元素:返回栈顶值(不删除,直接取q1队头)
int myStackTop(MyStack* obj) {
return QueueFront(obj->q1); // q1队头始终是栈顶,直接返回
}
// 判断栈是否为空:空返回true,非空返回false
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(obj->q1); // 主队列q1为空,则栈为空
}
// 销毁栈:释放栈和两个队列的所有内存,避免内存泄漏
void myStackFree(MyStack* obj) {
// 第一步:销毁两个队列的节点内存
QueueDestroy(obj->q1);
QueueDestroy(obj->q2);
// 第二步:释放两个队列本身的内存
free(obj->q1);
free(obj->q2);
// 第三步:释放栈结构的内存
free(obj);
}
/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate(); // 创建栈
* myStackPush(obj, x); // 入栈元素x
* int param_2 = myStackPop(obj); // 出栈,返回栈顶元素
* int param_3 = myStackTop(obj); // 获取栈顶元素
* bool param_4 = myStackEmpty(obj);// 判断栈是否为空
* myStackFree(obj); // 销毁栈,释放内存
*/
核心思路
- 两个队列配合:
◦ q1 始终保存栈的所有元素,且队头就是栈顶元素,这样 pop 和 top 操作可以直接在 O(1) 时间完成。
◦ q2 仅在 push 时使用,用来临时存储新元素,再把 q1 的元素转移过来,保证新元素在队头。
- 入栈逻辑:
◦ 新元素先入队到 q2,然后把 q1 的所有元素依次出队并入队到 q2。
◦ 交换 q1 和 q2,让 q1 成为新的主队列,此时 q1 的队头就是刚入栈的元素。
关键注释重点说明
-
队列核心操作:标注了内存开辟检查、空队列/单节点队列边界处理,避免野指针和内存泄漏;
-
入栈核心逻辑:分三步标注新元素入辅助队列、主队列元素转移、队列交换,清晰解释"队列转栈"的原理;
-
出栈核心逻辑:标注先取值再删除的原因(因为QueuePop是void类型,无返回值);
-
内存管理:在Destroy/Free函数中,标注了先释放节点、再释放结构体的顺序,避免内存泄漏。
方法二:
cpp
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode *next;
QDataType val;
} QNode;
typedef struct Queue
{
QNode *phead;
QNode *ptail;
int size;
} Queue;
// QueueCreate 创建队列
void QueueInit(Queue *pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// QueueDestroy 销毁队列
void QueueDestroy(Queue *pq)
{
assert(pq);
QNode *cur = pq->phead;
while (cur)
{
QNode *next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// QueuePush 队尾插入
void QueuePush(Queue *pq, QDataType x)
{
assert(pq);
QNode *newnode = (QNode *)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
// QueuePop 队头删除
void QueuePop(Queue *pq)
{
assert(pq);
assert(pq->size != 0);
// QNode *next=pq->phead->next;
// free(pq->phead);
// pq->phead=next;
// if(pq->phead==NULL)
// {
// pq->ptail=NULL;
// }
if (pq->phead->next == NULL) // 如果只有一个节点
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else // 多个节点
{
QNode *next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
// QueueFront 返回队头元素
QDataType QueueFront(Queue *pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
// QueueBack 返回队尾元素
QDataType QueueBack(Queue *pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
// QueueEmpty 判断队列是否为空
bool QueueEmpty(Queue *pq)
{
assert(pq);
return pq->size == 0;
}
// 返回元素个数
int QueueSize(Queue *pq)
{
assert(pq);
return pq->size;
}
// 遍历队列
void QueuePrint(Queue *pq)
{
assert(pq);
QNode *cur = pq->phead;
while (cur)
{
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
typedef struct {
Queue *q1;
Queue *q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* stack = (MyStack*)malloc(sizeof(MyStack));
stack->q1 = (Queue*)malloc(sizeof(Queue));
stack->q2 = (Queue*)malloc(sizeof(Queue));
// 初始化队列
QueueInit(stack->q1);
QueueInit(stack->q2);
return stack;
}
void myStackPush(MyStack* obj, int x) {
if(!QueueEmpty(obj->q1))
{
QueuePush(obj->q1,x);
}
else
{
QueuePush(obj->q2,x);
}
}
int myStackPop(MyStack* obj) {
//假设法
Queue* empty = obj->q1;
Queue* nonempty = obj->q2;
if(!QueueEmpty(obj->q1))
{
empty = obj->q2;
nonempty = obj->q1;
}
//不为空前size-1导走,删除最后一个就是栈顶数据
while(QueueSize(nonempty)>1)
{
QueuePush(empty,QueueFront(nonempty));
QueuePop(nonempty);
}
int top = QueueFront(nonempty);
QueuePop(nonempty);
return top;
}
int myStackTop(MyStack* obj) {
if(!QueueEmpty(obj->q1))
{
return QueueBack(obj->q1);
}
else
{
return QueueBack(obj->q2);
}
}
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(obj->q1) && QueueEmpty(obj->q2);
}
void myStackFree(MyStack* obj) {
QueueDestroy(obj->q1);
QueueDestroy(obj->q2);
free(obj->q1);
free(obj->q2);
free(obj);
}
/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate();
* myStackPush(obj, x);
* int param_2 = myStackPop(obj);
* int param_3 = myStackTop(obj);
* bool param_4 = myStackEmpty(obj);
* myStackFree(obj);
*/
3. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x)将元素 x 推到队列的末尾int pop()从队列的开头移除并返回元素int peek()返回队列开头的元素boolean empty()如果队列为空,返回true;否则,返回false
说明:
- 你 只能 使用标准的栈操作 ------ 也就是只有
push to top,peek/pop from top,size, 和is empty操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
cpp
typedef int STDataType;
typedef struct stack{
STDataType* a;
int top;
int capacity;
}ST;
// 初始化和销毁
void STInit(ST *pst)
{
assert(pst);
pst->a = NULL;
// top指向栈顶数据的下一个位置,相当于size
pst->top = 0;
// top指向栈顶数据
// pst->top = -1;
pst->capacity = 0;
}
void STDestroy(ST *pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
// 入栈和出栈
void STPush(ST *pst, STDataType x)
{
assert(pst);
//扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType *tmp = (STDataType *)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST *pst)
{
assert(pst);
assert(pst->top > 0);//top=0时,栈为空
pst->top--;
}
// 获取栈顶元素
STDataType STTop(ST *pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判断栈是否为空
bool STEmpty(ST *pst)
{
assert(pst);
return pst->top == 0;
}
// 获取栈中元素个数
int STSize(ST *pst)
{
assert(pst);
return pst->top;
}
typedef struct {
ST pushst;
ST popst;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue *obj=(MyQueue*)malloc(sizeof(MyQueue));
STInit(&(obj->pushst));
STInit(&(obj->popst));
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
STPush(&(obj->pushst),x);
}
int myQueuePeek(MyQueue* obj) {
if(STEmpty(&(obj->popst)))
{
//倒数据
while(!STEmpty(&(obj->pushst)))
{
int top=STTop(&(obj->pushst));
STPush(&(obj->popst),top);
STPop(&(obj->pushst));
}
}
return STTop(&(obj->popst));
}
int myQueuePop(MyQueue* obj) {
int front=myQueuePeek(obj);
STPop(&(obj->popst));
return front;
}
bool myQueueEmpty(MyQueue* obj) {
return STEmpty(&(obj->popst)) && STEmpty(&(obj->pushst));
}
void myQueueFree(MyQueue* obj) {
STDestroy(&(obj->popst));
STDestroy(&(obj->pushst));
free(obj);
}
/**
* Your MyQueue struct will be instantiated and called as such:
* MyQueue* obj = myQueueCreate();
* myQueuePush(obj, x);
* int param_2 = myQueuePop(obj);
* int param_3 = myQueuePeek(obj);
* bool param_4 = myQueueEmpty(obj);
* myQueueFree(obj);
*/
本题核心思路是利用两个栈的"先进后出"特性,通过数据倒换模拟队列的"先进先出"特性,一个栈专门负责入队(push栈),一个栈专门负责出队(pop栈),仅在pop栈为空时,将push栈的所有数据倒入pop栈,实现顺序反转。
一、核心原理
栈的特性是先进后出,队列是先进先出;将一个栈的所有元素倒入另一个栈,元素的访问顺序会被反转,两次"先进后出"即可实现"先进先出",这是本题的核心逻辑。
二、数据结构设计
-
先实现通用的栈结构:包含动态数组(a)、栈顶指针(top,指向栈顶下一个位置)、容量(capacity),并封装栈的初始化、销毁、入栈、出栈、取栈顶、判空、获取元素个数等基础操作。
-
队列结构(MyQueue):嵌套两个栈,pushst 负责接收入队元素,popst 负责提供出队/取队首元素,两个栈配合完成队列所有操作。
三、各接口实现思路
- 队列创建(myQueueCreate)
• 为队列结构体(MyQueue)申请堆内存,避免栈上内存销毁问题。
• 分别初始化队列内的pushst和popst两个栈,保证栈的初始状态(数组空、top=0、capacity=0)。
- 入队(myQueuePush)
• 入队操作直接复用栈的入栈功能,将元素直接压入push栈,时间复杂度O(1)。
• 无需操作pop栈,减少不必要的性能消耗。
- 取队首元素(myQueuePeek)
这是核心接口,实现push栈到pop栈的按需倒数据(仅pop栈空时倒):
-
先判断pop栈是否为空,若为空则执行数据倒换;若不为空,直接返回pop栈的栈顶(即队列首元素)。
-
倒数据:循环将push栈的栈顶元素压入pop栈,同时弹出push栈的栈顶,直到push栈为空。
-
数据倒换后,pop栈的栈顶就是队列的首元素,直接返回pop栈栈顶即可。
• 倒数据的时间复杂度为O(n),但每个元素仅被倒换一次,均摊时间复杂度仍为O(1)。
- 出队(myQueuePop)
• 直接复用myQueuePeek接口:先通过myQueuePeek保证pop栈有数据,且获取到队首元素。
• 对pop栈执行出栈操作(弹出栈顶),完成队列的出队,最后返回获取到的队首元素。
- 判空(myQueueEmpty)
• 队列的空状态要求两个栈同时为空:若push栈有元素但pop栈空,仅需倒数据即可获取队首/出队,队列并非真正为空。
- 队列销毁(myQueueFree)
• 先分别销毁队列内的push栈和pop栈(释放栈的动态数组,重置栈的参数)。
• 最后释放队列结构体本身的堆内存,避免内存泄漏。
四、关键优化点
-
按需倒数据:仅当pop栈为空时才将push栈的所有数据倒换,而非每次入队/出队都倒换,避免重复操作,保证性能。
-
动态栈实现:栈采用动态数组实现,支持自动扩容(初始容量4,满后2倍扩容),避免固定数组的容量限制,适配任意数量的元素入队。
-
接口解耦:队列的所有操作均复用栈的基础接口,降低代码耦合度,便于维护和修改。
五、特性总结
• 入队操作:O(1) 时间复杂度。
• 出队/取队首:均摊 O(1) 时间复杂度(每个元素仅倒换一次)。
• 空间复杂度:O(n),n为队列中元素个数,两个栈的总存储空间为n。
• 满足队列先进先出的核心特性,完全模拟队列的所有基础操作。
4. 设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为"环形缓冲器"。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。Front: 从队首获取元素。如果队列为空,返回 -1 。Rear: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty(): 检查循环队列是否为空。isFull(): 检查循环队列是否已满。
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.enQueue(4); // 返回 false,队列已满 circularQueue.Rear(); // 返回 3 circularQueue.isFull(); // 返回 true circularQueue.deQueue(); // 返回 true circularQueue.enQueue(4); // 返回 true circularQueue.Rear(); // 返回 4
**提示:**所有的值都在 0 至 1000 的范围内;操作数将在 1 至 1000 的范围内;请不要使用内置的队列库。
cpp
typedef struct {
int* a;
int head,tail,k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//多开一个解决栈溢出问题
obj->a = malloc(sizeof(int)*(k+1));
obj->head=obj->tail=0;
obj->k=k;
return obj;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head==obj->tail;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->tail+1) % (obj->k+1)==obj->head;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))
return false;
obj->a[obj->tail]=value;
obj->tail++;
obj->tail %= (obj->k+1);
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
obj->head++;
obj->head %= (obj->k+1);
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[obj->head];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[(obj->tail-1+obj->k+1) % (obj->k+1)];
// int rear=obj->tail == 0 ? k : obj->tail-1;
// return rear;
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
/**
* Your MyCircularQueue struct will be instantiated and called as such:
* MyCircularQueue* obj = myCircularQueueCreate(k);
* bool param_1 = myCircularQueueEnQueue(obj, value);
* bool param_2 = myCircularQueueDeQueue(obj);
* int param_3 = myCircularQueueFront(obj);
* int param_4 = myCircularQueueRear(obj);
* bool param_5 = myCircularQueueIsEmpty(obj);
* bool param_6 = myCircularQueueIsFull(obj);
* myCircularQueueFree(obj);
*/
数组实现循环队列的核心思路
这个实现的核心是用数组+双指针+多开1个空间来模拟队列的环形结构,解决普通数组队列"假溢出"和"空/满状态无法区分"的问题。
一、核心设计思路
- 底层存储
◦ 使用动态数组 a 作为底层存储。
◦ 为了区分队列的"空"和"满"状态,数组的实际容量是 k+1(k 是题目要求的队列容量)。
◦ 这样做避免了队列头尾指针重叠时,无法判断是队空还是队满的问题。
- 双指针控制
◦ head 指针:指向队头元素的下标。
◦ tail 指针:指向队尾的下一个可插入位置。
◦ 入队时,新元素放在 tail 位置,然后 tail 后移;出队时,head 直接后移。
- 环形复用机制
◦ 当 head 或 tail 到达数组末尾时,通过取模运算 % (k+1) 回到数组开头,实现数组的环形复用。
◦ 这避免了普通数组队列中"删除队头后,前面空间无法复用"的假溢出问题。
- 空/满状态判断
◦ 队空:head == tail(头尾指针重合,没有元素)。
◦ 队满:(tail + 1) % (k+1) == head(tail 的下一个位置是 head,预留一个空位区分空/满)。
二、各接口的实现思路
- 创建队列 myCircularQueueCreate
• 分配 MyCircularQueue 结构体的内存。
• 为数组 a 分配 k+1 个整型的空间。
• 初始化 head 和 tail 为 0,k 为传入的容量。
- 入队 myCircularQueueEnQueue
• 先调用 myCircularQueueIsFull 判断队列是否已满,满则返回 false。
• 将元素存入 a[tail],然后 tail 后移一位并取模。
• 返回 true 表示入队成功。
- 出队 myCircularQueueDeQueue
• 先调用 myCircularQueueIsEmpty 判断队列是否为空,空则返回 false。
• head 直接后移一位并取模,相当于删除队头元素。
• 返回 true 表示出队成功。
- 获取队头 myCircularQueueFront
• 队空时返回 -1,否则直接返回 a[head]。
- 获取队尾 myCircularQueueRear
• 队空时返回 -1。
• 队尾元素的下标是 (tail - 1 + k + 1) % (k+1)(处理 tail=0 的边界情况),返回该位置的值。
- 判空 myCircularQueueIsEmpty
• 直接判断 head == tail。
- 判满 myCircularQueueIsFull
• 判断 (tail + 1) % (k+1) == head。
- 销毁队列 myCircularQueueFree
• 先释放数组 a 的内存,再释放结构体 obj 的内存,避免内存泄漏。