【数据结构】吃透单链表!!!(详细解析~)

目录

  • 前言:
  • 一.顺序表的缺陷 && 介绍链表
    • 1.顺序表的缺陷
    • 2.介绍链表
      • (1)链表的概念
      • (2)链表的结构
      • (3)链表的功能
  • 二.单链表的实现
    • 1.创建节点的结构
    • 2.头文件函数的声明
    • 3.函数的实现
      • (1)打印单链表
      • (2)创建一个节点
      • (3)尾插
      • (4)头插
      • (5)尾删
      • (6)头删
      • (7)查找
      • (8)在pos位置前插入
      • (9)在pos位置后插入
      • (10)删除pos位置
      • (11)删除pos位置后的节点
      • (12)清理单链表
  • 三.全部代码
    • 1.SList.h
    • 2.SList.c
    • 3.Test.c

前言:

上篇文章介绍了顺序表,这篇文章开始着重讲解链表了。
链表有很多种:单、双链表,循环、非循环链表还有带头、不带头的链表。本篇的主要内容是单链表(无头,单向,非循环)
链表对比顺序表有哪些不同之处,接下来会带大家一起了解~

一.顺序表的缺陷 && 介绍链表

1.顺序表的缺陷

1.头部和中间的插入删除效率都较低,时间复杂度为O(N)。需要挪动数据。
2.空间不够用了,增容需要申请新空间拷贝数据释放旧空间。会有不小的消耗。(尤其是异地扩容)
3.扩容会有一定的空间浪费。(例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间)

2.介绍链表

针对顺序表的缺陷,就有了链表这个数据结构

(1)链表的概念

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
特点:按需申请释放
在这里插入图片描述

顺序表是数组存储数据的,空间是连续的(可通过一个指针找到所有的值),通过size标记直到没有数据(前面的为size的个数即有效数据)。
链表的每个节点的大小没有关系,也不连续(多次malloc开辟出来的空间是随机的)。它是通过一个头指针(phead)先找到第一个节点,然后通过第一个节点的指针找到第二个节点,第二个节点的指针找到第三个节点,以此类推(通过指针链接)。每个位置的节点都有指针指向下一个,当下一个为空指针的时候,就结束。

(2)链表的结构

物理图:
在这里插入图片描述
逻辑图:
在这里插入图片描述
链表的节点组成(单链表):
在这里插入图片描述

注意:链表的最后一个节点的next指向空

看到这有些小伙伴可能有些疑惑,链表的每个节点是不连续的,为什么上面的两个图中每个节点都有线连接起来变成看似连续的呢?其实不是这样的,以上的两张图是为了方便理解。实际在内存中每个节点的地址是随机的,只不过用这个节点的指针(next)找到了下一个节点的地址,所以才能实现链接。

(3)链表的功能

链表的功能与顺序表类似,无非是增删查改,在某位置的插入与删除,对数据内容进行管理和操作。

二.单链表的实现

还是以多文件的形式分模块写

SList.h——函数和类型的声明
SList.c——函数的实现
Test.c——进行测试

1.创建节点的结构

单链表一个节点的结构:

存放数据:data
结构体指针:next

注意:不能这样写:

typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
	SListDataType data;
	SLTNode* next;
}SLTNode;//  <-重定义开始生效的位置

因为typedef重定义结构体类型的名称是在上面有箭头的一行开始生效,生效了才能使用,在前面就提前使用就会出现错误。

正确写法:

typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
	SListDataType data;
	struct SListNode* next;
}SLTNode;

2.头文件函数的声明

1.打印单链表
2.创建一个节点
3.尾插
4.头插
5.尾删
6.头删
7.查找(包含修改)
8.在pos位置前插入
9.在pos位置后插入
10.删除pos位置的节点
11.删除pos位置后一个的节点
12.清理单链表

//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);

3.函数的实现

(1)打印单链表

创建一个结构体指针变量(cur),使它指向第一个节点(把头指针覆给cur)。利用循环如果cur不是空指针,就打印cur所指向的数据,然后cur往后走(到下一个节点)。直到cur为空跳出,最后打印NULL(最后一个节点为空指针)。

