C语言中的数据结构- -链表(1)

news2025/3/16 4:07:40

前言

前几节我们学习了C语言中的数据结构--顺序表,该数据结构类型相较于普通的数组而言有很多的优势,但是它还是在一定层面上存在着一些缺陷,可以归纳为以下三点:

1. 中间/头部的插⼊删除,时间复杂度为O(N)【数组的遍历会浪费较多的时间】

2. 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗

3. 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间

那么存不存在某种数据结构可以很好地解决上述的问题呢?答案是肯定的,针对上述的三个问题,数据结构--链表可以很好的解决,那么我们废话不多说,正式开始链表的学习

链表的概念以及结构

概念

链表是一种物理存储结构上非连续、非顺序的储存结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表也是线性表的一种

链表的结构和我们日常生活中的火车非常相似,车厢每节是独立存在的,每节车厢之间可以相互连接,车厢可以随意地增加或者减少。

链表的组成也是一样,链表是由一个一个的节点组成,节点可以随意地增加或者减少,每个节点里面都包含有两个部分,一个部分存的有自己本身的数据,另外一个部分存有指向下一个节点的地址的指针

结构

我们定义节点的结构为 struct SListNode

S:single                 Node:节点

所以我们应该这么声明节点结构类型:

struct SListNode
{
	int data;
	struct SListNode* next;  //指向下一个节点的指针
};

链表的分类

链表的分类多种多样,分为带头、不带头单向、双向循环、不循环

这些情况组合起来就有2*2*2=8种情况

带头:指的是链表中有哨兵位节点,哨兵位节点就是动态申请的头节点(后面有题目深入了解),后面我们在实现链表时称呼的头节点实际上指的是第一个有效的节点,是一种习惯性的称呼方式,并不是真正的节点

单双向:单向指的是链表只能从前往后去遍历,只有一个方向;双向既能从前往后遍历,又能从后往前遍历,有两个方向

循环与不循环:不循环链表的尾节点指向空指针,循环链表的尾节点指向第一个节点

编写

链表的打印

此时我们创建4个节点,我们先给四个节点赋值,再想办法用指针连接这四个节点

	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

我们将 next 成员赋予下一个节点的的地址,由此类推,在最后一个节点处的 next 成员中赋予空指针 NULL ,此时就构成了一个链表

	//链表是由一个一个的节点组成的
	//创建几个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	//将四个节点连接起来
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

我们可以再创建一个指针让它指向 node1 ,我们把这个指针叫做 plist

我们此时再来定义一个链表的打印函数,通过打印链表来判断编写是否有错误:

我们可以在打印完所有节点里面的的内容后打印一个 NULL

那么由这些我们可以编写出代码如下:

//打印元素
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

此时可以确认打印代码编写成功

链表的尾部插入

但是我们在编写代码的时候不可能做到一个一个的手动往代码里面输入节点里面的数据,我们想试试能不能把链表的插入封装成一个函数

我们先来试着理解一下,假设我们要在链表的尾部插入数据 x = 6,此时我们需要在链表内新申请一个节点newnode ,但是我们怎么才能把这个新创建的节点和之前的链表联系起来?

若追溯之前的假设,我们在链表的第四个位置存入的是空指针 NULL,我们此时应该把这个 取值为NULL的 next 指针重新赋值,把它赋予我们尾插新节点的地址

总的来说,尾插需要先找到尾节点,再将尾节点与新的节点连接起来

我们发现,每次我们插入一个节点都需要申请空间,为了编写代码更加高效,我们可以把它封装成一个函数:

//新建内存空间
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;
}

我们可以据此写出代码如下:

//尾插
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = SLTBuyNode(x);
	//找尾节点
	SLTNode* ptail = phead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	//ptail此时就是指向尾节点
	ptail->next = newnode;
}

但是仔细想想,我们可以发现这个代码隐藏着一个错误:

链表刚开始创建的时候是一个空链表,也就是说,当前的 phead 指针指向的就是NULL,此时在结束 while 循环的条件中,需要使用到 ptail->next 的解引用,但是我们无法对空指针进行解引用

