数据结构-(单链表篇)

对于上文我们了解到顺序表在实际使用的时候,有很多的缺陷,于是就产生了“链表”,下面我们就来看看链表的世界是怎样的呢?


目录

简介

链表的形式:

接口类型:

接口实现:

链表打印:

链表结点的创建:

链表尾插:

链表头插:

链表尾删:

链表头删:

链表结点查询:



简介

链表为了避免出现顺序表的插入数据多次挪动数据的情况,专门设计出了一种(数据+指针)的形式,当我们想要插入数据的时候,只需要让指针指向这个数据就可以了,时间复杂度也从顺序表的O(n)变为了此时的O(1)。(注意!!!是插入的时间复杂度变成了O(1) , 但是相对而言其实检索的时间复杂度又变成了O(N))

插入的操作 , 只需要将结点的指针域指向需要插入的结点的后一个结点 , 然后再将需要插入的结点的前一个结点的指针域指向新的节点的数据域即可 , 此时的操作只有两个步骤所以时间复杂度为O(1)

 

但是我们查询的时候 , 由于指针的关系 , 我们必须遍历每一个位置 ,每一个位置的指针我们都需要遍历 , 这就需要大量的操作 , 于是在最坏的情况下我们的时间复杂度应该是O(N)

链表的形式:

链表是由两个域组成的:数据域 + 指针域

数据域:用来存放数据的

指针域:用来指向指定的数据的

像我们上述描述的链表的方式叫做:

逻辑图:

 但是实际上在电脑的ROM中存储的形式并没有这样的可观,而是以一种连续存储的方式进行存放数据的,这种方式叫做“物理图”

物理图:

物理图的形式对于指针变量他只会在内存中寻找属于自己的地址,而不会像逻辑图那样清晰明了有着(->)的箭头来表示下一个结点的位置。

所以我们在定义链表这个结构体的时候,通常是如下的定义方式:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int DataType;   //数据类型
typedef struct LinkListNode {
	DataType data;    //数据域
	DataType* next;   //指针域
};

 每一个数据结构的设计都是为了完成接口最后实现功能的,所以下面就是具体的实现过程:

接口类型:

void LinkListPrint(LinkList* phead);   //链表打印
void LinkListPushBack(LinkList** phead, DataType x);  //链表的尾插
LinkList* LinkListCreatNode(LinkList* phead, DataType x);  //创建一个链表结点
void LinkListPushFront(LinkList** phead, DataType x);   //链表的头插
void LinkListPopBack(LinkList** phead);   //链表尾删
void LinkListPopFront(LinkList** phead);  //链表头删

bool LinkListFind(LinkList* phead, DataType x);  //结点查找
void LinkListInsert(LinkList** phead, DataType x);  //结点插入
void LinkListEarse(LinkList** phead , int pos);  //链表删除
void LinkListDestory(LinkList** phead);  //链表销毁

接口实现:

链表打印:

链表打印也就是我们要将存放在链表这个结构中的数据域的数据打印出来

void LinkListPrint(LinkList* phead);   //链表打印

void LinkListPrint(LinkList* phead)   //链表打印
{
	LinkList* cur = phead;      //创建一个链表的代替指针结点,不能因为打印这个小操作而让头结点持 
                                  续往后移动
	                            //等到需要从头结点开始访问的时候又要重新将头节点移动到头部位置
	while (cur != NULL) {            //所指的指针的域不能为空,如果结点为空的话,那就类似于顺序 
                                       表中的越界
		if (cur->next == NULL) {        //如果链表结点的下一个结点指向的是NULL(也就是越界的地 
                                          址)时,我们直接打印数据
			printf("%d  ", cur->data);
		}
		else {
			printf("%d -> ", cur->data);  //当链表结点指向的下一个结点的位置不是最后一个的时候
			                              //我们就可以将数据打印的后面加上->表示数据往后传递
		}
		cur = cur->next;              //链表结点持续往后移动
	}
}

对于上述

LinkList* cur = phead

 cur->next == NULL 

cur = cur->next

这三种方式我来进行解释:

