前言
栈和队列都是特殊的线性表,二者的实现是基于前面学过的顺序表和链表的,所以在功能和实现方面不会有太大的问题,重点是要关注栈有什么的特点和队列又有什么区别,以及二者的应用场景是什么等等。
栈专题
栈的概念
栈是一种特殊的线性表,栈只允许在固定的一端插入或删除数据。进行数据插入和删除的一端被称为栈顶,另一端称为栈底。此外,向栈插入数据的操作叫入栈/进栈/压栈,从栈中删除数据的操作叫出栈
栈的结构
由于栈只允许在固定的一端插入或删除数据,因此栈数据的插入与删除遵从后进先出(Last in First out)原则。其结构必须满足该原则,所以可供实现栈的结构可以有顺序表、单链表和带头双向循环链表。即以下几种:
不过需要注意几种情况,就是当用单链表来实现时,为了方便数据的插入和删除操作,一般用头节点作为栈顶,尾节点作为栈底。事实上,与其用带头双向循环链表来实现栈,倒不如用单链表来实现更加方便,因此为了实现栈一般只采用两种结构,即顺序表和单链表。
栈的实现(顺序表实现)
结构创建
函数接口
(1)栈初始化(StackInit)
此处需要注意,top初始化为什么值是自己定义的,如果想让top指向栈顶就需要把top初始化为-1,如果想让top指向栈顶的下一个元素,那就把top初始化为0。另外,top初始值不同可能会导致以下接口的实现都有细枝末节的变化,应当细心处理。
(2)栈销毁(StackDestroy)
(3)入栈函数(StackPush)
void StackPush(ST* ps, STDataType x)//入栈
{
assert(ps);
if (ps->capacity == ps->top)
{
//扩容
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
ST* ptr = (ST*)realloc(ps->arr, sizeof(STDataType) * newCapacity);
if (ptr == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->arr = ptr;
ps->capacity = newCapacity;
}
ps->arr[ps->top++] = x;
}
入栈函数主要的重点是扩容,扩容存在两种情况,一是原容量为零需要指定分配一块大小的空间;二是原容量不为零,扩容时以指定倍数增容。扩完容后记得别忘了要保存新的容量大小,在top处增加新的数据,最后别忘了top++。
(4)出栈函数(StackPop)
(5)栈判空(StackEmpty)
当top为0时,栈为空,表达式结果为真,返回true
(6)获取栈顶元素(StackTop)
(7)获取栈中有效数据个数(StackSize)
补充:栈的遍历打印
栈只能从一端插入或删除数据,因此要想遍历打印栈,就必须一边出栈一边打印。
队列专题
队列概念
与栈一样,队列也是一种特殊的线性表。相较于栈,队列规定在一端插入数据,另一端删除数据具有先进先出(First in first out)的特点。其中插入数据的操作叫入队列,入队列的一端称为队尾;删除数据的操作叫出队列, 出队列的一端称为队头。
队列结构
队列要求元素先进先出,因此在实现时应当要求两端一端入队列,一端出队列。所以可用顺序表、单链表以及带头双向循环链表。此处同样排除最复杂的结构带头双向循环链表,本文使用单链表来实现队列,但并不是说顺序表不能实现,大家也可以尝试写写。
队列实现
结构创建
图上为节点类型的声明
为避免后续频繁使用二级指针,可以将头节点和尾节点放在结构体中统一管理,此后只需创建传结构体变量,接着传结构体变量的地址就可以在调用的函数中修改头指针和尾指针的值了。另外,由于队列的接口函数中有一个是求队列中元素的个数的,所以可以再定义一个成员size方便后续的函数实现。
队列接口函数
(1)初始化队列(QueueInit)
void QueueInit(Queue* pq)
{
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
需要注意,传递给QueueInit函数的参数为Queue类型的指针,以下接口函数亦然。
(2)销毁队列(QueueDestroy)
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;//cur记录当前位置的节点,用cur来遍历队列
while (cur)
{
QNode* next = cur->next;//next记录下一个节点的地址
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;//最后记得将头尾指针置空,以免出现野指针
pq->size = 0;
}
销毁队列的思路是遍历队列,释放每个节点,最后记得将变量该置空的置空该赋0的赋0。
(3)入队列(QueuePush)
void QueuePush(Queue* pq, QueueDataType x)//入队列
{
assert(pq);
QNode* NewNode = (QNode*)malloc(sizeof(QNode));
if (NewNode == NULL)
{
perror("malloc fail");
return;
}
NewNode->data = x;
NewNode->next = NULL;
//以上均为创建新节点,下面才是插入数据的实现
if (pq->phead == NULL)//队列为空
{
pq->phead = pq->ptail = NewNode;
}
else//队列不为空
{
pq->ptail->next = NewNode;
pq->ptail = pq->ptail->next;
}
pq->size++;
}
注意区分队列为空和队列不为空的情况,若队列为空则直接将新节点与头尾指针相连接,若不为空队列则在队列的尾部插入数据。每次插入之后都要记得更新一下ptail,让ptail指向尾节点,并且size++。
(4)出队列(QueuePop)
void QueuePop(Queue* pq)//出队列
{
assert(pq);
assert(pq->phead);
if (pq->phead == pq->ptail)//一个节点
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else//多个节点
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
出队列在实现时采用单链表的头删,这是由队列的特性决定的。在头删时需要注意,当队列只有一个节点时,应单独处理,最后也别忘给size--。
(5)获取队头元素(QueueFront)
(6)获取队尾元素(QueueBack)
(7)获取队列中元素个数(QueueSize)
前面在Queue中设计成员size就是为了方便实现这个函数。
(8)队列判空(QueueEmpty)
此处判空与栈判空相同如果表达式为假返回false,为真返回true。
结语
以上就是栈和队列的所有内容了,这些都是些基础的玩意,本篇博客只是简单地总结了一下栈和队列的内容,并不能提高大家对这块知识的理解,后续会找时间写几篇数据结构相关的经典习题,希望对大家有帮助。