数据结构栈和队列,看完这一篇就够了(万字配动图配习题)
Posted ^jhao^
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构栈和队列,看完这一篇就够了(万字配动图配习题)相关的知识,希望对你有一定的参考价值。
栈和队列
文章很长,如有需要,请先收藏哟❤❤❤❤❤❤
前言
栈和队列在数据结构中也是重要的一部分,他们都是特殊的线性表,许多需要用到我们的栈和队列,回想一下,我们以前写过的递归,其实都是可以通过我们的栈去模拟实现,而队列的使用也是十分多的,让我们一起学习学习他们的实现
一、栈
1.栈的结构
我们模拟实现栈的结构非常简单,只是在我们的线性表的基础上更改,不允许了我们的随机访问,而是得通过接口来实现数据的插入和删除,
我们的数据都是得从栈顶入栈,从栈顶出栈
注释:
压栈/进栈/入栈:栈的插入操作,入数据在栈顶
出栈: 站的删除操作,出数据也在栈顶
来看看下面这个动图加深理解吧:
代码展示栈的结构:
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
解释:这上面_a数组表示我们要的是一个动态开辟的数组,_top表示我们图中所示的数组,_capacity表示我们的容量大小,当_top与_capacity相同的时候我们就可以进行扩容操作了
首先我们创建一个stack.h,stack.c,和一个用来测试接口的test_stack_queue.c文件
常见接口:
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
2.初始化栈
听过我之前讲的函数栈帧的同学肯定知道我要写的第一个接口是什么
–初始化,不清楚的同学点击跳转函数栈帧
看完之后你就能明白为什么要初始化?不初始化的后果?
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = ps->_top = 0;
}
3.压栈操作
每次只是需要将top位置更新,因为是底层实现用的数组,所以说内存不够需要扩容,我们就分开一个函数CheckCapacity(),每次检测容量是否用完,
这里的增容是可以1.5或者2倍,但是增容意味着空间开太大会造成浪费,增容太少又造成频繁增容效率低下,所以我们一般选择适中的2倍。
void CheckCapacity(Stack* ps)
{
assert(ps);
if (ps->_capacity == ps->_top)
{
int newcapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* tmp= (STDataType*)realloc(ps->_a,sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
printf("增容失败\\n");
exit(-1);
}
ps->_a = tmp;
ps->_capacity = newcapacity;
}
}
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
CheckCapacity(ps);
ps->_a[ps->_top] = data;
ps->_top++;
}
4.出栈操作
出栈的时候需要检测是否栈内有无元素,没有元素的话我们就报错处理 ,当然这里判断栈是否为空我们也可以封装一个函数
栈是否为空
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top ==0;//0为空
}
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->_top--;
}
5.获取栈顶元素
也是要判断一下是否在栈内有无元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->_a[ps->_top-1];
}
6.获取栈中有效元素个数
_top的位置就是元素的总数
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top;
}
7.销毁栈
销毁栈的过程,销毁栈是重要的过程,我们的_a数组是动态开辟的,没有释放会造成内存泄漏
当然,注意看这里的ps指针是不能置空的,在之前的详解指针这一节的常见错误中有提及!
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
//free(ps); 错误
ps->_capacity = ps->_top = 0;
}
二、队列
1.队列的结构
队列的结构会比栈的难懂一些,因为队列的属性就是先入先出,所以采用顺序表的话我们的头删就要去移动数据覆盖第一个位置,或者用一个指针记录头的位置,头删的时候用移动头节点来表示,但是这都不如链表来的方便,链表在我们的头部删除是O(1)的时间复杂度,并且不用删除队尾元素,所以我们这里使用我们的单链表结构来实现队列!
链表的初阶
链表的进阶
// 链式结构:表示队列
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;//队列中结点的结构
// 队列的结构
typedef struct Queue
{
QNode* _front;//头指针
QNode* _rear;//尾指针
}Queue;
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
动图:
我们先封装队列中的一个结点,结点中有指向下一个元素的指针,和自身的值,然后因为我们的队列是用单链表实现的,而队列有一个获取队尾元素的接口,所以我们设置尾指针_rear能够是我们不用每次遍历找到尾在进行插入
2.队列的初始化
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;
q->_rear = NULL;
}
3.队列的插入
队列的插入就和单链表的头插一模一样,就是记得判断一开始我们的队头和队尾都是NULL,后续只需要移动_rear(队尾指针)!!!
动图:
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* tmp = (QNode*)malloc(sizeof(QNode));
tmp->_next = NULL;
tmp->_data = data;
if (q->_rear == NULL)
{
q->_front = q->_rear = tmp;
}
else
{
q->_rear->_next = tmp;
q->_rear = tmp;
}
}
4.队列的删除
动图:(当删除最后一个结点时易忽略处理_front 指针)
尾删需要注意是否队列中存在元素!!这里
的逻辑比较简单,但要注意当队列元素只剩一个的时候我们的队尾指针也要置成NULL,不然就会造成野指针
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
QNode* first = q->_front->_next;
if (first == NULL)
q->_rear = NULL;//处理这一步
free(q->_front);
q->_front = first;
}
5.获取队列头/尾部元素
这个就很简单啦!!
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_front->_data;
}
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_rear->_data;
}
6.获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
int n = 0;
QNode* cur = q->_front;
while (cur)
{
n++;
cur = cur->_next;
}
return n;
}
7.检测队列是否为空
如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL;
}
8.销毁队列
这里要遍历队列,因为每一个结点都是我们从堆上开辟的,我们就要一个个的释放
void QueueDestroy(Queue* q)
{
assert(q);
while (!QueueEmpty(q))
{
QNode* tmp = q->_front->_next;//保存下一个结点
free(q->_front);
q->_front = tmp;
}
q->_front = q->_rear = NULL;
}
从上面的图我们就能看出来,出栈的时候我们只能从栈顶出,出来的结果刚好与入的值相反,其实同一组数据入栈在出栈顺序不同的时候是有不同的结果的,
三、循环队列
解释:
循环队列的使用场景:当我们在去打疫苗的时候排队,注射疫苗的位置有限,我们前面排队的先打,后面的后进去的后打.
循环队列就是固定了队列的大小空间
设计循环队列
题目分析:这道题目是让我们设计一个循环队列,先说结论我们这里的循环队列所用的是循序表实现,因为我们在这个场景下不需要扩容,顺序表随机访问率高.其次如果使用单链表,在进行删除头部元素的时候我们需要找到上一个指针,为了效率就得有双向链表,但有简单又高效的结构我们优先去使用顺序表.
循环链表的结构
typedef struct {
int *a;//指向我们的数组
int front;//队头
int rear;//队尾,指向的位置是我们存放的位置
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj =(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->a =(int*)malloc(sizeof(int)*(k+1));
obj->front =obj->rear=0;
obj->k = k;
return obj;
}
循环链表的实现
题目看起来并不难,但有一个点确实需要琢磨,我们用顺序表的时候,一开始我们的队头元素的指针和队首元素的指针是指向同一处的,这个时候我们是认为他是题目看起来并不难,但有一个点确实需要琢磨,我们用顺序表的时候,一开始我们的队头元素的指针和队首元素的指针是指向同一处的,
这个时候是整个数组我们都已经放满了呢还是一个元素都没放入,这就会出现分歧
解决方案:
方案1.
聪明的同学可能一下子就想着说我们可能可以开一个标记矩阵,放过元素的我们标记一下,删除的时候我们把他的状态也标记一下,这个方案毫无疑问是可行的,但是空间开的就比较多了,下面有一种更好的
方案2.
我们在原来的基础上多开一个数组,当我们的队头指针与队尾指针相遇时是我们的数组还没元素,当_rear的下一个元素是我们的_front时才是我们整个数组放满了,下图为放满的情况
然后这题要对下标进行判断,下标若走到-1,则更新成k,若走到k+1则更新成0
typedef struct {
int *a;//指向我们的数组
int front;//队头
int rear;//队尾,指向的位置是我们存放的位置
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj =(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->a =(int*)malloc(sizeof(int)*(k+1));
obj->front =obj->rear=0;
obj->k = k;
return obj;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(((obj->rear+1)%(obj->k+1)) ==obj->front)
return false;
//插入元素
obj->a[obj->rear] = value;
++obj->rear;
obj->rear %= (obj->k+1);
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(obj->front == obj->rear)//删除元素front指针往前走
return false;
obj->front++;
if(obj->front == obj->k+1)
obj->front =0;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(obj->front == obj->rear)
return -1;
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(obj->front ==obj->rear)
return -1;
int x = obj->rear -1;
if(x ==-1)
x= obj->k;
return obj->a[x];
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear ==obj->front;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return ((obj->rear+1)%(obj->k+1)) ==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
obj->a =NULL;
free(obj);
}
四、一些习题
1.有效的括号
链接:有效的括号
这题要用到我们的栈,对于C语言的学习者,我们并没有库,大家可以用下面我在码云发布的栈的代码来写
思路:我们遇到左括号的时候就入栈,遇到右括号的时候就拿栈顶数据比较,只要不符合就返回false,若字符遍历完且栈里的数据都出完了,则括号都能一一匹配
class Solution {
public:
bool isValid(string s) {
//只要是左括号我们就入栈,遇到右括号我们就从栈顶pop一个数据看与他是否匹配
stack<char> st;
int len =s.size();
for(int i =0;i<len;i++)
{
char ch =s[i];
if(ch =='('||ch =='{'||ch=='[')
{
st.push(ch);
}
else
{
if(st.empty())
{
return false;
}
else
{
char ch2 = st.top();
st.pop();
if((ch ==')'&&ch2 !='(')
|| (ch =='}'&&ch2 !='{')
|| (ch ==']'&&ch2!='['))
return false;
}
}
}
if(st.empty())
return true;
return false;
}
};
2. 用队列实现栈
看到前面的不要慌,只是队列的实现而已,我们为了练习队列可以就用我们实现的队列
思路:用两个队列分别为q1,q2,每次push数据往其中为有数据的入,都没有就随便选一个入,pop数据的时候就将有数据的导入到没数据的,在判断是否为最后一个,是的话我们就pop掉且不用入有空的队列了
myStackPush数据的时候只用往nonempty(不为空)的队列入,动图:
myStackPop都要导到另一个为空的队列在pop掉最后一个数据,动图:
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;//队列中结点的结构
// 队列的结构
typedef struct Queue
{
QNode* _front;
QNode* _rear;
}Queue,queue;
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;
q->_rear = NULL;
}
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* tmp = (QNode*)malloc(sizeofSpringboot 整合RabbitMq ,用心看完这一篇就够了