此时代码一定会报错,所以我们需要处理空链表的情况:

如果判断为空链表,此时我们只需要将 phead 赋予 newnode指针,所以我们可以对代码进行修改:

//尾插
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	//空链表和非空链表判断
	SLTNode* newnode = SLTBuyNode(x);
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//找尾节点
		SLTNode* ptail = phead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//ptail此时就是指向尾节点
		ptail->next = newnode;
	}
}

现在的尾插代码就正式编写成功了

我们此时来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(plist, 1);
	SLTPrint(plist);
}
int main(void)
{
	SListTest02();
	return 0;
}

更改错误

我们发现此时代码出现了错误,这是为什么呢?因为我们采取的是值传递,要想改变newnode我们应该采取传地址的操作。此时可能有人就有疑问了:直接传入 plist 变量不也是相当于传入了地址吗?其实我们仔细想想就可以发现其中的问题:直接传入 plist 的时候,代表的是 plist 指向第一个节点的指针,而非 plist 自身的地址,我们只有传入 plist 自身的地址的时候才会有效,由此我们传入地址

	SLTPushBack(&plist, 1);

由于我们传入的是一级指针,我们接受的时候就需要用二级指针来接受

void SLTPushBack(SLTNode** phead, SLTDataType x)

所以我们再来修改一下尾插代码:

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//空链表和非空链表判断
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//ptail此时就是指向尾节点
		ptail->next = newnode;
	}
}

此时我们测试一下:

代码现在就正常了,我们需要注意的是,二级指针 pphead 不能为空指针,但是一级指针 *pphead 可以为空,注意不要混淆概念了

链表的头部插入

刚刚我们学习了尾部插入,此时我们理解头部插入就更加轻松了

void SLTPushFront(SLTNode** pphead, SLTDataType x)

开始插入之前,我们同样的需要开辟新的内存空间,newnode 需要与第一个节点连接在一起,然后把头节点指向 newnode 的节点,由此我们可以写出代码如下:

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

头插的实现相较于尾插简单很多,我们写个代码来检测一下吧:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPrint(plist);
}
int main(void)
{
	SListTest02();
	return 0;
}

代码的实现没有任何问题

链表的尾部删除

因为改代码实现的是链表的尾部删除,所以无论是 pphead 还是 *pphead 都不可以为空

assert(pphead && *pphead);

我们此时还需要使用 ptail 来寻找尾节点,此时有人可能有问题了:我们既然找到尾节点了,是不是可以直接释放尾节点呢?答案是否定的,因为在尾节点的前一个节点中的 next 变量存入的是尾节点的地址,如果直接释放掉尾节点,此时该指针会变成野指针,所以在每次删除以后我们都需要把尾节点的前一个节点设置为空指针,所以我们不仅需要找到尾节点还需要找到尾节点的前一个节点,我们来试着编写代码:

//尾部删除
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);

	SLTNode* prev = *pphead;//前节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	//prev ptail
	free(ptail);
	ptail = NULL;
	prev->next = NULL;
}

排查

此时代码就已经编写完成了,我们来考虑一下特殊情况:

若是链表里面只有一个节点,初始的情况下我们定义的两个指针 prev 和 ptail 指向的都是第一个节点,此时 ptail 为空,while循环没有开始就已经结束了,我们就直接释放掉 ptail 了,此时因为 prev 指向的节点也是这个节点,所以也被释放掉了,所以此时就不能让 prev 指向的 next 成员赋予空指针了,此时就出现问题了

修改

知道了存在这个问题,我们对 *pphead 指向的 next 指针进行判断

if ((*pphead)->next == NULL)
//尾部删除
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表有多个节点
	else
	{
		SLTNode* prev = *pphead;//前节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev ptail
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

这里需要注意,因为 -> 的优先级大于 * 所以需要加上括号

加上来判断以后,我们的代码就修改完成了,我们来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
}
int main(void)
{
	SListTest02();
	return 0;
}

此时代码运行完美,代码编写成功

链表的头部删除

