欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool
系列文章推荐
冰冰学习笔记:一步一步带你实现《顺序表》
冰冰学习笔记:一步一步带你实现《单链表》
冰冰学习笔记:一步一步带你实现《双向带头循环链表》
前言
**栈**和队列也是数据结构中的一种,二者都可以使用链表或者顺序表实现。对于栈来说,其最大的特点就是数据**先入后出**,每一个栈都有栈顶和栈底,数据总是放入栈顶,也总是从栈顶拿出。基于这些特点,实现栈最好的方式是使用顺序表,因为我们仅对顺序表进行尾插尾删就可实现栈。
**队列**也是一种特殊的线性表。队列的特点是数据遵循**先入先出**的原则,每个队列都有对头和队尾,数据总是从对尾进入,从队头拿出。相比于栈,队列则更适合使用链表,因为队列在对头出数据更加方便,顺序表需要移动数据,效率太低。
下面我们开始介绍栈与队列的具体实现方式。
一、栈的实现
栈,归根结底只是一种特殊的线性表,前面的文章中我们具体介绍了线性表中顺序表的实现方式,因此栈的实现对于我们来说小菜一碟。栈通常的接口含有初始化,入栈,出栈,获取栈顶元素,判断栈是否为空,计算栈的元素个数,栈销毁。
1.1结构创建与初始化函数
如前文所讲,栈的本质还是顺序表,因此栈的结构中应该包含三个元素:栈的存储数据类型、栈顶元素下标,存储空间大小。
对于初始化函数,我们也可以一开始就为存储元素的空间开辟一部分,也可以在空间扩容时增加。这里采取的是后面统一扩容的方式,因此初始化函数只需要将top与capacity进行归零操作,然后将a赋值为空指针。
函数代码:
void StackInit(ST* ps)
{
assert(ps);
ps->capacity = ps->top = 0;
ps->a = NULL;
}
1.2入栈函数
入栈操作是将数据从栈顶放入,由于我们实现的方式是顺序表,因此我们可以将数组开头视为栈底,数组结尾视为栈顶。因此数据入栈即为将数据插入到数组尾部,也就是顺序表的尾插操作。
这样一分析,我们的思路就清晰了。插入函数进来一定要先判断原有空间是否充足,能够存储数据,如果不能需要先开辟空间然后将数据存储进去。空间开辟完成后,将插入数据放入到顺序表尾部,也就是栈顶指向的位置,然后更新栈顶top指向的位置。
函数代码:
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if ( ps->capacity == ps->top )
{
int newsize = ps->capacity==0?4:2*ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->a,newsize*sizeof(STDataType));
if ( tmp == NULL )
{
perror("StackPush::realloc");
exit(-1);
}
ps->a = tmp;
ps->capacity = newsize;
}
ps->a[ps->top] = x;
ps->top++;
}
1.3出栈函数
由于栈必须满足栈空间的数据从栈顶取出,因此,出栈函数就意味着删除栈顶的元素,及顺序表中数组尾部的元素,也就是尾删。
实际操作中我们并不需要真正的将内存空间释放,我们只需要将top向后移动即可,这样顺序表访问不到元栈顶的数据,即为删除。**注意**:此处调用了判空函数,因为只有当栈中含有元素的时候才能正确执行删除数据功能,栈为空,数据无法删除,应直接报错。
函数代码:
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
1.4获取栈顶元素
获取栈顶元素函数与出栈函数不同。出栈函数是将栈空间中栈顶的元素弹出,也就是删除销毁,获取栈顶函数是将栈顶空间保存的数据取出,并不会改变栈空间的数据存储。因此我们只需要将顺序表中top所指向的上一个空间的位置存储的数据返回即可。
函数代码:
STDataType StackTop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top-1];
}
1.5判断栈是否为空
我们在判断栈是否为空的函数时使用了布尔类型,如果栈为空则返回true,不是空则返回false。使用布尔类型,我们得先包含头文件<stdbool.h>,这样C语言才能识别布尔类型。
我们可以使用top来确定栈空间是否为空,若栈为空,则top必定为0。使用if语句进行判断可以,但是不够简洁,我们完全可以直接返回top==0,如果满足即为真,不满足即为假。
函数代码:
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
1.6计算栈空间与栈销毁
计算栈空间元素个数的函数比较简单,由于栈是顺序表结构实现的,因此栈顶下标top的大小即为栈空间的元素个数,直接返回即可。
栈销毁函数就是将开辟的空间进行释放,与顺序表的释放函数一致。
函数代码:
//栈销毁
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
//计算栈空间元素个数
STDataType StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
二、队列的实现
队列的性质和栈恰恰相反,队列采用先进先出的原则,插入数据的一端称为队尾,拿出数据的一端称为队头,队列的代码实现中通常包含队列初始化、入队、出队、获取队头元素、获取队尾元素、队列判空、队列元素个数获取、队列销毁。
2.1队列结构创建与初始化函数
队列的搭建可以采用顺序表或者链表,由于队列需要使用尾增与头删,所以顺序表实现并不划算,原因在于顺序表进行头删的时候,元素需要移动,增加了时间复杂度。在这里我们采用的是链表的结构。
如图所示,队列的结点中还是和单链表一致,含有存储元素data,以及指向下一个结点的指针next。但是队列结构中含有两个指针,分别为头指针head、尾指针tail,以及一个保存队列元素个数的size变量。head指针指向队列的头,尾指针记录队列的尾,这样数据插入的时候就不用遍历链表找尾部,直接连接在记录尾部的指针tail中。单链表中我们并没有采用此种方式,原因在于队列的性质规定,队列只能从尾部进行数据插入,为了方便可以采用这种情况。但是,单链表中数据插入的方式不仅尾插一种,任意位置的插入还需要遍历寻找指针,此种方式虽能解决尾插,但是意义不大,其他算法还是O(N)。
队列的初始化函数与单链表相似,初始化时不需要开辟头结点(开辟也可以),只需要将head指针与tail指针指向NULL,size的大小变为0。
函数代码:
//队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
2.2入队列函数
入队列函数本质就是单链表的尾插函数,因为我们队列结构体中包含了记录尾指针的tail指针,因此我们不需要遍历寻找尾指针,直接将新开辟的结点newnode连接到尾指针tail后面即可。队列结构中含有记录元素个数的数据size,因此插入结点后应该将size进行自增。
注意,由于我们并没有开辟头结点,所以我们需要判断是否为第一次的数据插入,如果是第一次插入数据,tail和head为空,无法将数据直接连接在tail后方,因此需要将数据直接连接在head后面。
函数代码:
void QueuePush(Queue* pq, QueueDataType x)
{
assert(pq);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if ( newnode == NULL )
{
perror("QueuePush::malloc");
exit(-1);
}
newnode->Data = x;
newnode->next = NULL;
//第一次插入
if (pq->tail == NULL )
{
pq->head = pq->tail = newnode;
}
//其他情况
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;//元素个数自增
}
2.3出队列函数
队列的性质决定了实现队列的链表结构只能采用头删。我们需要将head所指向的结点进行释放,然后将head向后移动成为新的队头,size减少。但是删除之前我们要对队列内容进行检查,检查队列空间是否为空,只有不为空的情况下才能进行删除。
但是我们要注意只有一个链表结点的情况,此时head和tail均指向该节点,如果直接释放掉head,并将head向后移动,会造成tail指针变为野指针的问题。因此我们需要在释放后,tail指针和head指针一起向后移动。
函数代码:
//队头删除数据
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
//只有一个结点
if ( pq->head == pq->tail )
{
free(pq->head);
pq->head = pq->tail = NULL;
}
//其他情况
else
{
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;//元素个数减少
}
2.4获取队头、队尾数据
队列的数据获取函数是将队头或队尾的数据进行拿出,不改变队列的空间数据结构。获取队头数据直接返回head指针所指向的结点的data,队尾数据则为tail指针指向的结点的data。
** 注意:获取数据函数与删除函数一样,都需要判空,空队列没有数据可以获取,调用函数会获取失败。**
代码函数:
//队头数据拿出
QueueDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->Data;
}
//队尾数据拿出
QueueDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->Data;
}
2.5队列元素个数计算
队列元素个数的计算可以采用遍历队列来获取,但是此种方式的时间复杂度为O(N),当然也可以定义一个全局变量来进行记录,但是此种状况容易代码混乱。本文采用的方式是在创建队列结构时在结构内部定义了一个size来记录数据个数,不会引起代码混乱。size在插入结点或者删除结点的时候随之变化,因此元素个数可直接返回size。
函数代码:
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
2.6队列判空与队列销毁
判空函数的实现并不复杂,当队列为空时,对应的链表中head指针和tail指针都将指向NULL,我们只需要返回判断即可。
队列销毁函数就是将队列创建的结点一一释放,其做法与单链表释放一样,采用循环遍历释放即可。
函数代码:
//队列判空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
//销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while ( cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
总结
只要了解了顺序表和单链表的结构与功能的实现,栈和队列的实现相对简单。本文采用顺序表实现栈,链表实现队列,但是不代表只能这么实现,仁者见仁智者见智,每种实现方式都有存在的意义。C语言标准并没有明确规定,栈与队列必须使用顺序表还是链表实现,两种实现方式均可,但是顺序表具备尾插尾删的优势,因此栈的实现多半采用该结构。链表更适合头删操作,因此队列多采用该种结构进行实现。顺序表与单链表的具体实现方式可以观看本人的系列文章,已放入推荐文章中。本文所讲解的具体代码已放入仓库中,可自行下载参考。
栈实现代码链接:栈的顺序表结构实现 · 67cbfd9 · 冰冰棒/数据结构仓库 - Gitee.com
队列实现代码链接:队列的实现与队列练习题 · 4ff55d4 · 冰冰棒/数据结构仓库 - Gitee.com
版权归原作者 bingbing~bang 所有, 如有侵权,请联系我们删除。