栈的理解
栈是⼀种特殊的线性表,它的逻辑结构肯定是线性的。
它只允许在固定的⼀端进行插入和删除元素操作。在栈中,只允许从栈顶进行插入和删除数据。
在栈顶中这一端进行数据插入和删除,另⼀端称为栈底。
栈中的数据元素遵守 (也可以称为先进后出)后进先出LIFO(Last In First Out)的原则。
先进后出解释:最先进到栈中的数据,出栈时最后从栈里出去
进栈:栈的插⼊操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。

也可以理解为后来者居上!
那栈使用什么结构来实现呢?
如下:

这些结构都可以实现,我们要取最好的进行实现:
进行如下比较:
从时间复杂度角度分析,它们都为O(1),没法比较;
我们可以根据它们向内存中申请空间的大小进行比较:
双向链表有两个指针变量,向内存中申请的空间较大,所以先将双向链表排除了
数组增容所增的空间是一片连续的空间,而单链表的节点的申请则是不连续的,对于栈这样的频繁插入和删除数据的结构来说,用数组实现更好!
栈的实现
栈的定义:
思路:它的底层逻辑是数组,所以类比顺序表,定义如下:
核心代码:
//栈定义
typedef int STDatatype;
typedef struct stack
{
STDatatype* arr;//
int capacity;//栈的空间大小
int top;//栈顶 -- 与顺序表的size中类似 它的既可以用来记录当前栈中的有效数据个数,
//其次也可以作为在栈中插入删除数据的栈顶的位置
}ST;
栈的初始化
核心代码:
void StackInit(ST* ps)
{
//先断言,再进行初始化操作
assert(ps);
ps->arr = NULL;
ps->capacity = ps->top = 0;
}
入数据
思路:
00.断言
01.判断空间是否足够 – 这里可以不用单独将判断空间是否足够进行单独分装,因为这是栈,只有push,没有pushback frant等操作,再栈中只用一次
02.空间足够进行入数据
思路图:

void StackPush(ST* ps, STDatatype x)
{
//00.断言
assert(ps);
//01.判断空间是否足够 -- 这里可以不用单独将判断空间是否足够进行单独分装,因为这是栈,只有push,没有pushback frant等操作,再栈中只用一次
if (ps->capacity == ps->top)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDatatype* tmp =(STDatatype*) realloc(ps->arr, newCapacity * sizeof(STDatatype));
//newCapacity的类型是int的,这里的ralloc申请的是字节数,所以这里是 多少个STDatatype类型的数据
if (tmp == NULL)
{
perror("ralloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
//02.空间足够进行入数据
ps->arr[ps->top++] = x;
//在栈中,入数据时只能取栈顶的元素
}
判断栈是否为空
核心代码:
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
出数据
思路图:

核心代码:
void StackPop(ST* ps)
{
//00.断言,除了传参不能为空,其栈也不能为空
assert(ps);
assert(!StackEmpty(ps));
//01.栈不为空,直接将栈顶元素出了
--ps->top;
}
取出栈顶元素
思路图

核心代码:
STDatatype StackTop(ST* ps)
{
//00.断言
assert(ps);
assert(!StackEmpty(ps));
//01.进行去栈顶元素
return ps->arr[ps->top - 1];
//top - 1 解释:top是指栈中有效数据的个数,这里它是作为下标,下标是从0开始的,所以他要 -1;
}
栈中的数据不能被遍历,也不能随机访问,只能将取出栈顶元素和出数据联合使用,达到打印的效果
以下是代码:
//这里的stackPop 可以和stacktop 联合使用以达到打印的效果
//循环出栈,直到栈为空
while (!StackEmpty(&st))
{
//取出栈顶元素
STDatatype ret = StackTop(&st);
printf("%d ", ret);
StackPop(&st);
}
记录栈中的元素个数
核心代码:
int STSize(ST* ps)
{
assert(ps);
return ps->top;//top记录的就是有效数据的个数
}
相关的算法题:
有效的括号
思路:
- 做这道题之前先将栈的基本功能实现一遍
- 创建栈并初始化, 创建指针遍历ps保存字符串s,开始循环比较:若为左括号,入栈;右括号,是否与栈顶元素相匹配:匹配,栈顶元素出栈,ps++往后进行遍历;不匹配,销毁并返回false
- 遍历完成后,跳出循环,进入只有在左括号的情况中进行判断
特殊情况:
只有左括号 :在最后判断栈是否为空,为空销毁并返回false;
只有右括号:在右括号的条件下,判断栈是否为空,为空,销毁并返回false
题目中的括号没有优先级,但需要正确的匹配
核心代码:
bool isValid(char* s)
{
//创建一个栈,并将其初始化
ST st;
StackInit(&st);
char* ps = s;
while(*ps != '\0')//这里应该是不等于
{
//左括号,入栈
if (*ps == '(' || *ps == '[' || *ps == '{')
{
StackPush(&st, *ps);
}
else//右括号进行匹配,若匹配,则让栈顶元素ch出栈,ps继续往后++;若不匹配,则销毁后返回false。
{
if (StackEmpty(&st))
{
StackDestroy(&st);
return false;//如果一开始就是右括号的情况下,直接返回false
}
char ch = StackTop(&st);//取出栈顶元素进行比较
if ((*ps == ')' && ch == '(') || (*ps == ']' && ch == '[') || (*ps == '}' && ch == '{'))
{
StackPop(&st);
}
else
{
StackDestroy(&st);//必须做到先销毁后返回
return false;
}
}
ps++;//不能加*,这里是让他的地址往后进行循环的
}
bool ret = StackEmpty(&st) == true;//当只有一个左括号时,栈中不为空,返回false
StackDestroy(&st);
return ret;
}
队列
队列理解
指的是只在一端进行插入数据操作,在另外一端只进行删除数据操作的一种特殊的线性表。它具有先进行出的原则,也就是先进到队列的数据,从队列中出去时先出的原则。
入队列 :进行插入操作的一端叫队尾;
出队列:进行删除操作的一端叫队头;

那它到底怎么实现呢?
数组? ---- 满足队列的基本规则后,我们将数组的起始下标作为队头,将另外一端作为队尾,后发现:在删除数据时,队头的上界(最坏的情况)的时间复杂度为O(N);插入数据时队尾的上界的时间复杂度为O(1);
单列表?---- 满足队列的基本规则后,单链表的两端,不管那一端作为队头和队尾,总会有一端的时间复杂度为O(N),另一端为O(1);

简单比较二者后,我们对单链表多定义一个尾结点ptail,让它作为队尾;头节点作为队头
两端的时间复杂度都为O(1)
下面是改进后单链表示意图:

那为什么不让phead作为队尾,让ptail作为队头?
让phead作为队尾插入数据可以,但如果让ptail作为队头删除数据,由于是单链表,删除节点后找不到该节点的前一个结点,所以不可以让他俩的位置进行互换
因此,我们将单链表作为队列的底层逻辑结构
队列的实现:
队列结构定义
由于队列的底层逻辑是单链表,而单链表又是由结点组成的,因此我们在定义队列结构时,需要定义两个结构体,一个是队列的,另外一个是队列结点的。
核心代码:
//队列结点定义
typedef int QDataType;
typedef struct QueueNode
{
QDataType x;
struct QueueNode* next;
}Queue;
//队列定义
typedef struct Queue
{
QueueNode* phead;
QueueNode* ptail;
}Queue;
队列初始化
核心代码:
void QueueInit(Queue* pq)//函数记得带上返回值
{
assert(pq);
pq->size = 0;
pq->phead = pq->ptail = NULL;
}
在队尾入队列
思路:先创建一个新的队列结点newnode,判断队列是否为空,若为空,将pq中的phead 和 ptail指向newnode;不为空,pq中的ptail的next 的指向为newnode,并让ptail的指向更改为newnode。
示意图:

核心代码:
void QueuePush(Queue* pq, QDataType x)
{
//00.断言
assert(pq);
//01.创建新结点并将新结点初始化
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)//只要是申请空间,就要进行判断是否申请失败
{
perror("malloc fail !");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
//02.从队尾中入新节点 需要分类讨论对列为空还是不为空
//为空时
if (pq->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
else//不为空
{
pq->ptail->next = newnode;
pq->ptail = newnode;//等价于 pq->ptail = pq->ptail->next;
}
}
判断队列是否为空
该函数必须要放到调用它的函数的上面,因为代码运行是从上而下运行的,没有找到定义的,自然就会报错。
核心代码:
bool QueueEmptory(Queue *pq)
{
assert(pq);
return pq->phead == NULL && pq->ptail == NULL;
//二者写一个也可以
}
在队头出队列
示意图:

核心代码:
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmptory(pq));
//只有一个结点
if (pq->phead == pq->ptail)
{
free(pq->phead);
pq->phead == pq->ptail == NULL;
}
//有多个节点
else
{
////方法1:
//QueueNode* del = pq->phead;
//pq->phead = del->next;
//free(del);
//方法2:
QueueNode* Next = pq->phead->next;
free(pq->phead);
pq->phead = Next;
}
}
取队尾的值
直接返回队尾的值即可
核心代码:
QDataType QueueFrant(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
取队头的值
直接返回队头的值即可
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
记录队列有效数据个数
思路:在创建队列之初增加个变量size,入数据时pq->size++,出数据时pq->size–最后返回pq->size即可
核心代码:
int QueueSize(Queue* pq)
{
assert(pq);
//int size = 0;
QueueNode* pcur = pq->phead;
//while (pcur != NULL)
//{
// size++;
// pcur = pcur->next;
//}
////这种做法是不合理的,因为队列是一端进一端出的,队列不能够被遍历;
// 这里的时间复杂度为O(N),若客户有大量的数据,当我们进行遍历求个数时会导致程序运行效率低下
//优化方法:在创建之初就创建一个变量size,在初始化时个数为0,入数据时++;出数据时——
//调用这个方法时,直接返回即可。s
return pq->size;
}
队列的销毁
思路:遍历队列依次进行销毁,将队列中结构体的成员该置空的置空,该置零的置零
核心代码:
void QueueDestroy(Queue* pq)
{
//断言
assert(pq);
assert(!QueueEmpty(pq));
//遍历销毁
QueueNode* pcur = pq->phead;
while (pcur)
{
QueueNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
//这里还需要手动释放phead 和ptail以及size
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
相关算法题:
我们再做下面两道题时看它是需要实现谁,在具体实现对应功能时画图想想之前实现这个功能时的具体操作,然后用已有结构的功能实现另外一个结构功能的转化。不懂的可以画两个队列或者两个栈并找几个数据举例来说明
用队列实现栈
在做下面这些操作之前,需要手动完成队列功能的实现
我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能
栈的定义
思路:用队列实现栈,因此这里需要创建两个队列q1 和 q2
核心代码:
//创建两个队列
typedef struct {
Queue q1;
Queue q2;
} MyStack;
栈的初始化
思路:创建指向栈的指针变量,并向内存中申请MyStack个大小的空间,用已有队列初始化的功能对MyStack进行初始化,返回指针变量。
核心代码:
//stackInit()
MyStack* myStackCreate() {
//现在还没有栈,需要我们创建栈并对其进行初始化
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&pst->q1);
QueueInit(&pst->q2);
return pst;
}
入栈
思路:找不为空的队列,将数据通过队列入队的功能入到栈中
核心代码:
void myStackPush(MyStack* obj, int x) {
if (!QueueEmpty(&obj->q1))
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}
出栈
思路: 找到不为空的队列,找到该队列,将其size-1数据导入到另外一个队列中去,最后剩下1个有效数据,直接出栈
核心代码:
int myStackPop(MyStack* obj) {
//01.创建两个指针变量指向两个队列,找不为空的队列,这里两个指针变量指向队列没有说必须是noneQ = &obj->q1, Queue* empQ = &obj->q2,也可以用noneQ = &obj->q2,Queue* empQ = &obj->q1只要找到不为空的就行;也算是另外一种方法
Queue* noneQ = &obj->q1;//为什么这里和前面的不一样要带*号 -- 这里创建的是指针变量指向的是队列q1的地址
Queue* empQ = &obj->q2;
if (!QueueEmpty(&obj->q2))
{
noneQ = &obj->q2;
empQ = &obj->q1;
}
while(QueueSize(noneQ) > 1)
{
int frant = QueueFrant(noneQ);
QueuePush(empQ, frant);
QueuePop(noneQ);
}
int pop = QueueFrant(noneQ);//这里的类型为啥时int -- 因为取队头元素的返回值就是int
QueuePop(noneQ);
return pop;
}
取栈顶元素
思路:找不为空的队列,取队尾元素
下面注释的代码是错误的思路,下面的代码虽然在逻辑上是正确的,经过画图得知:这里在使用时,会导致两个队列都不为空,而我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能
核心代码:
int myStackTop(MyStack* obj) {
// Queue* noneQ = &obj->q1;//为什么这里和前面的不一样要带*号
// Queue* empQ = &obj->q2;
// if (!QueueEmpty(&obj->q2))
// {
// noneQ = &obj->q2;
// empQ = &obj->q1;
// }
// while(QueueSize(noneQ) > 1)
// {
// int frant = QueueFrant(noneQ);
// QueuePush(empQ, frant);
// QueuePop(noneQ);
// }
// return QueueFrant(noneQ);
//这里的代码在逻辑上是正确的,经过画图得知:这里在使用时,会导致两个队列都不为空,而我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能
//优化后代码: --- 思路:找不为空的队列,取队尾元素
if (!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}//记得带上大括号
栈的判空
思路:栈的判空就是队列的判空,直接调用即可,前提是两个队列必须同时满足不为空才可以
核心代码:
bool myStackEmpty(MyStack* obj) {
//栈的判空就是队列的判空,直接调用即可,它两必须同时满足才可以
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
栈的销毁
思路:栈的销毁就是队列的销毁,这里需要注意的是我们要对初始化时创建的指针pst也需要进行置空
核心代码:
void myStackFree(MyStack* obj) {
//栈的销毁就是队列的销毁,在初始化时创建的指针pst也需要进行置空
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
obj = NULL;
}
用实现队列
再实现这些之前,我们需自己将栈实现一遍
定义队列
思路:创建两个栈一个专门用来入栈的,另外一个专门用来出栈,从而实现队列
核心代码:
typedef struct {
ST pushST;
ST popST;
} MyQueue;
队列初始化
思路:创建指针变量向内存中申请MyQueue个大小的空间,用已有栈初始化的功能对MyQueue进行初始化,返回指针变量。
核心代码:
MyQueue* myQueueCreate() {
MyQueue* pst = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&pst->pushST);
StackInit(&pst->popST);
return pst;
}
入队列
思路:往pushST中插入数据
核心代码:
void myQueuePush(MyQueue* obj, int x) {
StackPush(&obj->pushST, x);
}
出队列
思路:判断popST是否为空,不为空,直接pop;为空,先将pushST中的数据导入到popST中再pop
若为空的具体操作:
使用入栈功能将pushST中的数据导入到popST中,然后将pushST中的数据出栈(也就是从栈中移除),依次循环进行,直到pushST为空
pop的具体操作:
取栈顶元素并保存,再将其出栈(也就是从栈中移除),最后将之前保存的栈顶元素进行返回
核心代码:
int myQueuePop(MyQueue* obj) {
if (StackEmpty(&obj->popST))
{
//将数据从pushST导入到popST中
while(!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));//stackpop功能有返回值的,返回的是出栈后的值
StackPop(&obj->pushST);
}
}
//取栈顶,删除栈顶元素并返回栈顶数据
int top = StackTop(&obj->popST);//stacktop 返回的是栈顶的值
StackPop(&obj->popST);
return top;
}
取队头数据
思路:和前面出队列一样,但是这里不需要移除栈顶元素(队列的先进先出原则,在栈pushST中插入数据1 2 3,此时栈顶元素为3,通过导数据导到栈popST中,此时该栈的栈顶元素为1,依次取栈顶元素,最后的取出的是1 2 3,满足队列的先进先出原则,因此该思路有效)
示意图:
核心代码:
int myQueuePeek(MyQueue* obj) {
if (StackEmpty(&obj->popST))
{
while(!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
return StackTop(&obj->popST);
}
队列判空
思路:我们是用两个栈实现的队列,所以要想队列为空,那就要做到两个栈同时为空
核心代码:
bool myQueueEmpty(MyQueue* obj) {
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
队列的销毁
思路:我们是用两个栈实现的队列,所以要想销毁队列,那就要做到两个栈都要进行销毁,除此之外,我们在初始化时创建指向MyQueue的指针变量也要释放并置为空
void myQueueFree(MyQueue* obj) {
StackDestroy(&obj->pushST);
StackDestroy(&obj->popST);
free(obj);
obj = NULL;
}
设计循环队列
循环队列定义:循环队列⾸尾相连成环,环形队列可以使⽤数组实现,也可
以使⽤循环链表实现
循环队列的底层逻辑我们更推荐数组
原因:1.数组的空间固定且连续,若用链表实现,需要申请一个节点的空间,且这些节点的空间可能不连续,还需要使用指针的指向将它们连接起来,这势必会有空间的损耗;
2.在对数组的某个位置插入删除数据时,不需要考虑该位置是否为空,直接插入覆盖即可; 若使用链表,我们需要判断该节点是空的还是非空的,若为空才可将新数据插入到该节点中,很麻烦
循环队列的定义
//循环队列的定义
typedef struct {
//底层结构是数组,需要创建数组
int* arr;
//一个指向头,一个指向尾
int front;
int rear;
//防止后面判满时找不到k,这里才创建capacity用来记录空间大小
int capacity;
} MyCircularQueue;
对循环队列进行插入数据之前,需要判断队列是否满了
判断方法如下:

核心代码:
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1) % (obj->capacity + 1) == front;
}
往循环队列中入数据
思路:
- 插入数据之前判断队列是否满了,这里有判断队列为满的函数,直接调用即可,如果满了返回false,表示不能插入数据
- 从上面代码出来后,说明没满,可以插入数据:将要插入的数据插入到下标为rear的位置,插入后rear++,又因为这里是循环队列,如果继续让rear++,它会超过申请的空间造成越界,因此这里用 obj->rear %= obj->capacity + 1 来让rear回到起始的位置让它达到循环入数据的效果,最后插入成功返回true
//往循环队列插入数据
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
//01. 插入数据之前判断队列是否满了,这里有判断队列为满的函数,直接调用即可,如果满了返回false,表示不能插入数据
if (myCircularQueueIsFull(obj))
{
return false;
}
//02. 从上面代码出来后,说明没满,可以插入数据:将要插入的数据插入到下标为rear的位置,插入后rear++,又因为这里是循环队列,不能越界,用 obj->rear %= obj->capacity + 1 来让rear回到起始的位置防止越界,最后插入成功返回true
obj->arr[rear++] = value;
obj->rear %= (obj->capacity + 1);//等价于 obj->rear = obj->rear % (obj->capacity + 1);
return true;
}
在循环队列出数据时需要先判断队列是否为空
方法如下:

核心代码:
//判断队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear == obj->front;
}
在循环队列中出数据
思路:
01.出数据时队列不能为空,这里需要判空
02.开始出数据,直接让front++,后面插入数据时直接覆盖即可
03.因为这里是循环队列,加到最后front会超出我们所申请的空间大小造成越界,这里用obj->front %= obj->capacity + 1 来让front来到起始位置让他继续出数据以达到循环的效果,最后出数据成功返回true
//在循环队列中出数据
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
//01.出数据时队列不能为空,这里需要判空
if (myCircularQueueIsEmpty(obj))
{
return false;
}
//02.开始出数据,直接让front++,后面插入数据时直接覆盖即可
obj->front++;
//03.用obj->front %= obj->capacity + 1 来让front来到起始位置防止越界,最后出数据成功返回true
obj->front %= obj->capacity + 1;//等价于obj->front = obj->front % (obj->capacity + 1);
// capcacity + 1:为啥要 + 1,程序给的是k个空间,我们实际申请的是 k + 1 个 int的空间,要按照实际来,所以要 + 1;
return true;
}
取队首元素
思路:先去判空,若为空返回-1,不为空直接返回队头元素
int myCircularQueueFront(MyCircularQueue* obj) {
//01.判空
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
//02.若不为空,返回队头元素
return obj->arr[obj->front];
}
取队尾元素

思路:
01.取队尾元素时,队列不能为空,需要先去判空,若为空,返回-1
02.取队尾元素就是取下标为rear - 1;创建变量prev;
正常情况:让prev = rear - 1返回obj->arr[prev];
特殊情况:如果rear == 0,则让prev = capacity,返回obj->arr[prev]
int myCircularQueueRear(MyCircularQueue* obj) {
//01.取队尾元素时,队列不能为空,需要先去判空,若为空,返回-1
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
//02.取队尾元素就是取下标为rear - 1;创建变量prev;
//正常情况:让prev = rear - 1返回obj->arr[prev];
//特殊情况:如果rear == 0,则让prev = capacity,返回obj->arr[prev]
int prev = obj->rear - 1;
if (obj->rear == 0)
{
prev = obj->capacity;
}
return obj->arr[prev];
}
循环队列的释放
思路: 释放掉创建指向数组的地址,和 指向循环队列的地址
//释放掉创建指向数组的地址,和指向循环队列的地址
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->arr)
free(obj);
obj = NULL;
}
4476

被折叠的 条评论
为什么被折叠?