头部删除不能简单的直接释放掉第一个节点,因为要是直接释放第一个节点,会导致 next 成员也被释放,此时就无法找到第二个节点

所以我们需要先把下一个指针的地址存起来,再释放掉第一个节点,再让 *pphead 指针向后挪动一位,走到第二个节点的位置,据此我们可以写出代码:

//头部删除
void SLTPopFront(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);

	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

我们同样的需要考虑优先级问题:

排查

此时我们来考虑一下特殊情况:若是只有一个节点,此时 *pphead 指向这一个节点,而 next 成员指向头节点的下一个节点,也就是 NULL 。此时 *pphead 指针走向了 next 指针的位置,该代码在特殊情况下没有出现问题

那么此时我们来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPrint(plist);
}
int main(void)
{
	SListTest02();
	return 0;
}

代码并不存在问题,编写成功

链表的查找

因为我们不需要改变链表里面的内容,所以我们可以直接传入 plist ,表示指向第一个节点的地址

查找的本质就是遍历链表

我们再创建一个变量 pcur 让 pcurr 去遍历全链表。如果直接让 phead 去遍历链表,则遍历完以后 phead 会停留在 NULL 处

我们根据这些条件试着编写代码:

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

最后来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPrint(plist);

	//测试查找
	SLTNode* find = SLTFind(plist, 3);
	if (find == NULL)
	{
		printf("没有找到\n");
	}
	else
	{
		printf("找到了\n");
	}
}
int main(void)
{
	SListTest02();
	return 0;
}

测试结果正确,代码编写成功

在指定位置之前插入数据

我们需要三个数据:

1.指向第一个节点的指针的地址 SLTNode **pphead

2.指定位置 SLTNode* pos

3.插入的数据 x

pphead 和 *pphead (说明链表也为空,因为链表为空的话 pos 的值就不存在)都不能为空,同时 pos 自己本身也不能为空

第一步,我们需要找到 pos 位置的前一个节点,我们创建一个变量 prev 来找到

然后,我们让 pos 前的指针指向 newload ,让 newnode 指向 pos

据此,我们可以写出代码:

//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* prev = *pphead;

	while (prev->next != pos)
	{
		prev = prev->next;
	}

	newnode->next = pos;
	prev->next = newnode;
}

排查

我们来考虑一下特殊情况

1.若是 pos 的位置为1,此时 *pphead 和 pos 以及 prev 都指向第一个节点,此时我们在 newnode 之前插入了一个新的节点 newnode ,此时 prev 向下遍历查找 pos ,因为 pos 和 prev 在同一个位置,就找不到 pos ,prev 就会一直向后查找并且找到空指针,就会存在对空指针解引用的问题,若 pos == *pphead,说明是头插,直接调用头插代码

 2.若是 pos 的位置为最后一个节点,此时 prev 向下遍历查找 pos,这种情况能够找到,不存在问题

所以我们应该对代码进行修改

修改

//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* prev = *pphead;
	//若 pos == *pphead,说明是头插,直接调用头插代码
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		newnode->next = pos;
		prev->next = newnode;
	}
	
}

根据以上分析的结果,我们可以修改代码如上:

我们来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	SLTNode* find = SLTFind(plist, 3);
	SLTInsert(&plist, find, 11);
	SLTPrint(plist);
}
int main(void)
{
	SListTest02();
	return 0;
}

代码成功运行,编写成功

在指定位置之后插入数据

我们要想实现在指定位置之后插入数据,需要使用到两个变量

1.插入数据的位置 SLTNode* pos

2.插入的数据 SLTDataType x

通过和在指定位置之前插入数据的比较,我们会产生这样的疑问:为什么在指定位置之后插入数据不需要使用头节点?

原因很简单,要在 pos 后插入数据,只需要找到 pos 和 pos 后面的节点,并且把 x 插入 pos 和pos 后面那个节点的中间位置就行,pos 的下一个节点可以通过 pos 里面的指针找到,而在指定位置前插入数据不能找到之前的数据,所以在指定位置之后插入数据不需要用到头节点

所以我们可以声明函数如下:

void SLTInsertAfter(SLTNode* pos, SLTDataType x);

