一、栈的实现与应用
栈的概念
什么是栈
栈(Stack)是只允许在一端进行插入或删除操作的线性表。进行插入和删除操作的一端称为栈顶,另外一端称为栈底,并且满足“后进先出”的原则LIFO(Last In First Out),下面是栈的示意图:
压栈\入栈\进栈
向栈中插入元素的过程叫做压栈,也叫做入栈或进栈,下面是压栈示意图:
出栈
删除栈中的元素叫做出栈,出栈的元素只能是栈顶的元素,下面是出栈示意图:
总结:从上面的入栈出栈可以看出栈的“后进先出”的特点,也就是后面进栈的元素会先出栈,而先进栈的元素会后出栈。
栈的实现
栈的实现方式有两种,一种采用单链表的方式,而另一种采用顺序表的方式(数组),若采用单链表的方式,入栈与出栈就采用单链表的头插或者头删的方式即可满足栈的后进先出的要求;若采用数组的方式,数组的尾插与尾删也可以满足后进先出的特点。这两种实现方式类似,这里我们采用数组的方式来实现栈。
1、定义栈与栈内的数据类型 :
typedef int STDataType; //栈内的数据类型为 int 类型
typedef struct Stake
{
STDataType* a; //数组
int top; //栈顶
int capacity; //容量
}ST;
2、栈的初始化与销毁:
初始化:
void STInit(ST* pst) //初始化
{
assert(pst);
pst->a = NULL;
//top若初始化为 0 ,则top指向的是栈顶的下一个数据,入栈时先插入元素,在++top;
//top若初始化为 -1 ,则top指向的是栈顶的数据,入栈时先++top,在插入元素。
pst->capacity = pst->top = 0;
}
销毁
void STDestroy(ST* pst) //销毁
{
assert(pst);
free(pst->a); //因为是malloc开辟的一块空间,所以程序结束时要释放
pst->a = NULL;
pst->capacity = pst->top = 0;
}
3、入栈与出栈 :
入栈:
在插入元素之前,要考虑是否需要扩容,在什么情况下需要扩容呢?当 top == capacity 的时候,表明栈的空间已经满了,此时需要扩容;若不需要扩容,将元素插入到栈顶位置即可。
void STPush(ST* pst, STDataType x) //入栈
{
assert(pst);
if (pst->capacity == pst->top) //当top与capacity相等时,表示栈内空间已经存储满了
{
int NewCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
//1、当capacity == 0时,初始空间赋为 4 个;
//2、当capacity ! = 0时,将空间扩容为原来空间的 2 倍;
// 一个数据的大小 * 新的空间容量
STDataType* new = (STDataType*)realloc(pst->a, sizeof(STDataType) * NewCapacity);
if (new == NULL) //扩容失败打印错误信息
{
perror("STBuyCapacity:realloc");
exit(-1); //不正常退出
}
pst->a = new;
pst->capacity = NewCapacity;
}
pst->a[pst->top] = x; //向栈顶插入数据
pst->top++; //插入元素后 top ++
}
出栈:
出栈的代码十分简便,只需要将 top - - 即可(后面插入数据的时候会覆盖原来的数据),但删除栈内的数据时还需要考虑栈内是否有数据,通过判断 top 的值即可;top == 0 时表示无数据。
void STPop(ST* pst) //出栈
{
assert(pst);
assert(pst->top != 0); //当top == 0时,表明栈里面无数据,不可以删除
pst->top-- ;
}
4、 取栈顶的元素:
因为 top 指向的时栈顶的下一个元素,所以栈顶的元素下标就是 top -1。
STDataType STTop(ST* pst) //取栈顶的数据
{
assert(pst);
assert(pst->top != 0); //当栈里无数据时,访问 a[pst->top - 1] 的元素时会越界访问。
return pst->a[pst->top - 1];
}
5、判断栈是否为空:
栈为空就返回 true,否则返回 false。
bool STEmpty(ST* pst) //判断栈是否为空
{
assert(pst);
return pst->top == 0;
//1、若 top == 0,则栈为空 , pst->top == 0 的值为 true
//2、若 top ! = 0,则栈不为空,pst->top == 0 的值为 false
}
6、获取栈内数据个数:
栈内的数据个数即是 top 的值,返回即可。
int STSize(ST* pst) //获取数据个数
{
return pst->top;
}
源代码:
头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stake
{
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); //获取数据个数
.C文件
#include "Stack.h"
void STInit(ST* pst) //初始化
{
assert(pst);
pst->a = NULL;
//top初始化为 0 ,top指向的是栈顶的下一个数据
//top初始化为 -1 ,top指向的是栈顶的数据
pst->capacity = pst->top = 0;
}
void STDestroy(ST* pst) //销毁
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
void STPush(ST* pst, STDataType x) //入栈
{
assert(pst);
if (pst->capacity == pst->top) //当top与capacity相等时,表示栈内空间已经存储满了
{
int NewCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
//1、当capacity == 0时,初始空间赋为 4 个;
//2、当capacity ! = 0时,将空间扩容为原来空间的 2 倍;
// 一个数据的大小 * 新的空间容量
STDataType* new = (STDataType*)realloc(pst->a, sizeof(STDataType) * NewCapacity);
if (new == NULL) //扩容失败打印错误信息
{
perror("STBuyCapacity:realloc");
exit(-1); //不正常退出
}
pst->a = new;
pst->capacity = NewCapacity;
}
pst->a[pst->top] = x; //向栈顶插入数据
pst->top++; //插入元素后 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); //当栈里无数据时,访问 a[pst->top - 1] 的元素时会越界访问。
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst) //判断栈是否为空
{
assert(pst);
return pst->top == 0;
//1、若 top == 0,则栈为空 , pst->top == 0 的值为 true
//2、若 top ! = 0,则栈不为空,pst->top == 0 的值为 false
}
int STSize(ST* pst) //获取数据个数
{
return pst->top;
}
栈的应用
通过上面对栈的理解与掌握,下面我们通过一个练习来加强我们对栈的理解:
这里通过思考,采用栈的方法来实现会更加方便,因为C语言没有现成的栈,所以要将我们刚刚所实现的栈给利用起来。
思路:如果是 左括号 ,就入栈;如果是 右括号,就取出栈顶的数据与之相匹配,看他们两个括号是否匹配,如果不匹配的话,就不是有效的括号;如果匹配,就继续向后遍历;若遍历结束后,栈内还有剩余的括号,说明也不是有效括号。
下面以 { ( ) [ ] } 为例,画出示意图,方便我们理解:
参考代码:
typedef char STDataType;
typedef struct Stake
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* pst) //初始化
{
assert(pst);
pst->a = NULL;
//top初始化为 0 ,top指向的是栈顶的下一个数据
//top初始化为 -1 ,top指向的是栈顶的数据
pst->capacity = pst->top = 0;
}
void STDestroy(ST* pst) //销毁
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
void STBuyCapacity(ST* pst) //扩容
{
int NewCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity; //考虑是否为空
STDataType* new = (STDataType*)realloc(pst->a, sizeof(STDataType) * NewCapacity);
if (new == NULL)
{
perror("STBuyCapacity:realloc");
exit(-1); //不正常退出
}
pst->a = new;
pst->capacity = NewCapacity;
}
void STPush(ST* pst, STDataType x) //入栈
{
assert(pst);
if (pst->capacity == pst->top)
{
STBuyCapacity(pst);
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst) //出栈
{
assert(pst);
assert(pst->top);
pst->top-- ;
}
STDataType STTop(ST* pst) //取栈顶的数据
{
assert(pst);
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst) //判断栈是否为空
{
assert(pst);
return pst->top == 0;
}
bool isValid(char* s) {
int len = strlen(s);
if(len%2) //如果长度为奇数,一定是无效括号
{
return false;
}
ST a;
STInit(&a);
for(int i = 0;i<len;i++)
{
if(s[i] == '(' || s[i] == '[' || s[i] == '{') //左括号入栈
{
STPush(&a,s[i]);
}
else //右括号的情况
{
if(STEmpty(&a)) //如果栈为空,说明第一个括号是右括号,是无效括号
{
return false;
}
char tmp = STTop(&a); //取出栈顶的括号与其相比较
STPop(&a); //取出括号后要删除栈顶的括号
if((s[i] == ')' && tmp != '(') ||
(s[i] == ']' && tmp != '[') ||
(s[i] == '}' && tmp != '{'))
{
//不匹配的话要销毁栈
STDestroy(&a);
return false;
}
}
}
bool ret = STEmpty(&a); //如果栈里面还有数据,说明不是有效括号
STDestroy(&a);
return ret;
}
二、队列的实现与应用
队列的概念
队列也是一种线性表,他只允许在一端进行插入操作,而在另一端进行删除操作;进行插入操作的一端叫做队尾,进行删除操作的一端叫做队头,并且满足先进先出(First In First Out),与栈的先进后出(First In Last Out)相反。
下面是队列的示意图:
队列的实现
队列的实现方式依然有两种方式,一种是采用顺序表(数组)的方式;另一种是采用链表的方式。下面我们来看一下采用单链表的方式如何实现队列。
首先因为队列是在队头与队尾进行删除插入操作的,所以定义队列时需要记录头指针(head)和尾指针(tail),这样才方便我们进行删除插入操作。
1、队列结构的构建
跟链表结构一样,队列也是用一个一个节点所构成的,所以队列需要定义两个结构体;一个结构体用于表示每一个节点;另一个结构体记录头、尾指针和数据的个数。
typedef int QDataType; //将数据类型重定义方便以后修改
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* head; //记录队头的指针
QNode* tail; //记录队尾的指针
int size; //队列中的数据个数
}Queue;
2、队列的初始化
队列的初始化很简单,将结队列构体中的各个变量赋为空即可。
void QueueInit(Queue* pq) //初始化
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
3、队列的插入与删除
1->队列的插入(入队列)
队列的插入相当于链表中的尾插。但在插入时需要注意:当插入第一个节点时需要单独考虑。
下面是队列插入示意图:
当插入第一个元素时,tail 与 head 都指向第一个元素。
我们向队列当中连续插入1、2、3、4,来观察tail 、haed指针和 size 的变化。
代码实现:
void QueuePush(Queue* pq, QDataType data) //插入
{
QNode* newnode = (QNode*)malloc(sizeof(QNode)); //开辟一个新节点
if (newnode == NULL) //节点开辟失败
{
perror("QueuePush:malloc");
exit(-1);
}
newnode->next = NULL; //新节点赋值
newnode->val = data;
if (pq->size == 0) //如果队列中没有数据的情况
{
pq->head = pq->tail = newnode; //头尾指针都指向这个节点
}
else //队列当中有数据的情况
{
pq->tail->next = newnode; //将新节点连接在tail指针即可
pq->tail = newnode; //tail 指向新的尾
}
pq->size++; //每插入一个 size 都要++
}
2->队列的删除(出队列)
队列的删除对应到链表上来看,也就是链表的头删,但注意:当 size == 0 时不可进行删除操作。
示意图:
从上面的入队列与出队列可以看出:
入队顺序: 1、2、3、4出队顺序: 1、2、3、4
代码实现:
void QueuePop(Queue* pq) // 删除
{
assert(pq && pq->size != 0);
QNode* del = pq->head; //记录要删除的节点
pq->head = pq->head->next; //让 head 指针向后走
if (pq->head == NULL) //只有一个节点时,haed 向后走一步后指向了 NULL,
{ //但此时的 tail 还指向原来的第一个节点,所以要将 tail 也置为 NULL,
pq->tail = NULL; //不然的话此时的 tail 就是野指针
}
free(del);
del = NULL;
pq->size--;
}
4、获取队列头部、尾部元素
QDataType QueueFront(Queue* pq) // 获取队列头部元素
{
assert(pq && pq->head != NULL); //保证 head 的有效性
return pq->head->val;
}
QDataType QueueBack(Queue* pq) // 获取队列队尾元素
{
assert(pq && pq->tail != NULL);
return pq->tail->val;
}
5、获取队列中有效元素个数
int QueueSize(Queue* pq) // 获取队列中有效元素个数
{
assert(pq);
return pq->size;
}
6、检测队列是否为空(空返回true,非空返回false)
bool QueueEmpty(Queue* pq) // 检测队列是否为空,如果为空返回true,如果非空返回false
{
assert(pq);
return pq->size == 0;
//1、size == 0 时 pq->size == 0 表达式为 true;
//2、size ! = 0 时 pq->size == 0 表达式为 false;
}
7、销毁队列
void QueueDestroy(Queue* pq) // 销毁队列
{
assert(pq);
while (pq->head) //从 head 节点开始向后遍历,销毁每一个用 malloc 开辟的节点
{
QNode* del = pq->head; // 记录销毁节点
pq->head = pq->head->next;
free(del);
del = NULL;
}
pq->tail = NULL;
pq->size = 0;
}
源代码
头文件
#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* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq); //初始化
void QueuePush(Queue* pq, QDataType data); //插入
void QueuePop(Queue* pq); // 删除
QDataType QueueFront(Queue* pq);// 获取队列头部元素
QDataType QueueBack(Queue* pq);// 获取队列队尾元素
int QueueSize(Queue* pq);// 获取队列中有效元素个数
bool QueueEmpty(Queue* pq);// 检测队列是否为空,如果为空返回true,如果非空返回false
void QueueDestroy(Queue* pq);// 销毁队列
.C文件
#include "Queue.h"
void QueueInit(Queue* pq) //初始化
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType data) //插入
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("QueuePush:malloc");
exit(-1);
}
newnode->next = NULL;
newnode->val = data;
if (pq->size == 0)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq) // 删除
{
assert(pq && pq->size != 0);
QNode* del = pq->head; //记录要删除的节点
pq->head = pq->head->next; //让 head 指针向后走
if (pq->head == NULL) //只有一个节点时,haed 向后走一步后指向了 NULL,
{ //但此时的 tail 还指向原来的第一个节点,所以要将 tail 也置为 NULL,
pq->tail = NULL; //不然的话此时的 tail 就是野指针
}
free(del);
del = NULL;
pq->size--;
}
QDataType QueueFront(Queue* pq) // 获取队列头部元素
{
assert(pq && pq->head != NULL);
return pq->head->val;
}
QDataType QueueBack(Queue* pq) // 获取队列队尾元素
{
assert(pq && pq->tail != NULL);
return pq->tail->val;
}
int QueueSize(Queue* pq) // 获取队列中有效元素个数
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq) // 检测队列是否为空,如果为空返回true,如果非空返回false
{
assert(pq);
return pq->size == 0;
//1、size == 0 时 pq->size == 0 表达式为 true;
//2、size ! = 0 时 pq->size == 0 表达式为 false;
}
void QueueDestroy(Queue* pq) // 销毁队列
{
assert(pq);
while (pq->head)
{
QNode* del = pq->head;
pq->head = pq->head->next;
free(del);
del = NULL;
}
pq->tail = NULL;
pq->size = 0;
}
队列的应用
有了上面对栈和队列的理解,接下来我们通过刷题进一步加深对队列的理解。
解题思路:题目的要求是要我们使用 队列 来模拟构建一个栈,并且只能使用队列的标准操作,也就是上面我们所讲到的队列的各种操作。因为栈是 后进先出 ,而队列是 先进先出 ,所以要想只使用一个队列肯定是不行的,所以这里我们就想到使用 2 个队列来模拟实现 栈的结构。
当入栈时,与队列的入队列相同;而出栈时,因为队列只能 先进先出,所以要想将队尾的数据删除,只能将队尾之前的数据先插入到另一个队列,在删除队尾数据即可。
示意图:
注:在插入数据的时候要插入到队列不为空的那一个队列,如果两个队列都为空,则随便选择一个队列插入即可。
参考代码:
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
void QueueInit(Queue* pq) //初始化
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType data) //插入
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("QueuePush:malloc");
exit(-1);
}
newnode->next = NULL;
newnode->val = data;
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq) // 删除
{
assert(pq && pq->head != NULL);
QNode* del = pq->head;
pq->head = pq->head->next;
if (pq->head == NULL) //只有一个节点,将 tail 也要置空
{
pq->tail = NULL;
}
free(del);
del = NULL;
pq->size--;
}
QDataType QueueFront(Queue* pq) // 获取队列头部元素
{
assert(pq && pq->head != NULL);
return pq->head->val;
}
QDataType QueueBack(Queue* pq) // 获取队列队尾元素
{
assert(pq && pq->tail != NULL);
return pq->tail->val;
}
int QueueSize(Queue* pq) // 获取队列中有效元素个数
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq) // 检测队列是否为空,如果为空返回true,如果非空返回false
{
assert(pq);
return pq->size == 0;
}
void QueueDestroy(Queue* pq) // 销毁队列
{
assert(pq);
while (pq->head)
{
QNode* del = pq->head;
pq->head = pq->head->next;
free(del);
del = NULL;
}
pq->tail = NULL;
pq->size = 0;
}
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&(obj->q1)); //使用队列的初始化
QueueInit(&(obj->q2));
return obj;
}
void myStackPush(MyStack* obj, int x) {
if(!QueueEmpty(&obj->q1)) //如果队列1不为空接插入数据到队列1
{
QueuePush(&obj->q1,x);
}
else //否则插入队列2
{
QueuePush(&obj->q2,x);
}
}
int myStackPop(MyStack* obj) {
Queue* UnEmpty = &obj->q1; //假设q1不为空,q2为空
Queue* Empty = &obj->q2;
if(QueueEmpty(&obj->q1)) //如果q1为空,则假设错误
{
UnEmpty = &obj->q2;
Empty = &obj->q1;
}
while(UnEmpty->size>1) //将队尾之前的数据插入到另一个队列
{
int tmp = QueueFront(UnEmpty); //取出队头数据
QueuePush(Empty,tmp); //插入到另一个为空的队列
QueuePop(UnEmpty); //再删除队头的数据
}
int ret = QueueFront(UnEmpty);//此时UnEmpty队列还剩下最后一个数据,也就是栈顶数据
QueuePop(UnEmpty);
return ret;
}
int myStackTop(MyStack* obj) {
// 栈顶的数据即不为空的队列中的队尾数据
Queue* UnEmpty = &obj->q1;
if(QueueEmpty(&obj->q1))
{
UnEmpty = &obj->q2;
}
return QueueBack(UnEmpty);
}
bool myStackEmpty(MyStack* obj) {
//两个队列都为空,栈才为空
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
obj = NULL;
}