顺序表和链表
物理结构:数据在内存存储上的结构
逻辑结构:人为想象出来的结构
线性表:逻辑结构一定是线性的,物理结构不一定是线性的。
一、顺序表
顺序表是线性表的一种,其逻辑结构一定是线性的,但是它的物理结构取决于它的底层结构,顺序表的底层结构是数组,所以顺序表的物理结构也是线性的。
1.初始化顺序表
用户在调用顺序表相关操作的函数时,传址调用可能会传来
NULL
,因此需要对指针ps
断言:
//SeqList.h
#include <assert.h>
//SeqList.c
//……
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);//等价于assert(ps != NULL);
//……
}
//……
插入数据和删除数据的区分:凡是插入数据都要判断空间是否为满,凡是删除数据都要判断顺序表是否为空。
2.插入数据
凡是插入数据都要判断空间是否为满。
(1)尾插
因为size是顺序表中有效数据的个数,指向顺序表中最后一个有效数据的下一位,所以在空间足够的时候,直接把要插入的数据赋值给arr[size]即可,插入数据后,有效数据增加一位,即size++。但是会遇到空间不够的情况,那么这时就要扩容。
注意是插入数据的过程中,当size == capacity时,要扩容,一般是扩大到原来空间的2倍。为什么是插入数据的过程中呢?比如我们在上面的初始化顺序表时,将size和capacity都赋值为0,这时也满足size == capacity
,但是这时将空间扩大到原来的2倍的话,结果就还是0,解决方法如下:写一个三目操作符表达式。
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//SeqList.h
void SLPushBack(SL* ps, SLDataType x);
//SeqList.c
void SLPushBack(SL* ps, SLDataType x)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(2);//非零退出码
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
ps->arr[ps->size++] = x;
}
为什么不直接用SLDataType* arr接收呢?realloc()函数返回值可能是NULL,会导致之前已有的数组空间丢失:
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
我们可以借助调试来检查自己写的代码:
也可以看到扩容的变化:
下面的插入数据的实现方法都要判断空间是否为满,那么可以把检查空间是否为满单独封装到一个函数中,需要判满时直接调用该函数即可:
//SeqList.h
void SLCheckCapacity(SL* ps);
//SeqList.c
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(3);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
尾插方法代码的改进:
//SeqList.c
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);//等价于assert(ps != NULL);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
时间复杂度:O(1)
(2)头插
实现步骤:
- 断言指针
- 判断空间是否为满
- 从后往前,依次将数据赋值给后一位
- 最后i落在下标0处,赋值x
- size++
//SeqList.c
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
时间复杂度:O(n)
还是可以借助调试检查代码写得是否正确以及头插数据时的变化:
(3)在指定位置之前插入数据
- 断言指针是否为空。
- 断言
pos
的范围:assert(pos >= 0 && pos <= ps->size);
,pos == 0
时就是头插,pos == ps->size
时就是尾插。- 检查空间是否为满。
- 在
pos
指定的数据之前插入,即在pos
的位置插入,也就是说pos
及之后的数据都向后挪动一位。- 在
pos
的位置插入指定数据。++size
完整代码如下:
//SeqList.h
void SLInsert(SL* ps, int pos, SLDataType x);
//SeqList.c
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
++ps->size;
}
3.删除数据
凡是删除数据都要判断顺序表的有效数据个数是否为空:
assert(ps->size);
(1)尾删
因为size
指向最后一个有效数据的下一位,所以直接--size
有效数据就会少一位,也就是最后一个数据被删掉了。
//SeqList.h
void SLPopBack(SL* ps);
//SeqList.c
void SLPopBack(SL* ps)
{
assert(ps && ps->size);
--ps->size;
}
不过直接--size
后会影响头插、尾插的操作吗?可以借助调试看看:
尾删执行后,size
变为2
:
尾插执行后,进来的数据4
存储在原先3
的位置,size
变为3
:
(2)头删
- 断言指针和顺序表中的有效数据个数不为空
- 从前往后,数据依次向前覆盖
--size
//SeqList.h
void SLPopFront(SL* ps);
//SeqList.c
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
(3)在指定位置删除数据
- 断言指针不为空以及顺序表有效数据个数不为零。
- 断言
pos
的范围:assert(pos >= 0 && pos < ps->size);
pos
之后的数据往前挪动一位--size
代码如下:
//SeqList.h
void SLErase(SL* ps, int pos);
//SeqList.c
void SLErase(SL* ps, int pos)
{
assert(ps && ps->size);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
4.查找数据
找到了,返回数据的下标;没找到,返回-1。
代码如下:
//SeqList.h
int SLFind(SL* ps, SLDataType x);
//SeqList.c
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
5.销毁
代码如下:
//SeqList.h
void SLDesTroy(SL* ps);
//SeqList.c
void SLDesTroy(SL* ps)
{
if (ps->arr)
free(ps->arr);
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
二、单链表
函数形参有二级指针pphead的原因:
单链表初始化plist指向空,传址调用,形参改变实参,下面实现方法可能会改变头结点。
可以把顺序表头插、头删的时间复杂度降为O(1)
在堆上malloc建立一个个新的结点,各个结点的地址不是连续的,因此链表的物理结构不是连续(线性)的,链表是线性表的一种,所以它的逻辑结构一定是线性的。
链表是由结点构成的,结点数目为0,则该链表为空链表;结点数目为大于等于1,则该链表为非空链表。结点是由两部分组成:存储数据 + 指向下一个结点的地址。即
定义链表的结构 == 定义结点的结构
//SList.h (Single List——单链表)
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;//指向下一个结点的指针
}SLTNode;
//typedef struct SListNode SLTNode;
手动构造一个链表并打印出来:
//SList.h
//打印单链表
void SLTPrint(SLTNode* phead);//phead:头结点
//SList.c
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;//cur——current当前的
while (pcur != NULL)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
pcur/尾插的ptail 指针的作用:避免后续访问第一个结点访问不到,不影响从头遍历链表,phead始终是头结点。若用phead遍历链表,会出现与之相反的情况。
//test.c
void test01()
{
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
node2->data = 2;
node3->data = 3;
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//打印单链表
SLTNode* plist = node1;//知道首结点的地址就可以知道后面结点的地址
SLTPrint(plist);
}
int main()
{
test01();
return 0;
}
图解pcur = pcur->next;
:
1.尾插
单链表为空:链表里一个结点都没有,即NULL。
初始化单链表为空,指针plist = NULL,后续尾插结点,指针plist由空指向新结点,这个新结点作为第一个结点,头结点因此被改变,所以传指针地址,用二级指针接收。
存储进链表的数据用结点存储,插入到链表里的是结点,而不是数据,所以单独创建一个函数实现申请一个新结点:
//SList.c
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
画图分析尾插法过程:
//SList.h
void SLTPushBack(SLTNode** pphead, SLTDataType x);//传递的是头结点的地址,用二级指针pphead接收
//SList.c
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//等价于assert(pphead != NULL);*pphead表示头结点,可以为空,表示空链表
SLTNode* newnode = SLTBuyNode(x);
//链表为空,phead直接指向newnode结点
if (*pphead == NULL)
{
*pphead = newnode;
}
//链表不为空,找到尾结点,将尾结点和新结点连接起来
else
{
SLTNode* ptail = *pphead;
while (ptail->next)//等价于while (ptail->next != NULL)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
借助调试检查:
2.头插
//SList.h
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//SList.c
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
顺序表的头插的时间复杂度:O(n)
单链表的头插的时间复杂度:O(1),可见运用单链表的头插的时间效率更高
3.尾删
当单链表中只有一个结点时,用下列代码实现会出现对空指针的解引用操作:
//SList.h
//尾删
void SLTPopBack(SLTNode** pphead);
//SList.c
//尾删
void SLTPopBack(SLTNode** pphead)//用二级指针:可能把头结点尾删了,头结点会改变
{
assert(pphead && *pphead);//确保链表也不为空
//当单链表中只有一个结点
if ((*pphead)->next == NULL)//“箭头”的优先级比*高
{
free(*pphead);
*pphead = NULL;
}
//当单链表中结点数目大于1
else
{
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while ((*pphead)->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
对于
prev->next = NULL; free(ptail); ptail = NULL;
,若是先释放ptail
,则prev->next
会成为野指针。
4.头删
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
5.查找
//SList.h
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//SList.c
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
6.在指定位置之前插入数据
链表不为空
- 若
pos
指定位置是头结点,newnode
结点直接头插。- 若
pos
指定位置是除头结点以外的结点,那么要找到pos
的前一个结点prev
,然后再把newnode
结点插入到prev
和pos
之间,即涉及到的指针有3个。
//SList.h
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//SList.c
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
//pos指向头结点
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
//pos指向除头结点以外的结点
else
{
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev newnode pos
newnode->next = pos;
prev->next = newnode;
/*
上面两行代码调换顺序也行
prev->next = newnode;
newnode->next = pos;
*/
}
}
7.在指定位置之后插入数据
涉及到的结点有3个:
pos
、pos的下一个结点pos->next
、newnode
,pos->next由pos结点得出,所以不需要头结点来遍历,即便pos是头结点,头结点不会因此而改变,因为找的是头结点的下一个结点。综上,也不需要二级指针来存储头结点的地址。
//SList.h
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//SList.c
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
pos->next = newnode;
/*
上面两行代码直接调换顺序不可取,改进如下
next = pos->next;
pos->next = newnode;
newnode->next = next;
*/
}
将上述的两行代码交换位置pos->next = newnode;newnode->next = pos->next;
,不可行的原因及改进:
- 原因:
- 改进:用指针
next
保存pos->next
考虑所有可能出现的情况,不可能出现头插,可以出现尾插:
8.删除pos结点
- 涉及的指针有3个:
pos
、pos所指结点的前一个结点prev
(prev需要借助头结点遍历单链表)、pos所指结点的下一个结点pos->next
。- 先销毁
pos
结点,会导致找不到pos->next
结点;正确做法:先prev->next = pos->next;
,在销毁pos
结点。
//SList.h
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//SList.c
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//pos指向头结点,头删
if (pos == *pphead)
{
SLTPopFront(pphead);
}
//pos指向除头结点以外的结点
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
9.删除pos之后的结点
让pos
直接指向pos->next->next
,那么pos->next
就是原先的pos->next->next
,会导致找不到原先的pos->next
:
所以先把pos->next
用指针del
保存起来:
//SList.h
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//SList.c
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
//pos del del->next
pos->next = del->next;
free(del);
del = NULL;
}
10.销毁单链表
直接删除结点,会导致phead
和pcur
(遍历指针)指向未知的地址,也就是说会找不到下一个结点,所以用指针next
把下一个结点保存起来。pcur
不为空,先保存下一个结点,再销毁指针,然后pcur = next;
,最后phead
置为空。
//SList.h
//销毁单链表
void SLTDestroy(SLTNode** pphead);
//SList.c
//销毁单链表
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
三、链表的分类
链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
链表说明:
带头链表中的头结点不用来存储任何有效的数据,只用来占位子、“放哨的”,即“哨兵位”。在讲单链表时,会将单链表的第一个结点说明为“头结点”,这个表述是为了方便理解,实际上将单链表的第一个结点说明为“头结点”是错误的。即单链表没有头结点,带头链表才有头结点。
单向链表:只能从一个方向遍历,即从左往右遍历,不能从右往左遍历
双向链表:
next
:指向下一个结点(后继结点)
prev
:指向前一个结点(前驱结点)
带环链表是循环链表的一种
虽然有这么多链表结构,但是我们实际中最常用的还是两种结构:不带头单向不循环链表(简称为单链表)和带头双向循环链表(简称为双向链表)
四、双向链表
全称:带头双向循环链表
链表是由结点构成的,由此可得双向链表的结构如下:
//List.h
//定义双向链表的结构
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//typedef struct ListNode LTNode;
双向链表中的结点里的指针不会指向NULL
的
单链表和双向链表为空链表时的唯一区别:
plist
是指向链表中的第一个结点的,知道了第一个结点就知道了整个链表。
单链表为空:plist
指向NULL
。
双向链表为空:只有“哨兵位”(不存储任何有效的数据),next
、prev
指针都指向自己。
双向链表是循环结构,那么如何遍历双向链表呢?若想打印双向链表中的结点存储的数据,那么就是打印除头结点(“哨兵位”:不存储任何有效的数据)以外的结点中的数据,若像单链表一样遍历,会把“哨兵位”打印以及会发生死循环的情况。应该把pcur
初始化成指向第一个有效数据的指针,再遍历:
双向链表初始化时就要有一个“哨兵位”头结点,双向链表的初始化可以类比为牛肉罐头:
- 给你空罐头,自己放肉:先让plist指向NULL,类比于空罐头,再让plist指向初始化好的“哨兵位”头结点,类比于向空罐头里放肉,这里plist的指向改变了,所以传plist的地址,用二级指针来接收。
//test.c
#include "List.h"
void test01()
{
LTNode* plist = NULL;
LTInit(&plist);
}
int main()
{
test01();
return 0;
}
//List.c
#include "List.h"
//向操作系统申请一个新结点
LTNode* buyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
//双向链表的初始化
void LTInit(LTNode** pphead)
{
*pphead = buyNode(-1);
}
- 没有牛肉罐头,那就向操作系统申请空罐头再放肉。最后函数返回指向头结点的指针就行。
//test.c
#include "List.h"
void test01()
{
LTNode* plist = LTInit();
}
int main()
{
test01();
return 0;
}
//List.c
#include "List.h"
//向操作系统申请一个新结点
LTNode* buyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
//双向链表的初始化
LTNode* LTInit()
{
LTNode* phead = buyNode(-1);
return phead;
}
buyNode()
里随便给一个整型数据就行,因为都会不被视为有效的数据:
1.尾插
尾插函数的参数用一级指针接收还是二级指针接收?初始化的时候plist由空指向哨兵位结点,这时plist存储的就是哨兵位结点的地址,结点里的指针next、prev存储的也是哨兵位结点的地址,后续尾插数据始终在哨兵位后面插入,是对哨兵位结点进行解引用,改变的是结点里的指针next、prev的值,实参plist始终不会被改变,plist始终存储的是哨兵位结点的地址,因此尾插函数的参数用一级指针接收:
插入数据时,双向链表涉及到的指针指向的修改太多,可以先改变链表之外的结点的指向,也就是改变新结点的指针的指向,这样不会影响原链表的结点的指针指向。
找新结点插入后,受到影响的结点:phead->prev、newnode、phead
//List.c
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
//phead newnode phead->prev
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
assert(phead);
必须有(“哨兵位”)头结点,没有的话就不是一个双向链表结构。
若链表为空,此时链表里只有头结点,上述代码也是适用的,结点phead->next
还是头结点:
2.头插
虽然是头插,但是是除头结点以外的第一个结点之前插入,plist因此还是指向头结点,头插方法不会改变plist指向,用一级指针接收。
//List.c
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
//phead newnode phead->next
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
3.尾删
链表的头结点不为空且链表不为空:
//List.c
//链表为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;//或者return phead->prev == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));//链表、头结点都不为空,取一个非。相当于phead != NULL 且 phead->next != phead
}
直接删除d3,d2->next、head->prev会是野指针:
完整代码如下:
//List.c
//链表为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;//或者return phead->prev == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));//相当于phead != NULL 且 phead->next != phead
LTNode* del = phead->prev;
//del->prev del phead
phead->prev = del->prev;
del->prev->next = phead;
free(del);
del = NULL;
}
4.头删
直接删除del的话,phead->next、del->next->prev会是野指针,并且找不到phead、del->next指针所指的结点。
//List.c
//头删
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
//phead del del->next
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
5.查找
//List.c
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
6.在pos位置之后插入数据
数据保存在新的结点里,插入到链表里。
双向链表,给出任意一个结点指针,那么这个指针都能把链表遍历完,所以不需要传递头结点。
断言pos不为空。若pos为空,则没办法在pos后插入结点。
pos有没有可能是头结点呢?“在pos位置之后插入数据”函数的pos参数是用来接收“查找”函数的返回值(返回值类型是LTNode*类型的指针,这个指针变量作为参数传递给pos)指针的,这也是为什么先写“查找”函数的原因,所以pos不可能为头结点。指针变量find作为参数传递给pos:
//List.c
//在pos位置之后插入数据
void LTInsertAfter(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = buyNode(x);
//pos newnode pos->next
newnode->prev = pos;
newnode->next = pos->next;
pos->next = newnode;
pos->next->prev = newnode;
}
7.删除pos位置的结点
同理,pos不可能是头结点。
//List.c
//删除pos位置的结点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
8.销毁
把链表中的所有结点都销毁,包括头结点,头结点因此会被改变,所以用二级指针接收。
直接删除d1,会找不到d2。
//销毁
void LTDesTroy(LTNode** pphead)
{
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(*pphead);
*pphead = NULL;
}
二级指针写法没有错,但是为了接口一致性,例如:使用库里面已经写好的方法,传过去的参数得到了统一。
双向链表函数实现方法中有时传一级、有时传二级,为了不增加使用者的记忆负担,统一参数,都传一级。
用一级指针接收,形参的改变不会影响实参,phead置为NULL,但是形参plist没有置为NULL,实参里面结点对应的地址还给操作系统了,此时plist是个野指针。最后还要手动的将plist置为空:
用一级指针接收完整代码如下:
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}