pos 的下一个节点的地址为 pos->next ,我们先让 newnode 指向 pos->next ,再让 pos 指向 newnode 节点,此时函数的功能就实现了

此时我们再考虑一个问题:

1.我们先让 newnode 指向 pos->next ,再让 pos 指向 newnode 节点

2.我们先让 pos 指向 newnode 节点,再让 newnode 指向 pos->next

请问这两种方法相同吗?如果不同则哪种方法是正确的?

答案是方案一是正确的,方案二存在错误。理由很简单,方法二中我们先把 pos->next 指向 newnode ,而此时因为 pos->next 指针被替换为 newnode ,所以就无法通过pos->next 指针找到 pos后的下一个节点了

方案二解决办法:创建一个新的指针变量存入 pos 后面一个节点的地址

我们按照方案一进行代码的编写,那么代码就很容易实现了:

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

我们再来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	//测试查找
	SLTNode* find = SLTFind(plist, 4);
	SLTInsertAfter(find, 11);
	SLTPrint(plist);
}

代码完美运行,编写成功

删除pos节点

要想实现删除节点的功能,我们需要改变链表里面的数据,所以此时我们需要使用到两个变量

1.指向第一个节点的指针 SLTNode** pphead

2.指定的位置 SLTNode* pos

要删除 pos 节点,我们需要找到 pos 前的节点和 pos 后的节点 ,然后将他们连接起来,所以此时我们需要创建一个变量 prev 来找到 pos 之前的节点

知道了代码的原理,那么它的实现就很简单了:

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);

	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}

	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

排查

我们来考虑一下特殊情况:

若链表中只有一个节点,那么 prev 和 pos 都指向第一个节点,此时又会出现和链表的尾部删除一样的问题,可能会对空指针解引用,所以我们再来修改一下代码:

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头节点
	if (pos == NULL)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

我们此时来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	测试查找
	//SLTNode* find = SLTFind(plist, 4);
	//SLTInsertAfter(find, 11);
	//SLTPrint(plist);

	//删除pos节点
	SLTNode* find = SLTFind(plist, 4);
	SLTErase(&plist, find);
	SLTPrint(plist);
}

代码成功运行,编写成功

删除pos之后的节点

要想实现删除 pos 之后的节点的功能,此时只需要用一个变量

1.指定位置 SLTNode* pos

理由很简单,和链表在指定位置之后插入数据的原理相似,pos 可以找到 pos 后面的第二个节点,删除 pos 后的节点以后直接将 pos 与 pos 后面的第二个节点相连接,就可以实现该功能

我们此时要创建临时变量 del ,避免对空指针的解引用

知道原理以后,代码就很好实现了:

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

我们此时来测试一下:

void SListTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	//删除pos后节点
	SLTNode* find = SLTFind(plist, 3);
	SLTEraseAfter(find);
	SLTPrint(plist);
}

代码成功运行,编写成功

销毁链表

销毁链表只需要用到 *pphead 这一个变量,因为链表在逻辑结构上面是连续的,所以只需要知道头节点,然后依次销毁就行

如果直接释放掉 *pphead ,就会找不到下一个节点,所以我们要创建一个 next 变量将它存起来,同时创建一个变量 pcur 找到头节点,因为直接让 *pphead 去遍历的话,遍历结束的时候 *pphead 会找到最后一个节点所指向的空指针,可能会存在隐患

知道原理了,我们对代码进行编写:

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

全代码整合

Slist.h

#include <assert.h>

//定义节点的结构
//数据 + 指向下一个节点的指针
typedef int SLTDataType;

typedef struct SListNode
{
	int data;
	struct SListNode* next;
}SLTNode;

//链表的打印
void SLTPrint(SLTNode* phead);
//链表的尾部、头部插入
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾部删除
void SLTPopBack(SLTNode** pphead);
//头部删除
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

Slist.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

//打印元素
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

//新建内存空间
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;
}



//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//空链表和非空链表判断
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//ptail此时就是指向尾节点
		ptail->next = newnode;
	}
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