cur 本质上就是一个哨兵指针,  也就是举个例子:

我是老板 , 我现在想算一笔账 , 但是我又怕自己把账单算错 , 我就会找我的背锅侠(会计师)来帮我算, 因为他比较专业 , 所以我自己就不用动数据 

同时此时cur就是充当会计的角色 , 它就是用来遍历这些单链表中的一些元素 , 而不让单链表自己动 , 这就是哨兵的作用.

 LinkList* cur = phead:

上述我们可以看到phead和cur在同一位置,因为我们 LinkList* cur = phead  ,所以导致cur在链表头结点的位置,为什么要出现cur的存在呢?大家没有想过cur的出现很多余呢?并没有,cur就类似于我们在C语言的学习中的“哨兵”,不会影响原来的数据的形式,对于打印来说的话,最好不要改变链表原来的状态,此时我们就定义一个变量cur来代替phead的功能,让cur不断地访问整个链表,等到函数结束的时候,cur也就随之消失了,此时数据已经打印出来了,而且原链表内部的数据还没有发生变化。

 cur->next == NULL 

此时我们能看到,cur已经在链表的最后了,我们打印的数据形式是:

当链表的非最后一个结点的位置时:打印的数据是类似于:1->2->

但是当我们最后的结点打印的时候为了美观,我们让最后一个结点的数据打印方式变为单独数值3

由此一来我们就可以看到上述的数据实际上的打印数据的形式是:1->2->3

而cur->next也就是为我们警告,cur这个结点的下一个结点指向空节点,也就表明这个结点是最后一个结点了。

cur = cur->next

cur = cur->next  这段代码表示的是,将这个结点指向下一个结点,此时就实现了向后挪动一位的操作

也就是让cur进行挪位 , 使得cur不断地向后移动位置

链表结点的创建:

不管我们如何插入一个结点在链表的上面,我们总是要先有一个结点,因为链表的单位也就是链表结点。

当我们创建一个结点的时候我们就要考虑到两个问题:

1:数据该如何存入到链表结点中

2:结点的指针域该指向哪里

当我们有了这两个问题的时候,我们就要寻找链表之间是如何链接的。

我们知道一个链表结点有两个域:分别为数据域和指针域

数据域就是存储数据的,指针域就是存储下一个结点的位置的,那么我们的下一个节点的位置应该指向哪里,我们创造出来的结点也就是在内存中创建的一个和任何链表都没有链接的结点,他应该是指向NULL,等到需要插入结点到链表的时候,我们就可以调用结点

但是!!!当我们创建结点的时候有可能会开辟空间失败,此时我们就需要报错,使用exit(-1)来表示异常退出。

好多小伙伴对C语言中的exit()函数不太了解,下面我简单描述一下吧!

C语言中的exit()函数的功能是关闭所有的文件,终止正在进行的进程。

对于exit()内部的返回值

exit(0):表示正常返回

exit(x):其中x表示非0,此时表示的是异常返回。

现在我们了解到,原来当我们exit返回值为-1(也就是x:非0数值)的时候就会爆出异常,直接终止程序,关闭所有文件,这也为我们提供了一个检错的方式,这样我们就能知道malloc开辟内存失败了。

我们有的小伙伴就会说了,那么为什么会malloc失败呢?

其实大部分情况下,malloc开辟的内存空间这个操作根本不会失败,malloc开辟的空间是在堆内存上的,在堆上的内存很大的

举个栗子:1M = 1024kb = 1024 * 1024 byte = 2^20byte  =1,048,576 byte

1M大的内存就有100万个字节,你自己在堆内存上开辟一个8byte(数据域:4byte(int) ,指针域(int*) :4byte),会失败吗?除非你很衰,要不然这么好的事情不会轮到你身上。

更何况在栈区上我们的栈区大小:8M

堆区就更大了,32位操作系统下的堆区:4G

