目录
栈和队列的简要介绍
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶
栈可以用链表实现也可以用顺序表实现,但是压栈和出栈都是对尾部的元素进行操作,即尾插尾删,顺序表的尾插尾删的时间复杂度是O(1),但是对于链表而言,头插头删是O(1),尾插尾删是O(N),所以这里实现栈用顺序表!!
1、栈的初始化
因为栈是用来存放数据的,所以有存放数据的一块空间设为 _a, 栈的容量_capacity 栈的数据个数(也是栈顶)_top三个属性,定义一个结构体包括这三个属性,就是一个栈的类型了
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
定义好栈的结构后,就要先初识化一下栈,让指向存储数据的空间的指针_a为空,容量和数据个数都为0,完成栈的初始化;(再后面数据入栈的时候再来开辟空间,动态改变数据个数和容量)
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
2、入栈
初识化好栈后,就可以把元素压到栈里了,因为栈是一个简化版的顺序表,从下标为零开始插入元素,可以理解为刚插入的元素就是在顺序表的最后边(把顺序表逆时针竖过来 理解成栈的顶),然后元素个数自增1,当元素个数等于栈的容量时就要进行扩容了,这里是两倍两倍地扩容,以此来实现栈。
实现的代码非常简单,只有短短几行。
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
CheckCapacity(ps);
ps->_a[ps->_top] = data;
ps->_top++;
}
入栈就是增加元素,每次增加一个元素,_top自加1,增加的元素多了就会使栈的容量不足,这是就要进行扩容了,这里写了一个checkcapacity的函数来检查是否需要扩容,需要则该函数会自动完成扩容。
//检查栈的容量是否满了
void CheckCapacity(Stack* ps)
{
assert(ps);
if (ps->_top == ps->_capacity)
{
STDataType newtop = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* newStack = (STDataType*)realloc(ps->_a, sizeof(STDataType)*newtop);//这里要注意的是这里的*2总是写错写到sizeof里面 是要写在外面的
if (newStack == NULL)
{
printf("realoc fail!\n");
exit(-1);
}
ps->_a = newStack;
ps->_capacity = newtop;
}
}
思想:如果栈的容量和元素个数相等了就要进行扩容,因为初始化的时候是设置的容量和元素个数都为0,_a为空,那么扩容是就要判断容量是否为0,如果为0就先开辟4个元素的空间供栈使用,否则就按照原来容量的两倍进行扩容。扩容失败就结束程序,成功就把开辟的空间的地址赋值给原来指向存储数据空间的指针_a,扩容完成后栈的容量变为原来二倍。
3、出栈
实现了入栈就要实现出栈,既然压栈和出栈是类似往弹夹里压子弹出子弹,那么出栈就是把最上面的元素拿出来即可,上面说了栈是一个简单的顺序表,入栈是把元素依次往后放,最后边的元素就是栈顶的元素,我们出栈就是拿栈顶的元素出来,也就是把顺序表最后边的元素拿出来(对应下标为顺序表(或栈 都是一样的 栈就是阉割了的顺序表)的元素个数减1),然后让栈的元素个数自减即可完成出栈操作。
这里是直接让栈的元素个数自减1,这样就让出栈的元素的下面一个元素就是新的栈顶元素了
注意:这里出栈的条件必须是栈的元素个数大于零!!!
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);//只有是大于0才能进行pop
ps->_top--;
}
4、获取栈顶元素
类似于出栈的逻辑,取出栈顶的元素,但并不是拿出栈,所以不用让栈的元素个数自减。只要返回栈中_a指向的这块空间下标为元素个数减一的元素即可。
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
STDataType ret = ps->_a[ps->_top - 1];
return ret;
}
5、获取栈中元素个数
这个很简单,我们是入一个元素就让(表示栈的元素个数的变量)_top自增1,所以要得到栈的元素个数就返回栈里的_top即可。
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
STDataType ret = ps->_a[ps->_top - 1];
return ret;
}
6、判断栈是否为空
直接返回元素个数是否等于0即可
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0;
}
7、栈的销毁
我们把元素入栈是存放在_a指向的空间中的,入栈的过程中可能有多次扩容,我们在销毁栈的时候就把这块空间释放掉,把指向这块空间的指针_a置空防止出现野指针,然后让元素个数和容量都为空即可。
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
二、队列
队列的特性与栈恰恰相反,栈是后入先出,队列是先入先出。
如同排队一样,先来的先进行,后来的后进行;
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
队列的实现线性表和链表都可以,那么到底那种更好呢?
分析一下,根据队列的特性,是先进先出,进是尾插,出是头删,链表的尾插效率低头删效率高,线性表尾插效率高,头删效率低。但是我们可以通过链表结构设一个头和尾,插入数据就让尾移动到队尾更新尾的位置,这样就可以弥补链表尾插效率低的缺陷了。所以这里用链表结构实现队列。
一、初始化队列
上面分析了为了更好的管理数据,在队列里有管理数据出入的头尾指针,以及存储数据的结点,结点包括一个数据和和下一个结点的地址。
实现:
// 链式结构:表示队列
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* _head;
QNode* _tail;
}Queue;
设置好了队列结构后就要对队列进行初始化了->
把头尾指针都置空。
void QueueInit(Queue* q)
{
assert(q);
q->_head = NULL;
q->_tail = NULL;
}
2、队尾入数据
因为这里队列是一个简化的单向链表,队尾入数据就是相当于尾插,只是这里尾插后要记录 尾指针,方便下一次尾插的进行,弥补了链表尾插效率低下的不足;
插入数据先开辟一个结点,把数据放到这个结点中,再链接在尾的后面,如果队列是空(头和尾都是空),就把这个结点当作头和尾,否则就是连在尾的后面,尾更新一下就可以了。
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)//判读是否开辟成功
{
printf("newnode fail!\n");
exit(-1);
}
newnode->_data = data;
newnode->_next = NULL;
if (q->_head == NULL)//如果是第一次插入就让开辟的结点当头和尾
{
assert(q->_tail == NULL);
q->_head = q->_tail = newnode;
}
else
{
q->_tail->_next = newnode;
q->_tail = newnode;//or q->_tail->next=q->_tail->next;
}
}
3、队头出数据
因为是链表的结构,队头出数据就是头删,非常简单,直接保存头结点后面一个结点的地址,销毁掉头结点,让头结点的下一个结点当头结点即可。
但是这里有一个小细节需要注意!
1.常规思想(缺乏严谨性)
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
assert(q->_head && q->_tail);//如果是头和尾都是空了那么就是没有元素不用删了
QNode* next = q->_head->_next;
free(q->_head);
q->_head = next;
}
2.改进后的写法(加一个判断条件)
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
assert(q->_head && q->_tail);//如果是头和尾都是空了那么就是没有元素不用删了
//这里有个小细节要注意 如果只是按照普通的头删思想 会导致最后出现尾指针变成野指针 就是释放最后一个结点的时候 被释放后tail还是指向被释放的那块空间的
if (q->_head->_next == NULL)
{
free(q->_head);
q->_head = q->_tail = NULL;
}
else
{
QNode* next = q->_head->_next;
free(q->_head);
q->_head = next;
}
}
4、获取队头元素
因为我们保存了头尾指针,想要获取队头元素就直接返回头指针指向结点里的数据即可。
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(q->_head);
return q->_head->_data;
}
5、获取队尾元素
因为我们保存了头尾指针,想要获取队尾元素就直接返回尾指针指向结点里的数据即可。
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(q->_tail);
return q->_tail->_data;
}
6、获取队列元素个数
定义一个计数器size,遍历队列,只要结点不为空,size++,当为空结点是就结束,此时的count就是队列元素个数。
拓展:也可以在队列的结构中加一个size 即在Queue中再定义一个size,每次插入元素就size++,最后只需要返回Queue中的size即可,省去了遍历的步骤!
// 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
QNode* cur = q->_head;
int size = 0;
while (cur)
{
size++;
cur = cur->_next;
}
return size;
}
7、判断队列是否为空
只需返回头结点是否为空,如果为空就是没有插入数据,可以以此来判断队列是否为空。
bool QueueEmpty(Queue* q)
{
assert(q);
return q->_head == NULL;
}
8、销毁队列
// 销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->_head;
while (cur)
{
QNode* next = cur->_next;
free(cur);
cur = next;
}
q->_head = q->_tail = NULL;//记得置空 防止出现野指针
}
源码链接:
Queue2 · 张华/c++code - 码云 - 开源中国 (gitee.com)
Stack2 · 张华/c++code - 码云 - 开源中国 (gitee.com)
此文到此就结束了,各位看官下篇见~~~