//尾部删除
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表有多个节点
	else
	{
		SLTNode* prev = *pphead;//前节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev ptail
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

//头部删除
void SLTPopFront(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);

	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* prev = *pphead;
	//若 pos == *pphead,说明是头插,直接调用头插代码
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		newnode->next = pos;
		prev->next = newnode;
	}
	
}

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}


//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头节点
	if (pos == NULL)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

结尾

本节的内容较多,讲解了链表的概念及实现,链表的实现相较于顺序表而言更加简单,同时链表的优势也比顺序表大,那么下一节我们来学习链表的应用,本节的所有内容就到此结束了,谢谢大家的浏览!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1598344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一VUE——组件基础

文章目录 认识组件如何使用注册方式 组件间传递数据propsprops的验证 组件事件组件事件的验证v-model与自定义事件结合使用 组件插槽动态组件 认识组件 组件由template&#xff0c;script&#xff0c;style三部分组成。 如何使用 定义组件注册组件调用组件 注册方式 全局注…

在STM32中给固定的地址写入一个值,并通过memory窗口进行查看

首先对STM32中存储数据的地方有一个了解 一个是FLASH,一个是RAM RAM是易失存储器&#xff0c;FLASH是非易失存储器&#xff0c;这是最直观的解释。 主要记住以下几点&#xff1a; RAM&#xff08;随机存储器&#xff09;&#xff1a;既可以从中读取数据&#xff0c;也可以写…

面试八股——数据库——分库分表

垂直策略 垂直分库 垂直分表 水平策略 水平分库 水平分表&#xff08;和水平分库差不多&#xff0c;区别是但这些表可以在同一个库内&#xff09;

spring-cloud微服务gateway

核心部分&#xff1a;routes(路由)&#xff0c; predicates(断言)&#xff0c;filters(过滤器) id&#xff1a;可以理解为是这组配置的一个id值&#xff0c;请保证他的唯一的&#xff0c;可以设置为和服务名一致 uri&#xff1a;可以理解为是通过条件匹配之后需要路由到&…

Oauth2.1第三方授权前后端分离实现

前言 Spring Cloud 整合 Spring Security Oauth2 请看我上一篇文章 在当今的数字化时代&#xff0c;随着微服务架构的流行和前后端分离技术的广泛应用&#xff0c;如何实现安全、高效的用户认证与授权成为了开发者们面临的重要挑战。Spring Cloud与Spring Security OAuth2作为J…

小程序商城和微商城的区别

移动互联网电商发展迅速&#xff0c;各种商城系统的类型越来越多&#xff0c;人们选择要从多方面考量再进行评估&#xff0c;选择过程变得困难了很多&#xff0c;比如现在火热的微商城和小程序商城&#xff0c;很多人都不太能分清楚。今天&#xff0c;我们就一起来看看这两种商…

BCLinux8U6系统部署oceanbase分布式数据库社区版之三、分布式数据库部署

本文是在完成步骤一、准备 OBD 中控机&#xff0c;步骤二3台数据库服务器准备后&#xff0c;正式开始oceanbase分布式数据库安装。 前序步骤&#xff1a;BCLinux8U6系统部署oceanbase分布式数据库社区版之一、准备 OBD 中控机 BCLinux8U6系统部署oceanbase分布式数据库社区版…

请求头包含“boundary=----WebKitFormBoundary”的request抓包

对于请求头包含“boundary----WebKitFormBoundary”&#xff0c;不能直接使用request.post请求&#xff0c;这类请求是文件上传请求。 s common_login(name, password) # 获取浏览器对象url archive_url /XXX# 这里先定义一个fields参数&#xff0c;格式为你可能需要一个包…

Linux yum搭建Keepalived,2 台机器都有虚拟 IP 问题

文章目录 Keepalived 搭建一、安装二、keepalived配置1、配置文件详解global_defs模块参数vrrp_instance模块参数vrrp_script模块参数 2、修改配置文件3、启动服务 Tips:1️⃣问题&#xff1a;两台机器上面都有VIP的情况2️⃣完整配置文件 Keepalived 搭建 服务IP服务器Keepal…