LinkList* LinkListCreatNode(LinkList* phead , DataType x) {     //使用链表结点的形式作为返回值,传递的就是一个链表结点
	LinkList* NewNode = (LinkList*)malloc(sizeof(LinkList));   //malloc创建的结点必须类型也是链表结点类型的
	if (NewNode == NULL) {     //如果新节点没有开辟成功的话,就自动退出
		printf("Malloc Failed!\n");
		exit(-1);
	}
	else {
		NewNode->data = x;  //将数据域的x存入结点中的数据域中
		NewNode->next = NULL;   //链表结点指向NULL
	}
	return NewNode;     //此时我们返回的是一个结点型的返回值 , 那么我们的函数定义是就应该是一个结点型的类型
}

链表尾插:

上述我们也知道了链表结点的创建,那么我们尾插的时候,就需要创建一个结点,然后再将结点链接到链表的最后的位置;

那么我们插入到一个链表的时候,我们并不知道这个链表是不是有节点的,于是分为两种情况:

1、无结点的

无节点的就是往链表尾插这个接口传入的链表就是一个刚刚初始化或者从来没有插入过的链表,此时的情况就如图所示:

头结点没有指向任何东西,那么此时我们想要尾插的话,那就只能将新节点直接赋值到头结点内部,让头结点直接指向此时的新节点,形成一个有节点的链表。

这是一个创建新的结点

此时我们直接将新节点放置到链表当中,变成一个有一个结点(NewNode)的新单链表。

2、有结点的:

当我们尾插寻找的链表是有结点的链表的时候,我们就需要找到他的尾结点,然后在尾结点进行新节点的插入。

那么我们该如何寻找一个尾结点呢?

很简单呀!就像上面我给大家介绍的“哨兵”岗位的cur(tips:其实就是一个替头节点跑腿的),让cur遨游在链表之中,不要去动phead(头结点)的位置。

什么叫尾结点呢?

尾结点顾名思义就是链表的最后一个结点

尾结点有什么特征呢?

尾结点的特征就是他的指针域指向的就正是NULL(也就是下一个结点是NULL或者说后面没有节点了)

 

我们此刻就可以将新节点NewNode放在cur的结点的后面啦!

cur->next = NewNode;

有的小伙伴就会问到,那么结点的最后不应该是指向NULL,为什么不让NewNode指向NULL呢?

宝,你恐怕是忘了,咱们在创建新节点NewNode的时候不是已经让他自动指向NULL了嘛!

下面是尾插的具体代码实现:

void LinkListPushBack(LinkList** phead, DataType x)  //链表的尾插  ,对于想要真正的改变链表结 
                                                     //点的话
                                                     //那么传递过来的是一级指针的地址
                                                     //接收的时候就应该用二级指针来接收
{
	//如果刚进来的链表本身就为空的话,我们就不能寻找尾结点,而是将插入的新节点赋予到这个节点上
	LinkList* NewNode = (LinkList*)malloc(sizeof(LinkList));        //创建一个新节点
	NewNode = LinkListCreatNode(phead, x);       //将创建的结点直接赋值到新节点上
	if (*phead == NULL) {          //如果刚进来的结点是空的话,那就把新节点NewNode赋值到头结点上
		*phead = NewNode;
	}
	else {     //如果有节点存在的话 , 我们就要寻找尾结点
		LinkList* cur = *phead;       //使用cur来遍历链表,寻找尾节点
		while (cur->next != NULL) {        //当cur->next == NULL的时候,就代表已经是最后一个节 
                                             点了
			cur = cur->next;
		}
		cur->next = NewNode;         //将新结点直接赋值到尾结点上
	}
}

 对于上述的函数内部为什么phead是二级指针?

因为我们创建的链表是这样创建的:

LinkList* s1 = NULL;  

此时我们看到s1是一个结构体指针(因为链表此时本来就是一个结构体),如果我们接收的时候是使用  LinkList*phead 来接收的话,那就说明我们将指针的数值传递过来了(值传递),要是大家还不明白,我来举个例子恐怕大家就“柳暗花明又一村”了。

值传递:

地址传递:

通过上面对于“值传递”和“地址传递”的说明,我们就能明白,因为定义的链表的时候我们就是用一级指针来进行传递的,然而想要改变的时候,就是通过二级指针来进行地址上实际的变化的。 

