在我们数据结构中,最简单的一种数据结构,便是线性表,其逻辑关系为线性关系,那么我们就一起来了解一下线性表的几种基本结构
线性表
线性表 ( linear list ) 是 n 个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 ...线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
这便是线性表的基础概念,线性表主要分为顺序表和链表这两种数据结构,下面我们来深入了解一下顺序表与链表的概念及各种操作
顺序表
顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。顺序表一般可以分为:1. 静态顺序表:使用定长数组存储。2. 动态顺序表:使用动态开辟的数组存储。
对于我们顺序表而言,本质上就是数组,且必须是从左到右(低地址到高地址)依次连续的,且其物理结构和逻辑结构是一致的,地址排列与逻辑排列都是依次连续的
静态顺序表中,因为数组的关系,在定义时必须就定义了大小,所以其长度定义完毕之后就不可以再改变了
动态顺序表则在我们定义时采用malloc函数动态开辟,当空间大小不足时在采用realloc函数自动的动态扩容,因为容量是可以改变的,所以被称为动态顺序表,我们在使用时一般使用动态顺序表,事实上,顺序表是有一定缺陷的。1,动态增容有性能消耗,且易造成空间的浪费。2,当在头部插入数据时,需要挪动整个数组,此时时间复杂度便会提升到O(n),效率较低
顺序表区别于数组的地方
顺序表:1.擦哈如数据的过程中可以动态增长,数组不行
2.要求存储的数据是连续的
下面我们来观察一下顺序表的结构体实现以及基本的接口实现
#pragma once
#include<stdio.h>
//#define N 100//静态顺序表(不推荐,无法自动扩容)
//struct SeqList
//{
// int a[N];
// int size;
//};
typedef int SeqDataType;//将数据类型用typedef修改为 SeqDataType,方便日后修改
typedef struct SeqList//修改结构体名称
{
SeqDataType* a;//实际数据类型为int型,在上方SeqList
int size;//有效数据的个数
int capacity;//容量
}SeqList,SEQ;//修改过后的结构体名称
void SeqListInit(SeqList* pq);//初始化
void SeqListDestory(SeqList* pq);//销毁
void SeqListPushBack(SeqList* pq, SeqDataType x);//尾插
void SeqListPushFront(SeqList* pq);//头插
void SeqListPopBack(SeqList* pq);//尾删
void SeqListPopFront(SeqList* pq);//头删
int SeqListFind(SeqList* pq, SeqDataType x)//查找
void SeqListInsert(SeqList* pq,int pos, SeqDataType x)//插入
void SeqListErase(SeqList* pq, int pos)//删除
void SeqListModify(SeqList* pq, int pos, SeqDataType x)//修改
注意上述接口的实现要进行传址传参下面我们来对这些接口一个一个实现
1.初始化SeqListInit()函数
void SeqListInit(SeqList* pq)
{
//assert(pq!=NULL);
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
注意:(1).只能在结构体定义的地方初始化
(2).SeqListInit函数就是将结构体中的值赋初始值,具体类型赋具体类型的初始值
(3).assert()函数的作用是断言,当我们给这个函数传入空指针时,自动终止程序,若没有这个函数就会引起程序的崩溃,这个函数的存在是为了方便我们查错
2.销毁SeqListDestory()函数
void SeqListDestory(SeqList* pq)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->capacity = pq->size = 0;
}
与SeqListInit()函数相对的就是SeqListDestory()函数,这个函数的作用就是在我们对顺序表完成了一系列操作之后对于顺序表进行销毁的函数,释放掉数组(因为数组可能是动态开辟的),并将其他值赋为初始值
3..尾插SeqListPushBack()函数
void SeqListPushBack(SeqList* pq, SeqDataType x)
{
assert(pq);
SeqCheckCapacity(pq);//检查容量是否够
pq->a[pq->size] = x;
pq->size++;
}
//SeqListInsert(SeqList* pq,pq->size, SeqDataType x)
尾插函数并不复杂,只需要将插入的值赋给最后一位pq->size,并将size+1就好了
4.检查容量SeqCheckCapacity()函数
void SeqCheckCapacity(SeqList* pq)
{
if (pq->size == pq->capacity)
{
int newcapacity = pq->capacity = 0 ? 4 : pq->capacity * 2;
SeqDataType*newA = realloc(pq->a, sizeof(SeqDataType)*newcapacity);//用newA来接收新扩的数组
if (newA = NULL)//若扩容失败
{
printf("realloc fail\n");
exit(-1);//退出
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
我们在对顺序表进行插入操作时,会碰到顺序表数组容量满的情况,在容量满时我们就需要对数组调用realloc函数进行扩容,此函数将扩容操作进行了封装,当容量满时,若容量为0,则将容量赋为4,若容量不为0,则将容量扩为原来的二倍,当然可能扩容失败,用newA来接收扩容的数组,如果失败则退出,成功则将扩容后的数据赋回原数组
这里我们对realloc函数进行一个补充说明:
事实上realloc函数并不一定是在原地扩容,当需要扩容的大小比较大时,一般在原空间后面就没有足够的可用空间了,就需要在其他地方进行扩容,去新的地方扩新的空间,将原来的数据拷贝过去,并且释放掉原来的空间
5.头插SeqListPushFront()函数
void SeqListPushFront(SeqList* pq, SeqDataType x)
{
assert(pq);
SeqCheckCapacity(pq);
int end = pq->size - 1;//找最后一位数据
while (end >= 0)//循环依次向后挪动一格
{
pq->a[end + 1] = pq->a[end];
end--;
}
pq->a[0] = x;//将首位置置为x
pq->size++;//长度加1
}
//SeqListInsert(SeqList* pq,0, SeqDataType x)
当我们在顺序表表头添加数据时,需要将所有的数据向后挪动一位,将第一个位置空出来赋入要插入的值,而后将有效数据长度+1即可
6. 尾删SeqListPopBack()函数
void SeqListPopBack(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
pq->size--;
}
//void SeqListErase(SeqList* pq, pq->size)
尾删事实上很简单,只需要将有效长度-1就可以了,就将最后一个数据覆盖掉了,注意size不能小于等于0,否则无法删除,需要加断言或者判定
7.头删SeqListPopFront()函数
void SeqListPopFront(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
int begin = 0;
while (begin < pq->size - 1)
{
pq->a[begin] = pq->a[begin + 1];
}
pq->size--;
}
//void SeqListErase(SeqList* pq, 0)
在我们的头删函数中,只需要将数组下标从1到pq->size-1的所有数据向前移动1,最后再将size--即可
7.查找SeqListFind()函数
int SeqListFind(SeqList* pq, SeqDataType x)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
if (pq->a[i] == x)
{
return i;
}
}
return -1;
}
查找函数就跟在数组中一样,遍历整个数组当要查找的之等于某一项时,返回下标,未找到,返回-1
8.插入 SeqListInsert()函数
void SeqListInsert(SeqList* pq,int pos, SeqDataType x)//pos:需要插入的位置
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
SeqCheckCapacity(pq);
int end = pq->size - 1;
while (end >= pos){
pq->a[end + 1] = pq->a[end];
--end;
}
pq->a[pos] = x;
pq->size++;
}
对于插入函数而言,我们需要将数据插入pos位置,而后需要将pos到size-1序号的数据全部向后移1位最后将size+1,注意不能忽略容量不够的条件,需要引入SeqCheckCapacity(pq)函数来进行判断。
其实我们的插入函数还可以应用于之前的头插尾插,在相应的函数后面会进行调用
9.删除SeqListErase()函数
void SeqListErase(SeqList* pq, int pos)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
int begin = pos;
while (begin <= pq->size - 1)
{
pq->a[begin] = pq->a[begin + 1];
++begin;
}
pq->size--;
}
对于我们的删除函数而言,只需要将删除的位置pos后面的向前移动1即可,就可以完成对pos位置的覆盖,前面需要注意pos要大于0,pos要小于size
同样的,我们学习了删除函数便可以将其复用到头删尾删函数中
10修改SeqListModify()函数
void SeqListModify(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}
这便是顺序表的基本内容了,那么我们学习了顺序表骂它存在哪些问题呢
1. 中间 / 头部的插入删除,时间复杂度为 O(N)2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。3. 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100 ,满了以后增容到 200 ,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
这都是顺序表不足的地方,接下来我们引入一种新的线性表,链表,让我们来看看链表能否解决顺序表的这些问题呢?
链表
对于我们的链表而言,顾名思义,就像链子一样将数据(通常是结构体)链起来,那么我们的链表事实上是使用结构体,结构体中分为两个域,一个是数据域(存储数据),一个是指针域(指向下一节点的地址),空间是按需所取的,其物理结构和逻辑结构是不一定连续的
这种储存方式使得链表有了一些很好的特性,不需要进行主动定量扩容,当我们需要给链表添加新的成员时,只需要将链表中最后一位的指针域指向需要加入的那个结构体,便可将新的数据链入链表,在头部插入数据时也不需要挪动整个链表,只需要将插入数据的指针域指向首元素的地址便可以实现,这样就避免了顺序表的一些缺陷
但是我们的链表也不是十全十美的,它也有自己的缺陷,比如不能完成随机访问,无法用下标去访问结点,只能通过从头遍历来依次找到需要操作的结点,所以链表和顺序表各有优劣,需要根据实际情况调用
这便是链表的逻辑结构与物理结构,逻辑结构上我们用箭头指向下一节点,而实际在物理存储上是一个节点存在两个域,一个数据与存放数据,一个指针域存放下一节点的地址,地址不一定连续,有时还会设立哨兵位的头节点,不存储数据只存储第一个节点的地址
实际上上面我们只是介绍了一种链表的结构,其实链表的结构有8种之多
1. 单向、双向2. 带头、不带头3. 循环、非循环
这6组情况分别组合,形成了我们不同的8种链表结构,在这里我们仅来实现,单向不带头不循环的链表,这是链表最简单的结构,我们到后面会再介绍双向带头循环链表
接下来我们对于链表的基本结构和几个常用的接口进行实现
#pragma once
#include<stdio.h>
typedef int SLTDataType;
typedef struct SListNode//链表的单个结点结构
{
SLTDataType data;//节点数据域
struct SListNode* next;//结点next指针域
}SLTNode;
//单项+不带头+不循环
void SListPrint(SLTNode* plist);//打印链表
void SListPushBack(SLTNode* plist, SLTDataType x);//尾插
1.打印SListPrint函数
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur != NULL)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
当我们打印链表中的数据时,需要将链表从头遍历到尾遍历链表,只需要定义一个cur指针指向头节点,然后挨个向后移动打印直到最后一个即可完成操作
2.创建新节点BuySLTNode函数
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//开辟一个节点大小的空间
node->data = x;//将数据值x赋给新节点
node->next = NULL;//将结点next域指针值为NULL
return node;//返回节点
}
这个函数的作用是当我们需要给链表插入一个新的节点时用于创建并赋值给新节点
3.尾插SListPushBack函数
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//创建新节点
if (*pplist == NULL)//当指针为空,链表是空的
{
*pplist = newnode;//直接将新节点赋给头节点
}
else
{
SLTNode* tail = *pplist;//将tail指针指向头结点
while (tail->next != NULL)//向后遍历到最后一位
{
tail = tail->next;
}
tail->next = newnode;//将数据赋给tail->next
}
}
注意,这里我们传的是二级指针,因为c语言的限制,我们在链表的尾插操作时,需要移动指针,要将tail指针向后移动一个节点,改变了plist指着的指向(存储地址),我们要想改变函数外面的链表,就需要将指针的指针传进来(传址传参)
4.头插SListPushFront函数
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);//创建节点
newnode->next = *pplist;//将新节点链到头节点前面
*pplist = newnode;//将新节点置为头节点
}
同样的,我们头插也需要改变plist指针的指向,所以这里我们传入的是指针的指针,头插逻辑并不复杂,就是将新节点的next域指向头节点,再将新节点置为新的头节点即可
5.尾删SListPopBack函数
void SListPopBack(SLTNode** pplist)
{
if (*pplist == NULL)//当为空表时
{
return;
}
else if ((*pplist)->next == NULL)//当仅有一个节点时
{
free(*pplist);//释放掉这个节点
*pplist = NULL;//将指针置空
}
else
{
SLTNode* prev = NULL;//设置前驱节点
SLTNode* tail = *pplist;//设置尾结点
while (tail->next != NULL)//循环尾结点到最后一位
{
prev = tail;//前驱节点跟在tail后面
tail = tail->next;//tail结点不断后移
}
free(tail);//释放掉已经移动到最后一位的tail指针
tail = NULL;//可略
prev->next = NULL;//将前驱节点的next域置空
}
}
当我们实现尾删函数时,需要做的就是将最后一个结点释放,并将倒数第二个节点的next域置为NULL即可,此时我们需要判断集中极端情况,当为空表时直接返回,当仅有一个结点就将这个节点删除,同样的,因为我们传入plist指针在头部为空的情况下会改变,所以我们传参时依旧传的是指针的指针,传址传参
那么我们对传二级指针与不传做一个小的总结,当我们传入的plist指针可能会发生改变的时候,我们便需要对其进行传二级指针,如果不需要改变就不需要传
6.头删SListPopFront函数
void SListPopFront(SLTNode** pplist)
{
if (*pplist == NULL)//当链表为空,则返回
{
return;
}
else
{
SLTNode* next = (*pplist)->next;//将plist的next指针存到next指针中
free(*pplist);//再释放掉plist
*pplist = next;//让next指针成为新的plist指针
}
}
在我们头删中,plist指针是一定会改变的,所以必须要传二级指针,而我们想要直接删除头部的结点,释放掉plist指针时,就会有找不到plist->next的位置,所以我们需要先将plist->next指针存入到next指针中再进行一系列的操作
7.查找SListFind函数
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;//初始化cur指针,使其指向plist
while (cur)//循环cur
{
if (cur->data == x)//找到x位置
{
return cur;//将结点指针返回
}
cur = cur->next;//后移
}
return NULL;//未找到返回NULL
}
在我们查找函数中,对于plist指针不需要做出改变,所以我们不需要进行传址传参,用一级指针便可实现,当然,根据习惯,也可以使用二级指针,查找函数通过一遍遍历便可实现,当找到目标,则返回节点,未找到则返回NULL,同时查找函数也兼具修改的功能,只需要在找到后将新节点的数据替换x即可
8.后插SlistInsertAfter函数
void SlistInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);//引入新结点
newnode->next = pos->next;//将新节点的next域指向pos的下一个结点
pos->next = newnode;//将pos的next域指向新节点完成后插
}
后插函数逻辑很简单,就是再pos位置之后插入一个新的节点,注意,在链链表时要先将newnode->next指针指向pos后一个结点,这样不会存在找不到pos下一个结点的情况,再将pos的next域指向newnode完成后插
9.前插SListBeforeInster函数
void SListBeforeInster(SLTNode** pplist,SLTNode* pos, SLTDataType x)
{
asser(pos);
SLTNode* newnode = BuySLTNode(x);//引入新结点
if (pos == *pplist)//头插
{
newnode->next = pos;
*pplist = newnode;
}
SLTNode* prev = NULL;//设置前驱结点
SLTNode* cur = *pplist;//将cur指针指向plist
while (cur != pos)//循环cur指针直到找到pos位置
{
prev = cur;//将prev跟在cur后面
cur = cur->next;//cur指针后移
}
prev->next = newnode;//将prev的next域指向新节点
newnode->next = pos;//新节点的next域指向pos完成插入
}
我们的前插函数较为复杂,因为单链表的特性,我们无法通过之后的节点去寻找前一个结点的地址,所以在前插函数中我们引入了一个prev指针跟在cur后面进行移动,此时当我们的cur指针找到位置时,便可通过prev指针将前面的节点与插入的新节点链接,又因为我们可能插入数据到第一个位置,此时会变为头插,将plist指针会有所改变,所以传入二级指针
10.后删SListEraseAfter函数
void SListEraseAfter(SLTNode*pos)
{
assert(pos);//屏蔽空结点情况
if (pos->next == NULL)//当仅有一个结点时,直接返回
{
return;
}
else
{
SLTNode* next = pos->next;//将pos->next结点存入next中
pos->next = next->next;//将pos结点指向下一个的下一个
free(next);//释放next
}
}
对于我们的后删函数而言,我们只需要绕过要删除的节点,将其前一个与后一个连接起来,最后再释放掉这个节点即可
11.删除
void SListErase(SLTNode** pplist, SLTNode*pos)
{
asser(pos);
if (pos == *pplist)//头删
{
SLTNode* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
else
{
SLTNode* prev = NULL;//初始化前驱节点
SLTNode* cur = *pplist;//初始化cur结点
while (cur != pos)//循环cur结点
{
prev = cur;//前驱节点跟在cur后面
cur = cur->next;//cur后移
}
prev->next = cur->next;//cur前驱节点指向cur下一个结点
free(cur);//释放cur结点
}
}
因为删除函数有可能会删除头节点变为头删,所以我们要对其传入二级指针,删除跟前面前插逻辑类似,引入一个前驱标记节点来进行操作即可
带头双向循环链表
因为我们在链表中会有8中结构,这里我们再来介绍另一种结构相对复杂的链表,带头双向循环链表
我们同线性表与单链表一样,我们也对双向链表的存储方式以及简单操作进行实现
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;//指向下一节点的指针
struct ListNode* prev;//指向上一节点的指针
LTDataType data;//数据域
}ListNode;
ListNode* BuyListNode(LTDataType x);//创建新的节点
void ListInit();//初始化结点
void ListPushBack(ListNode*phead, LTDataType x);//尾插
void ListPrint(ListNode* phead);//输出
void ListPushFront(ListNode* phead, LTDataType x);//头插
void ListPopBack(ListNode* phead);//尾删
void ListPopFront(ListNode* phead);//头删
void ListInert(ListNode*pos, LTDataType x)//插入
int ListEmpty(ListNode* phead)//判空
void ListErase(ListNode* pos)//删除
int ListSize(ListNode* phead)//计数
void ListDestory(ListNode* phead)//销毁
1.创建节点BuyListNode函数
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));//开辟一个节点大小的空间
node->next = NULL;//将next指针置为NULL
node->prev = NULL;//将prev指针置为NULL
node->data = x;//将数据域置为x
return node;//返回节点
}
2.初始化 ListInit函数
void ListInit()
{
ListNode*phead = BuyListNode(0);//创建一个新的节点将数据域置为0
phead->next = phead;//将phead->next连到phead上
phead->prev = phead;//将phead->prev连到phead上
return phead;
}
事实上当我们对一个双向循环链表进行初始化时,需要让它创建的的头节点的next依旧指向自己,prev也指向自己,最后将结点返回即可完成
3.尾插
void ListPushBack(ListNode*phead, LTDataType x)
{
assert(phead);//屏蔽空指针
ListNode*tail = phead->prev;//找到尾指针
ListNode* newnode = BuyListNode(x);//创建新节点
tail->next = newnode;//将位置真的next连到newnode上
newnode->prev = tail;//将尾指针的prev连到尾
newnode->next = phead;//新节点的next连到头节点
phead->prev = newnode;//头节点的prev连到新结点
}
我们可以看到,双向循环链表虽然在结构上有些复杂,但是在操作上比单链表简单很多,对于尾指针的查找也方便很多,我们只需要将加入新节点所断开的指针指向新节点即可
尾插代码对于仅有一个结点时的情况也适用
4.打印ListPrint函数
void ListPrint(ListNode* phead)
{
ListNode* cur = phead->next;//将cur置于phead->next
while (cur != phead)//循环链表直到cur指到头节点
{
printf("%d ", cur->data);//打印
cur = cur->next;//后移
}
printf("\n");
}
因为我们的链表是循环的,所以我们不存在尾结点最后只想NULL的情况,而是直接指向了头节点,重新循环回去,所以此时我们遍历就是从头结点的下一个节点循环到头结点结束
5.头插 ListPushFront函数
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);//屏蔽空指针
ListNode* first = phead->next;//将first指针指向phead->next,存储之前的phead->next
ListNode* newnode = BuyListNode(x);//开辟新结点
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
同尾插类似,我们可以提前将phead->next指针存储下来,将新节点连接到head结点后面,再将新节点的next连到我们存储的节点中即可,注意各自调整prev指针
6.尾删ListPopBack函数
void ListPopBack(ListNode* phead)
{
assert(phead);//屏蔽空指针
ListNode* tail = phead->prev;//储存要删的tail
ListNode* tailPrev = tail->prev;//储存tail前一结点
free(tail);//释放tail删除
tailPrev->next = phead;//将tail前一结点连到头节点
phead->prev = tailPrev;//调整prev
}
尾删需要做的就是找到要删除结点的前一结点,再将其与头节点相连,断开与尾的链接,释放先前的尾结点即可
7.头删ListPopFront函数
void ListPopFront(ListNode* phead)
{
assert(phead);//屏蔽空指针
assert(phead->next != phead);//屏蔽单一节点情况
ListNode* first = phead->next;//储存要删的节点
ListNode* second = first->next;//储存要删节点的后一结点
free(first);//释放目标节点
phead->next = second;//链接到后一节点
second->prev = phead;
}
头删也只需要找到头节点的下一节点与下下一结点即可,再将头节点连到下下结点,释放要删的结点即可
8.插入 ListInert函数
void ListInert(ListNode*pos, LTDataType x)//在pos位置上插入数据为x的一个节点
{
assert(pos);
ListNode*prev = pos->prev;
ListNode*newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
事实上,在我们完成插入操作时,pos位置实际是我们nownode节点的下一位置,我们只需将前一位置储存,再在他们两个中间插入newnode结点即可
9.删除ListErase函数
void ListErase(ListNode* pos)
{
assert(pos);//屏蔽空指针
ListNode*prev = pos->prev;//储存要删除的前一结点
ListNode*next = pos->next;//储存要删除的后一节点
prev->next = next;//将前一结点的next连到后一结点
next->prev = prev;//将后一结点的prev连到前一结点上
free(pos);//释放要删除的结点
}
在删除中我们需要做的同样也是先进行相关结点的存储,然后再进行操作
10.判空ListEmpty函数
int ListEmpty(ListNode* phead)
{
return phead->next == phead ? 1 : 0;
}
在这里当我们链表为空时返回1,不为空时返回0,判空的条件即为phead->next==phead这代表phead连接自己,没有其他的节点,即为空
11.计算结点个数ListSize函数
int ListSize(ListNode* phead)
{
assert(phead);
int size = 0;
ListNode*cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
个数计算也不复杂,将链表遍历一遍,设置个计数器即可
12.销毁ListDestory函数
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode*cur = phead->next;
while (cur != phead)
{
ListNode*next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;//注意写在外面
}
销毁函数也是在遍历过程中一个一个释放,要注意释放时存储下一节点的指针,不然找不到了,当我们遍历完一遍时还剩余了一个phead头节点,此时我们有两种方式可以销毁它。1.在函数中因为销毁phead要改变phead,所以我们可以通过传二级指针来进行传值传参销毁置空,2.或者我们可以在使用完链表之后在主函数种置空
以上便是我们双向循环链表的各种操作,接下来我们对顺序表以及链表的优缺点来个对比
基础的优缺点上图已经有所说明,总的来说就是顺序表可随机访问,空间不够需要扩容易空间浪费,挪动数据消耗较大。而链表则是内存成块申请不会浪费,挪动数据损耗小,不支持随机访问。我们对其CPU高速缓存命中率这一特点来进行展开描述,当我们对链表与顺序表进行遍历时,从效率上看来说是顺序表效率更高的,因为在遍历时寄存器会先对数据进行扫描,去自带的高速缓存L1 Cache中查看,若在缓存中,则称之为命中,不在,则未命中,而若不在缓存中时,系统便会再次加载若干字节给缓存,在就不需要预加载,而因为我们的顺序表地址都是连续的,所以我们缓存中一次加载若干字节便会包含我们的数据,预加载次数少,对于链表,因为地址不连续,所以在扫描过后命中率很低,基本每次都需要进行预加载,加载次数越多,消耗越大,所以我们的顺序表效率更高
以上便是我们的链表基本概念