逻辑图:
在这里插入图片描述
物理图:
在这里插入图片描述
注意:与顺序表不同,顺序表传过来的指针一定不为空;链表传过来的指针可能为空,比如链表没有节点,头指针指向的就是NULL,所以不需要断言头指针。

所以在测试的文件里(Test.c)刚开始要让头指针指向NULL

SLTNode * plist = NULL;

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

(2)创建一个节点

为了方便后面的尾插、头插等操作,所以写个函数来创建一个新节点。
新节点的类型也是结构体指针,用malloc函数开辟一个新节点。如果新节点为空就报错。然后给新节点的data赋值,next为空,返回这个节点(方便其他的函数使用)

SLTNode* BuySLTNode(SListDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

(3)尾插

尾插一个新节点,假设有多个节点,首先要找到尾,定义一个变量tail去遍历链表找到尾
注意:用tail遍历找尾再尾插时不能写成:

       SLTNode* tail = *pphead;
       while (tail)
		{
			tail = tail->next;
		}
		tail = newnode;		

这段代码看似没有什么问题,其实是与正确的代码差别很大。
tail刚开始指向第一个节点,如果不为空,到下一个节点;当tail为空时跳出循环,把newnode的值(新节点的地址)赋给tail。
如图:
在这里插入图片描述
这里有一个问题,tail里面存放的是新节点的地址,但是原来链表的最后一个节点的next指针并没有存放新节点的地址,也就是说最后一个节点没有与新节点连接起来,就没有尾插了。其次还有可能存在内存泄漏,新创建的节点丢了。

因为tail是局部变量,newnode和phead也是,它们出这个函数就销毁了,所以给tail这个变量赋新节点的地址没有用。

要成功完成尾插,就必须改变结构体的内容,让最后一个节点的next指针指向新节点的地址。

这里大家可能有些疑惑,既然tail销毁了,那么链表的这些节点会不会销毁呢?
答案是不会,因为这些节点是malloc出来的,malloc在堆上开辟的空间,只有自己主动free释放掉才能销毁。

正确的思路:
首先想到的是要改变结构体(节点)的内容,那么tail这个指针变量就不能到空结束,而是到最后一个节点结束(tail的next为空就结束,tail的位置指向最后一个节点)。

此时尾节点的next为空,我们要做的是让尾节点的next存放新节点的地址。让tail的next存放newnode的值(新节点的地址),就可以改变结构体的内容。
在这里插入图片描述

找尾尾插正确的一小段代码:

		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;

还有一种情况,如果刚开始链表没有节点,就不需要找尾了。直接将新节点的地址给头指针(plist)就行

但是这种情况要注意什么呢?
以下是错误示范:

	if (phead == NULL)
	{
		phead = newnode;
	}

这个代码的意思如图所示:
在这里插入图片描述
有两个问题:
一:plist没有改变,还是指向空指针;新节点丢了,可能造成内存泄漏。
二:newnode和phead是形参,形参是实参的拷贝,出这个函数就销毁了,改变phead并没有改变plist。

注意!!!:plist是一级指针,改变一级指针需要用到二级指针,并且有解引用操作。所以在函数的参数应该用二级指针来接收(传参时plist要有取地址符才能与二级指针类型对应)
在这里插入图片描述
正确的一小段代码:

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}

总结:
1.改变结构体,要用结构体指针
2.改变结构体指针,要有结构体指针的指针(即二级指针)

最后一点:什么时候要断言指针
当一级指针(* pphead)为空时不需要断言,因为如果刚开始链表没有节点,* pphead所指向的就是空指针。二级指针pphead存放的是一级指针的地址,一级指针的地址不可能为空,所以二级指针需要断言。