链表头插:

链表头插也就是所谓的在链表的头部进行插入结点的操作,那么既然需要在头部想要插入结点。

1:首先我们需要一个结点

我们创建一个结点,调用上面的LinkListCreatNode这样的结点接口,创建出一个结点。

2:我们需要将结点插在链表的头部

此时我们就会考虑到这个链表是否是空链表,如果是空链表的话,我们就直接将新节点赋值给头结点,此时链表的头结点指向的也就是唯一的一个结点。

if (*phead == NULL) {         //如果是空链表的话,就直接将新创建的结点赋值到头结点上
        *phead = NewNode;
    }

要是有节点怎么办?

我们前面一直都在提到“哨兵”cur的使用,现在你感觉到他的好处了吧!cur在插入数据、打印数据的时候并不会影响phead(头指针)的位置,于是就有了我们现在想要插入头部结点就不需要重新让头结点自己走到最初的位置,而是他根本没有改变位置。

那么我们可以将新节点(NewNode)的指针域指向头结点,但是还有一点,那就是要是将头结点的前面插上NewNode的时候,就是下面这番景象了:

此时phead并没有改变位置,但是NewNode也确实插入到头结点的位置,我们猛然发现,好像头指针的位置还在原来的位置,既然插入了新的头结点,那么我们就需要将新的头结点变成头指针指向的位置(也就是将头指针指向新节点的位置,让新结点成为头结点)。

具体代码和解释如下:

void LinkListPushFront(LinkList** phead, DataType x)   //链表的头插
{
	//第一:创建一个新结点
	LinkList* NewNode = (LinkList*)malloc(sizeof(LinkList));
	NewNode = LinkListCreatNode(*phead, x);
	//第二:考虑是否为空链表
	if (*phead == NULL) {         //如果是空链表的话,就直接将新创建的结点赋值到头结点上
		*phead = NewNode;
	}
	else {               //如果是非空链表的话,我们先将NewNode指向头结点,与此同时因为此时的头结 
                           点已经发生变化
		                 //要是不去修改此时头结点的位置,等到传递到主函数的时候,头结点就会变成第 
                           二结点
		                 //此时如果需要打印或者其他操作,开始的结点永远不是从插入的结点开始遍历
		NewNode->next = *phead;          //将新节点与插在头节点的前面
		*phead = NewNode;      //将新节点变为头结点,以便于后续的操作
	}
}

链表尾删:

链表尾删就是将链表的最后一个结点删除的操作,对于删除最后一个结点我们应该考虑问题:

1:链表是否为空

当链表为空的时候,你还要删除结点的话,那就会造成越界的问题,此时安排一个断言assert来警告用户

    assert(*phead != NULL);   //一旦为空节点的话,就直接报错,因为要是删除空节点就要越界

2:链表是否只有一个结点

当链表只有一个结点的时候,我们只需要将唯一的一个头结点直接free()、然后置空就好啦!

    if ((*phead)->next == NULL) {
        free(*phead);   //释放最初的结点
        *phead = NULL;   ///将唯一的结点置空
    } 

3:多个结点:

如果有多个结点的时候,我们如何删除最后的结点呢?

我们是不是应该先找到最后一个结点,然后将他free(),然后置空,我们可以来看一下这样做的后果是啥:

LinkList* cur = *phead;  //cur哨兵指针
        while (cur->next != NULL) {
            cur = cur->next;
        }
        //此时就找到尾结点cur了
        free(cur);     //此时的cur已经被系统回收了,前一个指针指向的已经是个野指针了
        cur = NULL; 

不是把40都删除了吗?怎么变成了随机值:

因为当我们free()的那一瞬间,此时的最后一个结点已经是随机值了(因为结点已经被释放了,属于这片内存的数据已经被系统回收了)。

于是我们发现这个方法不可行呀!

那么于是我们就有另外一个方法:

既然我们需要删除最后一个结点,那么如果我们让倒数第二个结点直接指向NULL,并且直接释放最后一个结点是不是更好,既删除了最后一个结点,还让此时的最后一个结点指向NULL。

