[ADT笔记]栈与队列及其C语言实现

栈与队列

参考书目是李春葆的《数据结构教程》

栈与队列的对比

名称英文又称(特点)
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都发生在头结点处。

对于各种函数的返回值一览:

函数顺序栈链栈
InitStackvoidvoid
DestoryStackvoidvoid
StackEmptyboolbool
Pushboolvoid
Popboolbool
GetTopboolbool

链栈与链队的获取元素都是在头结点,但是存元素是不同的,链栈是在头结点处存,链队是在尾部存。当然,这只是书上这样写,未必强制需要这样存取,只需要符合相应的原则即可,也就是栈的先进后出和队列的先进先出。

队列小结

依靠数组实现的队列应该有两个保存下标的int型变量,分布代表首位,并且首部变量应当保存首元素的下标-1,尾部变量保存尾元素的下标。

依靠链表实现的队列由两部分组成。一部分是链表节点的本体struct,另一部分是保存有指向头尾节点的struct。

基于此可以产生很多变式,例如只有尾节点指针的队列乃至于没有任何指针的队列。

对于各种函数的返回值一览:

函数顺序队链队
InitQueuevoidvoid
DestoryQueuevoidvoid
QueueEmptyboolbool
enQueueboolvoid
deQueueboolbool

区别其实只是入队操作的返回值。此前已经解释过了,链队只要内存不满就能入队,而顺序队的实现依靠数组,其大小是固定的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值