线性表是什么
线性表是一个数据结构,为什么叫线性表呢?是因为它的逻辑结构是连续的,在我们学习线性表之前其实我们就已经学过了一个数据结构,这个数据结构就是数组。数组是一个最基本的数据结构,它的物理结构和逻辑结构都是连续的。
物理结构和逻辑结构
在上面这一段文字中,我们知道了数组的物理结构和逻辑结构都是连续的。那么物理结构和逻辑结构都是什么意思呢?
物理结构
物理结构简而言之就是一块连续的存储空间。用以下这张图可以很好的解释这一个问题:
如图,这是数组a在内存空间里存放的位置关系,我们会发现它的物理空间是连续的。
逻辑结构
逻辑结构又是什么意思呢?逻辑结构简而言之就是逻辑上的结构。这种结构不是内存空间上的,是我们人为想象出来的。如果还是不理解的话,请看以下这张图片:
这就是逻辑结构,从这张图我们可以很清楚地看出这个东西逻辑结构是连续的。但是实际上它们的物理结构可能是这样子的:
我们会发现它的物理结构此时就不是连续的了,但是逻辑上连续,这就是逻辑结构。如果还是很迷糊的话,学了链表我们就可以理解了。
1.顺序表
顺序表的实现是基于数组这一最简单的数据结构的,这也就意味着,它的性质会与数组极为相似,也就是它的物理结构和逻辑结构也都是连续的,正是基于这一点,顺序表才可以实现随机访问的这一特点。那么接下来,我们来看看顺序表究竟是如何实现的吧。
1.1顺序表的实现
在实现顺序表之前,我们得了解顺序表需要哪些操作。首先,顺序表得进行一个初始化和销毁的操作,然后顺序表当然也支持插入删除操作,并且顺序表也支持查询数据元素的个数的操作,最后我们可以将我们的顺序表中的元素给打印出来。这些操作我们给它放入一个叫做SeqLsit.h的头文件里面以便我们快速查询我们实现的顺序表究竟支持一些什么操作。
//顺序表的实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* data; //顺序表的数据元素
int size; //顺序表的数据元素个数
int capcity; //顺序表的空间大小
}SL;
//顺序表的初始化与销毁
void SLInit(SL* psl);
void SLDestroy(SL* psl);
//检查顺序表的空间大小
void SLCheckCapcity(SL* psl);
//查看顺序表的大小
int SLLength(SL* psl);
//顺序表的插入删除
//头插头删
void SLPushHead(SL* psl, SLDataType x);
void SLPopHead(SL* psl);
//尾插尾删
void SLPushBack(SL* psl, SLDataType x);
void SLPopBack(SL* psl);
//在第i个位置插入删除
void SLInsert(SL* psl, int i, SLDataType x);
void SLDelete(SL* psl, int i);
//顺序表的打印
void SLPrint(SL* psl);
(注:检查顺序表的空间大小这个操作先不要求理解,后面实现顺序表的时候会提到这个操作)
现在我们就可以创建一个源文件为SeqList.c在这个源文件里面实现我们刚刚SeqList.h这个头文件中的提供的操作。
首先是SLInit()和SLDestroy()这两个函数。
//顺序表的初始化与销毁
//顺序表的初始化
void SLInit(SL* psl)
{
assert(psl); //检查是否传入一个空指针进入函数,是则终止,不是则继续执行初始化函数
psl->data = NULL; //将数据元素的首地址先置空
psl->size = psl->capcity = 0;
}
//顺序表的销毁
void SLDestroy(SL* psl)
{
if (psl->data)
free(psl->data); //由于我们进行插入删除操作时会对数据元素进行动态内存的开辟,所以我们需要free掉这一块动态内存
psl->data = NULL; //管理野指针
psl->size = 0;
psl->capcity = 0;
}
以上就是我们的初始化和销毁函数的实现。
我们接下来实现一个比较简单的操作,那就是查看顺序表的大小的操作。
//查看顺序表的大小
int SLLength(SL* psl)
{
assert(psl);
return psl->size;
}
我们实现查看顺序表的大小时可以直接返回我们结构体中的size变量。
以上就是对查看顺序表的大小的函数的实现。
那么接下来我们要实现的就是插入删除操作了
//检查顺序表的空间大小
void SLCheckCapcity(SL* psl)
{
assert(psl);
//判断顺序表的空间是否足够
if (psl->size == psl->capcity)
{
//判断顺序表的空间大小,如果为空则赋值,如果不为空则扩容双倍
int newCapcity = psl->capcity == 0 ? 4 : 2 * psl->capcity;
//如果顺序表扩容的时候没有完整的一块空间就另找一块空间进行扩容,并将原空间的值赋值给新空间。所以最好用realloc而不是malloc
SLDataType* tmp = (SLDataType*)realloc(psl->data, newCapcity * sizeof(SLDataType));
//realloc扩容的时候有可能指向空,如果为空则直接退出程序
if (tmp == NULL)
{
perror("realloc fail\n");
exit(-1);
}
psl->data = tmp; //将扩容后的地址更新
psl->capcity = newCapcity; //将扩容后的空间大小更新
}
}
//顺序表的插入删除
//顺序表的头插
void SLPushHead(SL* psl, SLDataType x)
{
assert(psl);
//检查空间大小是否足够
SLCheckCapcity(psl);
int i = 0;
//从后往前遍历顺序表
for (i = SLLength(psl); i > 0; i--)
{
psl->data[i] = psl->data[i - 1];
}
psl->data[0] = x; //将第一个数据元素用我们插入的数据元素覆盖
psl->size++; //更新数据元素的个数
}
//顺序表的头删
void SLPopHead(SL* psl)
{
assert(psl);
int i = 0;
//从前往后遍历顺序表,用后面的数据元素覆盖前面的数据元素
for (i = 0; i < SLLength(psl) - 1; i++)
{
psl->data[i] = psl->data[i + 1];
}
psl->size--; //更新数据元素的个数
}
//顺序表的尾插
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
//检查顺序表的空间大小是否足够
SLCheckCapcity(psl);
psl->data[psl->size] = x; //直接插入数据元素
psl->size++; //更新数据元素的个数
}
//顺序表的尾删
void SLPopBack(SL* psl)
{
assert(psl);
//尾删只需要让数据元素的个数减一即可。
//以下是两点理由:
//1.后期插入数据的话就可以直接覆盖掉我们需要删除的数据元素
//2.数据元素个数减一我们就不会访问到最后那个数据元素
psl->size--; //更新数据元素的个数
}
//顺序表的在指定位置插入
void SLInsert(SL* psl, int i, SLDataType x)
{
assert(psl);
int size = SLLength(psl);
//检查插入的位置是否合法,不合法就不插入
if (i > size + 1 || i <= 0)
{
printf("插入失败!\n");
return -1;
}
//检查顺序表的空间大小
SLCheckCapcity(psl);
int j = 0;
//从后往前遍历顺序表,将第i个位置之后的数据元素往后挪一位
//最后直接在第i个位置用我们需要插入的数据元素覆盖掉第i个位置的值就行
for (j = size; j >= i; j--)
{
psl->data[j] = psl->data[j - 1];
}
psl->data[i - 1] = x; //将第i个位置的数据元素覆盖
psl->size++; //更新数据元素的个数
}
//顺序表的在指定位置删除
void SLDelete(SL* psl, int i)
{
assert(psl);
int dest = 0; //dest指向的是第i个位置
//遍历第i个位置(包括)之后的数据元素,用后面的数据元素覆盖前面的数据元素
for (dest = i - 1; dest < SLLength(psl) - 1; dest++)
{
psl->data[dest] = psl->data[dest + 1];
}
psl->size--; //更新数据元素的个数
}
这一段代码看上去好像有点长,但是细细看我们就会发现它其实并不是很难。
代码分析:
- SLCheckCapcity函数的实现:
1.1 为什么SLCheckCapcity函数要实现
首先我们对顺序表进行插入操作时,我们得先开辟一块内存空间并且检查顺序表开辟的内存空间是否足够,这也就是为什么我们要实现一个检查内存空间大小的函数。
1.2 如何实现SLCheckCapcity函数
1.2.1 什么时候判断顺序表的空间大小不够
capcity这个变量里面存放的就是顺序表的空间大小,size这个变量里面存放的时顺序表中数据元素的多少。所以当size和capcity相等时也就是我们的内存空间不足的时候这个时候我们就需要开辟一块新的内存空间。
1.2.2 当顺序表的内存空间大小不够的时候我们该用哪一个动态内存开辟的函数呢?
首先,我们都知道动态内存开辟的函数有3个,分别是malloc、calloc、和realloc。其中malloc就只是开辟一个内存,这个开辟是随机的,如果这个开辟是随机的话,那么就不符合顺序表中的物理内存连续。所以不能用malloc,而calloc和malloc一样都是随机开辟的所以也不能用,只有ralloc这个是在原来的地址上开辟新的空间,所以我们得选用ralloc来实现动态内存的开辟。
1.2.3 开辟动态内存时开辟多少比较合适
在动态内存开辟时,一般都是开辟原来两倍的内存空间,但是我们在第一次开辟的时候会发现一个问题,那就是我们的capcity一开始被我们初始化为0了,所以我们得在动态内存开辟之前得把capcity赋值为别的数字。 - 头插头删的实现
2.1 头插的实现
要实现头插,我们首先得对顺序表进行遍历,将首元素后面的数据元素全部往后移一个位置,然后我们将需要插入的数据元素直接插入到首元素的位置,最后再将size+1就行了。
这里是头插的图示:
以上的过程就是一个完整的头插顺序表中的数据元素的过程。
2.2 头删的实现
要实现头删,我们也得对顺序表进行遍历,将首元素后面的数据元素全部往前移一个位置,将第一个数据元素用后面的数据元素覆盖掉。最后再将size-1就行了。至于最后一个数据元素我们可以不需要管他,因为等我们后期继续插入数据元素时会将这个数据重新覆盖掉。
这里是头删的图示:
以上就是一个完整的头删顺序表中的数据元素的过程。 - 尾插尾删的实现
顺序表的尾插尾删的实现是比较简单的。不需要遍历
3.1 尾插的实现
要实现尾插,我们直接往顺序表的最后直接插入数据元素就行了。最后再将size+1就行了。
这里是尾插的图示:
以上就是一个完整的尾插顺序表中数据元素的过程。
3.2 尾删的实现
要实现尾删,我们直接size-1就行了,因为当我们后面再插入数据元素时,我们新插入的数据元素就会将原来删除的数据元素进行覆盖。最后将size-1就行了。
尾删的图示基本上没有变动,所以就不放图示出来了。 - 指定位置插入删除操作的实现
4.1 指定位置插入操作的实现
指定位置插入操作和头插操作差不多,就是先找到该位置,然后对该位置以后的数据元素进行一个遍历操作,将后续的数据元素往后移一个位置,然后再将我们需要插入的数据元素直接插入到指定位置。同时size+1。
以上就是一个完整的在顺序表中指定位置插入数据元素的过程。
4.2 指定位置删除操作的实现
指定位置删除操作也和头删操作差不多,也是先找到该位置,然后对该位置后面的数据元素进行一个遍历操作,将后续的数据元素往前挪一个位置,将指定位置的数据元素覆盖。同时size-1。
这里是指定位置删除的图示:
以上就是一个完整的在顺序表中指定位置删除数据元素的过程。
以上就是插入删除操作的实现以及为什么要实现SLCheckCapcity函数。
最后,我们再来实现一下顺序表的打印。
//顺序表的打印
void SLPrint(SL* psl)
{
assert(psl);
int i = 0;
//遍历顺序表
for (i = 0; i < SLLength(psl); i++)
{
printf("%d ", psl->data[i]); //打印顺序表的数据元素
}
printf("\n");
}
顺序表的打印操作比较简单,只需要直接遍历一遍顺序表然后将顺序表中的数据元素打印出来就行了。
2.链表
在前面的学习中我们知道了顺序表的物理结构和逻辑结构都是连续的,那么这种数据结构有没有什么弊端呢?答案是肯定的。任何数据结构都有它的弊端,而顺序表的弊端和优势都是同一个东西。那就是它的物理结构是连续的。为什么这么说呢?
首先,顺序表的优势是它的物理结构是连续的,这也就意味着它可以实现随机访问这种快速访问的操作,时间复杂度是O(1)。
但是顺序表的物理结构连续同时也是它的弊端,为什么这么说呢?因为它的物理空间是连续的,所以它就需要一块完整的内存空间去存储这一段数据,如果需要存储的数据太大了而内存中没有这一整块内存空间存储这部分数据,就会导致数据出现丢失的结果。这就是顺序表的弊端。
那有没有一种数据结构能够将这一弊端消除呢?当然有,这就是我们接下来要介绍的链表。这也就侧面说明了链表的物理结构并不是连续的,它的物理结构可能是东一块西一块的,但是它的逻辑结构是连续的。
链表其实有很多类型的,由于篇幅有限,在此仅列出几种常用的链表的类型。
(注:本文中提到的链表的数据类型并不是链表的所有类型)
链表的构成
在了解链表是如何实现之前,我们得先了解一下链表的构成。
首先,链表中的结点由两部分组成,一部分是指针域一部分是数据域。
下面是链表的结点组成的图解:
- 数据域
为什么要设计数据域呢?数据域就是链表中存储数据元素的一个地方。我们存储的数据元素全部被放在了数据域中了。 - 指针域
那么指针域又是什么呢?指针域就是存储下一个结点的地址的地方。为什么要存储下一个结点的地址呢?因为链表的物理结构并不是连续的,所以我们得保存下一个结点的地址以便于我们访问下一个元素。
以上就是链表基本的构成。接下来我们介绍几种常用的链表的类型吧。
2.1不带头节点单链表
和顺序表一样,单链表中也提供了一些基本的操作。以下就是单链表中的常用的一些基本操作我将其放在LinkList.h头文件中了:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LLDataType;
typedef struct LinkList
{
//数据域
LLDataType data;
//指针域
struct LinkList* next;
}LList;
//单链表的销毁
void LLDestroy(LList** pplist);
//创建新节点
LList* CreatNewNode(LLDataType x);
//单链表的插入删除
void LLPushFront(LList** pplist, LLDataType x);
void LLPopFront(LList** pplist);
void LLPushBack(LList** pplist, LLDataType x);
void LLPopBack(LList** plist);
void LLInsert(LList** pplist, int i, LLDataType x);
void LLDelete(LList** pplist, int i);
//单链表的长度
int LLLength(LList* plist);
//单链表的打印
void LLPrint(LList* plist);
//单链表的查找
LList* LLFind(LList* plist, LLDataType x);
和顺序表一样,这个CreatNewNode函数是在插入操作时用到的,先不要求理解。
我们发现,单链表中似乎没有初始化这一个操作,这是为什么呢?因为单链表的初始化操作就是一个创建头节点的操作,但是我们这的单链表是不带头结点的单链表,自然也就没有了初始化的操作。
在这个头文件中,我们会发现一个和顺序表不同的地方,那就是这里的函数的参数几乎都是用二级指针。这是为什么呢?这是因为在不带头结点的单链表中,我们的初始化操作是没有的,取而代之的是一个指向单链表的第一个结点的指针。但是在插入删除操作的过程中我们可能会修改我们的头节点。如果用一级指针的话我们在函数内部就无法修改这个指针指向的位置,因为如果用一级指针就是对原指针的一种拷贝,修改不了原指针指向的位置,所以在这里我们函数内部的参数选用二级指针。
接下来,我们也可以创建一个LinkList.c的源文件来实现以下LinkList.h这个头文件中的操作。
首先是LLDestory这个函数:
//单链表的销毁
void LLDestroy(LList** pplist)
{
assert(pplist);
//定义前后指针来free链表的节点
LList* cur = *pplist; //下一个要free的节点
LList* dest = *pplist; //当前需要free的节点的位置
while (cur != NULL)
{
dest = cur; //更新dest的位置
cur = cur->next; //往后走一步
free(dest);
}
//循环结束后cur指向空指针
//管理创建的指针变量
*pplist = NULL; //free完整个链表之后pplist就是野指针了,需要管理起来
pplist = NULL;
dest = NULL; //dest在free后指向的还是一个非法的地址,所以也需要管理起来
}
在我们创建结点的过程中会malloc结点空间,所以我们需要遍历整个链表并且定义前后指针去free掉所有的结点空间,最后将我们的野指针管理起来就好了。
接下来就是插入删除操作的实现了。
//创建新节点
LList* CreatNewNode(LLDataType x)
{
//创建新节点
LList* newnode = (LList*)malloc(sizeof(LList));
//判空
if (newnode == NULL)
{
perror("malloc fail\n");
exit(-1);
}
//更新新节点的值
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//单链表的插入删除
//单链表的头插
void LLPushFront(LList** pplist, LLDataType x)
{
assert(pplist);
//开辟新节点
LList* newnode = CreatNewNode(x);
//因为newnode是我们现在的头节点,所以我们得把原来的头节点的地址交给newnode->next管理
newnode->next = *pplist;
//更新头节点地址
*pplist = newnode;
//另一种写法
//LLInsert(pplist, 1, x);
}
//单链表的头删
void LLPopFront(LList** pplist)
{
assert(pplist);
assert(*pplist);
LList* cur = *pplist; //表示要删除的节点的位置
LList* next = cur->next; //保留要删除的节点的位置的下一个地址
free(cur); //删除节点
cur = NULL; //将野指针进行管理
//更新头节点的地址
*pplist = next;
//另一种写法
//LLDelete(pplist, 1);
}
//单链表的尾插
void LLPushBack(LList** pplist, LLDataType x)
{
assert(pplist);
LList* cur = *pplist;
//找到单链表的最后一个位置
while (cur->next != NULL)
{
cur = cur->next;
}
//创建新节点
LList* newnode = CreatNewNode(x);
//尾插
cur->next = newnode;
//另一种写法
//LLInsert(pplist, LLLength(*pplist) + 1, x);
}
//单链表的尾删
void LLPopBack(LList** pplist)
{
assert(pplist);
assert(*pplist);
//前后指针遍历单链表
LList* prev = *pplist;
LList* cur = (*pplist)->next;
//如果只剩一个数据元素就直接头删
if (cur == NULL)
{
LLPopFront(pplist);
return;
}
//遍历单链表,使得cur指向的是单链表最后一个节点
while (cur->next != NULL)
{
prev = cur; //prev存放的是cur前一个节点的地址
cur = cur->next; //更新cur指向的地址
}
free(cur); //删除尾节点
cur = NULL; //管理野指针
prev->next = NULL; //更新尾指针
}
//单链表的指定位置插入
void LLInsert(LList** pplist, int i, LLDataType x)
{
assert(pplist);
//判断i的位置是否合法
if (i <= 0 || i > LLLength(*pplist) + 1)
{
printf("插入失败!\n");
exit(-1);
}
//头插
if (i == 1)
{
//创建新节点
LList* newnode = CreatNewNode(x);
newnode->next = *pplist; //将原来头节点的地址交给newnode管理
*pplist = newnode; //更新头节点地址
//另一种写法
//LLPushFront(pplist, x);
}
else
{
LList* cur = *pplist;
LList* prev = *pplist;
int count = 1;
//遍历单链表,找到第i个位置
while (count < i)
{
prev = cur; //更新prev指向的地址
cur = cur->next; //更新cur指向的地址
count++;
}
LList* newnode = CreatNewNode(x);
newnode->next = cur;
prev->next = newnode;
}
}
//单链表的指定位置删除
void LLDelete(LList** pplist, int i)
{
assert(pplist);
assert(*pplist);
//判断i的位置是否合法
if (i <= 0 || i > LLLength(*pplist) + 1)
{
printf("插入失败!\n");
exit(-1);
}
if (i == 1)
{
LList* cur = *pplist; //表示要删除的节点的位置
LList* next = cur->next; //保留要删除的节点的位置的下一个地址
free(cur); //删除节点
cur = NULL; //将野指针进行管理
//更新头节点的地址
*pplist = next;
//另一种写法
//LLPopFront(pplist);
}
else
{
int count = 1;
LList* prev = NULL;
LList* cur = *pplist;
//遍历单链表,找到第i个位置
while (count < i)
{
prev = cur; //更新prev指向的地址
cur = cur->next; //更新cur指向的地址
count++;
}
prev->next = cur->next; //将cur节点从单链表中摘除
free(cur); //删除我们需要删除的节点
cur = NULL; //管理野指针
}
}
这一段代码和顺序表的那一段代码一样看上去好像内容挺多挺难的,但是实际上这一段代码是比较简单的。
我们一个个来分析:
- CreatNewNode函数的实现:
1.1 为什么要实现CreatNewNode函数
单链表是由一个个结点连接而成的,所以在我们插入新的数据元素时,我们就需要一个函数去创建我们的新结点。这就是CreatNewNode函数的成因。
1.2 如何实现CreatNewNode函数
1.2.1 函数参数的确定
因为这个函数是用来插入数据元素的,所以在我们创建新的结点的时候,我们得将这个数据元素插入到我们的数据域,所以这个函数需要的参数是一个数据元素。
1.2.2 函数返回值的确定
因为这个函数的目的是创建一个新的结点,所以在这个函数内部创建完了结点之后我们得把这个结点的地址返回出去。
1.2.3 函数的实现
因为链表的物理结构不一定是连续的,所以我们创建的结点的位置可以是随机的,所以我们就没必要用calloc函数和realloc函数来创建结点,直接用malloc创建一个新的结点并且将我们需要插入的数据元素直接插入到该结点的数据域就行了。 - 单链表的头插头删的实现:
2.1 头插的实现
单链表非常适合头插,它的头插头删不需要遍历一整块链表。直接修改单链表的头指针即可。
这里是单链表头插的图示:
2.2 头删的实现
单链表的头删和头插很相似,就是对单链表的头节点的更改。
这里是单链表头删的图示:
- 单链表的尾插尾删的实现:
3.1 单链表的尾插
接下来是单链表的尾插操作,这个操作也是一个比较简单的操作,首先我们得遍历一整个单链表并找到这个单链表的最后一个结点的地址。然后将创建的新的结点的地址交给单链表中最后一个结点的next指针进行管理。最后将新创建的结点的next指针指向空指针,这就完成了单链表的尾插操作。
以下是单链表尾插操作的图示:
3.2 单链表的尾删
单链表的尾删操作也需要先遍历一遍单链表找到最后一个结点和前一个结点的地址,并用两个指针将这两个结点的地址保存起来,然后直接free掉单链表最后一个结点的地址,再用刚刚保存的单链表最后一个结点的前一个结点的next指针直接指向空。
以下是单链表尾删操作的图示:
- 单链表的指定位置插入删除的实现:
4.1指定位置插入操作的实现
指定位置插入操作也是比较简单的一个操作,首先我们找到指定的位置,然后直接完成插入操作即可。
以下是单链表指定位置插入操作的图示:
4.2指定位置删除操作的实现
指定位置删除操作和指定位置插入操作很相似,在这里就不在赘述。直接看图示即可。
以下是单链表指定位置删除操作的图示:
实现完以上这些操作,接下来就是一些非常简单的操作了。下面统一介绍一下剩下哪些操作:
- 单链表的长度
单链表的长度很好求,直接带计数器遍历一遍单链表,最后返回计数器的值就行。下面是代码示例:
//单链表的长度
int LLLength(LList* plist)
{
int count = 0; //计数器
LList* cur = plist;
//遍历单链表
while (cur != NULL)
{
count++;
cur = cur->next;
}
return count;
}
- 单链表的打印
打印单链表和求单链表的长度的操作差不多,也是遍历一遍单链表并将其数据域中的值打印出来。以下是代码示例:
//单链表的打印
void LLPrint(LList* plist)
{
LList* cur = plist; //遍历单链表的指针变量
//遍历单链表,打印每一个节点的值
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
- 单链表的查找
查找操作和上面两种操作基本一样,这里不在赘述,直接看代码示例吧:
//单链表的查找
LList* LLFind(LList* plist, LLDataType x)
{
assert(plist);
LList* cur = plist;
//利用cur去遍历单链表
while (cur != NULL)
{
//如果找到就退出循环,如果没有就继续遍历
if (cur->data == x)
{
break;
}
else
{
cur = cur->next;
}
}
return cur;
}
2.2带头双向循环链表
这个链表和单链表有什么不同呢?
带头双向循环链表和单链表有三点不同:
- 这个链表在单链表的基础上增加了一个prev指针,这个指针是指向前一个结点的地址的。我们会发现,在我们的单链表中,我们是找不到结点的前一个结点的地址的,我们只能通过前后指针遍历去记录下我们的前一个结点的地址,但是这在带头双向循环链表中有过优化。
- 当然,除了这一点,带头双向循环链表还比单链表多了一个带有哨兵位的头结点。当我们有这个头结点就意味着在我们的函数中就不需要二级指针了,因为我们的链表的头结点永远不会改变,所以我们只要用一级指针就够了。
- 带头双向循环链表它是循环的,也就是说在这个链表中是没有空指针的,它的最后一个结点的next指针直接指向了这个哨兵位的头结点,这也是我们需要注意的一个点,因为我们可能稍不留神就写了一个死循环。
带头双向循环链表提供的操作和单链表差不多,所以这里便不再赘述。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType val;
struct ListNode* prev;
struct ListNode* next;
}ListNode;
//双向链表的初始化与销毁
ListNode* ListCreate();
void ListDestory(ListNode* phead);
//开辟新节点
ListNode* CreatNode(LTDataType x);
//双向链表的插入
//头插
void ListPushFront(ListNode* phead, LTDataType x);
//尾插
void ListPushBack(ListNode* phead, LTDataType x);
//双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
//双向链表的删除
//头删
void ListPopFront(ListNode* phead);
//尾删
void ListPopBack(ListNode* phead);
//双向链表删除pos位置的节点
void ListErase(ListNode* pos);
//双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x);
//双向链表打印
void ListPrint(ListNode* phead);
文章的篇幅似乎有点长,所以这个链表我们只会挑几个和单链表不同的几个操作进行解释。
首先第一个就是我们的初始化操作,这个操作就是我们创建一个带有哨兵位的头结点并返回出来 ,这个头结点的数据域可以随便放东西,指针域的前驱指针和后继指针均指向自己。
第二个就是尾插尾删操作,由于我们的带头双向循环链表有前驱指针,所以我们直接用前驱指针就可以访问到我们的链表的尾部,然后通过正常的插入删除操作就可以实现我们的尾插和尾删操作。
第三个就是双向链表的查找操作,这个操作就是我们需要在带头双向循环链表中找到带有特定数据的结点并返回出来,也就是直接遍历一遍我们的双向链表并在数据域中找到我们需要找的数据并返回该结点就行了。
以下是这个链表的源代码:
//开辟新节点
ListNode* CreatNode(LTDataType x)
{
//创建新节点
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail\n");
exit(-1);
}
//给新节点中的数据元素赋值
newnode->val = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode; //将创建的新节点传出函数
}
//创建返回链表的头结点.
ListNode* ListCreate()
{
//phead是我们新开辟的哨兵位的头节点
ListNode* phead = CreatNode(-1);
//让哨兵位的头节点的前后指针都指向自己
phead->prev = phead;
phead->next = phead;
return phead; //返回哨兵位的头节点
}
// 双向链表销毁
void ListDestory(ListNode* phead)
{
assert(phead);
//使用前后指针销毁双向链表中的数据元素
ListNode* cur = phead->next; //销毁的数据元素的地址
ListNode* next = NULL; //当前需要销毁的数据元素的下一个数据元素的地址
//遍历双向链表
while (cur != phead)
{
next = cur->next;
free(cur); //销毁当前节点
cur = next; //更新需要销毁的节点的地址
}
free(phead); //销毁哨兵位的头节点
//管理野指针
cur = NULL;
next = NULL;
}
// 双向链表打印
void ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
//遍历双向链表
while (cur != phead)
{
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
//开辟新节点
ListNode* newnode = CreatNode(x);
ListNode* prev = phead->prev; //找到双向链表的最后一个数据元素的地址
//将新开辟的节点插入进双向链表
//从prev走到phead
prev->next = newnode;
newnode->next = phead;
//从phead走回prev
phead->prev = newnode;
newnode->prev = prev;
}
// 双向链表尾删
void ListPopBack(ListNode* phead)
{
assert(phead);
//判断双向链表是否为空,为空则不进行删除并提示
if (phead->prev == phead)
{
printf("链表中没有元素,删除失败!\n");
return;
}
ListNode* cur = phead->prev; //双向链表最后一个数据元素的地址
ListNode* prev = cur->prev; //双向链表倒数第二个数据元素的地址
//将双向链表倒数第二个数据元素和哨兵位建立联系
prev->next = phead;
phead->prev = prev;
//删除尾节点
free(cur);
//管理野指针
cur = NULL;
}
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
//开辟新节点
ListNode* newnode = CreatNode(x);
ListNode* next = phead->next; //当前第一个节点的地址
//进行头插
//从phead走到next
phead->next = newnode;
newnode->next = next;
//从next走回phead
next->prev = newnode;
newnode->prev = phead;
}
// 双向链表头删
void ListPopFront(ListNode* phead)
{
assert(phead);
//检查双向链表是否有数据元素,如果没有则不进行删除操作并提示
if (phead->next == phead)
{
printf("链表中没有元素,删除失败!\n");
return;
}
ListNode* cur = phead->next; //当前双向链表第一个数据元素的地址
ListNode* next = cur->next; //当前双向链表第二个数据元素的地址
//将双向链表第二个数据元素与哨兵位建立联系
phead->next = next;
next->prev = phead;
//删除头节点
free(cur);
//管理野指针
cur = NULL;
}
// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
//遍历双向链表
while (cur != phead)
{
//找到了就返回该数据元素在双向链表中的地址
if (cur->val == x)
{
return cur;
}
//没找到,继续遍历双向链表
else
{
cur = cur->next;
}
}
//遍历完了找不到就返回一个空指针出去
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
//开辟新节点
ListNode* newnode = CreatNode(x);
//找到pos位置的前一个数据元素
ListNode* prev = pos->prev;
//从prev走到pos位置
prev->next = newnode;
newnode->next = pos;
//从pos位置走回prev
pos->prev = newnode;
newnode->prev = prev;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
//找到pos位置的前一个节点和后一个节点
ListNode* prev = pos->prev;
ListNode* next = pos->next;
//将pos位置的前一个节点和后一个节点建立联系
prev->next = next;
next->prev = prev;
//删除pos位置
free(pos);
//管理野指针
pos = NULL;
prev = NULL;
next = NULL;
}
以上就是整个线性表中比较重要的几个数据结构的介绍。
本文中所有的源代码均上传到gitee中了,这是我的gitee网址,需要自取。https://gitee.com/g_666/c-language-and-cplusplus-data-structures