我们让Front作为cur之前的结点,每次让cur往后移动,并且每次都让Front = cur,让Front每次都是在cur的前面,直到cur真的到了最后一个结点的时候,此时Front不就是倒数第二个结点了,直接让

Front->next = NULL;

再让

free(cur);

此时就是这样的情况:

代码如下:

void LinkListPopBack(LinkList** phead)   //链表尾删
{
	//1:看是否为空链表
	//if (*phead == NULL) {    //看链表指针指向的是不是空节点
	//	printf("空节点,不需要删除");
	//}
	assert(*phead != NULL);   //一旦为空节点的话,就直接报错,因为要是删除空节点就要越界
	//2:看是否只有一个结点
	if ((*phead)->next == NULL) {
		free(*phead);   //释放最初的结点
		*phead = NULL;   ///将唯一的结点置空
	}
	else {
		//3:找最后一个结点:其实应该是找倒数第二个结点,让倒数第二个结点直接指向空
		LinkList* Front = *phead;      //前结点指针
		LinkList* cur = Front;   //后结点指针
		while (cur->next != NULL) {   //找到最后一个结点
			Front = cur;   //让Front每次都在cur的前面
			cur = cur->next;
		}
		//此时出来的时候cur->next == NULL , Front->next = cur;
		free(Front->next);   //释放最后一个结点
		Front->next = NULL;  //也就是直接将倒数第二个结点的指针域指向NULL(也就是直接将最后一 
                               个结点删除)
	}
	
}

链表头删:

链表头删就是将头节点删除,但是其中还有一些细节大家得注意起来:

1:链表是否为空?

链表要是为空的话,那就说明链表不能被头删了。

    assert(*phead != NULL);   //如果是空链表就不能进行头删了

2:链表是否是一个结点?

 链表要是只有一个结点的话,那么我们要是删除结点的话,那就是直接释放了链表的头结点,让头结点直接指向NULL

3:要是有多个结点的话

多个结点的时候我们想要删除头结点,那我们在想一个问题?要是头结点被删除了,那么头指针该指向哪里呢?

对咯!那不就是第二个结点不是变成头结点了嘛,那我们就直接让头指针指向第二个结点。

此时下面是删除头结点之后的新链表:

此时我们已经删除了头结点,此时的第二个结点也就直接指向了头结点。

具体代码如下:

{
	//先检查是否为空链表
	assert(*phead != NULL);   //如果是空链表就不能进行头删了
	//如果只有一个头结点的话
	if ((*phead)->next == NULL) {
		free(*phead);    //直接回收
		*phead = NULL;   //然后置空
	}
	//下面是多个结点的
	else {
		//先找第二个结点准备变成第头结点
		LinkList* cur = (*phead)->next;   
		//然后释放第一个结点
		free(*phead);
		//让第二个结点变成头结点
		*phead = cur;

	}

}

链表结点查询:

结点查询就是在单链表中寻找我们关键数据的结点,只需要我们定义一个”哨兵”cur去遍历整个链表,要是找到了的话,我们就返回true,要是没有找到的话,那就返回false

bool LinkListFind(LinkList* phead, DataType x);  //结点查找

对于上述的bool类型的返回值,我们只需要调用库函数#include<stdbool.h>就行了

对于细心的同学就会看到,我们上述LinkListFind函数内部的形参好像是个一级指针,不是一直传递的是二级指针吗?为啥还传递一级指针?

因为我们要做的只是查找的动作,查找的动作只需要值传递

我们的目的就只是为了寻找到是否存在这个目标结点,不需要对链表做任何改变

当我们需要改变链表的时候,我们就会使用址传递,我们将形参和实参放在同一片内存空间进行修改。对于值传递和址传递不太了解的同学,请看——数据结构-(顺序表篇)——,这是我写的关于数据结构(顺序篇)的学习笔记。

代码如下:

bool LinkListFind(LinkList* phead, DataType x)  //结点查找
{
	LinkList* cur = phead;       //哨兵指针
	while (cur != NULL) {        //当cur一直遍历到NULL的时候,就停止
		if (cur->data == x) {      //如果找到了目标结点
			return true;       //返回true
		}
		cur = cur->next;   //一直让cur往后移动
	}
	return false;      //如果没有找到的话,那就返回false
}

当我们找到的时候,我们就返回true,当我们遍历完整个链表的时候,发现还是没找到的时候,我么就返回false,但是在主函数那端要是想要判断是否找到了,可以写一个if-else语句来判断。

上述只是LinkListFind接口的一种玩法,LinkListFind还有另外一种玩法:

我们使用链表结点的接收方式来接收传递的结点:

LinkList* LinkListFind(LinkList* phead, DataType x) {
	LinkList* cur = phead;       //哨兵cur用来遍历整个链表
	while (cur != NULL) {       //当cur没有遍历到NULL,就一直遍历
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;       //cur一直往后移动
	}
	return NULL;
}

当接收的是链表结点的时候,我们在主函数这边可以寻找到底有几个我们需要找的关键节点,并且还能改变该节点的数值。

具体代码如下:

LinkList* LinkListFind(LinkList* phead, DataType x) {
	LinkList* cur = phead;       //哨兵cur用来遍历整个链表
	while (cur != NULL) {       //当cur没有遍历到NULL,就一直遍历
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;       //cur一直往后移动
	}
	return NULL;
}

链表结点插入:

对于链表插入,我们要考虑以下几个问题:

1:得先有一个结点呀!

    //对于结点插入,我们得考虑几个问题,
    // 1:首先得有一个结点
    LinkList* NewNode = LinkListCreatNode(phead, x);

2:链表是否为空?

如果链表为空的话,我们就可以直接将新结点赋值给头结点。

//  因为此时pos是传递过来的链表指针,对指针进行判断
// 如果此时的pos的位置是空的话,也就是链表是空的话,直接将需要插入的结点赋值给头结点

if (pos == NULL) {
        *phead = NewNode;
    }

3:链表是否只有一个结点?

若链表只有一个结点的时候,我们想要在这个结点的原位置插入一个元素,那就等同于头插。

    // 如果有多个结点我们就需要找到我们需要找到的pos位置的前面,找到前面之后,让前一个结点的指针域指向NewNode,
    //    让NewNode的指针指向pos位置的结点,这样就成功地将NewNode插入了pos位置

else if (pos->next == NULL) {
        NewNode->next = *phead;      //
        *phead = NewNode;
    }

插入前:

 插入后:

4:若链表有很多的结点,我们所做的就是寻找pos的结点

此时我们定义一个“哨兵”cur来进行遍历,直到cur->next = pos的结点的时候,此时cur就是pos位置的前一个结点,插入进去不就是代替了pos位置的元素了嘛!

此时我们就要开始寻找pos位置(使用遍历cur的方式)

最开始:

直到遍历到cur->next = pos的位置:

 找到这个pos结点的时候,我们将cur->next = NewNode , 再将NewNode->next = pos

就完成了结点插入的操作。

代码如下:

void LinkListInsert(LinkList** phead, LinkList* pos, DataType x) {
	//对于结点插入,我们得考虑几个问题,
	// 1:首先得有一个结点
	LinkList* NewNode = LinkListCreatNode(phead, x);
	// 2: 因为此时pos是传递过来的链表指针,对指针进行判断
	// 3:如果此时的pos的位置是空的话,也就是链表是空的话,直接将需要插入的结点赋值给头结点
	if (pos == NULL) {
		*phead = NewNode;
	}
	// 4:如果有多个结点我们就需要找到我们需要找到的pos位置的前面,找到前面之后,让前一个结点的指 
          针域指向NewNode,
	//    让NewNode的指针指向pos位置的结点,这样就成功地将NewNode插入了pos位置
	else if (pos->next == NULL) {
		NewNode->next = *phead;
		*phead = NewNode;
	}else{
		LinkList* cur = *phead;
		while (cur->next != pos) {
			cur = cur->next;
		}
		//此时cur已经是pos位置的前一个结点了
		cur->next = NewNode;         
		NewNode->next = pos;
	}
}

