栈与队列
参考书目是李春葆的《数据结构教程》
栈与队列的对比
名称 | 英文 | 又称(特点) |
---|---|---|
栈 | stack | 后进先出表 |
队列 | queue | 先进先出表 |
栈
基本操作
- 初始化
- 销毁
- 判断是否为空
- 进栈push
- 出栈pop
- 取栈顶元素
基本逻辑
维护一个数组和一个top变量。
数组从下标为0的地方开始存储。
top存储的内容是下标,是栈顶元素的下标。特殊情况是没有元素时储存的为-1。
由上不难知道:
- 栈满的条件是
top == maxsize - 1
,亦即数组最大的下标。 - 栈空的标志是
top == -1
,这也是判空的依据 - (C语言实现)需要手动释放资源
- 入栈的前提是检查栈是否满,不满才能入栈
- 出栈前检查栈是否为空,非空才能出栈
- 取栈顶元素就看栈是否为空,非空则可取
栈的顺序储存结构实现
#define maxsize 50
//顺序存储结构
typedef struct{
int data[maxsize];
int top;
}SqStack;
//初始化
void InitStack(SqStack* &s){
s = (SqStack*)malloc(sizeof(SqStack));
s -> top = -1;
}
//销毁栈
void DestoryStack(SqStack* &s){
free(s);//只有malloc的才需要手动释放
}
//判断是否栈空
bool StackEmpty(SqStack* s){
return (s -> top == -1);
}
//入栈
bool Push(SqStack* &s, int e){
if(s -> top == maxsize - 1){
return false;
}
s -> top += 1;
s -> data[s -> top] = e;
}
//出栈
bool Pop(SqStack* s, int& e){
if(s -> top == -1){
return false;
}
e = s -> data[s -> top];
s -> top -= 1;
return true;
}
//获取栈顶元素
bool GetTop(SqStack* s, int& e){
if(s -> top == -1){
return false;
}
e = s -> data[s -> top];
s -> top -= 1;
return true;
}
初始化一个栈的操作类似于这样:
SqStack* st;
InitStack(st);
共享栈
目的:解决两个相同类型的栈,其中一个栈没满而另一个栈溢出的情况。
原先的物理结构本应该是两个数组,或者说两块分别连续的储存空间。但是为了解决空间资源利用不均衡,就给放到一块,逻辑上认为左侧是一个栈的底端,右侧(数组下标最大处)是另一个栈的底端,两个栈的top向数组的中间移动。
由上可知
top1 == top2 - 1
时栈满top1 == -1
为栈1空,top2 == maxsize
为栈2空。- 共享栈的操作需要传入一个参数指明是对哪一个栈进行操作
栈的链式存储结构实现
优点是几乎不会出现栈溢出的情况(内存满了除外)
形式和链表很像,一般有头哨兵节点。
由上可知
- 空栈的判定是头哨兵节点s有
s -> next == NULL
- 首节点是栈顶节点
- 尾节点是栈底节点
- 由上两点可知:出/入栈操作是从头部进行的
//顺序存储结构
typedef struct linknode{
int data;
struct linknode* next;
}LinkStnode;
//初始化
void InitStack(LinkStnode* &s){
s = (LinkStnode*)malloc(sizeof(LinkStnode));
s -> next = NULL;
}
//销毁栈
void DestoryStack(LinkStnode* &s){
LinkStnode* p = s;
LinkStnode* q = s -> next;
while(q != NULL){
free(p);
p = q;
q = p -> next;
}
free(p);
}
//判断是否栈空
bool StackEmpty(LinkStnode* s){
return (s -> next == NULL);
}
//入栈
void Push(LinkStnode* &s, int e){
LinkStnode* p = s -> next;
s -> next = (LinkStnode*)malloc(sizeof(LinkStnode));
s -> next -> data = e;
s -> next -> next = p;
}
//出栈
bool Pop(LinkStnode* s, int& e){
if(s -> next == NULL){
return false;
}
LinkStnode* p = s -> next;
e = p -> data;
s -> next = p -> next;
free(p);
return true;
}
//获取栈顶元素
bool GetTop(LinkStnode* s, int& e){
if(s -> next == NULL){
return false;
}
e = s -> next -> data;
return true;
}
显然,这里的Push()
不必有一个bool类型的返回值,因为它不是靠数组实现的,只有数组才会出现空间不够用的情况,因在那时才需要判断是否能够成功。
队列
栈是只从一端进出,队列是从一端进而从另一端出。
名字 | 英文 | 定义 | 操作 |
---|---|---|---|
队首 | front | 进行删除的一端 | 离队(dequeue) |
队尾 | rear | 进行插入的一端 | 入队(enqueue) |
基本运算
- 初始化队列
- 销毁队列
- 判断是否为空
- 入队
- 出队
分类
抽象数据类型
- 顺序队:采用顺序存储结构的队列
- 链队:采用链式存储结构的队列
学cs61a时老师说过,数据抽象是一种加强数据的表示与操作之间的抽象障碍的方法。
操作是我们逻辑上认为的,但是底层实现是各种各样的(并且不同的数据结构在不同的条件下是有利有弊的,这在上面的顺序栈和链式栈对比中是有所体现的),所以我们能基于不同的储存结构来设计出不同的实现方式。
顺序队
具有一个数组和两个int变量,分别是front和rear。
front保存队首元素的下标减一,rear保存队尾元素的下标。
由上可知:
- 初始时
front == rear == -1
- 入队导致
rear+=1
- 出队导致
front+=1
- 由上述可知队空的条件是```front == rear````
#define maxsize 50
using namespace std;
typedef struct{
int data[maxsize];
int front,rear;
}SqQueue;
//初始化
void InitQueue(SqQueue* &q){
q = (SqQueue*)malloc(sizeof(SqQueue));
q -> front = q -> rear = -1;
}
//销毁队列
void DestoryQueue(SqQueue* &q){
free(q);
}
//判断队列是否为空
bool QueueEmpty(SqQueue* q){
return (q -> front == q -> rear);
}
//入队
bool enQueue(SqQueue* &q, int e){
if(q -> rear == maxsize - 1){
return false;
}
q -> rear += 1;
q -> data[q -> rear] = e;
return true;
}
//出队
bool deQueue(SqQueue* &q, int &e){
//判断队是否下溢出
if(q -> front == q -> rear){
return false;
}
q -> front += 1;
e = q -> data[q -> front];
return true;
}
显然,上述结构有两种溢出
rear == maxsize - 1
的上溢出front == rear
的下溢出
在下溢出时未必会有rear == maxsize - 1
,也就是可能本身还有储存空间,这种溢出被称为假溢出。
而且本身这样的利用效率就是不高的。
循环队列
对于上述情况考虑使用循环队列/环形队列(circular queue)。设置上只需要简要修改一下front和rear即可
front = (front + 1) % maxsize
rear = (rear + 1) % maxsize
如上即可使之循环,并且front
有可能大于rear
,而且这是合理的。在这种设置下,如果读取的下标越过了上界就会自动从数组的下界继续读取。
这样一个队列很显然有一个需要考虑的问题,如果装满maxsize
个元素,很显然的问题是此时也有front == rear
但是空队列时也存在front == rear
,显然这就不能判定到底是否存在满队列的情况。
固然可以考虑增加一个新的isFull
变量来保存是否满队,但是这还是有些麻烦。如果将队列的最大元素个数设置为maxsize - 1
,这样对于原先程序的改动就更加少了。
typedef struct{
int data[maxsize];
int front,rear;
}SqQueue;
//初始化
void InitQueue(SqQueue* &q){
q = (SqQueue*)malloc(sizeof(SqQueue));
q -> front = q -> rear = 0;
}
//销毁队列
void DestoryQueue(SqQueue* &q){
free(q);
}
//判断是否为空
bool QueueEmpty(SqQueue* q){
return (q -> front == q -> rear);
}
//入队
bool enQueue(SqQueue* &q, int e){
if((q -> rear + 1) % maxsize == q -> front){
return false;
}
q -> rear = (q -> rear + 1) % maxsize;
q -> data[q -> rear] = e;
return true;
}
//出队
bool deQueue(SqQueue* &q, int &e){
if(q -> front == q -> rear){
return false;
}
q -> front = (q -> front + 1) % maxsize;
e = q -> data[q -> front];
return true;
}
有必要注意的是入队的false判定,如果写q -> rear == q -> front - 1
是不可以的,因为如果如果front是0,不可能有rear == -1
的情况,所以这个bool表达式永远都是假的,因此必须对(q -> rear + 1) % maxsize == q -> front
进行判定。
其中队内元素个数永远是(rear - front + maxsize) % maxsize
。
此外亦可以使用如下的方式:
typedef struct{
int data[maxsize];
int front,count;
}SqQueue;
这样可以根据首元素的下标和元素个数推断尾元素的位置。这样还可以储存maxsize
个元素,比上面的方法多了一个元素的位置。
链队
这是照着书的思路实现的,它固然能用,但是未必是最佳的,毕竟打破了抽象壁垒。例如在很的地方都有的出队操作,判断队伍是否为空是自己写了函数的,这里应当使用那个函数,而不是再去手动写判空的操作。
typedef struct qnode{
int data;
struct qnode* next;
}DataNode;
typedef struct
{
DataNode* front;
DataNode* rear;
}LinkQuNode;
void InitQueue(LinkQuNode* &q){
q = (LinkQuNode*)malloc(sizeof(LinkQuNode));
q -> front = q -> rear = NULL;
}
void DestoryQueue(LinkQuNode*& q) {
DataNode* pre = q -> front;
DataNode* p;
if (pre != NULL) {
p = pre -> next;
while (p != NULL)
{
free(pre);
pre = p;
p = p -> next;
}
free(pre);
}
free(q);
}
bool QueueEmpty(LinkQuNode* q){
return q -> rear == NULL;
}
void enQueue(LinkQuNode* &q, int e){
//两种情况,空队入队和非空队入队
//先创建新节点,在判断情况具体连接到哪里
DataNode* p = (DataNode*)malloc(sizeof(DataNode));
p -> data = e;
p -> next = NULL;
if(q -> rear == NULL){
q -> rear = p;
q -> front = p;
}
else{
q -> rear -> next = p;
q -> rear = p;
}
}
bool deQueue(LinkQuNode* &q, int &e){
//两种情况,空队出队和非空队出队
if(q -> rear == NULL){
return false;
}
DataNode* t = q -> front;
if(q -> front == q -> rear){
q -> front = q -> rear = NULL;
}
else{
q -> front = q -> front -> next;
}
e = t -> data;
free(t);
return true;
} ,
链队的一些解释
关于删除操作,手动申请的空间才需要手动删除
说实话入队操作我一开始是这么写的:
void enQueue(LinkQuNode* &q, int e){
//两种情况,空队入队和非空队入队
//先创建新节点,在判断情况连接到
if(q -> rear == NULL){
q -> front = (DataNode*)malloc(sizeof(DataNode));
q -> front -> data = e;
q -> front -> next = NULL;
q -> rear = q -> front;
}
else{
q -> rear -> next = (DataNode*)malloc(sizeof(DataNode));
q-> rear-> next -> data = e;
q -> rear = q -> rear -> next;
}
}
对比一下,书里把malloc写在if-else
语句外面,显然更简便。
同理,我写的出队操作也是这样
bool deQueue(LinkQuNode* &q, int &e){
//两种情况,空队出队和非空队出队
if(q -> rear == NULL){
return false;
}
else if (q -> front == q -> rear)
{
e = q -> front -> data;
free(q -> front);
q -> front = q -> rear = NULL;
}
else{
DataNode* temp = q -> front;
e = q -> front -> data;
q -> front = q -> front -> next;
free(temp);
}
return true;
}
栈小结
依靠数组实现的栈维护一个定长数组和一个int型
保存顶部元素下标
的top
变量,在没有元素时top变量的值是-1
。虽然struct里面没有maxsize
的定义,但是这也是一个需要知道的变量,它可以使用#define
定义。
除去顺序栈还有使用链表实现的链栈,链栈需要一个不存东西的头结点,在InitStack
时创建,随后的push或者pop都发生在头结点处。
对于各种函数的返回值一览:
函数 | 顺序栈 | 链栈 |
---|---|---|
InitStack | void | void |
DestoryStack | void | void |
StackEmpty | bool | bool |
Push | bool | void |
Pop | bool | bool |
GetTop | bool | bool |
链栈与链队的获取元素都是在头结点,但是存元素是不同的,链栈是在头结点处存,链队是在尾部存。当然,这只是书上这样写,未必强制需要这样存取,只需要符合相应的原则即可,也就是栈的先进后出和队列的先进先出。
队列小结
依靠数组实现的队列应该有两个保存下标的int
型变量,分布代表首位,并且首部变量应当保存首元素的下标-1
,尾部变量保存尾元素的下标。
依靠链表实现的队列由两部分组成。一部分是链表节点的本体struct,另一部分是保存有指向头尾节点的struct。
基于此可以产生很多变式,例如只有尾节点指针的队列乃至于没有任何指针的队列。
对于各种函数的返回值一览:
函数 | 顺序队 | 链队 |
---|---|---|
InitQueue | void | void |
DestoryQueue | void | void |
QueueEmpty | bool | bool |
enQueue | bool | void |
deQueue | bool | bool |
区别其实只是入队操作的返回值。此前已经解释过了,链队只要内存不满就能入队,而顺序队的实现依靠数组,其大小是固定的。