目录
1.栈
1.1 概念与结构
栈:是一种线性表,(在逻辑结构上一定连续,在物理结构上不一定连续,取决于底层是循序表还是链表),其只允许在固定的一端进行插入和删除元素操作,进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
那么我们应该用数组还是用链表来作为栈的底层结构呢?
我们可以看到这三种数据类型的结构,他们插入数据和删除数据的复杂度上分析的话都是没有问题的,三种结构都是可以的,那么既然时间复杂度上看不出来,就从空间复杂度上看,这么这里的双向链表就更吃内存,因为双向链表里面有两个指针,而单链表里面只有一个指针,双链表比单链表多一个指针,那么在32尾机器上会多4个字节,而在64为操作系统上会多八个字节,所以首先排除双向链表。
接下来我们看数组和单链表,数组我们要增容,增容的话我们用到了realloc,realloc原地增容还好,但是如果操作分配的内存不够增容,那么就需要异地增容,就需要开辟新空间,拷贝数据,释放旧空间,这里确实会存在性能的消耗,但是增容我们每次都是成2倍的去增容,所以增容,哪怕是异地增容的操作次数是非常小的,可以忽略不计,也就意味着大多数情况下插入数据我直接往栈顶插入即可,不需要每次都向操作系统去申请空间,而对于单链表来说,每次插入数据的话都需要向操作系统申请一个节点的空间,每次删除都需要释放掉这块空间,更频繁一些,那么就更推荐数组作为栈这个数据结构的底层结构。
1.2 栈的实现
1.2.1 栈的初始化
代码:
//初始化
void STInit(ST* ps)
{
//不能传空指针
assert(ps);
//指向栈的指针置为空
ps->arr = NULL;
//内存大小以及有效元素个数
ps->capacity = ps->top = 0;
}
栈的很多接口和顺序表是一样的。
测试:
1.2.2 栈顶入数据
对于栈这样的数据结构来说,只能从栈顶出数据以及入数据,不能像顺序表那样做头插,尾插以及指定位置的插入。
代码:
//栈顶入数据
void StackPush(ST* ps, STDataType x)
{
//不能传空指针
assert(ps);
//判断是否需要增容,当空间大小和有效元素个数相等时,就需要增容
if (ps->capacity == ps->top)
{
//计算需要增容多大空间,初始capacity是0,所以要判断
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//增容
STDataType* newArr = (STDataType*)realloc(ps->arr, sizeof(STDataType) * newCapacity);
//判断增容是否成功
if (newArr == NULL)
{
//打印错误信息并且退出
perror("realloc fail!\n");
exit(1);
}
//代码执行到这说明空间增容成功
ps->capacity = newCapacity;
ps->arr = newArr;
}
//将arr指向的空间的第top位置赋值x,top自增,因为栈顶要往上走
*(ps->arr + ps->top++) = x;
}
入栈流程:
测试:
代码执行完初始化,arr指针指向空,空间大小和有效元素个数都为0。
将所有数据插入之后,此时空间大小capacity是8,因为第一次增容4个字节用完,插5的时候不够,增容2倍,变成8,top是有效元素而个数,1!5总共5个有效元素。
1.2.3 栈的判空
代码:
//判断栈是否为空
//布尔类型要记得加头文件,stdbool.h
bool StackEmpty(ST* ps)
{
//不能传空指针
assert(ps);
//当有效元素个数为0是表示栈为空,返回true
return ps->top == 0;
}
1.2.4 栈顶出数据
代码:
//栈顶出数据
void StackPop(ST* ps)
{
//不能传空指针
assert(ps);
//栈不能为空
assert(!StackEmpty(ps));
--ps->top;
}
出栈流程:
测试:
在执行出栈操作之前我们栈中的数据是1~5,有效数据是5个,也就是top是5。
出栈代码执行完成之后,我们的top也有是有效元素格式是4,下面看虽然还是1~5,但是5已经不是有效元素了。
1.2.5 取栈顶元素
代码:
//取栈顶元素
/*
取栈顶的元素就可以循环打印出栈里面的数据
*/
STDataType StackTop(ST* ps)
{
//不能传空指针已经栈为u空不能取数据
assert(ps && !StackEmpty(ps));
//返回栈中的top减一位置的数据
return *(ps->arr + ps->top - 1);
}
取栈顶元素流程:
测试:
我们不能像之前那样写个打印函数,函数里面写个for循环来打印我们的数据结构中的数据,因为栈这种数据结构不能遍历,所有我们只能取栈顶的元素打印出来然后将打印的元素出栈,继续取栈顶的元素,以此类推,直到栈为空。
如果想要取1,就必须把4,3,2这三个数据出栈,然后取栈顶元素就可以取到1。
注意:栈里的数据不能被遍历,也不能被随机访问。
入栈的顺序是1,2,3,4,出栈的顺序是4,3,2,1,支持栈数据结构的后进先出特性。
1.2.6 获取栈中有效元素个数
代码:
//获取栈中有效元素个数
int StackSize(ST* ps)
{
//不能传空指针
assert(ps);
return ps->top;
}
测试:
出栈前有效元素个数为4个,全部数据出栈,有效元素个数为0。
1.2.7 栈的销毁
代码:
//销毁
void STDestroy(ST* ps)
{
//不能传空指针
assert(ps);
//判断指向栈的指针是否为空
if (ps->arr != NULL)
{
//销毁动态申请的空间
free(ps->arr);
//将该指针置为空指针,防止变为野指针
ps->arr = NULL;
}
//将空间大小和有效元素格式置为0
ps->capacity = ps->top = 0;
}
测试:
此时,执行销毁之前栈中有5个有效的元素。
执行销毁之后,arr指针为空,空间大小和有效元素个数都为0。
1.3 栈数据结构的代码
2. 队列
2.1 概念与结构
概念:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out))
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
可以取到队尾数据,也可以取到队头数据, 但是队列这样的数据结构是不能遍历的。
入数据在队尾,出数据在队头,那么应该使用数组比较好还是链表比较好呢?
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
队列这样的数据结构该如何去定义呢?
上面我们把链表的头定义为队头,链表的为定义为队尾,那么能不能换一下呢?
2.2 队列的实现
2.2.1 队列的初始化
代码:
//初始化队列
void QueueInit(Queue* pq)
{
//不能传空指针
assert(pq);
//将phead和ptail都置为空,表示这是一个空的队列
pq->phead = pq->ptail = NULL;
//将队列有效元素个数置为0
pq->size = 0;
}
测试:
断点打到初始化函数之前,此时phead和ptail都为初始值。
初始化代码执行完成后,phead和ptail指针都为NULL。
2.2.2 入队列
代码:
// 入队列,队尾
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//申请新的节点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
//判断malloc的返回值是否为空
if (newnode == NULL)
{
//反正值为空打印错误信息,退出
perror("malloc error!\n");
exit(1);
}
//为新申请的节点的data赋值
newnode->data = x;
//将新申请的节点的next指针置为空
newnode->next = NULL;
//处理非空队列和空队列两种情况
if (pq->phead == NULL)
{
//队列为空时新节点既是头节点也是尾节点
pq->phead = pq->ptail = newnode;
}
else
{
//队列不为空
//将新节点插入到ptaild的next位置
pq->ptail->next = newnode;
//将ptail指向新的节点,让新的节点变成尾节点
pq->ptail = newnode;
//下面这种和上面的效果一样pq->ptail->next位置就是ptail
//pq->ptail = pq->ptail->next;
}
//有效元素个数自增
pq->size++;
}
入队列流程:
测试:
第一个数据入队前我们的队列还是空的,ptail和phead都是空指针。
第三个数据插入完成我们可以看到q队列下面的phead和ptail都指向有效的节点。
2.2.3 队列判空
代码:
//队列判空
//返回类型是布尔类型,一定要加头文件stdbool.h
bool QueueEmpty(Queue* pq)
{
//不能传空指针
assert(pq);
//当头节点为空的时候,就说明队列为空,然后将false取反就为true。
return !pq->phead;
}
2.2.4 出队列
代码:
// 出队列,队头
void QueuePop(Queue* pq)
{
//不能传空指针以及队列不能为空
assert(pq && !QueueEmpty(pq));
//处理只有一个节点的情况,避免ptail变成野指针
if (pq->phead == pq->ptail)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else//处理有多个节点的情况
{
//出队列是从队头出
//保存要出队的节点
QueueNode* del = pq->phead;
//phead指向下一个节点
pq->phead = pq->phead->next;
//释放要删除的节点
free(del);
del = NULL;
}
//有效元素个数自减
pq->size--;
}
出队列流程:
问题1:
测试:
当我断点打在出队之前,队列中存的三个数据1,2,3。
当三条出队语句执行完成后,此时队列中的phead指针和ptail指针都为空,说明队列中没有数据。
如果在继续出队,断言会报错,队列为空不能出数据。
2.2.5 取队头以及队尾数据
代码:
//取队头数据
QDataType QueueFront(Queue* pq)
{
//不能传空指针以及队列不能为空
assert(pq && !QueueEmpty(pq));
//直接返回第一个节点的data数据
return pq->phead->data;
}
//取队尾数据
QDataType QueueBack(Queue* pq)
{
//不能传空指针以及队列不能为空
assert(pq && !QueueEmpty(pq));
//直接返回尾节点的data数据
return pq->ptail->data;
}
测试:
2.2.6 队列有效元素个数
代码:
//队列有效元素个数
int QueueSize(Queue* pq)
{
//不能传空指针
assert(pq);
//返回有效元素个数
return pq->size;
}
问题1:
测试:
2.2.7 队列销毁
代码:
//销毁队列
void QueueDestroy(Queue* pq)
{
//不能传空指针以及队列不能为空
assert(pq && !QueueEmpty(pq));
//遍历队列,释放节点
QueueNode* pcur = pq->phead;
//保存第二个节点
QueueNode* next = pq->phead;
//pcur为空时说明所以节点释放完成
while (pcur)
{
//保存下一个节点
next = pcur->next;
//释放当前节点
free(pcur);
//pcur走向下一个节点
pcur = next;
}
//指向队头和队尾的指针置为空
pq->phead = pq->ptail = NULL;
//将队列有效元素个数置0
pq->size = 0;
}
测试:
当我的断点打在销毁函数之前,我们的队列中有1,2,3,4,5,6,6个有效数据。
销毁代码执行完成之后,此时我们的ptail和phead都为空,size也为0。