栈
栈的概念基础
栈:栈是一种特殊的线性表,它只允许在固定的一端插入和删除元素的操作。进行数据的插入和删除操作的一段称为栈顶,另一端称为栈底。栈中的数据元素的遵守先进后出(后进先出)LIFO(Last In First Out)的原则。
就像弹夹一样,装弹夹从一端装子弹,射出子弹也是从弹夹一段取子弹。
栈的实现方式
对于栈的实现,有数组栈和链式栈。
链式栈:因为链表是有单链表和双链表的:
如果用表尾做栈顶,尾插尾删,就要设计成双向链表,否则删除数据效率低,
如果要用头做栈顶,头插头删,就可以设计成单链表。
而对于单链表和双链表的选择,当然是越简单越好,因为栈之后一头可以插入数据和删除数据,所以单链表就行了。
数组栈:
对于栈的实现方式,数组的方式是更好的,数组的尾插尾删的效率很高的,虽然链表的尾插尾删效率也很高,但是数组的数据利用率更高。但是其实两种都可以,但是数组要好一点。
栈的实现
数组栈的实现:
#pragme once
#include <stdio.h>
#include <stdlib>
#include <assert.h>
Typedef int STDataType;
Typedef struct Stack
{
STDataType* a;
int top; //栈顶的位置
int capacity; //容量空间
}ST;
void StackInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDatraType)*4);
if (ps->a == NULL) //判断malloc函数是否使用成功
{
printf("realloc fail\n");
exit(-1);
}
ps->top = 0; //top的初始化
ps->capacity = 4; //capacity的初始化
}
对于top的初始化值,可以是0,也可以是-1,是0意味着top指向的是栈顶数据的下一个,在给值的时候,就要先给值在top++:是-1意味着top指向栈顶数据,就得先top++在给值。
如下图所示:
上图左边是栈底,如图所示,这两种方式都是一样的,用哪一种都可以。
栈的栈顶数据的插入
这里需要注意的是栈为空的情况,下面实现的realloc函数是对空间进行扩展,如果传的指针参数是NULL的话就和malloc函数实现的效果一样。
void StackPush(ST* ps,STDataType x)
{
assert(ps);
if(ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a,ps->capacity*2sizeof(STDataType));
if (tmp == NULL) //判断realloc函数是否使用成功
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
ps->a = tmp; //对开辟的空间进行分布
ps->capacity = newCapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
对于整个栈进行删除
这个函数主要有两大用处,第一是栈里面有数据的时候直接对栈进行删除,第二是使用下面的StackPop函数对栈顶数据进行删除的时候,如果把栈给删空了,也要使用这个函数来把栈给删除了。
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
栈的栈顶数据进行删除
对于栈顶的数据,只需要吧top进行“top--”的操作就行了,因为之前实现的插入操作会把之前保存的数据给覆盖掉。但是需要注意的是,当“top--”一直到栈的数据为空的情况,这时候就需要判断一下。
当top--到top=0 的情况的时候,这个栈就为空了(这里是top初始化为top=0的情况)
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0); //判断栈是否为空
//ps->a[ps->top - 1] = 0;
ps->top--;
}
取栈顶的数据
这里需要注意的是栈为空的情况,为空访问就会出错,所以要加一个断言。
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0); //判断这个栈是否为空,为空就报错
return ps-a[ps->top - 1];
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
判断栈为空的函数
写这个函数的目的主要是为了实现代码可读性,且方便我们写操作函数。
这个函数中使用了bool这个数据类型, 这个类型是在c99中加入的新的数据类型,需要引用头文件(<stdbool.h>)。这个类型有两个返回值,ture(1)和false(0)。
可以使用return ture和return flase的形式使用返回值
也可以用1和0的形式来使用返回值
如下所示:
//方法一
bool stackEmpty(ST* ps)
{
assert(ps);
if (ps->top == 0)
return true;
else
return false;
}
//方法二
bool stackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
取栈中间的数据
我们知道这个栈使用数组的方式来实现的,可以直接使用下标来访问数据,但是为了符合栈的特殊性质,我们需要使用循环来从栈顶来一个一个数据来寻找我们需要的数据,如下实现:
void TestStack2()
{
ST st;
StackInit(&st);
StackPush(&st,1);
StackPush(&st,2);
StackPush(&st,3);
StackPush(&st,4);
while (!StackEmpty(&st))
{
printf("%d",StackTop(&st));
StackPop(&st);
}
StackDestroy(&st);
}
int main()
{
TestStack2();
return 0;
}
还有一种实现数组栈顶方式--静态方式实现数组栈,如下所示:
typedef int STDataType;
#define N 10
typedef struct Stack
{
STdataType _a[N];
int _top; //栈顶
}stack;
这种静态分布在实际当中不太实用,如果静态数组栈数组满了,就不能再插入数据了。我们之前实现数组栈都是用的是malloc和realloc函数的方式实现的动态分布方式实现的,对于改大小方面要简单使用一些。
队列
队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列有
先进先出FIFO(First In First Out)的原则。
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
对于队列的实现方式,同样有两种实现方式--链表和数组,而之前栈是两种都可以,数组相对要好一些,而在队列里,数组结构相对较麻烦了,数据结构无论那一边为队头,队尾,实现插入和删除的时候,是需要挪动数据的,而链表实现头插,尾删,或者是头删,尾插相对而言是比较简单的。所以,我们在这里使用链式方式来实现队列。
只是在实现链式队列的时候,和之前实现的单链表不太一样,之前的单链表有一个头指针phead,假设是phead指向结点为队头,那出数据很简单,就把phead指向的数据删掉,在让哦phead指向下一个。但是如数据不一样了,我们需要利用循环取找尾结点在插入数据,这样有些麻烦了,我们在单链表的基础上,加一个尾指针tail,指向最后一个结点,那操作和出数据大同小异了。
队列的代码定义和初始化
typedef int QDataType
typedef struct QueueNode
{
struct QueueNode* next; //指向下一个数据的指针
QDataType data; //结点的值
}QueueNode;
typedef struct Queue
{
QueueNode* head; //头指针
QueueNode* tail; //尾指针
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void TestQueue1()
{
Queue q;
QueueInit(&q);
}
int main()
{
TestQueue1();
return 0;
}
判断队列是否为空的函数
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
销毁整个队列
利用循环一个一个的删除(头结点(phead)头指针指向的结点为队头),需要注意容易出错的循环条件(如代码中注释的一样)。
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while(cur != NULL) //如果while循环的条件使用(cur != pq->tail)会导致有一个数据删不完
{
QueueNode* next = cur->next; //先存储下一个结点位置
free(cur); //在释放这个结点
cur = next; //指向下一个结点
}
pq->next = pq->tail = NULL;
}
队列的插入数据
其实就是我们单链表的尾插,只是比之前的尾插多了一个尾结点的更新,每一次只需要插入一个结点,而且插入的位置也是最后一个,所以对于里面的内容可以直接定义,next指针直接指向NULL。
需要注意的是要判断这个队列是否为空,为空的话就要把头结点phead和尾结点tail指向创建的新结点上。
void QueuePush(Queue* pq,QDataType x)
{
assert(pq);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode)); //创建一片空间为要插入的新的
//结点
newnode->next = NULL;
newnode->data = x;
if(pq->head == NULL) //判断这个队列是否为空
{
pq->head = pq->tail = newnode; //让头尾指针都指向队列中唯一的结点
}
else
}
pq->tail->next = newnode; //让插入之前的最后一个结点的next指针指向新插入的
//结点
pq->tail = newnode; //更新尾指针
}
}
队列的数据删除
对于队列的数据删除,其实就是单链表的头删,同样是先指后删,先把头指针下一个结点的位置储存起来,在把第一个结点也就是头指针指向的结点删除就行了。
在全部数据删完之后,因为头指针一直在轮换,他会被置空,但是tail尾指针一直没有改变,在全部数据删完之后,他一样会指向它之前指向的那个位置,这些形成了野指针。所以当全部数据删完之后,要判断一下,把尾指针置空。------有时候我们看见代码没有挂掉就认为这代码成功了,其实并不是,就像上述这样的问题,但是,有时候我们不容易发现这些问题,这时候就要调试。
就如下面将要实现的取队尾数据的操作,我们是通过tail指针来寻找队尾的,就会取到随机值。但是其实我们在写去队尾的函数的时候会判断一下这个函数为不为空的情况,这里只是想举个例子提醒一下,写出来的代码就算当时没有报错,不意味着这个代码就过了,他有可能崩掉,挂掉没有报错而已。
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
if(pq->head == NULL) //判断队列是否被删完
{
pq->tail = NULL; //将tail尾指针置空
}
}
取队尾和队头的数据 计算队列数据个数
两种操作到要注意队列为空的情况,队列为空的时候这个函数就会解引用空指针崩掉。
取队头数据
QDataType QueueFront(Qeueu* pq)
{
assert(pq);
assert(!QueueEmpty)); //判断队列尾空去情况,为空就报错
return pq->head->data;
}
取队尾数据
QueueType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty); //判断队列为空的情况
return pq->tail->data;
}
队列个数
int QueueSize (Queue*pq)
{
assert(pq);
int n = 0;
QueueNode* cur = pq->head;
while(cur)
{
++n;
cur = cur->next;
}
return n;
}
除了上一种算队数据个数的方法,还可以在结构体里多定义个变量假设是_size这个变量,来记录数据个数(插入就++,删除就--)。
打印队列
void TestQueue2()
{
Queue q;
QueueInit(&q);
QueuePush(&q,1);
QueuePush(&q,2);
QueuePush(&q,3);
QueuePush(&q,4);
while(!QueueEmpty(&q)) //利用循环打印队列
{
QDataType front = QueueFront(&q);
printf("&d",front);
QueuePop(&q);
}
printf("\n");
}
int main ()
{
TestStack2();
return 0;
}
队列典型例子
在一些医院,营业厅,银行等一些人比较多的地方,为了维持公平的排队,都会采用抽号机的机制,而这种抽号机的机制就是用典型的队列来实现的。
如下图所示,假设有三个窗口来办理业务,首先需要办理业务的人先到抽号机排队抽号,抽号从一边按顺序进行排列,我们假设已经从1抽到99号了,然后每一个办理业务的窗口都有一个按钮,按一下就从抽号机里储存的数据里从另一边拿出一个号来办理业务。抽号机用队列的实现方式,来公平的对人们进行业务办理的排队,先来的先被服务