文章目录
- 线性表---顺序表和链表
- 1.线性表
- 2.顺序表
- 2.1概念
- 2.2 静态顺序表与动态顺序表
- 2.3接口的实现
- 2.3.1顺表的初始化
- 2.3.2扩容
- 2.3.3顺序表尾插
- 2.3.4顺序表的尾删
- 2.3.5顺序表的头插
- 2.3.6顺序表的头删
- 2.3.7顺序表的查找
- 2.3.8顺序表的任意位置插入
- 2.3.9顺序表的任意位置删除
- 2.3.10顺序表的打印
- 2.3.11顺序表的销毁
- 2.4顺序表的总结
- 3.顺序表的问题与思考
- 4.链表
- 4.1链表的概念
- 4.2链表的结构
- 4.3链表的分类
- 4.3.1单向和双向
- 4.3.2带头不带头
- 4.3.3循环和非循环
- 5. 单项不带头不循环的链表实现
- 5.1接口
- 5.2接口的实现
- 5.2.1 购买节点
- 5.2.2单链表的尾插
- 5.2.3 单链表的尾删
- 5.2.4单链表的头插
- 5.2.5单链表的头删
- 5.2.6单链表的查找
- 5.2.7 单链表的任意位置插入
- 5.2.8单链表任意位置的删除
- 5.2.9单链表的打印
- 5.2.10单链表的销毁
- 5.3单链表的总结
- 6双向带头循环链表的实现
- 6.1接口
- 6.2 接口的实现
- 6.2.1购买节点
- 6.2.2双向链表的初始化
- 6.2.3双向链表的尾插
- 6.2.4双向链表的尾删
- 6.2.5双向链表的头插
- 6.2.6双向链表的头删
- 6.2.7双向链表的查找
- 6.2.8双向链表的任意位置插入
- 6.2.9双向链表的任意位置删除
- 6.2.10双向链表的打印
- 6.2.11双向链表的销毁
- 6.3双向带头循环链表总结
- 7.链表与顺序表的区别
线性表—顺序表和链表
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储
2.顺序表
2.1概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
2.2 静态顺序表与动态顺序表
静态顺序表
这样定义的顺序表是静态的,也就是在程序运行时他所开辟的空间就是固定的,不可以在改变,只有改变代码中的MAX才可以改变数组大小。
这种方法:直接将空间写“死”,并不是一个好的方法。
好方法在下面
动态顺序表
这便是动态顺序表,当表满了之后会自动扩容(扩容函数实现)
相对于静态顺序表,它的空间在程序运行期间,并不是固定的,而是随着数据的增多而扩大表的容量。
2.3接口的实现
这里是对动态顺序表的实现。
typedef int SLTDataType;
typedef struct SeqList
{
SLTDataType *arr;
int sz; //当前表中数据个数
int capacity; //当前表的最大容量
}SeqList;
//顺序表的初始化
void SeqListInit(SeqList* sl);
//扩容函数
void Capacity(SeqList* sl);
//顺序表的尾插
void SeqListPushBack(SeqList* sl, SLTDataType x);
//顺序表的尾删
void SeqListPopBack(SeqList* sl);
//顺序表的头插
void SeqListPushFront(SeqList* sl, SLTDataType x);
//顺序表的头删
void SeqListPopFront(SeqList* sl);
//顺序表的查找
int SeqListFind(SeqList* sl, SLTDataType x);
//顺序表的任意位置插入
void SeqListInsert(SeqList* sl,int pos, SLTDataType x);
//顺序表的任意位置删除
void SeqListErase(SeqList* sl, int pos);
//顺序表的打印
void SeqListPrint(SeqList* sl);
//顺序表的销毁
void SeqListDestroy(SeqList* sl);
这些就是顺序表的基本功能
2.3.1顺表的初始化
//顺序表的初始化
void SeqListInit(SeqList* sl)
{
assert(sl); //判断sl是否为NULL
sl->arr = NULL; // 初始化可以开辟空间也可以不开辟,这个是不开辟的
//sl->arr = (SLTDataType*)malloc(sizeof(SLTDataType)*4); 这个是开辟空间的
sl->sz = 0;
sl->capacity = 0;
//sl->capacity = 4 开辟空间了,容量就要初始化
}
2.3.2扩容
//扩容函数
void Capacity(SeqList* sl)
{
assert(sl);
//判断容量是否为0
//如果是0的话使容量变为4
//如果不为0的话使容量扩大2倍
sl->capacity = (sl->capacity == 0 ? 4 : sl->capacity * 2);
//通过realloc函数进行扩容
SLTDataType* tmp = (SLTDataType*)realloc(sl->arr,sl->capacity * sizeof(SLTDataType));
if (tmp == NULL)//判断是否开辟成功
{
//开辟不成功
perror("Capacity::realloc:");
exit(-1);
}
//开辟成功,将地址赋给sl->arr
sl->arr = tmp;
}
2.3.3顺序表尾插
//顺序表的尾插
void SeqListPushBack(SeqList* sl, SLTDataType x)
{
assert(sl);
//判断顺序表容量是否够
//如果空间不够就扩容
if (sl->capacity == sl->sz)
{
Capacity(sl);
}
//空间够了就开始写入数据
sl->arr[sl->sz] = x;
//写入后使sz++
sl->sz++;
}
2.3.4顺序表的尾删
//顺序表的尾删
void SeqListPopBack(SeqList* sl)
{
assert(sl);
//如果顺序表中没有数据就不能再删除
assert(sl->sz);
//如果有数据开始删除
sl->sz--;
}
有人肯定觉得删除数据后要将顺序表删除位置的数据置为0
sl->arr[sz-1] = 0
sl->sz--
其实写这一步的意义不打,你下次再次写的时候你直接就将原来数据覆盖了,再者说,如果你删除的数据要是0的话,你将它置为0不就多次一举
置与不置都可以,不会产生任何影响,我觉得没有必要取多写那一步。如果你有强迫症的话,你可以写那一步,反正都不影响结果,代码自己看着舒服就好😄
2.3.5顺序表的头插
//顺序表的头插
void SeqListPushFront(SeqList* sl, SLTDataType x)
{
assert(sl);
//先检查容量
if (sl->capacity == sl->sz)
{
Capacity(sl);
}
//头插
//需要将数据依次向后移,把第一个位置空下来取放入数据
int i = sl->sz;
while (i)
{
sl->arr[i] = sl->arr[i - 1];
i--;
}
sl->arr[0] = x;
//同时由于插入了数据,sz++
sl->sz++;
}
2.3.6顺序表的头删
//顺序表的头删
void SeqListPopFront(SeqList* sl)
{
assert(sl);
//判断顺序表中是否有数据
assert(sl->sz);
//头删就是将后面的数据依次向前移,将第一个元素给覆盖了
int i = 0;
while (i < sl->sz - 1)
{
sl->arr[i] = sl->arr[i + 1];
i++;
}
//最后将sz--
sl->sz--;
}
2.3.7顺序表的查找
遍历元素,依次与要查找的数据进行对比
//顺序表的查找
int SeqListFind(SeqList* sl, SLTDataType x)
{
assert(sl);
//这里我写的是空表的话就不查找,直接报错,错误使用
assert(sl->sz);
//通过循环将顺序表中的元素依次对比。
int i = 0;
while (i < sl->sz)
{
if (sl->arr[i] == x)
return i;
i++;
}
//如果没有找到数据,就返回-1
//为什么是-1呢?
//其实只要是个负数都可以,一般用-1
//0和正数是不行的,因为可能是数组的下标。
return -1;
}
2.3.8顺序表的任意位置插入
任意位置插入和头插基本思路一样,依次向后移动,只不过不是插在第一个,而是任意的位置,但是这个位置要合理
//顺序表的任意位置插入
void SeqListInsert(SeqList* sl, int pos, SLTDataType x)
{
assert(sl);
//检查插入位置是否合理
assert(pos>=0 && pos<=sl->sz);
//先检查容量
if (sl->capacity == sl->sz)
{
Capacity(sl);
}
//将pos位置(含pos位置)依次向后移
//把pos位置空出来,将数据插入
//这里的pos对应这数组下标
int i = sl->sz;
while (i > pos)
{
sl->arr[i] = sl->arr[i - 1];
i--;
}
//在pos位置插入数据
sl->arr[pos] = x;
//最后将sz++
sl->sz++;
}
2.3.9顺序表的任意位置删除
任意位置删除,和头删的方法一样,将要删除的数据后面的数据依次向前移动,将其覆盖,只不过是将pos位置覆盖,而不是第一个数据,思路是相同的。
//顺序表的任意位置删除
void SeqListErase(SeqList* sl, int pos)
{
assert(sl);
//检查顺序表是否为空
assert(sl->sz);
//检查插入位置是否合理
assert(pos>=0 && pos<=sl->sz);
//将pos后面的数据(不含pos)依次向前移
int i = pos;
while (i < sl->sz - 1)
{
sl->arr[i] = sl->arr[i + 1];
i++;
}
//最后将sz--
sl->sz--;
}
2.3.10顺序表的打印
//顺序表的打印
void SeqListPrint(SeqList* sl)
{
assert(sl);
for (int i = 0; i < sl->sz; i++)
{
printf("%d ", sl->arr[i]);
}
printf("\n");
}
2.3.11顺序表的销毁
//顺序表的销毁
void SeqListDestroy(SeqList* sl)
{
assert(sl);
//将之前动态开辟的空间是否,再将其置为NULL,避免野指针
//sz与capacity置为0
free(sl->arr);
sl->arr = NULL;
sl->capacity = 0;
sl->sz = 0;
}
2.4顺序表的总结
看完这些,想必你们也大概知道顺序表了
我来总结一下吧:
只要是写入数据,就要判断容量是否够用,并且sz++
只要删除数据,就要判断表中是否有数据,并且sz–
尾插、尾删时间复杂度O(1);
头插头删时间复杂度为O(N);
随机插入与删除时间复杂度为O(1);
3.顺序表的问题与思考
问题:
1.顺序表的中间和头部的插入删除,时间复杂度为O(N)
2.异地扩容(不懂可以去了解一下realloc函数)要进行数据拷贝,释放旧空间。会有不小的消耗
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间 。
思考:
有没有什么可以解决这些问题呢?
有的,那就是我们下面需要讲的链表,他完美的解决了上面的这些问题。
疑问:
既然链表这么好,直接学链表不就好了,学顺序表有什么意义?
首先我要否定这种观点,链表有优点同时也有它的缺点,而这些缺点,顺序表就可以完美的解决,他们各有各的用处,学到后面就知道了。
4.链表
4.1链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
4.2链表的结构
逻辑上的结构
链表的逻辑图就是这样,一个结构连着一个结构,感觉他们是一个接着一个。其实并不是这样的
物理结构
着就是它的物理结构,实际在计算机中,这些数据并不一定是连着存储的,他们分散在堆区,靠next指针可以将他们连起来。
4.3链表的分类
4.3.1单向和双向
单向:
双向:
4.3.2带头不带头
带头:
不带头:
4.3.3循环和非循环
上面说了这么多中链表结构,他们互相组合共有八种:
- 单向不带头不循环
- 单项不带头循环
- 单向带头不循环
- 单向带头循环
- 双向不带头不循环
- 双向不带头循环
- 双向带头不循环
- 双向带头循环
说了这么多,大家肯定觉得害怕了吧。不用怕,其实学会一种其他都差不多。
我给大家讲两种:
1.单项不带头不循环
2.双向带头循环
有人就会说:这两种肯定差距很大吧,一看就是第一种特别简单,第二种特别难。
这么说也对。
第一种是结构最简单,但是实现是最复杂的
第二种是结构最难,实现最简单的
只要好好把这两种结构学好,其他结构就是手到擒来了。话不多说了,开始吧。
5. 单项不带头不循环的链表实现
5.1接口
// 1、无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x);、
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表的任意位置插入
// 在pos位置之前插入
void SListInsert(SListNode** pplist,SListNode* pos, SLTDateType x);
// 单链表的任意位置删除
void SListEarse(SListNode** pplist, SListNode* pos);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表的销毁
void SListDestroy(SListNode** pplist);
5.2接口的实现
5.2.1 购买节点
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
//判断是都从堆区申请到节点
if (newnode == NULL)
{
perror("BuySListNode::malloc:");
exit(-1);
}
//申请到的话
newnode->next = NULL;
newnode->data = x;
return newnode;
}
5.2.2单链表的尾插
先画图理解理解
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(pplist);
//获得新的节点
SListNode* newnode = BuySListNode(x);
//插入到现在的链表中
//先判断链表中是否有数据
//如果没有数据,不光要插入,还要将头指针*plist的值给改了
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
//先通过遍历找到尾,在将新节点给连上
SListNode* tail = NULL;
SListNode* cur = *pplist;
//找尾
while (cur)
{
tail = cur;
cur = cur->next;
}
//链接
tail->next = newnode;
}
}
肯定有人疑问,为什么是二级指针呢?
因为,在main函数中,定义的是一个指针变量,要改变一级指针的内容,变要用二级指针了。
肯定有人问,为啥要定义一个指针变量,定义一个普通的结构体变量不行吗?
在某些情况下是不行的
如果是结构体指针变量,他会将所有节点都存放到堆区,不会在栈区存放链表的节点,所以你返回后链表是可以正常使用的。
1、为什么,顺序表可以是普通变量呢,而不是指针?
顺序表它通过普通变量就可以将所有数据都存放的堆区,用指针变量会变得复杂。
2、链表就不能不用二级指针吗?好烦啊,不喜欢。
当然是可以的,只要你有办法将所有节点都存放到堆区就可以了。
通过带头的单链表,就可以。
3、既然可以为啥不说那个呢?说这个麻烦的,肯定没有那个好用
并不是这样的。在后面有些数据结构中,我们是要通过不带头的单链表完成的,所以还是要好好学不带头的单链表的,这个学会了,你的链表基本问题就不大了,加油😄。
5.2.3 单链表的尾删
画图
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
//要删除数据,得要现有数据吧,我们在这里断言一下
assert(*pplist);
//有数据
//找到尾将他删除,并且将他的前一个的next置空
//但是如果只有一个数据呢,又要改变头指针,将他置为NULL
//分情况讨论
//只有一个数据
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
//定义两个指针,一个是cur去变量找到尾,另一个是prev表示尾的前一个
SListNode* cur = *pplist;
SListNode* prev = NULL;
while (cur->next)
{
prev = cur;
cur = cur->next;
}
//将倒数第二个也就是prev置为NULL
prev->next = NULL;
//释放掉最后一个
free(cur);
}
}
5.2.4单链表的头插
画图:
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
//购买一个节点
SListNode* newnode = BuySListNode(x);
//如果没有节点呢,如果只有一个节点呢,如果有多个节点呢?
//写之前先去画图想想
//这里都不影响
//记录第一个节点
//尽管没有数据也不影响,画图看看吧
SListNode* front = *pplist;
//将新节点插入,成为第一个节点
*pplist = newnode;
//将原来的第一个节点链接在新的节点后面
newnode->next = front;
}
5.2.5单链表的头删
这里我就不画图,和上面的差不多,你们可以去画画看
// 单链表头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
//先要判断链表是否为空
assert(*pplist);
//不为NULL
//同样要想想只有一个数据的情况是不是需要特殊处理
//这里是不需要的
//记录第一个节点
SListNode* front = *pplist;
//记录第二个节点
SListNode* second = front->next;
//删除掉第一个节点,并且将第二个节点变为第一个
*pplist = second;
free(front);
}
1、为什么要定义这么多指针呢?
为了方便写程序,如果不定义多个指针,在实现有些功能上,你就需要考虑程序执行的顺序了,而定义多个指针就需要去管顺序问题。
例如:如果你不定义second这个指针,你就需要先将头指向第二个节点,再释放第一个节点
5.2.6单链表的查找
从前到后一个一个与要查找的数据进行比较,如果相同就返回这个数据的节点,如果找完都没有找到就返回NULL
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
//如果没有数据,那么我们就不查找了
assert(plist);
//循环查找
SListNode* cur = plist;
//这里的循环结束条件是cur,而不是cur->next,可以画图理解一下
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
//找完都没有找到,就但会NULL
return NULL;
}
1、为什么这里传的是一级指针呢?
首先你需要理解什么时候需要传二级指针,只有当我们需要去更改首节点的地址时才需要去传入二级指针,
只要不更改首节点地址,就可以不需要二级指针,而查找函数,它并不会去改,所以一级就够用了。
5.2.7 单链表的任意位置插入
如果只有一个数据的话就相当于头插,需要更改头指针,单独处理
// 单链表的任意位置插入
//在pos位置之前插入
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
//只有一个数据
if (*pplist == pos)
{
*pplist = newnode;
newnode->next = pos;
}
else
{
//找到pos位置之前的节点
SListNode* cur = *pplist;
SListNode* prev = NULL;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
//此时的cur指向的时pos前面的节点
prev->next = newnode;
newnode->next = pos;
}
}
1、为什么你这里的插入时的pos是一个节点呢,而不是一个数字代表第几个。你这个节点我怎么传啊。
首先说一下,你要传数字,也是可以的,自己改改就可以了(要判断出入的数字是否合法)
这里的节点是通过SListFind查找后得到的节点,在这个节点之前插入,所以是先查找再插入。
5.2.8单链表任意位置的删除
找到pos前一个位置,pos后一个位置,删除pos位置
想的不是很明白的话,就自己画图看看,画图真的很有用。
如果只有一个数据要单独处理
// 单链表的任意位置删除
void SListEarse(SListNode** pplist, SListNode* pos)
{
assert(pplist);
//先判断是否数据
assert(*pplist);
//有数据
//记录pos前面的位置,再记录pos位置后面的位置
//删除pos位置
//如果只有一个数据
if (*pplist == pos)
{
*pplist = NULL;
free(pos);
}
else
{
//pos之前的位置
SListNode* cur = *pplist;
SListNode* prev = NULL;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
//pos之后的数据
SListNode* next = pos->next;
//将pos之前与pos之后链接
prev->next = next;
//删除pos位置
free(pos);
}
}
5.2.9单链表的打印
// 单链表打印
void SListPrint(SListNode* plist)
{
//循环打印
SListNode* cur = plist;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL");
}
5.2.10单链表的销毁
// 单链表的销毁
void SListDestroy(SListNode** pplist)
{
assert(pplist);
//从头一个一个节点的释放
SListNode* cur = *pplist;
while (cur)
{
SListNode* prev = cur;
cur = cur->next;
free(prev);
}
//将头结点置为NULL
*pplist = NULL;
}
5.3单链表的总结
- 如果要插入数据,要传入二级指针
- 如果要删除数据,先要判断链表中是否有数据,同样也要传入二级指针
- 如果要改变头指针则要使用二级指针,如果不用修改头指针便不需要传入二级指针
- 它的头插、头删时间复杂度位O(1);
- 它的尾插、尾删、任意位置删除、任意位置插入时间复杂度为O(N);
6双向带头循环链表的实现
6.1接口
// 2、带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
//指向后面的指针
struct ListNode* next;
//指向前面的指针
struct ListNode* prev;
}ListNode;
//购买节点
ListNode* BuyListNode(LTDataType x);
//初始化双向链表
void ListInit(ListNode** pplist);
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的结点
void ListErase(ListNode* pos);
6.2 接口的实现
6.2.1购买节点
//购买节点
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("BuyListNode::malloc:");
exit(-1);
}
//创建成功
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
这里和单链表一样,只是多了一个指向前面的指针
6.2.2双向链表的初始化
去创建一个哨兵位的头,这个头没有实际一样,不去存储数据,只是为了当头
这里要用二级指针,和单链表一样,要改变头指针。
//初始化双向链表
void ListInit(ListNode** pplist)
{
assert(pplist);
//初始化双向链表就是创建一个头,这个头不关注它存储的数据,只是让他去做队列的头,会使程序简单好多
//所以这里传入-1,你也可以随意写入
ListNode* newnode = BuyListNode(-1);
//这里让他指向自己,相当于构成了循环,也是为了方便后面
newnode->next = newnode;
newnode->prev = newnode;
*pplist = newnode;
}
6.2.3双向链表的尾插
空表:
多个数据:
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode* newnode = BuyListNode(x);
//找到尾
ListNode* tail = plist->prev;
//将新的节点链接到尾后
//新的节点的prev指针指向tail
tail->next = newnode;
newnode->prev = tail;
//将头指针的prev 指向 新的尾
//新的尾的next指向头
plist->prev = newnode;
newnode->next = plist;
}
6.2.4双向链表的尾删
// 双向链表尾删
void ListPopBack(ListNode* plist)
{
assert(plist);
//判断链表中是否没有数据,如果没有数据就不能删除
assert(plist->next != plist);
//有数据
//找到倒数第一个节点与倒数第二节点
ListNode* tail = plist->prev;
ListNode* tailprev = tail->prev;
//删除最后一个节点
//头指向倒数第二个
//倒数第二个指向头
plist->prev = tailprev;
tailprev->next = plist;
free(tail);
}
6.2.5双向链表的头插
没有数据:
在这里插入图片描述
有数据:
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x)
{
assert(plist);
ListNode* newnode = BuyListNode(x);
//记录第一个节点
ListNode* front = plist->next;
//将他们链接
plist->next = newnode;
newnode->prev = plist;
newnode->next = front;
front->prev = newnode;
}
6.2.6双向链表的头删
拥有多个节点方法也是一样的,可以画图理解理解
// 双向链表头删
void ListPopFront(ListNode* plist)
{
assert(plist);
//判断链表中是否有数据
assert(plist->next != plist);
//有数据
//记录第一个节点,与第二个节点
ListNode* front = plist->next;
ListNode* second = front->next;
//将头结点与第二个链接,并且删除第一个节点
plist->next = second;
free(front);
}
6.2.7双向链表的查找
从头节点后的第一个节点开始遍历查找,知道再次到达头结点,查找完毕,没有找到就返回NULl
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x)
{
ListNode* cur = plist->next;
while (cur != plist)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
6.2.8双向链表的任意位置插入
更据pos位置的这个节点,可以找到它前面的节点,通过prev指针,而不用重新遍历查找
将新的节点插入。
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(plist);
ListNode* newnode = BuyListNode(x);
//找到pos之前的节点
ListNode* prev = pos->prev;
//链接
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
6.2.9双向链表的任意位置删除
找到pos前面的节点通过prev指针,找到pos之后的节点,通过next指针。
将pos节点释放,将pos之前的节点与pos之后的节点链接
// 双向链表删除pos位置的结点
void ListErase(ListNode* pos)
{
assert(plist);
//找到pos之前的节点,找到pos之后的节点
ListNode* prev = pos->prev;
ListNode* next = pos->next;
//删除pos,并且链接pos之前的节点与pos之后的节点
prev->next = next;
next->prev = prev;
free(pos);
}
6.2.10双向链表的打印
// 双向链表打印
void ListPrint(ListNode* plist)
{
assert(plist);
ListNode* cur = plist->next;
while (cur != plist)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
6.2.11双向链表的销毁
从头节点之后的第一个节点开始逐个释放,当再次回到头结点,就代表除头结点之外的节点已经释放完毕,最后将头结点释放,再将他置为NULL。
这里要传二级指针,因为要改变头指针。
// 双向链表销毁
void ListDestory(ListNode** pplist)
{
assert(pplist);
ListNode* cur = (*pplist)->next;
while (cur != *pplist)
{
ListNode* prev = cur;
cur = cur->next;
free(prev);
}
free(*pplist);
*pplist = NULL;
}
6.3双向带头循环链表总结
双向链表虽然结构上比单项链表复杂,但是它的操作简单,不要去考虑是否要更改头指针的情况,所有情况同意i处理,所以写代码会比单链表简单太多,只要你可以理解双向链表的结构,写起来就是轻轻松松。
初始化,和销毁要传入二级指针,因为要改变头指针的内容
删除时要判读是否有数据。
它的头删、尾删、头插、尾插、任意位置的删除、任意位置的插入,时间复杂度都为O(1)。
7.链表与顺序表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上连续存储 | 逻辑上连续,物理上不一定连续 |
随机访问 | 时间复杂度O(1) | 时间复杂度O(N) |
任意元素的插入与删除 | 要搬运数据,效率低,时间复杂度O(N) | 只需要修改指针的指向 |
插入 | 动态数组,空间不够要扩容 | 没有容量概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入与删除频繁 |
缓存利用率 | 高 | 低 |