void SLTPushBack(SLTNode** pphead, SListDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	//原来没有节点,改变结构体指针,用二级指针
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//原来有节点,改变结构体,用结构体指针
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

(4)头插

头插也需要用到二级指针,因为每次头插头指针(plist)都要连接新的节点。(改变了头指针)
头插时原来链表没有节点与原来链表有节点的思路是一样的
在这里插入图片描述
新节点连接第一个节点或者空指针,然后plist连接新节点

注意:两者的顺序不能换,因为如果先让plist连接newnode,那么原来链表plist头指针后面的节点就找不到了。newnode再连接plist所指向的下一个节点就是自己,导致死循环。

void SLTPushFront(SLTNode** pphead, SListDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

(5)尾删

前面的尾插、头插都有用到二级指针,那么尾删需不需要二级指针呢?接下来我们一点一点的分析:

尾删的大体思路是:找到尾,然后free释放掉尾节点就行。

但是链表有一个很重要的点:前后关联

这里我们定义一个指针变量tail去找尾,把尾节点删掉了,那么原来前一个节点变成新的尾节点,还需要用另一个变量当作原来尾节点的前一个节点,新的尾节点next指针就必须指向NULL只需要改变结构体),否则就访问野指针了。
有两种写法,这里只展现一种,就用tail一个指针变量,让它的下一个的下一个指针为空时停下(tail->next->next==NULL),此时tail->next就是最后一个节点,tail是前一个节点,修改新的尾节点的next,让tail->next为NULL(改变结构体)就行了。

以上只是包括一类情况:一个以上节点的时候是这样的
如果尾删把节点只删到剩下一个节点时,还是如此吗?

在这里插入图片描述
按前面的思路来走,遇到尾节点就把它的前一个节点的next置空。

依图分析,只有一个节点时,前一个节点就不是节点了,是头指针。要让头指针指向NULL,即改变头指针,就要用到二级指针了。
让 * pphead置空,就可以改变头指针

plist 等价于 * pphead

没有节点的情况:
断言 * pphead,为空就不能再删了

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//空
	assert(*pphead);
	//一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//一个以上的节点
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

(6)头删

通过前面的分析发现,有改变头指针所指向的内容就要用到二级指针,头删是把第一个节点除去,让头指针指向新的头节点。

画图分析:
在这里插入图片描述
当链表没有节点时不能再删了,所以要对 * pphead断言( * pphead等价于plist即第一个节点)

只有一个节点和有多个节点不需要分开处理,定义一个变量记录原来链表的第二个节点(新的头节点),free释放掉第一个节点,让头指针连接新的头节点

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	//空
	assert(*pphead);
	//非空
	SLTNode* newhead = (*pphead)->next;//注意优先级
	free(*pphead);//不需要置空,因为头指针直接连接新的头
	*pphead = newhead;
}

(7)查找

定义一个变量cur遍历链表,先判断cur所指向的数据是否等于x,如果相等,返回cur,否则往后走;找不到返回空指针。

SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

查找可以包含修改这个节点的数据

	SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改
	if (pos1 != NULL)
	{
		printf("找到了\n");
		pos1->data *= 100;
		SLTPrint(plist);
	}
	else
	{
		printf("找不到\n");
	}

(8)在pos位置前插入

要在pos位置前插入一个新节点,首先pos这个位置的节点必须存在,所以要断言pos(后面有pos位置插入删除的函数也要用到)
pos可能在任意一个位置,如果pos在第一个节点,就相当于头插了。头插要改变头指针所指向的内容,所以要用二级指针。直接调用头插的函数即可。

pos不在第一个节点的情况:
首先要定义一个变量prev,遍历链表找到并指向pos的前一个节点,因为插入新的节点必须前后连接起来(单链表的不足之处,后期文章用双向循环带头链表就非常简单)。
当prev->next != pos,往后走;==pos时跳出循环,让prev->next连接新节点,新节点的next连接pos,完成插入。
在这里插入图片描述

void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
	assert(pphead);
	assert(pos);
	//pos在第一个节点就是头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

(9)在pos位置后插入

因为在pos位置后插入新的节点,所以可以不用头指针了,找到pos位置的下一个节点即可。可以定义一个变量posNext为pos位置的下一个节点,让新节点的next连接posNext,pos->next连接新节点
不需要考虑是不是尾插,因为在哪插入都是一样的
在这里插入图片描述