还有!!!!!别走,还有另外一种插入方式:

那就是坐标插入法,我们只需要让指针移动的位置找到具体位置的结点,然后进行上述的插入也是可以实现结点插入的。

代码如下:

void SingleLinkListInsert2(SingleLinkList** ps, int pos, DataType x) {
	//先来查看这个单链表是不是空的
	SingleLinkList* NewNode = SingleLinkListCreatNode(x);
	if (pos == 0) {
		NewNode->next = *ps;
		*ps = NewNode;
	}
	else {
		//此时我们就得查看pos的位置是否合法了
		//若合法 , 就在pos位置插入元素 , 不合法的话 , 那就啥都别做
		int i = 0;
		SingleLinkList* cur = *ps;
		SingleLinkList* Front = *ps;
		while (i < pos) {
			Front = cur;
			cur = cur->next;
			i++;
			if (cur == NULL) {  		//结束之后要是cur的位置是空的了 , 那就说明pos位置根本就不合法 , 你开车都冲到天堂了 , 你说路在何方?
				printf("\npos位置不合法!\n");
				break;
			}
		}
			//此时已经找到了pos位置 , 那我们就插入这个位置
		if (cur != NULL) {
			NewNode->next = cur;
			Front->next = NewNode;
		}

	}
}

任意位置删除元素:

void SingleLinkListErase(SingleLinkList** ps , int pos);

我们要是想要在任意位置删除元素的话 , 那就必须保证你需要删除的元素的pos位置是有效的:

不能是如下:

其实上述的两种情况 , 我们都不了解 , 因为单链表没有size , 也就是单链表没有元素个数呀 , 我们得一个个数呀!

所以我们必须使用哨兵指针cur来帮我们扫描一下,朕的链表大军到底有多少兵力(结点数)?

让cur不断地向后移动 , 那么我们怎么知道什么时候扫描到pos位置呢?

此时我们就需要寻找我们的i来让他为我们计数 , 让我们知道我们的cur时候已经迭代到最后一个结点?

//默认去任意位置进行删除操作
void SingleLinkListErase(SingleLinkList** ps, int pos) {
	//首先看一下单链表是不是NULL
	assert(*ps);
	//在看一下单链表需要删除的元素的位置是否合理
	int i = 0;
	SingleLinkList* cur = *ps;
	SingleLinkList* Front = *ps;
	while (i < pos) {
		Front = cur;
		cur = cur->next;
		i++;
		if (cur == NULL) {
			printf("\npos位置不合法\n");
			break;
		}
	}
	if (cur != NULL) {
		Front->next = cur->next;
		free(cur);
	}
}

那么我们不想使用单链表了 , 我就想着把他直接销毁!!!!

这个销毁很有意思 , 有同学就会说了 ,我直接把头结点free了 , 他自己找不到其他的结点了 , 这不就直接销毁了吗 , 兄嘚!你要那么玩就寄了 , 内存得被你玩儿爆 , 都是垃圾数据了 , 我们秉持着创建一个销毁一个 , 争取不给内存添负担.

void SingleLinkListDestory(SingleLinkList** ps);  

此时我们看这段代码 , 其实本质上就是不断的迭代头结点 , 让头结点一直往后移动 , 每移动一次 ,  我们就把头结点free()交给内存,等到最后迭代完毕的时候 , 我们在将头结点置空

 

void SingleLinkListDestory(SingleLinkList** ps) {
	//先看看是不是空
	assert(*ps);  //要是空的话 , 就不用销毁了
	SingleLinkList* cur = *ps;
	while (cur) {   //
		SingleLinkList* After = (cur)->next;
		//想要销毁每一个结点 , 那就必须把每一个结点的后一个结点保留下来
		free(cur);
		cur = After;     //销毁前一个结点之后 , 就自动把后一个结点赋值到前一个结点上
	}
	*ps = NULL;  //等到最后就只剩下头结点了 , 此时直接将头结点置空
}


下面我给大家再来几道(寄道)LeetCode帮助大家更加深刻理解单链表:


 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值