数据结构_栈和队列
前言:此类笔记仅用于个人复习,内容主要在于记录和体现个人理解
栈
栈的概念
栈(Stack),一种特殊的线性表
线性:线状结构(一对一的关系)
![]()
与之相对的是非线性结构(一对多的关系或者说是多对一;以下是其中一种,树)
![]()
特殊在必须按照如下原则存储数据:
只允许在固定的一端进行插入和删除元素就是"后进先出"的原则(最后进来的最先出去)(Last In First Out,简称LIFO原则)
类似于一个有底的容器,依次将元素放进去,想要拿出来的话当然只能从最上面开始拿
不过也不一定非得都放进去再拿出来,可以拿一个放一个
可以参考手枪弹夹
可以都上满膛再打出去
也可以随便,上几个再打出去一个或几个
也可以参考便利贴
![]()
![]()
总之就是这个样子的
![]()
1.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出 栈的顺序是(C)。 A 12345ABCDE B EDCBA54321 C ABCDE12345 D 54321EDCBA 2.若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是(C) A 1,4,3,2 B 2,3,4,1 C 3,1,4,2 D 3,4,1,2
因为在入栈过程中可能有数据出栈,因此元素的出栈顺序会有多种情况,但一定满足LIFO原则
在函数的栈帧里面讲到过一种”栈“,它是虚拟进程地址空间划分的一个区域,用来函数调用时建立栈帧
它的遵循的原则跟数据结构里的“栈”一样,都是“后进先出”
但这两个“栈”并没有什么特殊联系,属于两个学科
栈的实现
栈的实现可以用两种,一种是数组,一种是链表,链表可单可双,都可以
如果是用数组的话,就是数组栈:用数组的头当作栈底,尾当作栈顶,尾插尾删就行
如果用链表的话,就是链式栈:
如果是双向循环链表,也是尾插尾删
如果是单链表的话,就让头当作栈顶,尾当作栈底,头插头删
因为单链表找尾很麻烦,但是头结点好找
![]()
一般来说认为数组栈要比链式栈好一点
因为虽然数组栈可能会有空间浪费(扩容),但由于物理存储上是连续的,cpu高速缓存命中率更高
数组栈的实现
top作为数组的权重,用来表示栈顶元素的位置
有两种表示的方法:
- top指向栈顶元素,一开始top=-1,有一个元素时top为0指向数组第一个元素,栈顶元素是数组的第一个元素;top+1的值等于元素个数
![]()
top指向栈顶元素的后一个元素,一开始top=0指向数组第一个元素,有一个元素时top=1指向第二个元素也就是栈顶元素的后一个元素;top的值等于元素个数
![]()
- 学了链表之后,实现栈和队列如同砍瓜切菜!!*
下面是用top指向栈顶元素实现的动态数组栈
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> typedef int STDataType;//之前一直没有明说,为什么要用别名,因为这是为了修改数据方便,如果数据是char,在这一改就都改了 //栈的结构 typedef struct Stack { STDataType *a;//用链表来存数据 int top;//记录栈顶元素的位置 int capacity;//栈容量 }ST; //栈的初始化 void StackInit(ST* ps); //栈的销毁 void StackDestory(ST* ps); //数据入栈/压栈 void StackPush(ST* ps, STDataType x); //出栈 void StackPop(ST* ps); //判断栈是不是空,是的话返回0,不是的话返回非0 bool StackEmpty(ST* ps); //返回元素个数 int StackSize(ST* ps); //返回栈顶元素 STDataType StackTop(ST* ps);
不需要额外的扩容函数,因为只有压栈的时候需要扩容,用不着复用
一定要考虑边界情况:栈为空
栈的初始化
void StackInit(ST* ps) { ps->a = NULL; ps->top = -1; ps->capacity = 0; }
栈的销毁
void StackDestory(ST* ps) { assert(ps); free(ps->a);//ps->a 是malloc出来的,所以必须要free ps->a = NULL; ps->top = ps->capacity = 0;//数组置空了top跟capacity跟着清零 //ps不用指向空,它只是main函数里的局部变量,ps只是一个结构体指针,并不是栈的本体,本体是数组 //更何况传的不是二级指针 }
数据压栈
void StackPush(ST* ps, STDataType x) { assert(ps); if((ps->top+1) == ps->capacity) { int newCapacity = ps->capacity == 0 ? 4 : (ps->capacity)*2; ps->a = (STDataType *)realloc(ps->a , newCapacity * sizeof(STDataType));//这里要注意扩容的大小的计算,是新空间元素个数乘以单个数据类型大小 ps->capacity = newCapacity;//别忘了更新数组大小 if(ps->a == NULL)//为了安全起见最好判断一下空间申请是否成功 { printf("realloc fail\n\n"); exit(-1); } } ps->a[(ps->top)+1] = x; ps->top++;//栈顶后移 }
出栈
void StackPop(ST* ps) { assert(ps); if(ps->top>-1) //也可以更暴力一点,assert(ps->top>-1) --ps->top; }
判断栈是不是空,是的话返回 非0( true ),不是的话返回 0( false )
bool StackEmpty(ST* ps) { return ps->top == -1;//栈空了 = 等式成立 = 返回值为真(ture,非0) }
bool 类型,返回值可以是非0(真),0(假),也可以是true(真)和false(假),在意义上两种是一样的
返回元素个数
int StackSize(ST* ps) { assert(ps); return ps->top+1; }
返回栈顶元素
STDataType StackTop(ST* ps) { assert(ps); return ps->a[ps->top]; }
队列
队列的概念
队列(Queue),一种只允许在一端插入数据,在另一端删除数据的特殊线性表。
像自来水管,从一端进水,另一端出水
遵循的原则是“先进先出”(最先进来的元素最先出去)(First In First Out,简称FIFO)
进行 插入 操作的位置称为队尾,进行 删除 操作的位置称为队头
![]()
因此出队列的情况要比出栈更简单,只有一种情况,那就是排在前面的一定会先出来,不论入队列过程中是否有数据出队列
队列的实现
使用 单链表 来实现
根据队列的需求,只需要进行 尾插 和 头删
至于为什么不是 头插 和 尾删,以及为什么不用数组
基于队列的原则,必须头删 或 头插 ,头部的处理,无论是头插还是头删,数组都比较麻烦,而链表效率高。
数组实现尾插尾删很简单,只要记住尾的位置就行,因此用数组栈。
单链表尾插比较简单,虽然要找到尾,需要先遍历一次链表,不过可以用一个尾指针来保存尾结点的位置
单链表尾删比较麻烦,必须要找尾,找尾必须遍历链表,每次尾删都要遍历一次,因为即使记录了尾结点的位置,删除了也会找不到,就算记录了尾结点上一个节点的位置,最终还是会删除掉,所以无论如何最后到要遍历找尾。
因此单链表+头删尾插 容易实现
\
定义两个结构体
一个是队列的结点,包含结点数据和next指针
一个是队列,包含队列的头指针和尾指针,也可以增加一个整型size来方便记录数据个数
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> typedef int QDataType; typedef struct Qnode { QDataType data; struct Qnode *next; }Qnode;//队列结点 typedef struct Queue { Qnode *head; Qnode *tail; int size; }Queue; //队列初始化 void QueueInit(Queue *pq);//pq的意思是phead of queue(大概 //销毁队列 void QueueDestory(Queue *pq); //入队列 void QueuePush(Queue *pq , QDataType x); //出队列 void QueuePop(Queue *pq); //判断队列是否为空 bool QueueEmpty(Queue *pq); //获取元素个数 size_t QueueSize(Queue *pq); //返回队头元素 QDataType QueueFront(Queue *pq); //返回队尾元素 QDataType QueueBack(Queue *pq);
一定要考虑边界情况:队列为空
队列初始化
void QueueInit(Queue *pq)//pq的意思是phead of queue(大概 { assert(pq); pq->head = NULL; pq->tail = NULL; pq->size = 0; }
队列销毁
void QueueDestory(Queue *pq) { assert(pq); while(pq->head) { Qnode *next = pq->head->next; free(pq->head); pq->head = next; } pq->size = 0; pq->tail = NULL; }
只有malloc的空间才需要销毁
入队列
void QueuePush(Queue *pq , QDataType x) { assert(pq); Qnode *newndoe =(Qnode *)malloc(sizeof(Qnode)); assert(newndoe); newndoe->data = x; newndoe->next = NULL; if(pq->head == NULL)//队列之存在两种情况,空或者不空,空的话头尾同时为空,不空的话头尾都不为空 //如果头为空或者尾为空(或者同时为空,这样的话下面就不用断言了),说明队列是空的 { assert(pq->tail == NULL);//确保头尾同时为空 pq->head = pq->tail = newndoe; } else { pq->tail->next = newndoe; pq->tail = newndoe; } pq->size++; }
一定要考虑边界情况
出队列
void QueuePop(Queue *pq) { assert(pq); assert(pq->head);//防止空队列 Qnode *newhead = pq->head->next; if(newhead == NULL)//如果队列只有一个元素的话 { free(pq->head); pq->head = pq->tail = NULL;//出队列之后队列就为空了 } else { free(pq->head); pq->head = newhead; } pq->size--; }
判断队列是否为空
bool QueueEmpty(Queue* pq) { assert(pq); //return pq->head == NULL && pq->tail == NULL; return pq->head == NULL; }
获取元素个数
size_t QueueSize(Queue *pq) { assert(pq); return pq->size; }
返回队头元素
QDataType QueueFront(Queue *pq) { assert(pq); assert(pq->head);//防止队列为空 return pq->head->data; }
返回队尾元素
QDataType QueueBack(Queue *pq) { assert(pq); assert(pq->tail);//防止队列为空 return pq->tail->data; }
栈和队列面试题
笔试题
![]()
可以看到“正确的顺序”就是在右括号之前,越靠后的左括号越先闭合
就是“LIFO"原则
思路:
用栈来解决,只有字符是左括号,就让它入栈,如果字符是右括号,那么就让栈顶的左括号出栈
这样就保证了在右括号出现的时候,是最后面的左括号跟右括号进行匹配
![]()
注意:
- 如果左括号的数量多于右括号,比如s里只有一个左括号,那么在遍历完字符串之后栈不为空,就返回false
- 如果右括号数量多于左括号,比如s里只有一个右括号,那么在栈为空的时候字符串元素是右括号,返回false
如何进入遍历字符串s的循化:
题目中传的参是char*,是s的首元素地址指针,s++就指向下一个元素,最后在指向完最后一个元素之后就会越界
字符串的细节 其实只要一个字符串赋过值(包括空字符串“”),就一定有一个字符’/0‘在里面,这是很基础的东西,但是往往会忘记,因为我们一般认为超过字符串的长度以后的索引都会越界,其实越界会取到‘/0’,取出来的是一个空字符‘’
所以只要s的元素也就是*s不是空字符就说明字符串没结束
因为c还不能用库,所以栈的实现需要自己造轮子,把上面写的栈的实现的代码先粘过去就行
然后完成题目
STDataType StackTop(ST* ps) { assert(ps); return ps->a[ps->top]; } bool isValid(char * s) { ST st; StackInit(&st); while(*s) { if(*s == '(' || *s == '{' ||*s == '[')//是左括号,入栈 { StackPush(&st , *s); s++; } else//右括号,让栈顶元素出栈 { if(StackEmpty(&st))//先判断栈是不是空 { return false; } char top = StackTop(&st);//获取栈顶元素 StackPop(&st);//出栈 if(*s == ')' && top != '('|| *s == ']' && top != '['|| *s == '}' && top != '{')//匹配判断 { return false; } else { s++; } } }//如果顺利循环完说明存右括号都匹配上了 bool ret = StackEmpty(&st);//看看栈是不是空,如果不是说明左括号有多余的左括号 StackDestory(&st);//销毁栈,防止内存泄漏 return ret;//这里很妙,用bool类型作为bool函数的返回值,省得分情况讨论返回真还是假 }
![]()
就是用两个队列来实现栈的特点,后进先出
具体实现方法就是,保持一个队列是空的,一个不是空的
入数据的话就让数据进到非空队列,出数据的话,把非空队列的最后一个元素以外的元素全都转移到空队列,然后再把原非空队列中的这最后一个数据出队列
![]()
题解
实现队列首先需要先自己造轮子,直接粘贴上Queue的实现代码就行
构造MyStack结构体,定义构造函数
typedef struct { Queue Qa; Queue Qb; } MyStack; MyStack* myStackCreate() { MyStack *obj = (MyStack *)malloc(sizeof(MyStack)); assert(obj); QueueInit(&obj->Qa); QueueInit(&obj->Qb); return obj; }
- 为什么myStackCreate函数里面用malloc构造obj而不是直接声明一个obj?
因为函数内构造的是局部变量,出了函数作用域就会被销毁,必须用malloc在堆上申请空间
- 为什么MyStack里面用的是Queue结构体而不是结构体指针?
这个栈里面包含着两个队列,就是单纯的需要包含两个队列,用指针干啥。。。。。
就像Queue的定义里面用的是Qnode * 那是单纯的需要链表指针,一个指向头,一个指尾
就算是用结构体指针,那结构体Queue本体在哪儿?还是要malloc出来(在MyStackCreat的时候),要不队列没法用啊,有指针没实体
![]()
而函数在传参的时侯传栈、队列什么的结构体指针那是为了修改栈、队列的结构体成员
将数据x压入栈顶
void myStackPush(MyStack* obj, int x) { assert(obj); if(QueueEmpty(&obj->Qb))//把数据放到非空的队列里 QueuePush(&obj->Qa , x);//注意这里,queuepush函数中有malloc结点的操作,因此最后一定要free掉 else QueuePush(&obj->Qb , x); }
移除并返回栈顶元素(指的是移除之前的栈顶元素)
int myStackPop(MyStack* obj) { assert(obj); Queue *emptyQ = &obj->Qa;//先假定a是空的,b不是空的 Queue *nonEmptyQ = &obj->Qb; if(!QueueEmpty(&obj->Qa))//要是假设错了换过来就行了 { emptyQ = &obj->Qb; nonEmptyQ = &obj->Qa; } while(QueueSize(nonEmptyQ) >1 ) { QueuePush(emptyQ , QueueFront(nonEmptyQ)); QueuePop(nonEmptyQ); } int top = QueueFront(nonEmptyQ);//注意要先nonEmpty的尾元素再free掉nonEmpty QueuePop(nonEmptyQ); return top; }
- 这里用了一个简单的方法分辨非空队列:先假定一个是空的,另一个不是空的,如果错了的话那就反过来
这样后面不用每个判断都写一遍
- 判断非空队列是不是只剩一个元素
一开始我写的是判断队列的head == tail ,写起来有点麻烦
杭哥的方法是直接调用现成QueueSize函数
这里体现了函数复用的简便性,还是杭哥的好
返回栈顶元素
int myStackTop(MyStack* obj) { assert(obj); if(QueueEmpty(&obj->Qa))//还是调用前面的函数,简单 return QueueBack(&obj->Qb); else return QueueBack(&obj->Qa); }
判空函数和free函数
bool myStackEmpty(MyStack* obj) { assert(obj); return QueueEmpty(&obj->Qa) && QueueEmpty(&obj->Qb); } void myStackFree(MyStack* obj) { assert(obj); QueueDestory(&obj->Qa);//队列的结点是malloc出来的,因此要free掉,而且要注意free的顺序,先是队列的结点,再是obj。队列只保存了结点指针,没有保存结点,所以free掉obj并不会free掉结点,反而会找不到结点的地址 QueueDestory(&obj->Qb); free(obj); obj = NULL; }
![]()
队列的特点是先进先出(FIFO),对于栈而言,最先进来的元素在最里面(栈底)
设置两个栈,一个专门用来入数据pushST,一个专门用来出数据popST
入队列直接进到pushST里面
一旦popST空了,就把pushST的数据都导过来,就保证了之前pushST里的最里面的元素(栈底,先进来的)到了popST里就变成了最外面的(栈顶,后进来的),就符合先进先出的原则了
![]()
![]()
题解
还是要自己造轮子实现栈,直接先粘贴上Stack的代码
构造MyQueue结构体,定义构造函数
typedef struct { ST pushST; ST popST; } MyQueue; MyQueue* myQueueCreate() { MyQueue *obj = (MyQueue *)malloc(sizeof(MyQueue)); assert(obj); StackInit(&obj->pushST); StackInit(&obj->popST); return obj; }
元素x入队列
void myQueuePush(MyQueue* obj, int x) { assert(obj); StackPush(&obj->pushST , x); }
出队列,并返回被移除的元素
int myQueuePop(MyQueue* obj) { assert(obj); if(StackEmpty(&obj->popST))//凡是涉及队头元素的,都要判断一下popST是不是空,是空就把pushST移过去 { while(!StackEmpty(&obj->pushST)) { StackPush(&obj->popST , StackTop(&obj->pushST)); StackPop(&obj->pushST); } } int top = StackTop(&obj->popST);//记录被移除的队头元素 StackPop(&obj->popST); return top;//返回被移除的队头元素 }
返回队头元素
int myQueuePeek(MyQueue* obj) { assert(obj); if(StackEmpty(&obj->popST))//凡是涉及队头元素的,都要判断一下popST是不是空,是空就把pushST移过去 { while(!StackEmpty(&obj->pushST)) { StackPush(&obj->popST , StackTop(&obj->pushST)); StackPop(&obj->pushST); } } return StackTop(&obj->popST); }
判空函数和free函数
bool myQueueEmpty(MyQueue* obj) { assert(obj); return StackEmpty(&obj->popST) && StackEmpty(&obj->pushST); } void myQueueFree(MyQueue* obj) { assert(obj); StackDestory(&obj->pushST); StackDestory(&obj->popST); free(obj); }
MyQueue设计符合**“解耦设计”** 解耦_百度百科 解耦设计 知乎 解耦设计 CSDN
低耦合,高内聚
MyQueue的实现用到了栈的接口,但是栈内部的实现跟MyQueue这个队列的没有关联,不管栈是数组栈还是链式栈,只要是后进先出的栈,能实现MyQueue的先进先出,栈和MyQueue的底层关联度是很低的。如果栈变成了链式栈,MyQueue照样能实现
简单来说就是环状队列,全程空间都是既定的,可以存储的数据个数是有限的,但是同一空间是可以循环利用的
入队和出队无非就是头尾指针的移动,不涉及空间的创建和销毁
这种队列既可以使用数组实现,也可以使用链表实现
数组的物理存储空间是连续的,CPU高速缓存命中率更高,更优一些些
以下使用数组实现的
在head和tail之间的空间的数据才位于队列里面,其外的空间虽然存储了数据但是并不属于队列,只是在“缓冲区域”
通过控制head和tail的位置就可以操控队列
- 队列什么时候为空,什么时候为满
让head指向队列第一个元素,tail指向最后一个元素的下一个空间
![]()
如果head和tail相等,说明队列是空的
![]()
但是如果队列满了的话,head和tail也是相等的
![]()
所以“存4开5”,如果规定循环队列的容量是4的话,那就开5个存储空间,只存4个数据,当4个空间满了的时候
tail指向第5个,如果tail的下一个是head的话,说明满了
![]()
- 数组在逻辑上是成环的,但是物理上并不成环,如何让数组循环起来:
头尾指针移动的时候需要判断是不是走到了数组最后一位(单链表不用,直接成环了)
如果到了最后一位,再往后走,就要到第一位
Eg:假设进行出队列,出队列完头指针要后移,此时要进行判断:
if(obj->head == k)
obj->head == 0;
else
obj->head +=obj->head;
或者用第二种方法,不用进行判断
其实数组的指针就是权重
让head和tail取模就行
如果要求可以存储的总数据个数k=5,因为要开6个空间,k=5的权重是第6个空间,因此head和tail取k +1的模就可以
如果head要后移,那就obj->head = (obj->head+1)%(obj->k+1)
typedef struct { int *a;//队列数组 int head;//队列头权重 int tail;//队列尾权重 int k;//记录总的存储空间大小 } MyCircularQueue; MyCircularQueue* myCircularQueueCreate(int k)//构造器,设置队列长度为 k { MyCircularQueue *obj = (MyCircularQueue *)malloc(sizeof(MyCircularQueue));//开队列结构体 assert(obj); obj->a = (int *)malloc(sizeof(int) * (k+1));//开队列数组 assert(obj->a);//防止malloc失败,不过其实因为这是题目,没必要进行malloc obj->head = obj->tail = 0;//初始化 obj->k = k;//记录总存储空间大小 return obj; } bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) //向循环队列插入一个元素。如果成功插入则返回真。 { assert(obj);//其实没有必要断言,因为这是题目,题目不会给队列空指针 if((obj->tail +1) % (obj->k +1) == obj->head)//判断是不是满了 return false; else { obj->a[obj->tail] = value;//要注意,tail是指向队列最后一个元素的后一个,因此尾插的话要给tail而不是tail+1 obj->tail = (obj->tail +1)%(obj->k +1); return true; } } bool myCircularQueueDeQueue(MyCircularQueue* obj)//从循环队列中删除一个元素。如果成功删除则返回真。 { if(obj->head == obj->tail)//判断是不是空 return false; else { obj->head = (obj->head +1)%(obj->k +1); return true; } } int myCircularQueueFront(MyCircularQueue* obj) //从队首获取元素。如果队列为空,返回 -1 { if(obj->head == obj->tail) return -1; else { return obj->a[obj->head]; } } int myCircularQueueRear(MyCircularQueue* obj) //获取队尾元素。如果队列为空,返回 -1 { if(obj->head == obj->tail) return -1; else { if(obj->tail == 0)//如果tail此时是在数组头的话,那说明队尾元素是数组最后一个元素 return obj->a[obj->k]; else return obj->a[obj->tail - 1]; } } bool myCircularQueueIsEmpty(MyCircularQueue* obj) //检查循环队列是否为空。 { return obj->head == obj->tail; } bool myCircularQueueIsFull(MyCircularQueue* obj) //检查循环队列是否已满。 { return obj->head == (obj->tail +1)%(obj->k +1); } void myCircularQueueFree(MyCircularQueue* obj) //销毁队列 { free(obj->a);//题目中一般只管free,不用最后指向空,不怕野指针 free(obj); }
选择题
1、2题见上
解析:
3和4题不用讲
第五题:
“长度是N,实际最多存储N-1个数据”
(在循环队列的面试题中,实际存储k个数据,长度是k+1)
思路一:
假设N是5,那么队列的有效长度最大就是4,也就是队列有效长度在0~4之间
就是%N(当然%N还是权重的范围,以实现循环,也就是%(k+1))
![]()
只有数据%N之后才能保证有效长度在0~4之间,因此找%N的选项就行
可以选几个值进行验证一下
Eg:如果front = 0 , rear = 4 (注意rear指向的是队列最后一个元素的下一个)
那么此时队列的实际有效长度是4
按照B的式子,rear - front = 4 , rear - front + N = 4 + 5 = 9
9%N = 9%5 = 4
长度为4 ,正确,和实际有效长度一样
思路二:
rear - front,无论rear和front哪个大,算的结果是不是负数,rear - front都表示rear领先front几个单位,如果结果是负数,说明rear领先了front一圈,跑到了front的后面,所以结果要加上N,就算rear比front大,相减的结果是正数,加上N之后,在%N就可以了,这样就把多的N模走了
结束
That’s all, thanks for reading!💐