void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* posNext = pos->next;
	newnode->next = posNext;
	pos->next = newnode;
}

(10)删除pos位置

删除pos位置的节点,必须把它的前一个节点与后一个节点连接起来,这里就要有头指针,去找pos位置的前一个节点。
我们要考虑一些情况,pos在第一个节点、中间某个节点和尾节点

当pos在第一个节点时,就是头删,要改变头指针指向的内容,所以要用二级指针,然后调用头删的函数即可

如果pos是在中间的某个节点或者尾节点呢?
其实两者的思路是一致的,把pos位置的节点删除,让前一个节点连接后一个节点就行(是尾节点的话,让前一个节点连接空指针)
在这里插入图片描述

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

(11)删除pos位置后的节点

要删除pos位置的后一个节点,除了pos这个位置要存在之外,pos位置的后一个节点也必须存在,所以pos->next要断言。假如pos是在尾节点,就没有意义了。

定义一个变量posNext为pos的下一个节点,然后使pos->next指向posNext->next,即把pos位置的节点与posNext的下一个节点连接起来,最后释放掉posNext

在这里插入图片描述

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//检查是否为尾节点
	SLTNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
	posNext = NULL;
}

(12)清理单链表

清理(销毁)链表,必须要一个一个节点清理,因为链表在物理结构上是不连续的。

定义一个变量cur遍历链表,每到一个节点把它释放掉。但是这里又有一个问题,当前节点被释放了,怎么到下一个节点呢?所以我们循环里再定义一个变量next为cur的下一个节点,释放完当前的cur,然后把next赋给cur,这样cur就能到下一个节点了。

最后全部节点释放完,头指针要指向空,这里又有改变头指针了,所以有二级指针。
在这里插入图片描述

void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

三.全部代码

1.SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
	SListDataType data;
	struct SListNode* next;
}SLTNode;
//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);

2.SList.c

#include "SList.h"
//打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
//创建一个节点
SLTNode* BuySLTNode(SListDataType 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, SListDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	//原来没有节点,改变结构体指针,用二级指针
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//原来有节点,改变结构体,用结构体指针
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//空
	assert(*pphead);
	//一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//一个以上的节点
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	//空
	assert(*pphead);
	//非空
	SLTNode* newhead = (*pphead)->next;//注意优先级
	free(*pphead);//不需要置空,因为头指针直接连接新的头
	*pphead = newhead;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
	assert(pphead);
	assert(pos);
	//pos在第一个节点就是头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* posNext = pos->next;
	newnode->next = posNext;
	pos->next = newnode;
}
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		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);
	assert(pos->next);//检查是否为尾节点
	SLTNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
	posNext = NULL;
}
//清理
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

3.Test.c

#include "SList.h"
test()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);//测试尾插
	SLTPrint(plist);

	SLTPushFront(&plist, 10);
	SLTPushFront(&plist, 20);
	SLTPushFront(&plist, 30);
	SLTPushFront(&plist, 40);//测试头插
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);//测试尾删
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPopFront(&plist);//测试头删
	SLTPrint(plist);

	SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改
	if (pos1 != NULL)
	{
		printf("找到了\n");
		pos1->data *= 100;
		SLTPrint(plist);
	}
	else
	{
		printf("找不到\n");
	}

	SLTNode* pos2 = SLTFind(plist, 10);//测试pos位置前插入
	if (pos2)
	{
		SLTInsert(&plist, pos2, 66);
		SLTPrint(plist);
	}

	SLTNode* pos3 = SLTFind(plist, 20);//测试pos位置后插入
	if (pos3)
	{
		SLTInsertAfter(pos3, 77);
		SLTPrint(plist);
	}

	SLTNode* pos4 = SLTFind(plist, 1);//测试删除pos位置
	if (pos4)
	{
		SLTErase(&plist, pos4);
		SLTPrint(plist);
	}

	SLTNode* pos5 = SLTFind(plist, 66);//测试删除pos位置的后一个节点
	if (pos5)
	{
		SLTEraseAfter(pos5);
		SLTPrint(plist);
	}

	SLTDestroy(&plist);
}
int main()
{
	test();
	return 0;
}

