(一)栈的概念以及结构概图
栈是限定仅在表尾进行插入或删除操作的线性表。因此,对栈来说,表尾端有其特殊含义,称栈顶(top),相应地,表头端称为栈底(bottom)。不含元素的空表称为空栈。
假设栈S=(a1,a2,...,an),则称a1为栈底元素,an为栈顶元素。栈中元素按a1,a2,...,an,的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按后进先出的原则进行的,如下图(a)所示:
因此,栈又称为后进先出(last in first out ,LIFO)的线性表,它的这个特性可以用图(b)的铁路调度站形象地表示。在日常生活中,我们也可以举很多和栈类似的例子。例如,堆书喜欢逐个往上堆,但拿书往往都是后放的书先拿,因为后放的书在最上面,先放的书在最底下;还有弹夹,新装的子弹先打出,最先装的子弹最后打出。栈的操作特点正式上述实际应用的抽象。在程序设计中,如果需要按照保存数据时相反的顺序来使用数据,则可以用栈来实现。
(二)栈的实现
栈同样也分顺序栈和链栈,但对于链表,顺序表更有益于栈的操作,因为栈的操作主要在表尾,所以用数组来实现表尾的插入与删除更优,所以以下就是对于顺序栈的实现。
1. 栈的创建
typedef int Datatype;
typedef struct Strack
{//定义一个结构体,确定栈的类型与结构,这里的定义可以参考顺序表的实现
Datatype* Data;//定义一个指针,指向栈的首元素的地址
int top;//表示栈中存入的实际元素个数,注意这里的top是整型,在书上或者资料我们容易把它理解为指针,实际上不是,这个应该和数组的下标更相似
int maxsize;//表示栈所能储存的最大的长度
};
跟顺序表的创建类似,首先我们要确定栈的结构,这里我们定义一个结构体,里面有可动态扩容的数据类型Data,以及表示栈的实际元素个数的整型top,和表示栈的最大长度的整型maxsize 。
注意:这里的Datatype表示的是栈中所存的数据的类型,不一定是整型,也可以是其他类型,这里举个例子说明,没有以偏概全。
2. 栈的初始化
void StrackInit(Strack* ps)
{//初始化一个栈,这里传入的ps表示指向Stack这个结构体的指针
assert(ps);//传入的指针不能为空
ps->Data = (Datatype*)malloc(sizeof(Datatype) * 6);//动态开辟6个大小为DataType的内存空间
if (ps->Data == NULL)
{//若申请内存空间不成功则退出程序
printf("动态申请内存空间失败\n");
exit(-1);
}
//若申请空间成功
ps->maxsize = 6;//因为动态申请了6个空间,所以最大栈长为6
ps->top = 0;//此时栈为空栈,实际栈长为0
}
初始化一个栈,函数传的肯定是指向栈这个结构体的指针,传入后我们首先要判断传入的指针是否为空,非空后,我们动态申请6个连续的Datatype类型的内存空间,用Data指针记录申请的连续空间的首地址, 然后我们进行判断,看动态申请内存空间是否成功,没有成功直接结束程序,成功后因为申请的是6个空间,但每个空间都是空的,所以将maxsize即栈的最大栈长等于6,栈的实际栈长top等于0,这样栈就初始化完成了。
3. 栈的销毁
void StrackDestory(Strack* ps)
{//栈的销毁
assert(ps);//传入的指针不能为空
free(ps->Data);//将指向栈地址的指针释放
ps->maxsize = 0;//因为栈被销毁,所以栈的最大长度变为0
ps->top = 0;//因为栈被销毁,所以栈的实际长度同样也变为零
ps = NULL;//将指针置空,防止野指针的产生
printf("栈销毁成功\n");
}
销毁一个栈,函数传参传的肯定也是Strack* ps,首先传进去我们首先要判断传入的指针是否为空,若不为空,因为ps->Data指针指向的是栈的基地址,我们用它控制栈,改变栈,所以我们只要将它释放掉就相当于毁了一个栈,栈销毁了以后将maxsize和top的值置零,因为栈没了,肯定都是为零的,最后将ps指针置空,防止野指针的产生。
4. 入栈操作
void StrackPush(Strack* ps, Datatype x)//尾插法
{//数据的入栈与栈的扩容
assert(ps);//传入的指针不能为空
if (ps->top == ps->maxsize)//判断栈是否满容
{
if (ps->maxsize == 0)
{//避免栈内存空间为零的情况使扩容出现0*2=0的bug
ps->maxsize == 1;
}
Datatype* temp = (Datatype*)realloc(ps->Data, (ps->maxsize+6) * sizeof(Datatype));//申请一个临时指针进行扩容
if (temp == NULL)//判断系统空间是否有内存空间进行扩容
{
printf("扩容失败\n");//如果没有空间进行扩容,则返回扩容失败
}
ps->Data = temp;
}
//元素入栈
ps->Data[ps->top] = x;
++ps->top;//插入一个数据后实际栈长加1
}
因为栈只能在栈顶进行插入和删除,所以我们采用尾插法进行数据的插入,开始我们肯定要判断传入的指针是否为空,然后再判断此时的栈是否是满容的状态,如果是则我们要对栈的内存进行扩容,扩容就是在原来的基础上加6个内存空间,具体的扩容操作前几篇博客中有细讲这里就不多说了,上述的一系列操作进行完毕后才是元素的入栈,操作和顺序表的尾插无异,就不多说了。
5. 出栈操作
void StrackPop(Strack* ps)
{//出栈操作
assert(ps);//传入的指针不能为空
if (ps->top > 0)
--ps->top;//用top控制元素的出栈入栈,top-1代表删了栈顶元素,至于里面值是多少不管
else
printf("此表已空\n");
}
首先我们需要判断传入的指针是否为空,若不为空则只需要让top的值减一就可以了,因为我们是靠top的值的变化来控制栈的元素的删减的,但说是删吧也不是我们平常理解的删,我们平常理解的删应该是直接把一个元素给干没了,但这里不是,这里是通过让实际表长减一来进行删除,假如我本来有按序排的四个元素在顺序表中,现在我让实际表长减一,那第四个元素就不管了,相当于删了,做一个不恰当的比喻,四十人的教室,坐了39个上课的,还有一个不是本班在这上自习的,我们上课的时候需要管他吗,不需要,我们上我们的,不影响。这里的出栈其实就是顺序表的尾删。
6. 获取栈顶元素
Datatype StrackTop(Strack* ps)
{//获取栈顶元素数据
assert(ps);//传入的指针不能为空
assert(ps->top > 0);//栈不能为空
return ps->Data[ps->top - 1];//返回栈顶元素
}
因为top表示的是逻辑位置的栈顶,而我们返回的是物理位置的栈顶,就比如一个栈,我们以此存放1,2,3三个元素,栈顶元素不应该是3嘛,但如果我们直接用ps->top返回则返回的是3元素的后一位的元素,所以就不行。
7. 获取栈中有效元素个数
int StrackSize(Strack* ps)
{//获取栈中有效元素个数
assert(ps);//传入的指针不能为空
return ps->top;//此时栈顶元素下标-1就为栈中有效元素个数
}
这里判断传入的指针是否为空,如果非空则之间返回ps->top即可,此时返回的值就是栈的有效元素个数。
8. 判断一个栈是否为空
void StrackEmpty(Strack* ps)
{//判断栈是否为空栈
assert(ps);//传入的指针不能为空
if (ps->top == 0)
{
printf("栈空\n");
}
else
printf("栈不为空\n");
}
这里首先判断传入的指针是否为空,非空则进行判断,如果top为零代表栈的有效元素个数为零,说明栈空,输出栈空即可,反之则非空。
9. 测试函数
void Test()
{//测试函数
Strack s;
StrackInit(&s);
StrackEmpty(&s);
StrackPush(&s, 1);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 2);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 3);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 4);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackDestory(&s);
StrackEmpty(&s);
}
10. 完整代码及运行结果
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
typedef int Datatype;
typedef struct Strack
{//定义一个结构体,确定栈的类型与结构,这里的定义可以参考顺序表的实现
Datatype* Data;//定义一个指针,指向栈的首元素的地址
int top;//表示栈中存入的实际元素个数,注意这里的top是整型,在书上或者资料我们容易把它理解为指针,实际上不是,这个应该和数组的下标更相似
int maxsize;//表示栈所能储存的最大的长度
};
void StrackInit(Strack* ps)
{//初始化一个栈,这里传入的ps表示指向Stack这个结构体的指针
assert(ps);//传入的指针不能为空
ps->Data = (Datatype*)malloc(sizeof(Datatype) * 6);//动态开辟6个大小为DataType的内存空间
if (ps->Data == NULL)
{//若申请内存空间不成功则退出程序
printf("动态申请内存空间失败\n");
exit(-1);
}
//若申请空间成功
ps->maxsize = 6;//因为动态申请了6个空间,所以最大栈长为6
ps->top = 0;//此时栈为空栈,实际栈长为0
}
void StrackDestory(Strack* ps)
{//栈的销毁
assert(ps);//传入的指针不能为空
free(ps->Data);//将指向栈地址的指针释放
ps->maxsize = 0;//因为栈被销毁,所以栈的最大长度变为0
ps->top = 0;//因为栈被销毁,所以栈的实际长度同样也变为零
ps = NULL;//将指针置空,防止野指针的产生
printf("栈销毁成功\n");
}
void StrackPush(Strack* ps, Datatype x)//尾插法
{//数据的入栈与栈的扩容
assert(ps);//传入的指针不能为空
if (ps->top == ps->maxsize)//判断栈是否满容
{
if (ps->maxsize == 0)
{//避免栈内存空间为零的情况使扩容出现0*2=0的bug
ps->maxsize == 1;
}
Datatype* temp = (Datatype*)realloc(ps->Data, (ps->maxsize+6) * sizeof(Datatype));//申请一个临时指针进行扩容
if (temp == NULL)//判断系统空间是否有内存空间进行扩容
{
printf("扩容失败\n");//如果没有空间进行扩容,则返回扩容失败
}
ps->Data = temp;
}
//元素入栈
ps->Data[ps->top] = x;
++ps->top;//插入一个数据后实际栈长加1
}
void StrackPop(Strack* ps)
{//出栈操作
assert(ps);//传入的指针不能为空
if (ps->top > 0)
--ps->top;//用top控制元素的出栈入栈,top-1代表删了栈顶元素,至于里面值是多少不管
else
printf("此表已空\n");
}
Datatype StrackTop(Strack* ps)
{//获取栈顶元素数据
assert(ps);//传入的指针不能为空
assert(ps->top > 0);//栈不能为空
return ps->Data[ps->top - 1];//返回栈顶元素
}
int StrackSize(Strack* ps)
{//获取栈中有效元素个数
assert(ps);//传入的指针不能为空
return ps->top;//此时栈顶元素下标-1就为栈中有效元素个数
}
void StrackEmpty(Strack* ps)
{//判断栈是否为空栈
assert(ps);//传入的指针不能为空
if (ps->top == 0)
{
printf("栈空\n");
}
else
printf("栈不为空\n");
}
void Test()
{//测试函数
Strack s;
StrackInit(&s);
StrackEmpty(&s);
StrackPush(&s, 1);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 2);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 3);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPush(&s, 4);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackPop(&s);
printf("栈内元素个数:%d,栈顶元素数据:%d\n", StrackSize(&s), StrackTop(&s));
StrackEmpty(&s);
StrackDestory(&s);
StrackEmpty(&s);
}
int main()
{
Test();
}
(三)队列的概念及特点
和栈相反,队列(queue)是一种先进先出(First In First Out,FIFO )的线性表。它只允许在表的一端进行插入,而在另一端删除元素。这和日常的排队是一致的,最早进入队列的元素最早离开。在队列中允许插入元素的一端称为队尾,允许删除元素的一端称为队头,假设队列为q=(a1,a2,a3,a4,...,an),那么a1则为队头元素,an则为队尾元素,队列中的元素a1,a2,a3,a4,...,an的顺序进入,退出队列也只能按照这个次序依次退出,也就是说只有前面的a1,a2,a3,a4,...都出队了an才能出队,如下图所示:
队列在程序设计中也经常出现。一个典型的例子就是操作系统中的作业排队。在允许多道程序运行的计算机系统中,同时有几个作业运行。如果运行的结果都需要通过通道输出,那么就要按要请求输入的先后次序排队。每当通道传输完毕可以接受新的输出任务时,队头的作业先从队列中退出做输出操作。凡是申请输出的作业都从队尾进入队列。
(四)队列的实现
1. 队列的创建
队列同样分为链式结构的队列和顺序结构队列,此处我们写的均为链式结构的队列,因为链队相较于顺序队更简单,更容易实现,效率更高。
typedef int Datatype;//定义链表结点的数据类型
typedef struct ListNode
{//定义结点的类型,这里的定义和链表的结点定义基本差不多
ListNode* next;//定义结点的指针域,这里的指针指向的是下一个结点的位置
Datatype data;//定义结点的数据域
};
typedef struct Queue
{//确定链表的类型,相当于确定队列的类型与结构
ListNode* head;//指向队头的对头指针
ListNode* tail;//指向队尾的队尾指针
};
首先在正式实现队列前我们需要先确定队列的结构,首先因为是链式结构存储,我们先确定每个结点的结构,结点我们用一个结构体来表示,每个结点有一个指针域next,和一个数据域data,注意此处的Datatype类型只是为了更方便理解int是一个数据类型而非只有整型,就是结点的数据域数据类型可以是整型,浮点型甚至是结构体不仅仅局限于整型。确定完结点的结构后,就应该确定队列的结构了,这里我们同样用一个结构体来表示,队列需要定义一个指向链表队头的指针head,和一个指向链表队尾的指针tail来表示。
2. 队列的初始化
void QueueInit(Queue* ps)
{//队列的初始化
assert(ps);//传入的指针不能为空
ps->head = NULL;
ps->tail = NULL;//初始化队列,由于开始队中没有数据,所以为空队,队头和队尾指针为空
}
首先判断传入的指针是否为空,若不为空则将队尾指针和队头指针置空,因为此时队中没有元素,这样队列的初始化就完成了。
3. 队列的销毁
void QueueDestory(Queue* ps)
{//队列的销毁
assert(ps);//传入的指针不能为空
while (ps->head)
{//当传入的队头指针不空时,执行以下操作
ListNode* Del = ps->head;//首相让一个指针记录队头结点的位置
ps->head = ps->head->next;//让队头指针后移指向下一个结点
free(Del);//删除队头的结点
}
//循环结束后,所有的结点删除完毕,所以此时的队列为空
Del = NULL;
ps->head = NULL;
ps->tail = NULL;//将队头与队尾的指针置空,防止野指针的产生
}
首先在执行销毁队列操作前需要判断传入的指针是否为空,非空则定义一个循环,当队头指针非空时,执行以下操作,我们定义一个指针记录队头结点的位置,然后让队头指针后移,释放之前的队头结点 最后所有的元素全部出队后让所有的指针置空防止也指针的产生。
4. 判断队列是否为空
void QueueEmpty(Queue* ps)
{//判断队列是否为空
assert(ps);//传入的指针不能为空
if (ps->head == NULL && ps->tail == NULL)
printf("队列为空\n");
else
printf("队列不为空\n");
}
如果队头与队尾指针均为空,则打印队列为空 ,否则打印队列不为空。
5. 队列的入队操作
void QueuePush(Queue* ps, Datatype x)
{//队列的入队操作
assert(ps);//传入的指针不能为空
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//为新结点动态申请一个内存空间
if (newnode == NULL)
{//若指针newnode 为空,则内存申请失败,跳出程序
printf("结点空间动态申请失败\n");
exit(-1);
}
//若新结点申请成功,则进行以下操作
newnode->next = NULL;
newnode->data = x;
//入队时,首先判断队列是否为空
if (ps->head == NULL && ps->tail == NULL)
{
ps->head = newnode;
ps->tail = newnode;//若为空队列,则让头尾指针直接指向新结点,因为此时只有一个结点所以这个结点既是队头也是队尾
}
else
{
ps->tail->next = newnode;
ps->tail = newnode;//此时若队列不空,则让队尾结点的指针域存放新结点的位置,就是让队尾结点的指针域指向新结点,然后让队尾指针指向新结点
}
}
首先判断传入的指针不能为空,然后我们再动态申请一个结点空间, 在对申请的结点进行判空,若newnode为空则动态申请结点空间失败,若申请空间成功则将结点的指针域置空,数据域赋值,以上操作成功后还有事情要做,就是判断传入的队列是否为空,若为空则此时的新结点就相当于既是队头也是队尾,所以要将队头与队尾指针都指向新结点,若传入的队列非空,因为是在队尾插入,属于链表的尾插,让原来队尾结点的指针域存放新结点的位置,然后再让队尾指针指向新插入的结点,这样入队操作就完成了。
6. 队列的出队操作
void QueuePop(Queue* ps)
{//队列的出队操作
assert(ps);//传入的指针不能为空
if (ps->head == ps->tail )
{//判断传入的队列是否为空
free(ps->head);
ps->head == NULL;
ps->tail == NULL;
printf("队列已空\n");//若传入的队列队头与队尾的指针相等,则队列为空,将队头和队尾指针置空
}
else
{//若队列不为空则进行以下操作
ListNode* Del2 = ps->head;//定义一个指针指向队头结点
ps->head = ps->head->next;
free(Del2);
Del2 = NULL;//将指针置空防止野指针的产生
}
}
首先传入的指针不能为空,然后判断队列是否是只有一个结点传入,若是则直接释放队头指针,让后将队头与队尾指针置空,然后打印队列已空,否则若传入的不是一个结点的队列,则我们定义一个结点记录队头结点的位置,然后让队头指针指向队头结点的下一个结点,然后释放队头结点,也就是出队,完成后将指针置空防止野指针的产生。
7. 获取队列头部元素
Datatype QueueHead(Queue* ps)
{//取队列头部的元素
assert(ps);//传入的指针不能为空
QueueEmpty(ps);//判断传入的队列是否为空
return ps->head->data;//返回队列头部元素
}
这里传入的指针不能为空,然后直接返回队头指针对应的数据元素即可。
8. 获取队列尾部元素
Datatype QueueTail(Queue* ps)
{//取队列尾部的元素
assert(ps);//传入的指针不能为空
QueueEmpty(ps);//判断传入的队列是否为空
return ps->tail->data;//返回队列尾部的元素
}
取队尾的元素直接返回队尾指针指向结点的数据域元素即可。
9. 判断队列的有效元素个数
int QueueSize(Queue* ps)
{//求队列的有效元素个数
assert(ps);//传入的指针不能为空
int size = 0;//用一个size记录有效元素的个数
ListNode* cur = ps->head;//用一个指针指向队头结点
while (cur)//进行遍历
{
cur = cur->next;
++size;
}
return size;
}
这里我们定义一个整型变量size(用于记录队列的有效元素个数)和一个指针cur(用于遍历队列) ,然后只要cur指针不为空,则一直遍历队列,每指向下一个结点,size都会加一,最后循环结束返回size的值。
10. 测试函数
void Test()
{
Queue q;
QueueInit(&q);
printf("队列中有效元素个数为:%d\n", QueueSize(&q));
QueueEmpty(&q);
QueuePush(&q, 1);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 2);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 3);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 4);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePop(&q);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePop(&q);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 5);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueueDestory(&q);
printf("队列中有效元素个数为:%d\n", QueueSize(&q));
QueueEmpty(&q);
}
11. 完整代码及运行结果
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
typedef int Datatype;//定义链表结点的数据类型
typedef struct ListNode
{//定义结点的类型,这里的定义和链表的结点定义基本差不多
ListNode* next;//定义结点的指针域,这里的指针指向的是下一个结点的位置
Datatype data;//定义结点的数据域
};
typedef struct Queue
{//确定链表的类型,相当于确定队列的类型与结构
ListNode* head;//指向队头的对头指针
ListNode* tail;//指向队尾的队尾指针
};
void QueueInit(Queue* ps)
{//队列的初始化
assert(ps);//传入的指针不能为空
ps->head = NULL;
ps->tail = NULL;//初始化队列,由于开始队中没有数据,所以为空队,队头和队尾指针为空
}
void QueueDestory(Queue* ps)
{//队列的销毁
assert(ps);//传入的指针不能为空
while (ps->head)
{//当传入的队头指针不空时,执行以下操作
ListNode* Del = ps->head;//首相让一个指针记录队头结点的位置
ps->head = ps->head->next;//让队头指针后移指向下一个结点
free(Del);//删除队头的结点
}
//循环结束后,所有的结点删除完毕,所以此时的队列为空
ps->head = NULL;
ps->tail = NULL;//将队头与队尾的指针置空,防止野指针的产生
}
void QueueEmpty(Queue* ps)
{//判断队列是否为空
assert(ps);//传入的指针不能为空
if (ps->head == NULL && ps->tail == NULL)
printf("队列为空\n");
}
void QueuePush(Queue* ps, Datatype x)
{//队列的入队操作
assert(ps);//传入的指针不能为空
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//为新结点动态申请一个内存空间
if (newnode == NULL)
{//若指针newnode为空,则内存申请失败,跳出程序
printf("结点空间动态申请失败\n");
exit(-1);
}
//若新结点申请成功,则进行以下操作
newnode->next = NULL;
newnode->data = x;
//入队时,首先判断队列是否为空
if (ps->head == NULL && ps->tail == NULL)
{
ps->head = newnode;
ps->tail = newnode;//若为空队列,则让头尾指针直接指向新结点,因为此时只有一个结点所以这个结点既是队头也是队尾
}
else
{
ps->tail->next = newnode;
ps->tail = newnode;//此时若队列不空,则让队尾结点的指针域存放新结点的位置,就是让队尾结点的指针域指向新结点,然后让队尾指针指向新结点
}
}
void QueuePop(Queue* ps)
{//队列的出队操作
assert(ps);//传入的指针不能为空
if (ps->head == ps->tail )
{//判断传入的队列是否为空
free(ps->head);
ps->head == NULL;
ps->tail == NULL;
printf("队列已空\n");//若传入的队列队头与队尾的指针相等,则队列为空,将队头和队尾指针置空
}
else
{//若队列不为空则进行以下操作
ListNode* Del2 = ps->head;//定义一个指针指向队头结点
ps->head = ps->head->next;
free(Del2);
Del2 = NULL;//将指针置空防止野指针的产生
}
}
Datatype QueueHead(Queue* ps)
{//取队列头部的元素
assert(ps);//传入的指针不能为空
QueueEmpty(ps);//判断传入的队列是否为空
return ps->head->data;//返回队列头部元素
}
Datatype QueueTail(Queue* ps)
{//取队列尾部的元素
assert(ps);//传入的指针不能为空
QueueEmpty(ps);//判断传入的队列是否为空
return ps->tail->data;//返回队列尾部的元素
}
int QueueSize(Queue* ps)
{//求队列的有效元素个数
assert(ps);//传入的指针不能为空
int size = 0;//用一个size记录有效元素的个数
ListNode* cur = ps->head;//用一个指针指向队头结点
while (cur)//进行遍历
{
cur = cur->next;
++size;
}
return size;
}
void Test()
{
Queue q;
QueueInit(&q);
printf("队列中有效元素个数为:%d\n", QueueSize(&q));
QueueEmpty(&q);
QueuePush(&q, 1);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 2);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 3);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 4);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePop(&q);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePop(&q);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueuePush(&q, 5);
printf("队列中有效元素个数为:%d 队列头部元素为:%d 队列队尾元素为:%d\n", QueueSize(&q), QueueHead(&q), QueueTail(&q));
QueueDestory(&q);
printf("队列中有效元素个数为:%d\n", QueueSize(&q));
QueueEmpty(&q);
}
int main()
{
Test();
}
(五)有头双向链表的实现
以下是概念图,比起无头单链表,多了一个头结点(这里的头结点往往数据域不存数据)以及指向前一个结点的指针。无头双向循环链表功能性相较于单链表更强,效率更高且更灵活。
1. 带头双向循环链表的结点类型的确定
首先在实现带头双向循环链表之前,我们先要确定结点的类型。
typedef int DataType;
typedef struct ListNode
{//确定结点类型
DataType data;//定义结点的数据域
struct ListNode* prev;//定义结点指向前一个结点的指针
struct ListNode* next;//定义结点指向后一个结点的指针
}ListNode;
注:这里的对int类型进行重定义是为了我们更好的看懂Data只是一种数据而不仅仅是int类型。所以我们在使用链表时存储的数据并不限制于int类型,这里仅仅是举例。
2. 创建一个结点
ListNode* ListCreateNode()
{//创建一个结点
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//为结点动态开辟一个内存空间
if (newnode == NULL)
{//判断空间申请是否成功,如果结点空间申请失败则终止程序
printf("结点空间申请失败\n");
exit(-1);
}
//如果结点空间申请成功则进行以下操作
newnode->prev = NULL;
newnode->next = NULL;//将前后亮=两个指针置空防止野指针的产生
return newnode;
}
3. 头结点的创立
ListNode* ListHeadNode()
{//创建一个头结点
ListNode* head = ListCreateNode();
head->prev = head;
head->next = head;//当只有头结点时,将头结点的前后指针均指向自身,因为表是循环的
return head;
}
4. 向带头双向链表中插入数据
4.1 尾部插入结点
void ListPushBack(ListNode* head ,DataType x)
{//链表的尾部插入结点
assert(head);//首先判断传入的头结点是否为空,非空则执行以下条件
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = head;//让新结点的前指针指向头结点
newnode->prev = head->prev;//让新结点的前指针指向原来的最后一个结点
head->prev->next = newnode;//让原来最后一个结点的后指针指向新结点
head->prev = newnode;//让头结点的前指针指向新创建的结点
newnode->data = x;//最后给新结点的数据域赋值
}
4.2 头部插法入结点
void ListPushFront(ListNode* head, DataType x)
{//链表的头部插入结点
assert(head);//首先判断传入的头结点是否为空,非空则执行以下条件
ListNode* newnode = ListCreateNode();//创建一个新的结点
ListNode* Phead = head->next;//因为这里的头插是插在头结点后一个结点的前面,所以我们定义一个指针指向头结点的后一个结点
Phead->prev = newnode;//让头结点后一个结点的前指针指向新的结点
newnode->next = Phead;//让新结点的后指针指向Phead这个结点
newnode->prev = head;//让新结点的前指针指向头结点
head->next = newnode;//让头结点的后指针指向新结点
newnode->data = x;//最后给新结点的数据域赋值
}
4.3 在任意结点的前面插入结点
void ListInsertFront(ListNode* pos, DataType x)
{//在任意结点的前面插一个结点,这里为什么要在前面插,因为这样可以更好的突出双向循环链表与单链表的区别,单链表如果要在某个结点的前面插入一个结点的话会比较麻烦因为要遍历找位置,但双向循环链表直接用prev这个指针就可以找到前一个结点的位置所以更加简单
assert(pos);//判断传入的指针是否为空
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = pos;//让新结点的后指针指向pos结点
newnode->prev = pos->prev;//让新结点的前指针指向pos前面的一个结点
pos->prev->next = newnode;//让pos前面的那个结点的后指针指向新结点
pos->prev = newnode;//让pos结点的前指针指向新结点
newnode->data = x;//最后存入数据
}
4.3 在任意结点的后面插入结点
void ListInsertBack(ListNode* pos, DataType x)
{//在任意位置结点后插入一个新结点
assert(pos);//判断传入结点
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = pos->next;//让新结点的后指针指向pos结点的后一个结点
newnode->prev = pos;//让新结点的前指针指向pos结点
pos->next->prev = newnode;//让pos结点的后一个指针的前指针指向新结点
pos->next = newnode;//让pos结点的后指针指向新结点
newnode->data = x;//最后存入数据
}
5. 带头双向链表结点的删除
5.1 循环链表的尾删
void ListPopBack(ListNode* head)
{//循环链表的尾删
assert(head);//判断传入的头指针是否为空
ListNode* Del = head->prev;//定义一个指针指向链表的最后一个结点
Del->prev->next = Del->next;//让最后一个结点的前一个结点的后指针指向头结点
Del->next->prev = Del->prev;//让头结点的前指针指向最后一个结点的前一个结点
free(Del);//删除结点
Del = NULL;//将指针置空,防止野指针的产生
}
5.2 循环链表的头删
void ListPopFront(ListNode* head)
{//循环链表的头删
assert(head);//判断传入的头指针是否为空
ListNode* Del = head->next;//定义一个指针指向头结点的后一个结点
Del->next->prev = head;//将头结点的后一个结点的后一个结点的前指针指向头结点
head->next = Del->next;//将头结点的后指针指向头结点后一个结点的后一个结点
free(Del);//删除所对应的结点
Del = NULL;//将指针置空,防止野指针的产生
}
5.3 任意位置删除一个结点
void ListPop(ListNode* pos)
{//任意位置删除一个结点
assert(pos);//判断传入结点是否为空
pos->next->prev = pos->prev;//将想要删除结点的后一个结点的前指针指向pos结点的前一个结点
pos->prev->next = pos->next;//将pos结点的前一个结点的后指针指向pos结点的的后一个结点
free(pos);//删除结点
pos = NULL;//将指针置空,防止野指针的产生
}
6. 在链表中查询数据
ListNode* ListFind(ListNode* head,DataType x)
{//在链表中查询数据
assert(head);//判断传入的头指针是否为空
ListNode* find = head->next;//定义一个指针指向头结点的后一个结点
while (find != head)//定义一个循环循环条件是find指针不指向头指针
{
if (find->data == x)
{
return find;//如果找到了想要的值,则返回find指针
}
find = find->next;//遍历链表
}
printf("没有找的你想要的数据\n");
return NULL;//没找到想要的数据,返回一个空指针
}
7. 带头双向链表的遍历
void ListPrint(ListNode* head)
{//循环链表的遍历
assert(head);//传入的头指针不能为空
printf("head->");
ListNode* print = head->next;//定义一个指针指向头指针的后一个结点
while (print != head)
{//定义一个循环,遍历链表并打印每个结点里面的数据域里面的值
printf("%d->", print->data);
print = print->next;
}
printf("head\n");
}
8. 有头双向链表的销毁
void ListDestory(ListNode* head)
{//链表的销毁,保留头结点
assert(head);//传入的头指针不能为空
ListNode* Del = head->next;//定义一个指针指向头结点的后一个结点
ListNode* temp = Del;//再定义一个指针指向该结点,用于遍历删除
while (Del != head)
{//一个一个删除结点
temp = temp->next;
free(Del);
Del = temp;
}
head->prev = head;
head->next = head;//删除完后,将头结点的前后指针指向自身,相当于置空保证循环
}
9. 主函数部分
int main()
{
ListNode* Head = ListHeadNode();
//printf("head->");//在控制台上显示头结点,便于理解
ListPushBack(Head, 1);
ListPrint(Head);
ListPushBack(Head, 2);
ListPrint(Head);
printf("%d\n", Head->prev->prev->data);
ListPushBack(Head, 3);
ListPrint(Head);
ListPushFront(Head, 5);
ListPrint(Head);
ListPushBack(Head, 4);
ListPrint(Head);
ListInsertFront(ListFind(Head, 5), 6);
ListPrint(Head);
ListPop(ListFind(Head, 2));
ListPopBack(Head);
ListPrint(Head);
ListPopFront(Head);
ListPrint(Head);
ListDestory(Head);
ListPrint(Head);
return 0;
}
10. 完整代码及运行结果
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int DataType;
typedef struct ListNode
{//确定结点类型
DataType data;//定义结点的数据域
struct ListNode* prev;//定义结点指向前一个结点的指针
struct ListNode* next;//定义结点指向后一个结点的指针
}ListNode;
ListNode* ListCreateNode()
{//创建一个结点
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//为结点动态开辟一个内存空间
if (newnode == NULL)
{//判断空间申请是否成功,如果结点空间申请失败则终止程序
printf("结点空间申请失败\n");
exit(-1);
}
//如果结点空间申请成功则进行以下操作
newnode->prev = NULL;
newnode->next = NULL;//将前后亮=两个指针置空防止野指针的产生
return newnode;
}
ListNode* ListHeadNode()
{//创建一个头结点
ListNode* head = ListCreateNode();
head->prev = head;
head->next = head;//当只有头结点时,将头结点的前后指针均指向自身,因为表是循环的
return head;
}
void ListPushBack(ListNode* head ,DataType x)
{//链表的尾部插入结点
assert(head);//首先判断传入的头结点是否为空,非空则执行以下条件
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = head;//让新结点的前指针指向头结点
newnode->prev = head->prev;//让新结点的前指针指向原来的最后一个结点
head->prev->next = newnode;//让原来最后一个结点的后指针指向新结点
head->prev = newnode;//让头结点的前指针指向新创建的结点
newnode->data = x;//最后给新结点的数据域赋值
}
void ListPushFront(ListNode* head, DataType x)
{//链表的头部插入结点
assert(head);//首先判断传入的头结点是否为空,非空则执行以下条件
ListNode* newnode = ListCreateNode();//创建一个新的结点
ListNode* Phead = head->next;//因为这里的头插是插在头结点后一个结点的前面,所以我们定义一个指针指向头结点的后一个结点
Phead->prev = newnode;//让头结点后一个结点的前指针指向新的结点
newnode->next = Phead;//让新结点的后指针指向Phead这个结点
newnode->prev = head;//让新结点的前指针指向头结点
head->next = newnode;//让头结点的后指针指向新结点
newnode->data = x;//最后给新结点的数据域赋值
}
void ListInsertFront(ListNode* pos, DataType x)
{//在任意结点的前面插一个结点,这里为什么要在前面插,因为这样可以更好的突出双向循环链表与单链表的区别,单链表如果要在某个结点的前面插入一个结点的话会比较麻烦因为要遍历找位置,但双向循环链表直接用prev这个指针就可以找到前一个结点的位置所以更加简单
assert(pos);//判断传入的指针是否为空
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = pos;//让新结点的后指针指向pos结点
newnode->prev = pos->prev;//让新结点的前指针指向pos前面的一个结点
pos->prev->next = newnode;//让pos前面的那个结点的后指针指向新结点
pos->prev = newnode;//让pos结点的前指针指向新结点
newnode->data = x;//最后存入数据
}
void ListInsertBack(ListNode* pos, DataType x)
{//在任意位置结点后插入一个新结点
assert(pos);//判断传入结点
ListNode* newnode = ListCreateNode();//创建一个新的结点
newnode->next = pos->next;//让新结点的后指针指向pos结点的后一个结点
newnode->prev = pos;//让新结点的前指针指向pos结点
pos->next->prev = newnode;//让pos结点的后一个指针的前指针指向新结点
pos->next = newnode;//让pos结点的后指针指向新结点
newnode->data = x;//最后存入数据
}
void ListPopBack(ListNode* head)
{//循环链表的尾删
assert(head);//判断传入的头指针是否为空
ListNode* Del = head->prev;//定义一个指针指向链表的最后一个结点
Del->prev->next = Del->next;//让最后一个结点的前一个结点的后指针指向头结点
Del->next->prev = Del->prev;//让头结点的前指针指向最后一个结点的前一个结点
free(Del);//删除结点
Del = NULL;//将指针置空,防止野指针的产生
}
void ListPopFront(ListNode* head)
{//循环链表的头删
assert(head);//判断传入的头指针是否为空
ListNode* Del = head->next;//定义一个指针指向头结点的后一个结点
Del->next->prev = head;//将头结点的后一个结点的后一个结点的前指针指向头结点
head->next = Del->next;//将头结点的后指针指向头结点后一个结点的后一个结点
free(Del);//删除所对应的结点
Del = NULL;//将指针置空,防止野指针的产生
}
void ListPop(ListNode* pos)
{//任意位置删除一个结点
assert(pos);//判断传入结点是否为空
pos->next->prev = pos->prev;//将想要删除结点的后一个结点的前指针指向pos结点的前一个结点
pos->prev->next = pos->next;//将pos结点的前一个结点的后指针指向pos结点的的后一个结点
free(pos);//删除结点
pos = NULL;//将指针置空,防止野指针的产生
}
ListNode* ListFind(ListNode* head,DataType x)
{//在链表中查询数据
assert(head);//判断传入的头指针是否为空
ListNode* find = head->next;//定义一个指针指向头结点的后一个结点
while (find != head)//定义一个循环循环条件是find指针不指向头指针
{
if (find->data == x)
{
return find;//如果找到了想要的值,则返回find指针
}
find = find->next;//遍历链表
}
printf("没有找的你想要的数据\n");
return NULL;//没找到想要的数据,返回一个空指针
}
void ListPrint(ListNode* head)
{//循环链表的遍历
assert(head);//传入的头指针不能为空
printf("head->");
ListNode* print = head->next;//定义一个指针指向头指针的后一个结点
while (print != head)
{//定义一个循环,遍历链表并打印每个结点里面的数据域里面的值
printf("%d->", print->data);
print = print->next;
}
printf("head\n");
}
void ListDestory(ListNode* head)
{//链表的销毁,保留头结点
assert(head);//传入的头指针不能为空
ListNode* Del = head->next;//定义一个指针指向头结点的后一个结点
ListNode* temp = Del;//再定义一个指针指向该结点,用于遍历删除
while (Del != head)
{//一个一个删除结点
temp = temp->next;
free(Del);
Del = temp;
}
head->prev = head;
head->next = head;//删除完后,将头结点的前后指针指向自身,相当于置空保证循环
}
int main()
{
ListNode* Head = ListHeadNode();
//printf("head->");//在控制台上显示头结点,便于理解
ListPushBack(Head, 1);
ListPrint(Head);
ListPushBack(Head, 2);
ListPrint(Head);
printf("%d\n", Head->prev->prev->data);
ListPushBack(Head, 3);
ListPrint(Head);
ListPushFront(Head, 5);
ListPrint(Head);
ListPushBack(Head, 4);
ListPrint(Head);
ListInsertFront(ListFind(Head, 5), 6);
ListPrint(Head);
ListPop(ListFind(Head, 2));
ListPopBack(Head);
ListPrint(Head);
ListPopFront(Head);
ListPrint(Head);
ListDestory(Head);
ListPrint(Head);
return 0;
}
本期博客主要写的是数据结构的一些内容的实现且代码偏多,大家好好消化,如有错误还望大家批评指正,我们下次再见。