顺序表 vs 链表:内存布局的“连续派”与“离散派”终极对决


物理结构:数据在内存存储上的结构
逻辑结构:人为想象出来的结构
线性表:逻辑结构一定是线性的,物理结构不一定是线性的。

一、顺序表

顺序表是线性表的一种,其逻辑结构一定是线性的,但是它的物理结构取决于它的底层结构,顺序表的底层结构是数组,所以顺序表的物理结构也是线性的。

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)头插

实现步骤:

  1. 断言指针
  2. 判断空间是否为满
  3. 从后往前,依次将数据赋值给后一位
  4. 最后i落在下标0处,赋值x
  5. 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)在指定位置之前插入数据

  1. 断言指针是否为空。
  2. 断言pos的范围:assert(pos >= 0 && pos <= ps->size);pos == 0时就是头插,pos == ps->size时就是尾插。
  3. 检查空间是否为满。
  4. pos指定的数据之前插入,即在pos的位置插入,也就是说pos及之后的数据都向后挪动一位。
  5. pos的位置插入指定数据。
  6. ++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)头删

  1. 断言指针和顺序表中的有效数据个数不为空
  2. 从前往后,数据依次向前覆盖
  3. --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)在指定位置删除数据

  1. 断言指针不为空以及顺序表有效数据个数不为零。
  2. 断言pos的范围:assert(pos >= 0 && pos < ps->size);
  3. pos之后的数据往前挪动一位
  4. --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.在指定位置之前插入数据

链表不为空

  1. pos指定位置是头结点,newnode结点直接头插。
  2. pos指定位置是除头结点以外的结点,那么要找到pos的前一个结点prev,然后再把newnode结点插入到prevpos之间,即涉及到的指针有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->nextnewnode,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;,不可行的原因及改进:

  1. 原因:
    在这里插入图片描述
  2. 改进:用指针next保存pos->next

考虑所有可能出现的情况,不可能出现头插,可以出现尾插:
在这里插入图片描述

8.删除pos结点

  1. 涉及的指针有3个:pos、pos所指结点的前一个结点prev(prev需要借助头结点遍历单链表)、pos所指结点的下一个结点pos->next
  2. 先销毁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.销毁单链表

直接删除结点,会导致pheadpcur(遍历指针)指向未知的地址,也就是说会找不到下一个结点,所以用指针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
双向链表为空:只有“哨兵位”(不存储任何有效的数据),nextprev指针都指向自己。
在这里插入图片描述

双向链表是循环结构,那么如何遍历双向链表呢?若想打印双向链表中的结点存储的数据,那么就是打印除头结点(“哨兵位”:不存储任何有效的数据)以外的结点中的数据,若像单链表一样遍历,会把“哨兵位”打印以及会发生死循环的情况。应该把pcur初始化成指向第一个有效数据的指针,再遍历:
在这里插入图片描述

双向链表初始化时就要有一个“哨兵位”头结点,双向链表的初始化可以类比为牛肉罐头:

  1. 给你空罐头,自己放肉:先让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);
}
  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;
}

五.顺序表与链表的分析

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值