在这里插入图片描述
总算把最费劲的写完了,感谢铁子们的观看,期待大家的支持~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

33.Netty源码之读写数据

highlight: arduino-light 写数据 写数据的三种方式 md 快递场景(包裹) Netty 写数据(数据) 揽收到仓库 write&#xff1a;写到一个 buffer 从仓库发货 flush: 把 buffer 里的数据发送出去 揽收到仓库并立马发货 (加急件) writeAndFlush&#xff1a;写到 buffer&#xff0c;立马…

搜狗拼音暂用了VSCode及微信小程序开发者工具快捷键Ctrl + Shit + K 搜狗拼音截图快捷键

修改搜狗拼音的快捷键 右键--更多设置--属性设置--按键--系统功能快捷键--系统功能快捷键设置--取消Ctrl Shit K的勾选--勾选截屏并设置为Ctrl Shit A 微信开发者工具设置快捷键 右键--Command Palette--删除行 微信开发者工具快捷键 删除行&#xff1a;Ctrl Shit K 或…

集群、负载均衡集群、高可用集群简介,LVS工作结构、工作模式、调度算法和haproxy/nginx模式拓扑介绍

一.集群的定义 1.定义 2.分类 &#xff08;1&#xff09;负载均衡集群&#xff08;LBC/LB&#xff09; &#xff08;2&#xff09;高可用集群&#xff08;HAC&#xff09; 二.使用集群的意义 1.高性价比和性能比 2.高可用性 3.可伸缩性强 4.持久和透明性高 三.常见的…

什么是单例模式

什么是单例模式 文章目录 什么是单例模式1. 单例(单个的实例)2. 单例模式应用实例3. 饿汉式 VS 懒汉式 1. 单例(单个的实例) 所谓类的单例设计模式&#xff0c;就是采取一定的方法保证在整个的软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一…

Windows系统修改域名DNS指向两种方式

一、直接打开对应文件进行修改 1、进入hosts文件目录&#xff1a;C:\Windows\System32\drivers\etc 2、右键打开HOSTS文件进行编辑&#xff0c;将需要对应的域名和IP地址进行配置 编写完成后 Ctrl s 进行保存即可。 二、使用DOS命令进行修改 1、按住键盘win键 r 打开命令…

基于MOEA/D求解电力系统中环境经济调度问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

操作符详解下(非常详细)

这里写目录标题 下标访问[ ]、函数调用()[ ]下标引用操作符函数调用操作符 操作符的属性&#xff1a;优先级、结合性优先级结合性 表达式求值整型提升整型提升的意义如何进行整体提升 算术转换问题表达式解析表达式1表达式2表达式3表达式4表达式5 总结 下标访问[ ]、函数调用()…

TiDB 多集群告警监控-中章-融合多集群 Grafana

作者&#xff1a; longzhuquan 原文来源&#xff1a; https://tidb.net/blog/ac730b0f 背景 随着公司XC改造步伐的前进&#xff0c;越来越多的业务选择 TiDB&#xff0c;由于各个业务之间需要物理隔离&#xff0c;避免不了的 TiDB 集群数量越来越多。虽然每套 TiDB 集群均有…

excel逻辑函数篇2

1、IF(logical_test,[value_if_true],[value_if_false])&#xff1a;判断是否满足某个条件&#xff0c;如果满足返回一个值&#xff0c;如果不满足则返回另一个值 if(条件,条件成立返回的值,条件不成立返回的值) 2、IFS(logical_test1,value_if_true1,…)&#xff1a;检查是否…

【深度学习 | 数据可视化】 视觉展示分类边界: Perceptron模型可视化iris数据集的决策边界

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

基于Three.js的WebXR渲染入门

