
🎬 Doro在努力 :个人主页
🔥 个人专栏 : 《MySQL数据库基础语法》《数据结构》
⛺️严于律己,宽以待人
在数据结构06中,我们完成了单链表的练习题以及初步认识了链表的分类,并在最后开启了双向链表的实现,接下来这篇文章我们将继续实现双向链表的下一步内容并认识一种新的数据结构,话不多说,让我们赶快开始。
文章目录
- 一、双向链表的实现
-
- 1.1打印函数
- 1.2判空函数
- 1.3尾删函数
- 1.4头删函数
- 1.5查找函数
- 1.6插入函数(pos位置之后插入)
- 1.7销毁函数
- 1.7删除函数(删除pos位置的节点)
- 1.8扩展1-新初始化函数
- 1.9扩展2-新销毁函数
- 1.10完整代码
-
- [1.10.1 test.c](#1.10.1 test.c)
- [1.10.2 List.h](#1.10.2 List.h)
- [1.10.3 List.c](#1.10.3 List.c)
- 二、顺序表与链表的分析
- 三、栈
-
- 3.1概念与结构
- 3.2栈的实现
- 3.3栈相关练习
-
- [3.3.1[有效的括号 - 力扣(LeetCode)](https://leetcode.cn/problems/valid-parentheses/description/)](#3.3.1有效的括号 - 力扣(LeetCode))
一、双向链表的实现
1.1打印函数
为了方便我们之后检验函数实现是否正确,我们可以实现一个打印函数,用来直接在屏幕上显示对双向链表操作之后的结果,就可以不用再在监视器中查看了,具体代码如下:
c
//List.h
//打印函数
void LTPrint(LTNode* phead);
//List.cpp
//打印函数
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;//从第一个节点开始,而不是从头节点开始
while (pcur != phead)//只要不是头节点就说明没有循环,可以接着继续打印
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");//打印完之后换行
}
1.2判空函数
要实现之后的头删、尾删函数需要判断传入的双向链表的指针是否有效,判断依据有以下两点:
- 传入的phead不能为NULL
- 传入的双向链表必须有节点
所有我们就可以单独写一个判空函数来判断传入的双向链表是否有效,具体代码如下:
c
//List.h
//判空函数
bool LTEmpty(LTNode* phead);
//List.c
//判空函数
bool LTEmpty(LTNode* phead)
{
assert(phead);//传入的指针不能为空
return phead->next == phead;//双向链表不能没有节点,只有一个头节点
}
1.3尾删函数
实现从双向链表的尾部开始删除节点,即让尾节点的前一个节点的后继指针指向头节点,让头节点的前驱节点指向尾节点的前一个节点,这个尾节点可以通过phead的前驱指针找到,示意图与具体代码如下:

c
//List.h
//尾删函数
void LTPopBack(LTNode* phead);
//List.c
//尾删函数
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));//结果为真继续
//所以当!LTEmpty(phead)为真,LTEmpty(phead)为假时继续
LTNode* del = phead->prev;//先保存尾节点的位置,方便之后释放
phead->prev->prev->next = phead;//尾节点的前一个节点的后继指针指向head
phead->prev = phead->prev->prev;//头节点的前驱指针指向尾节点的前一个节点
free(del);
del = NULL;
}
1.4头删函数
实现从双向链表的头部开始删除节点,但是这个头不是头节点,而是第一个节点,因为头节点只是起到一个指示的作用,实际存储有效数据的节点是从第一个节点开始的,第一个节点可以通过头节点phead的后继指针找到,示意图与具体代码如下:

c
//List.h
//头删函数
void LTPopFront(LTNode* phead);
//List.c
//头删函数
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));//结果为真继续,所有当!LTEmpty(phead)为真,LTEmpty(phead)为假时继续
LTNode* del = phead->next;
phead->next->next->prev = phead;//第一个节点的下一个节点的前驱指针指向头节点
phead->next = phead->next->next;//头节点的后继指针指向第一个节点的下一个节点
free(del);
del = NULL;
}
1.5查找函数
如何在双向链表中查找某一个值的位置?使用一个工作指针从第一个节点依次循环遍历,循环条件尾工作指针不等于头指针,如果找到,则返回工作指针,反之,则返回NULL,具体代码如下:
c
//List.c
//查找函数
LTNode* LTFind(LTNode* phead, LTDateType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
1.6插入函数(pos位置之后插入)
在指定的pos位置之后插入数据,就是需要我们开辟一个值为x的新节点,插入到pos指向的节点之后,通过改变pos节点、新节点、pos的下一个节点的指针指向来进行插入,示意图与具体代码如下:

c
//List.c
//插入函数(pos位置之后插入)
void LTInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* newNode = BuyNode(x);//开辟值为x的新节点
pos->next->prev = newNode;//pos的下一个节点的前驱指针指向新节点
newNode->next = pos->next;//新节点的后继指针指向pos的下一个节点
pos->next = newNode;//pos的后继指针指向新节点
newNode->prev = pos;//新节点的前驱节点指向pos
}
1.7销毁函数
销毁函数和初始化函数一样,需要改变指针的指向,所以要传二级指针,具体代码如下:
c
//销毁函数
void LTDesTroy(LTNode** pphead)//和初始化函数一样,需要改变指针的指向,所以要传二级指针
{
LTNode* pcur = (*pphead)->next;//从第一个节点开始,而不是头节点
while (pcur != *pphead)
{
LTNode* next = pcur->next;//先保存pcur的下一个节点,防止释放pcur之后找不到pcur之后的节点
free(pcur);
pcur = next;//向后依次销毁
}
//释放头节点
free(*pphead);
*pphead = NULL;
}
1.7删除函数(删除pos位置的节点)
删除pos位置的节点,就需要让pos的前一个节点的后继指针指向pos的后一个节点,让pos的后一个节点的前驱指针指向pos的前一个节点,示意图与具体代码如下:

c
//List.c
//删除函数(删除pos位置的节点)
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* pcur = pos;
pos->prev->next = pos->next;//pos的前一个节点的后继指针指向pos的后一个节点
pos->next->prev = pos->prev;//pos的后一个节点的前驱指针指向pos的前一个节点
free(pcur);
pcur = NULL;
}
1.8扩展1-新初始化函数
我们第一版初始化函数的具体代码如下:
c
//初始化函数
void LTIint(LTNode** pphead)
//需要传入一个二级指针,因为实例化对象就是一个一级指针,要改变一级指针的指向就是改变指针的内容
//要改变形参的内容就需要传入形参的地址
//一级指针的地址就是二级指针,所以这里是LTNode** pps
{
//(*pphead) = (LTNode*)malloc(sizeof(LTNode));
//if (pphead == NULL)
//{
// perror("malloc fail!\n");//开辟空间失败
// exit(1);//异常退出
//}
//(*pphead)->data = -1;//头节点存入的数据无效
//(*pphead)->next = (*pphead)->prev = (*pphead);//头节点的前驱指针和后继指针都指向头节点本身,满足循环链表
*pphead = BuyNode(-1);
}
第一版初始化函数是需要传入一个二级指针,并改变指针指向的,而其他增删改查的方法传入的都是一级指针,一会二级指针、一会一级指针就不方便用户使用,所以我们可以在初始化时,不传参数,而是接收函数的返回值,例如去加油站加油,我那一个空的桶让加油站帮我加满和我直接到加油站买一桶油是一样的,最后我都获得了一桶油,不过前一种是我需要有一个空油桶,后一种是加油站连油带桶一起给我。
所以我们的初始化函数可以不传参,直接调用函数,让函数返回一个指向双向链表的指针,就相当于连油带桶的给我。具体代码如下:
c
//test.c
//新初始化函数
LTNode* plist = LTInit();
//Lits.h
//新初始化函数
LTNode* LTInit();
//List.c
//新初始化函数
LTNode* LTInit()
{
LTNode* phead = BuyNode(-1);
return phead;
}
1.9扩展2-新销毁函数
第一版销毁函数如下:
c
//销毁函数
void LTDesTroy(LTNode** pphead)//和初始化函数一样,需要改变指针的指向,所以要传二级指针
{
LTNode* pcur = (*pphead)->next;//从第一个节点开始,而不是头节点
while (pcur != *pphead)
{
LTNode* next = pcur->next;//先保存pcur的下一个节点,防止释放pcur之后找不到pcur之后的节点
free(pcur);
pcur = next;//向后依次销毁
}
//释放头节点
free(*pphead);
*pphead = NULL;
}
我们可以参考新初始化函数的思想,减少使用二级指针,但是还是需要传递参数,因为销毁需要知道销毁的是哪个双向链表,但是传递的参数从二级指针变成了一级指针,具体代码如下:
c
//test.c
//LTDesTroy(&plist);
LTDesTroy(plist);
plist = NULL;
//List.h
//新销毁函数
void LTDesTroy(LTNode* phead);
//List.c
//新销毁函数
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;//从第一个节点开始,而不是头节点
while (pcur != phead)
{
LTNode* next = pcur->next;//先保存pcur的下一个节点,防止释放pcur之后找不到pcur之后的节点
free(pcur);
pcur = next;//向后依次销毁
}
//释放头节点
free(phead);
phead = NULL;//这里是形参被置为了NULL,实参需要手动置为NULL
}
这里我们传入一级指针,释放完头节点之后,只是把phead指向的空间给释放了,将phead置为NULL,只是将形参置为了空,并没有影响实参的指向,所以要在执行完销毁函数之后,手动将plist置为NULL。
1.10完整代码
1.10.1 test.c
c
#include"List.h"
void test01()
{
//LTNode* plist = NULL;
//新初始化函数
LTNode* plist = LTInit();
LTIint(&plist);
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushFront(plist, 9);
LTPrint(plist);
//LTPopBack(plist);
//LTPrint(plist);
//LTPopFront(plist);
//LTPrint(plist);
LTNode* ret = LTFind(plist, 4);
printf("%d\n", ret->data);
LTInsert(ret, 11);
LTPrint(plist);
LTErase(ret);
LTPrint(plist);
//LTDesTroy(&plist);
LTDesTroy(plist);
plist = NULL;
}
int main()
{
test01();
return 0;
}
1.10.2 List.h
c
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#define LTDateType int
typedef struct ListNode LTNode;
struct ListNode
{
LTDateType data;
struct ListNode* prev;//前驱指针
struct ListNode* next;//后继指针
};
// 初始化函数
void LTIint(LTNode** pphead);
//尾插函数
void LTPushBack(LTNode* phead, LTDateType x);
//创建新节点函数
LTNode* BuyNode(LTDateType x);
//头插函数
void LTPushFront(LTNode* phead, LTDateType x);
//尾删函数
void LTPopBack(LTNode* phead);
//打印函数
void LTPrint(LTNode* phead);
//头删函数
void LTPopFront(LTNode* phead);
//判空函数
bool LTEmpty(LTNode* phead);
//查找函数
LTNode* LTFind(LTNode* phead, LTDateType x);
//插入函数(pos位置之后插入)
void LTInsert(LTNode* pos, LTDateType x);
//删除函数(删除pos位置的节点)
void LTErase(LTNode* pos);
//销毁函数
void LTDesTroy(LTNode** pphead);
//新初始化函数
LTNode* LTInit();
//新销毁函数
void LTDesTroy(LTNode* phead);
1.10.3 List.c
c
#include"List.h"
//初始化函数
void LTIint(LTNode** pphead)
//需要传入一个二级指针,因为实例化对象就是一个一级指针,要改变一级指针的指向就是改变指针的内容
//要改变形参的内容就需要传入形参的地址
//一级指针的地址就是二级指针,所以这里是LTNode** pps
{
//(*pphead) = (LTNode*)malloc(sizeof(LTNode));
//if (pphead == NULL)
//{
// perror("malloc fail!\n");//开辟空间失败
// exit(1);//异常退出
//}
//(*pphead)->data = -1;//头节点存入的数据无效
//(*pphead)->next = (*pphead)->prev = (*pphead);//头节点的前驱指针和后继指针都指向头节点本身,满足循环链表
*pphead = BuyNode(-1);
}
//创建新节点函数
LTNode* BuyNode(LTDateType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("malloc fail!\n");
exit(1);
}
newNode->data = x;
newNode->next = newNode->prev = newNode;//也要满足循环
return newNode;
}
void LTPushBack(LTNode* phead, LTDateType x)
//这里为什么只用传一级指针,因为phead指向的是哨兵位
//而哨兵位在增删改查过程中一直没有发生改变
//在初始化过程中,我们传二级指针是为了修改哨兵位指向的空间地址
//而在增删改查过程中,这些操作都是通过哨兵位的data、next、prev等等数据进行修改的
//是通过哨兵位访问到的这些数据,但是并没有对哨兵位本身进行修改
//所以只需要传入一级指针,就能够对链表进行增删改查
//一定要注意修改的对象是谁
{
assert(phead);
LTNode* newNode = BuyNode(x);//创建新节点
newNode->prev = phead->prev;//新节点的前驱指针指向尾节点
phead->prev->next = newNode;//尾节点的后继指针指向新节点
newNode->next = phead;//新节点成为了新的尾节点,则新节点的后继指针就指向头节点
phead->prev = newNode;//头节点的前驱指针指向新节点,即新的尾节点
}
//头插函数
void LTPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* newNode = BuyNode(x);//创建新节点
newNode->next = phead->next;//新节点的后继指针指向头节点原来的后继节点
newNode->prev = phead;//新节点的前驱指针指向头节点
phead->next->prev = newNode;//头节点的原后继指针指向的节点的前驱指针指向新节点
phead->next = newNode;//头节点的现后继指针指向的节点为新节点
}
//尾删函数
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));//结果为真继续
//所以当!LTEmpty(phead)为真,LTEmpty(phead)为假时继续
LTNode* del = phead->prev;//先保存尾节点的位置,方便之后释放
phead->prev->prev->next = phead;//尾节点的前一个节点的后继指针指向head
phead->prev = phead->prev->prev;//头节点的前驱指针指向尾节点的前一个节点
free(del);
del = NULL;
}
//打印函数
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//头删函数
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));//结果为真继续,所有当!LTEmpty(phead)为真,LTEmpty(phead)为假时继续
LTNode* del = phead->next;
phead->next->next->prev = phead;//第一个节点的下一个节点的前驱指针指向头节点
phead->next = phead->next->next;//头节点的后继指针指向第一个节点的下一个节点
free(del);
del = NULL;
}
//判空函数
bool LTEmpty(LTNode* phead)
{
assert(phead);//传入的指针不能为空
return phead->next == phead;//双向链表不能没有节点,只有一个头节点
}
//查找函数
LTNode* LTFind(LTNode* phead, LTDateType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//插入函数(pos位置之后插入)
void LTInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* newNode = BuyNode(x);//开辟值为x的新节点
pos->next->prev = newNode;//pos的下一个节点的前驱指针指向新节点
newNode->next = pos->next;//新节点的后继指针指向pos的下一个节点
pos->next = newNode;//pos的后继指针指向新节点
newNode->prev = pos;//新节点的前驱节点指向pos
}
//删除函数(删除pos位置的节点)
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* pcur = pos;
pos->prev->next = pos->next;//pos的前一个节点的后继指针指向pos的后一个节点
pos->next->prev = pos->prev;//pos的后一个节点的前驱指针指向pos的前一个节点
free(pcur);
pcur = NULL;
}
//新初始化函数
LTNode* LTInit()
{
LTNode* phead = BuyNode(-1);
return phead;
}
//销毁函数
void LTDesTroy(LTNode** pphead)//和初始化函数一样,需要改变指针的指向,所以要传二级指针
{
LTNode* pcur = (*pphead)->next;//从第一个节点开始,而不是头节点
while (pcur != *pphead)
{
LTNode* next = pcur->next;//先保存pcur的下一个节点,防止释放pcur之后找不到pcur之后的节点
free(pcur);
pcur = next;//向后依次销毁
}
//释放头节点
free(*pphead);
*pphead = NULL;
}
//新销毁函数
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;//从第一个节点开始,而不是头节点
while (pcur != phead)
{
LTNode* next = pcur->next;//先保存pcur的下一个节点,防止释放pcur之后找不到pcur之后的节点
free(pcur);
pcur = next;//向后依次销毁
}
//释放头节点
free(phead);
phead = NULL;//这里是形参被置为了NULL,实参需要手动置为NULL
}
二、顺序表与链表的分析
| 不同点 | 顺序表 | 链表(单链表) |
|---|---|---|
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持:O(N) |
| 任意位置插入或者删除元素 | 可能需要搬移元素, 效率低O(N) | 只需要修改指针指向 (指定位置之后插入删除O(1)) |
| 插入 | 动态顺序表,空间不够时需要 扩容和空间浪费 | 没有容量的概念,按需申请释放 不存在空间浪费 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置高效插入和删除 |
三、栈
3.1概念与结构
栈是一种特殊的线性表,只允许在固定的一端进行插入和删除元素操作,一端叫做栈顶,另一端叫做栈底,栈中元素遵循后进出原则。
后进先出指的是我要往栈中存放一个数据就要从顶部放入,要从栈中取数据就需要从栈顶开始取,示意图如下:


这样关闭的一端叫做栈底,开口的一端叫做栈顶,栈顶是用来插入和删除数据的。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的逻辑结构是线性的,为什么是线性的?线性结构的特征如下:
- 元素之间存在一对一的关系
- 元素按顺序排列,每个元素有且仅有一个直接前驱和一个直接后继(除了首尾元素)
- 具有"第一个"和"最后一个"元素的概念
那我们来看栈是否满足这些特点:
- 每个元素(除了栈底和栈顶)都有且仅有一个直接前驱(下方元素),有且仅有一个直接后继(上方元素)
- 栈中元素按入栈顺序线性排列
- 每个元素在栈中有确定的、唯一的位置
- 元素访问遵循严格的顺序规则
说完栈的逻辑结构是线性的,接下来就需要判断栈的物理结构是否为线性结构了,栈是如何实现的呢?栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
如果用数组来实现,我们是选择将数组的头部还是数组的尾部作为栈顶呢?答案是尾部,因为栈顶是用来出入数据的,而我们可以直接使用数组的Size成员就可以找到当前数组的有效数据个数,从而进行插入和删除操作,所以用数组来实现栈,入栈和出栈的时间复杂度都是O(1)。
如果用链表来实现,我们选择的将链表的头部还是链表的尾部作为栈顶呢?答案是头部,因为我们可以直接通过链表的头插和头删进行栈的掺入和删除操作,所以用链表来实现栈,入栈和出栈的时间复杂度也都是O(1)。
但是如果考虑到空间复杂度的问题就是用数组实现更优了,为什么?假设数组和链表都需要存放3个整形类型的数据,对于数组,则直接开辟3个int大小的空间就可以,而对于链表,链表存放数据是需要开辟节点的,而节点的大小不仅仅只和存储的数据类型有关,还包括指针的大小等等,因此,选用数组实现栈,所需要的空间消耗比链表少,代价更低。
3.2栈的实现
我们用数组来实现栈,栈的定义如下:
c
// Stack.h
typedef int STDataType;
typedef struct Stack ST;
struct Stack
{
STDataType* arr;//指向栈的指针
int top;//指向栈顶的位置(在数组中也为栈的有效元素个数)
int capacity;//栈的空间大小
};
和顺序表的创建非常一样,但是原来的size成员变成了top,因为size的位置刚好就是栈顶top的位置,所以改为top以作区分。
3.2.1初始化函数
初始化栈的操作改变的是栈的内容,所以我们定义的是ST st,并传入一级指针,然后将指向数组的指针arr置为NULL,top和capacity都置为0。具体代码如下:
c
//Stack.c
//初始化函数
void STIint(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
3.2.2销毁函数
销毁栈操作中,我们需要将开辟了空间的数组指针arr的空间释放并置为NULL,如果arr本就是为NULL则无需释放,接着将top和capacity置为0。具体代码如下:
c
//Stack.c
//销毁函数
void STDesTroy(ST* ps)
{
if(ps->arr)
free(ps->arr);//释放数组空间
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
3.2.3入栈函数
入栈就是在栈顶的位置插入数据,而通过数组实现的栈的size成员指向的就是栈顶的位置,所以我们直接把原来的size定义为top。
在插入数据之前,还需要判断栈是否已经满了,判断条件就是top是否等于capacity,如果没有满就直接插入,否则就需要调用扩容函数对数组进行扩容,需要注意的是扩容是对ps->arr,即数组进行扩容,而不是对指向栈的指针开辟空间。示意图及具体代码如下:


c
//入栈------栈顶
void STPush(ST* ps,STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)//判断是否存满
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//如果为空,则先初始化为4,否则进行二倍扩容
STDataType* tmp = ps->arr;
tmp = (STDataType*)realloc(tmp, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
ps->arr = tmp;//将新开辟的空间赋值给数组指针
ps->capacity = newcapacity;//将新空间的大小赋值给capacity
}
ps->arr[ps->top] = x;//插入数据
ps->top++;//栈顶+1
}
3.2.4判空函数
在出栈之前,即删除栈中元素之前,要先判断栈是否为空,只有当栈不为空的时候才能删除,具体代码如下:
c
//判空函数
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//返回栈顶是否为0的判断结果
}
3.2.5出栈函数
出栈就是删除栈顶的元素,删除元素之前我们需要进行判空操作,栈顶的元素就在top-1的位置上,我们可以直接让栈顶下降一格,即top-1赋值给top,则原栈顶元素不在有效元素范围内了,示意图和具体代码如下:

c
//出栈
void STPop(ST* ps)
{
assert(!STEmpty(ps));//如果ps->为空,则STEmpty(ps)结果为false,!STEmpty结果为假,断言报错
ps->top--;
}
3.2.6取栈顶元素
我们可以通过top获取栈中栈顶的元素,即下标为top-1位置的元素,然后通过return返回,有了取栈顶操作和出栈操作,就可以实现循环出栈操作了,示意图和具体代码如下:

c
//Stack.c
//取栈顶元素
STDataType STTop(ST* ps)
{
assert(!STEmpty(ps));
return ps->arr[ps->top - 1];
}
//test.c
//循环出栈
while (!STEmpty(&st))//只要st不为空
{
STDataType top = STTop(&st);
printf("%d ", top);
STPop(&st);
}
printf("\n");
3.2.7获取栈中有效元素个数
要获取栈中有效元素个数,我们可以直接通过top就可以得到,具体代码如下:
c
//获取栈中有效元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
3.2.8相关代码
c
//test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
void test01()
{
ST st;
STIint(&st);
STPush(&st, 1);
STPush(&st, 2);
STPush(&st, 3);
STPush(&st, 4);
STPush(&st, 5);
STPush(&st, 6);
//STDesTroy(&st);
//STPop(&st);
//循环出栈
while (!STEmpty(&st))//只要st不为空
{
STDataType top = STTop(&st);
printf("%d ", top);
STPop(&st);
}
printf("\n");
printf("Size: %d\n", STSize(&st));
}
int main()
{
test01();
return 0;
}
c
//Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack ST;
struct Stack
{
STDataType* arr;//指向栈的指针
int top;//指向栈顶的位置(在数组中也为栈的有效元素个数)
int capacity;//栈的空间大小
};
//初始化函数
void STIint(ST* ps);
//销毁函数
void STDesTroy(ST* ps);
//入栈------栈顶
void STPush(ST* ps, STDataType x);
//打印函数
void STPrint(ST* ps);
//判空函数
bool STEmpty(ST* ps);
//出栈
void STPop(ST* ps);
//取栈顶元素
STDataType STTop(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
c
//Stack.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//初始化函数
void STIint(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//销毁函数
void STDesTroy(ST* ps)
{
if(ps->arr)
free(ps->arr);//释放数组空间
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//入栈------栈顶
void STPush(ST* ps,STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)//判断是否存满
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//如果为空,则先初始化为4,否则进行二倍扩容
STDataType* tmp = ps->arr;
tmp = (STDataType*)realloc(tmp, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
ps->arr = tmp;//将新开辟的空间赋值给数组指针
ps->capacity = newcapacity;//将新空间的大小赋值给capacity
}
ps->arr[ps->top] = x;//插入数据
ps->top++;//栈顶+1
}
//判空函数
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//返回栈顶是否为0的判断结果
}
//出栈
void STPop(ST* ps)
{
assert(!STEmpty(ps));//如果ps->为空,则STEmpty(ps)结果为false,!STEmpty结果为假,断言报错
ps->top--;
}
//取栈顶元素
STDataType STTop(ST* ps)
{
assert(!STEmpty(ps));
return ps->arr[ps->top - 1];
}
//获取栈中有效元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
3.3栈相关练习
我们栈相关的实现就到此结束了,现在我们来看几道算法题熟悉一下,如何使用栈这种数据结构。
3.3.1有效的括号 - 力扣(LeetCode)

这道关于栈的算法题主要的解题思路就是如果字符是右括号,就入栈,如果是左括号就取栈顶元素判断是否匹配,匹配成功,则出栈进行匹配下一个字符,反之返回false。
这里还有两个地方需要注意
- 当字符串中只有一个右括号时,就会直接跳过匹配返回true不符合题目要求,所以需要在所有字符都匹配结束之后,对栈判空,如果栈空,则返回true,非空,则返回false。
- 当字符串中第一个元素就是左括号时,就会直接取栈顶进行匹配,但是这时栈中还没有数据,是一个空栈,这时取栈顶就会断言报错,因此,在这之前我们也需要对栈进行判空,如果非空,则进行下一步,如果为空,则直接返回false。
c
typedef char STDataType;
typedef struct Stack ST;
struct Stack
{
STDataType* arr;//指向栈的指针
int top;//指向栈顶的位置(在数组中也为栈的有效元素个数)
int capacity;//栈的空间大小
};
//初始化函数
void STIint(ST* ps);
//销毁函数
void STDesTroy(ST* ps);
//入栈------栈顶
void STPush(ST* ps, STDataType x);
//打印函数
void STPrint(ST* ps);
//判空函数
bool STEmpty(ST* ps);
//出栈
void STPop(ST* ps);
//取栈顶元素
STDataType STTop(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
//初始化函数
void STIint(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//销毁函数
void STDesTroy(ST* ps)
{
if(ps->arr)
free(ps->arr);//释放数组空间
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//入栈------栈顶
void STPush(ST* ps,STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)//判断是否存满
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//如果为空,则先初始化为4,否则进行二倍扩容
STDataType* tmp = ps->arr;
tmp = (STDataType*)realloc(tmp, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail!\n");
exit(1);
}
ps->arr = tmp;//将新开辟的空间赋值给数组指针
ps->capacity = newcapacity;//将新空间的大小赋值给capacity
}
ps->arr[ps->top] = x;//插入数据
ps->top++;//栈顶+1
}
//判空函数
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;//返回栈顶是否为0的判断结果
}
//出栈
void STPop(ST* ps)
{
assert(!STEmpty(ps));//如果ps->为空,则STEmpty(ps)结果为false,!STEmpty结果为假,断言报错
ps->top--;
}
//取栈顶元素
STDataType STTop(ST* ps)
{
assert(!STEmpty(ps));
return ps->arr[ps->top - 1];
}
//获取栈中有效元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
//-------------------以上是栈的定义和常见方法---------------------------
bool isValid(char* s) {
ST st;
STIint(&st);
char* pi = s;
while(*pi != '\0')
{
//左括号入栈
if(*pi == '(' || *pi == '['|| *pi == '{')
{
STPush(&st,*pi);
}
//右括号出栈
else
{
//判断栈是否为空,栈不为空才能取栈顶,为空返回1,非空返回0
if(STEmpty(&st))
{
STDesTroy(&st);
return false;
}
char top = STTop(&st);
if((*pi == ')') && (top != '(')
|| (*pi == ']') && (top != '[')
|| (*pi == '}') && (top != '{')
)
{
STDesTroy(&st);
return false;
}
STPop(&st);
}
pi++;
}
//判断栈是否为空,为空则有效,非空则说明栈中还有元素未匹配
// if(STEmpty(&st))
// {
// STDesTroy(&st);
// return true;
// }
// STDesTroy(&st);
// return false;
bool ret = STEmpty(&st) ? true : false ;
STDesTroy(&st);
return ret;
}
今天我们接续上一篇文章完结了双向链表的相关知识,也讲解了一种新的数据结构------栈,并动手使用C语言实现了相关功能,也练习了一道相关习题。
在下一次见面中,我们将继续学习一种新的数据结构,也是一种特殊的线性表,让我们敬请期待!