跳到主要内容数据结构:栈与队列的实现与 OJ 题解析 | 极客日志C算法
数据结构:栈与队列的实现与 OJ 题解析
本文介绍了栈和队列两种线性数据结构。栈遵循后进先出原则,支持顺序和链式存储,重点讲解了顺序栈的初始化、入栈、出栈及销毁操作。队列遵循先进先出原则,推荐使用链式结构以避免空间浪费,详细阐述了队头队尾指针维护及基本操作。最后通过括号匹配的经典 OJ 题目,演示了如何利用栈的特性解决实际问题,包括入栈判断、出栈匹配及空栈检查等关键逻辑。
知识点速览
栈
何为栈?栈是一种线性结构,只允许在一端进行插入和删除元素的数据结构。
压栈(入栈):插入元素
出栈:删除元素
栈顶:进行插入和删除元素的一端
栈底:固定的,不允许进行插入和删除元素
存储特点:先进后出、后入先出。想象成一个一端开口的容器!
栈的存储结构分析
栈可以用两种结构来实现,一种是顺序栈(数组实现),另一种是链式栈(链表实现)。综合考虑,优先选择顺序栈。
顺序栈:
顺序栈出栈、入栈都很方便,栈顶指针也方便指向,有一个小缺点:扩容可能导致空间浪费。
链式栈:
链式栈虽然扩容很方便,无浪费情况,但是出栈时,不好控制栈顶指针,设置稍微复杂一点。比如单链表,需要将链表倒置过来,否则栈的存储特性就与其不符,出栈时需要知道倒数第二个链表节点。还有双链表结构,虽然不用去找倒数第二个节点,但是写起来没有那么的简单、快捷。
栈的基本操作
初始化栈、判断栈空、入栈、读取栈元素、出栈、销毁栈。
结构体定义
顺序栈只需要一个栈顶指针、一个空间指针即可。在初始化时给空间指针开辟空间,这里建议可以额外设置一个空间上限,方便以后扩容。
typedef struct Stack {
int top;
int* data;
int max;
}Stack;
初始化栈
栈的初始状态应该是栈顶指针在栈底的位置,再给栈开辟空间。
void Init(Stack* stack) {
stack->max = MAX;
stack->top = 0;
stack->data = (int*)malloc(sizeof(int) * MAX);
if (stack->data == NULL) {
printf("开辟失败\n");
return;
}
printf();
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
"空间开辟成功\n"
判断栈空
栈空的标志就是栈顶指针指向栈底。这里直接根据栈顶指针指向来判。为了接口的一致选择传址,同时防止代码被修改,可以用 const 修饰。
void IsEmpty(const Stack* stack) {
if (stack->top == 0) {
printf("栈为空\n");
return;
}
}
入栈
顺序栈的入栈就像将元素存进数组一样,只是需要控制栈顶指针的更新,考虑栈满需要扩容的情况。
void Push(Stack* stack, int data) {
if (stack->top == stack->max) {
int* pc = (int*)realloc(stack->data, sizeof(int) * stack->max * 2);
if (pc == NULL) {
printf("扩容失败\n");
return;
}
stack->data = pc;
stack->max *= 2;
}
stack->top++;
stack->data[stack->top - 1] = data;
printf("入栈成功\n");
}
读取栈元素
因为栈的存储特性,我们只能读取栈顶元素,同时读取栈元素并不会改变栈顶指针,需要额外一个变量去代替栈顶指针的移动来不断读取栈顶元素。这里需要判断栈空,为了与前面的接口适用,我们选择取地址,同时注意加 const 修饰,防止误触!注意:前置减减 后置减减的区别。
void Read(const Stack* stack) {
IsEmpty(stack);
int size = stack->top;
printf("栈元素:");
while (size) {
printf("%d ", stack->data[--size]);
}
}
出栈
出栈按照理论逻辑:栈顶指针每次指向靠近栈顶元素的末尾,因此最好先出栈,再改变栈顶指针。
注意:前置减减、后置减减的区别【前置是先减再使用,后置是先使用再减减】。
void Pop(Stack* stack) {
IsEmpty(stack);
stack->data[--stack->top];
printf("出栈成功\n");
}
销毁栈
void Destroy(Stack* stack) {
while (stack->top) {
Pop(stack);
}
free(stack->data);
stack->data = NULL;
printf("栈空间释放成功\n");
}
队列
队列也是一种线性数据结构,允许在一端存入数据、一端拿出数据。
队头:允许拿出(删除)数据的一端,也称队首
队尾:允许存入数据的一端
结构特点:拥有两个指针指向队头与队尾的元素,随着元素的变化发生改变
存储特点:先进先出
队列的结构分析
队列的实现也可以选择顺序结构、链式结构,但是总体考虑,以链式结构最佳。
顺序结构:
我们知道顺序结构移除某个元素是很方便的,但是队列有两个指针分别随着元素的变化发生改变,这导致出现了以下的情况:随着元素逐渐出去,队头指针 head 会不断移动,当不断地进数据出数据会导致空间浪费越来越大。
链式结构:
队列更好的实现方式是链式结构,因为较于顺序结构,虽然是采用节点来存储数据,但是它的出队列、入队列就是链表的头删、尾插,使用起来比顺序结构更效率,且没有空间的浪费。
队列的基本操作
初始化队列、入队、出队、获取队尾元素、获取队头元素、判断对空、销毁队列。
结构体定义
首先咱们采用的是链表来定义的,因此肯定需要一个链表结构、其次需要两个指针指向队尾队头的元素,为了不与链表发生混乱,我们将这两个指针单独放在一个结构体里面。
注意:队列指针的类型应该是链表类型的,因为它是指向链表的,后面通过队列指针来维护节点。
typedef struct List {
struct List* next;
int data;
}List;
typedef struct Queue {
struct List* head;
struct List* tail;
int size;
}Queue;
初始化队列
重点:咱们的初始化不是先初始化一个链表开辟头指针,而是开辟队列空间,在入队列的时候再开辟链表节点。
Queue* InitQueue() {
Queue* space = (Queue*)malloc(sizeof(Queue));
if (space == NULL) {
printf("队列空间开辟失败\n");
return NULL;
}
space->head = NULL;
space->tail = NULL;
space->size = 0;
printf("队列空间开辟成功\n");
return space;
}
入队列
重点:咱们得数据是放在链表里面的,所以应该先开辟一个链表节点用来放数据,然后队列指针指向之前需要判断,如果这是第一个存入的数据,那么队列指针指向这个节点;如果是第 N 个数据,那么就需要通过链表节点的 next 指针来进行连接节点。
如何通过队列空间来找到链表节点存储数据?因为队列指针类型是链表类型,通过队列指针找链表。
为何队列空间的判断最好用元素个数?这里存储的时候根据队列指针是否为空也可进行判断,因为链表末尾为空,队列指针如果为空,则表示指到了链表的末尾,没有节点。但是直观上根据元素个数判断更加直观,没有难度。
void Enqueue(Queue** space, int data) {
List* newnode = (List*)malloc(sizeof(List));
if (newnode == NULL) {
printf("节点空间开辟失败\n");
return;
}
newnode->data = data;
newnode->next = NULL;
if ((*space)->size == 0) {
(*space)->tail = newnode;
(*space)->head = newnode;
(*space)->size++;
} else {
(*space)->tail->next = newnode;
(*space)->tail = newnode;
(*space)->size++;
}
printf("入队成功\n");
}
出队列
先判断是否有元素可以出队,如果有则释放该链表节点后,再改变队列指针指向。
void Dequeue(Queue** space) {
if ((*space)->size == 0) {
printf("队列为空,无法出队\n");
return;
}
List* cur = (*space)->head;
(*space)->head = (*space)->head->next;
free(cur);
cur = NULL;
(*space)->size--;
printf("出队成功\n");
}
获取队尾元素
先判断队列是否为空,否则直接打印对应队尾指针指向节点的元素即可。
void GetRear(Queue* space) {
if (space->size == 0) {
printf("队列为空,无法获取\n");
return;
}
List* cur = space->tail;
printf("队尾元素:%d\n", cur->data);
}
获取队头元素
先判断队列是否为空,然后打印对应队列指针所指向节点的元素。
void GetFront(Queue* space) {
if (space->size == 0) {
printf("队列为空,无法获取\n");
return;
}
printf("队头元素:%d\n", space->head->data);
}
判断队空
这里咱们就不多说了,前面咱们一直都有这个判断,这里只是单独封装成一个函数。
void IsEmptyQueue(Queue* space) {
if (space->size == 0) {
printf("队列为空\n");
return;
}
}
销毁队列
咱们的队列是由链表完成的,应该先释放链表,再释放队列空间,否则就找不到链表位置了。
注意:队列里链表应该由队头销向队尾,因为链表的尾部在队尾这端。
同时注意二级指针指向的是一级指针的地址,对二级指针解引用一次,就拿到一级指针地址。
void DestroyQueue(Queue** space) {
if ((*space)->size == 0) {
printf("队列为空,无法销毁\n");
return;
}
List* cur = (*space)->head;
while ((*space)->head) {
(*space)->head = (*space)->head->next;
free(cur);
cur = NULL;
cur = (*space)->head;
}
free(*space);
*space = NULL;
printf("销毁成功\n");
}
栈和队列 OJ 题(典型)
题目分析:
有一个字符数组,里面的内容是几个括号,根据数组内容判断括号是否是完整对应来返回不同的值。这里就是根据字符的位数来判断对应位置的字符与其是否相匹配的问题。
实例讲解:
比如现在有一个字符串'()'
先建立一个栈,第一个字符是左括号'('入栈,下一次进入循环的字符是')',则出栈顶元素与其进行匹配,此时匹配成功,再次进入循环遇到循环结束条件,最后栈也为空,返回 true。
比如现在有一个字符串')['
先建立一个栈,此时第一个字符是右括号')',选择出栈,但是栈为空,则直接返回 false。
思维讲解:
我们可以建立一个栈,如果当前字符是'(''{''[',就存入栈里面,如果是')''}'']'就将栈顶的元素拿出来与之对比,如果匹配成功,就继续,直到数组内容到达'\0',否则返回 false。
(1)先实现拷贝对应的栈功能过来,这题需要初始化栈、入栈、出栈顶元素、判断栈空、销毁栈。
注意:改变相应存储的元素、空间指针类型为 char 类型。
这个判断栈空的函数我们可以简写,直接判断栈的元素个数,这对新手小白很友好。
下面是将之前实现栈的函数直接拷贝过来!注意判断栈空的函数小编直接简写了哦。
#define MAX 10
typedef char Datatype;
typedef struct Stack {
int top;
int max;
Datatype* data;
}Stack;
void Init(Stack* stack) {
stack->max = MAX;
stack->top = 0;
stack->data = (Datatype*)malloc(sizeof(Datatype) * MAX);
if(stack->data==NULL) {
return;
}
}
void Push(Stack* stack, Datatype data) {
if (stack->top == stack->max) {
Datatype* pc = (Datatype*)realloc(stack->data, sizeof(Datatype) * (stack->max) * 2);
if (pc == NULL) {
return;
}
stack->data = pc;
stack->max *= 2;
}
stack->top++;
stack->data[stack->top - 1] = data;
}
char Pop(Stack* stack) {
return stack->data[--stack->top];
}
void Destroy(Stack* stack) {
while (stack->top) {
Pop(stack);
}
free(stack->data);
stack->data = NULL;
}
(2)其次我们创建一个栈,再初始化(因为此题是利用栈完成的)
Stack stack;
Init(&stack);
(3)进入循环判断,直到遇到 \0 退出循环。
如果是左括号就入栈,然后字符指针后移一位。
如果是右括号就先判断是不是栈空,否则出栈拿栈顶元素与与其匹配,不匹配则返回。
while(*s) {
if(*s=='(' || *s=='[' || *s=='{') {
Push(&stack, *s);
s++;
} else {
if(stack.top==0) {
Destroy(&stack);
return false;
}
Datatype cur = Pop(&stack);
if(cur=='(' && *s!=')' || cur=='{' && *s!='}' || cur=='[' && *s!=']' ) {
Destroy(&stack);
return false;
} else {
s++;
}
}
}
(4)如果经过了上面的循环判断,也就是没有返回 false,可能是下面这种情况:
存在匹配成功的部分,但是字符串走到 \0 了,比如:'()['或者'{'栈不为空,返回 false。
bool result=(stack.top==0);
Destroy(&stack);
return result;
测试用例是不用判断数组没有元素的情况的,也就是说至少一个括号。