微信小程序wx.getLocation 真机调试不出现隐私弹窗

在小程序的开发过程中&#xff0c;首页中包含要获取用户地理位置的功能&#xff0c;所以在这里的onLoad&#xff08;&#xff09;中调用了wx.getLocation()&#xff0c;模拟调试时一切正常&#xff0c;但到了真机环境中就隐私框就不再弹出&#xff0c;并且出现了报错&#xff0…

ubuntu16.04安装Eclipse C/C++

1.安装 JDK 官网源码安装 首先打开JDK官网&#xff0c;JDK1.8的下载网址为&#xff1a;https://www.oracle.com/cn/java/technologies/downloads/#java8-windows&#xff0c;进入到网址如下图所示&#xff1a; 向下滑动到 JDK1.8的下载界面&#xff0c;如下图所示&#xff1a…

【软考】UML中的图之用例图

目录 1. 说明2. 建模2.1 说明2.2 语境建模2.3 需求建模 3. 图示4. 组成部分 1. 说明 1.用例图&#xff08;Use Case Diagram&#xff09;。2.展现了一组用例、参与者&#xff08;Actor&#xff09;以及它们之间的关系。3.用例图通常包括以下的内容&#xff1a;用例、参与者、用…

SpringBoot项目创建及简单使用

目录 一.SpringBoot项目 1.1SpringBoot的介绍 1.2SpringBoot优点 二.SpringBoot项目的创建 三.注意点 一.SpringBoot项目 1.1SpringBoot的介绍 Spring是为了简化Java程序而开发的&#xff0c;那么SpringBoot则是为了简化Spring程序的。 Spring 框架&#xff1a; Spring…

Python高质量函数编写指南

The Ultimate Guide to Writing Functions 1.视频 https://www.youtube.com/watch?vyatgY4NpZXE 2.代码 https://github.com/ArjanCodes/2022-funcguide Python高质量函数编写指南 1. 一次做好一件事 from dataclasses import dataclass from datetime import datetimedatacl…

如何解决selenium无头浏览器访问页面失败问题!!

无头浏览器简介 无头浏览器&#xff08;Headless browser&#xff09;是一种没有图形用户界面&#xff08;GUI&#xff09;的网络浏览器。它可以在后台运行&#xff0c;并通过编程接口进行控制和操作&#xff0c;而不需要显示界面。通常&#xff0c;传统的浏览器如 Chrome、Fi…

生产控制台厂家的技术要求深度解读

随着科技的不断进步和工业的快速发展&#xff0c;生产控制台在现代化生产线中的作用日益凸显。生产控制台作为生产线的“大脑”&#xff0c;要求厂家不仅具备高超的制造技术&#xff0c;还需对技术要求有深入的理解和掌握。本文将对生产控制台厂家的技术要求进行浅析。 生产控制…

vmware安装ubuntu-18.04系统

一、软件下载 百度网盘&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1fK2kygRdSux1Sr1sOKOtJQ 提取码&#xff1a;twsb 二、安装ubuntu系统 1、把ubuntu-18.04的压缩包下载下来&#xff0c;并且解压 2、打开vmware软件&#xff0c;点击文件-打开 3、选择我们刚刚解…

4.15 网络编程

思维导图 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <semaphore.h> #inclu…

白盒测试之分支-条件覆盖

白盒测试之分支-条件覆盖&#xff08;蓝桥课学习笔记&#xff09; 实验介绍 分支&#xff08;判定&#xff09;覆盖是设计一定量的测试用例使程序中的每个判断语句的真假分支都得到覆盖&#xff0c;但是分支覆盖不能保证判断语句中每个条件的真、假分支都得到覆盖。那么&…

Linux的学习之路:5、粘滞位与vim

摘要 这里主要是把上章没说完的权限的粘滞位说一下&#xff0c;然后就是vim的一些操作。 目录 摘要 一、粘滞位 二、权限总结 三、vim的基本概念 四、vim的基本操作 五、vim正常模式命令集 1、插入模式 2、从插入模式切换为命令模式 3、移动光标 4、删除文字 5、复…