1、Three.js 渲染管线快速概览 我不会花太多时间讨论 Three.JS 渲染管道的工作原理,因为它在互联网上有详细记录(例如,此链接)。 我将在下图中列出基础知识,以便更容易理解各个部分的去向。 2、WebXR 设备 API 入门 在我们深入了解 WebXR API 本身之前,您应该知道 WebX…

AutoSAR系列讲解(深入篇)13.7-Mcal Adc配置(上)

目录 一、AdcGeneral 二、AdcSafety 三、AdcConfigSet 在之前的章节中,咱们在Port的配置中讲解了工具的初步使用与一些技巧;在Dio的配置中讲解了生成的代码的内容;在mcu的配置里讲解了外部一些 第三方简便工具的使用。这一次咱们配合ADC模块,就详细的讲讲每个配置项的作…

STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器

只用STM32单片机SD卡耳机插座&#xff0c;实现播放MP3播放器&#xff01; 看过很多STM32软解MP3的方案&#xff0c;即不通过类似VS1053之类的解码器芯片&#xff0c;直接用STM32和软件库解码MP3文件&#xff0c;通常使用了labmad或者Helix解码库实现&#xff0c;Helix相对labm…

Kubernetes网络模型

Kubernetes 用来在集群上运行分布式系统。分布式系统的本质使得网络组件在 Kubernetes 中是至关重要也不可或缺的。理解 Kubernetes 的网络模型可以帮助你更好的在 Kubernetes 上运行、监控、诊断你的应用程序。 网络是一个很宽泛的领域&#xff0c;其中有许多成熟的技术。对于…

学习总结(TAT)

项目写完了&#xff0c;来写一个总的总结啦&#xff1a; 1.后期错误 Connection&#xff0c;Statement&#xff0c;Prestatement&#xff0c;ResultSet都要记得关闭接口&#xff1b;&#xff08;一定要按顺序关闭&#xff09;&#xff1b; 在写群聊的时候写数据库名的时候不要…

Spring项目使用Redis限制用户登录失败的次数以及暂时锁定用户登录权限

文章目录 背景环境代码实现0. 项目结构图&#xff08;供参考&#xff09;1. 数据库中的表&#xff08;供参考&#xff09;2. 依赖&#xff08;pom.xml&#xff09;3. 配置文件&#xff08;application.yml&#xff09;4. 配置文件&#xff08;application-dev.yml&#xff09;5…

Windows10上VS2022单步调试FFmpeg 4.2源码

之前在 https://blog.csdn.net/fengbingchun/article/details/103735560 介绍过通过VS2017单步调试FFmpeg源码的方法&#xff0c;这里在Windows10上通过VS2022单步调试FFmpeg 4.2的方法&#xff1a;基于GitHub上ShiftMediaProject/FFmpeg项目&#xff0c;下面对编译过程进行说明…

.netcore grpc身份验证和授权

一、鉴权和授权&#xff08;grpc专栏结束后会开启鉴权授权专栏欢迎大家关注&#xff09; 权限认证这里使用IdentityServer4配合JWT进行认证通过AddAuthentication和AddAuthorization方法进行鉴权授权注入&#xff1b;通过UseAuthentication和UseAuthorization启用鉴权授权增加…

Vulnhub系列靶机 Hackadmeic.RTB1

系列&#xff1a;Hackademic&#xff08;此系列共2台&#xff09; 难度&#xff1a;初级 信息收集 主机发现 netdiscover -r 192.168.80.0/24端口扫描 nmap -A -p- 192.168.80.143访问80端口 使用指纹识别插件查看是WordPress 根据首页显示的内容&#xff0c;点击target 点击…

pytorch 42 C#使用onnxruntime部署内置nms的yolov8模型

在进行目标检测部署时,通常需要自行编码实现对模型预测结果的解码及与预测结果的nms操作。所幸现在的各种部署框架对算子的支持更为灵活,可以在模型内实现预测结果的解码,但仍然需要自行编码实现对预测结果的nms操作。其实在onnx opset===11版本以后,其已支持将nms操作嵌入…