单链表与双链表,常见的插入和删除

关于数据结构之一的链表的学习:

链表的概念及结构

概念:链表是一种 物理存储结构上非连续 、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的 指针链 接 次序实现的 。
这是较为书面的解释,这里我画个图解释一下:
在这里插入图片描述
1的位置是当前链表的起始位置,我们称之为表头,它里面放着的是第一个数据的地址(2),而2里面不仅有数据还有p2,p2指向的是第二个数据的位置,第二个数据里面不仅有2,同时里面放着p3,p3指向下一个数据的位置,而4这里最后的指针指向的是NULL,也就是说链表就到这里结束。

就像一条火车,里面的数据就是我们的车厢,而指针就是将数据链接起来的链条,所以我们称之为链表。
那我们链表是怎么写的呢

#define SLTDateType int

typedef struct SlistNode
{
	int data;
	struct SlistNode* next;
} SlistNode;//起别名= struct SlistNode

同样,我们先来一个结构体,data就是我们除非囊的数据,而第二个参数是next,也就是下一个结构体的地址,当我们需要放11个数据的时候,顺序表我们可能直接创建了一整块数组然后往里面放,但链表则是一次放一个数据到里面,然后根据需求再开辟一空间放下一个数据。

链表的功能实现

链表的插入

定义一个新的节点

void SlistPushBack(SlistNode** pphead, SlistDateType x) 
{
    SlistNode* newnode = (SlistNode*)malloc(sizeof(SlistNode));
    if (newnode == NULL) 
	{
        printf("malloc fail\n");
    } else 
	{
        newnode->data = x;
        newnode->next = NULL;
    }
}

这段代码是一个用于将元素插入单链表末尾的函数。
函数的定义是 void SlistPushBack(SlistNode** pphead, SlistDateType x),它接受两个参数:一个指向单链表头指针的指针 pphead 和要插入的元素值 x。

首先,代码中使用 malloc 函数为新节点分配内存空间 sizeof(SlistNode),并将返回的指针赋值给 newnode 变量。
然后,代码检查分配内存空间是否成功,如果成功则将新节点的 data 成员设置为传入的元素值 x,并将新节点的 next 成员指针设置为 NULL。

指向单链表头指针的参数 pphead 是一个指向指针的指针,使用双重指针的原因是为了能够修改原始指针的值。
在函数内部,通过操作*pphead,我们可以修改原始传入的单链表头指针的值。如果我们只使用单指针 *phead,那么在函数内部修改该指针的值将不会影响到原始的指针。

例如,如果我们只使用单指针 *phead,我们无法通过 phead 来修改原始指针的值。而使用双重指针 **pphead,我们可以通过 *pphead 修改原始指针的值。

因此,使用双重指针作为参数可以使函数能够修改原始指针的值,从而正确地将新节点插入到链表中。

SListNode* BuySLisenode(SLTDateType x)
{
	SListNode*newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
	}
	return newnode;
}

这段代码定义了一个函数 BuySLisenode,用于创建一个新的单链表节点,并返回该节点的指针。
函数的定义是 SListNode BuySLisenode(SLTDateType x)*,它接受一个参数 x,表示要存储在节点中的数据。
首先,代码使用 malloc 函数为新节点分配内存空间 sizeof(SListNode),然后将返回的指针赋值给 newnode 变量。
接着,代码检查分配内存空间是否成功,如果成功则将新节点的 data 成员设置为传入的数据值 x,并将新节点的 next 成员指针设置为 NULL。
最后,函数返回新创建的节点的指针,以便在其他地方使用该节点。
这个函数提供了一个方便的方法来创建新的单链表节点,并可以在需要时使用。

我们拿到要插入的数据,先计算空间大小,然后进行判断,如果指针为空指针我们就打印开括失败,如果成功,我们就将数据放到新开括空间的里面,然后将其下一个指针指向空,并返回这个这一块空间的地址。

尾插

然后我们直接调用这个函数即可,尾插我们看代码分析:

 void SListPushBack(SListNode** pphead, SLTDateType x)
{
	SListNode* newnode = BuySLisenode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode; //头指针为空,那么新插入的指针节点就赋值给头指针
	}
	else
	{
		SListNode*cur=*pphead;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

这段代码是一个单链表尾部插入节点的函数 SListPushBack。函数接受一个指向指针的指针 pphead,表示单链表的头指针,以及一个数据值 x,表示要插入的节点的数据值。
首先,代码调用了之前提到的 BuySLisenode() 函数来创建一个新的节点,将返回的节点指针赋值给 newnode
然后,代码检查传入的头指针 ** pphead* 是否为空。如果头指针为空,说明链表为空,即链表中没有节点,因此将新节点赋值给头指针 pphead,完成节点的插入。
如果头指针不为空,说明链表中已经存在节点,此时需要找到链表的末尾节点,然后将新节点插入到末尾。代码使用一个临时指针 cur 初始化为头指针 ** pphead
,然后通过循环遍历到链表的末尾节点,即指向 NULL 的节点。在循环中,每次将 cur 更新为当前节点的下一个节点,直到找到末尾节点。
最后,将新节点的地址赋值给末尾节点的 next 指针,完成节点的插入。
通过这样的方式,函数可以将新节点插入到单链表的末尾,实现尾部插入操作。

头插

、既然实现了尾插,那我们再实现一个头插,相较于顺序表的头插还要移动数据,链表就简单一点看图:
在这里插入图片描述
原本我的1指向2,现在我中间插入一个5,我只需要将1指向的位置变成5,然后5的指针指向2即可。
在这里插入图片描述

看代码:

 void SListPushFront(SListNode** pphead, SLTDateType x)
{
	SListNode* newnode = BuySLisenode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
}

同样,我们先创建一个新的结构体,然后给它赋值,随后我们还是先判断当前链表有没有数据,没有就直接赋值,如果有,那就将我们新创建的结构体的下一个地址赋值为之前的第一个数据的地址,也就是我们传进来的那个地址解引用,然后我们再将原本的指向第一个元素的地址指向我们新创建的节点。

实现效果如下:

int main()
{
	SlistNode* List =NULL;   
	SlistPushBack(&list,3);  //尾插
	SlistPrint(list);
	SlistPushFront(&list,2) //头插
	SlistPrint(list)
	system("pause);
}

打印结果为

3->NULL
2->3->NULL

指定位置插入

然后我们来一个难一点的,在指定位置插入,这里说难一点是因为我们要先找到指定的位置,然后再插入数据,但如果熟练适用以上两个插入,那这里其实也没那么难:
在这里插入图片描述
我原本是这样的结构,我现在要在2的位置插入一个结构,是不是要先找到2数据的位置,然后将它原本指向3的地址赋给我的新节点,然后将新节点的地址给它:
在这里插入图片描述
然后我们就可以开始写代码了:

 void SeqListInsert(SListNode** pphead, size_t pos, SLTDateType x)
{
	SListNode* newnode = BuySLisenode(x); //构建一个新的节点
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SListNode*cur = *pphead;
		while (pos--)
		{
			if (cur == NULL)
			{
				printf("位置错误\n");
			}
			else
			{
				cur = cur->next;
			}
		}
		newnode->next = cur->next;  //
		cur->next = newnode;
	}
}

cur->next的指向位置原本是指向下一个位置,结果现在赋值给新节点的nownode,使得新节点的next指向了原本遍历节点指向的下一个数据,而cur->next指向了newnode,从而实现指定位置插入了一个新的节点。
这是用while循环实现的结果,
在这里插入图片描述
从实现结果来看,在2的后面插入一个节点为1的节点。

节点的删除

尾删

同样,一进来我们先判断传进来的地址是否有内容,如果没有内容我们就可以直接打印链表为空,随后我们拿两个指针,第一个从第二个结构体开始,第二个从起始位置,因为我们要靠第一个来判断结构体指针是否为空,如果结构体不为空,我们就让第一个和第二个分别指向下一个结构体,如果第一个结构体走到指针为空的结构体,那第二个正好是它的前一个,我们将第二个的指针置为空,然后释放第一个指针的位置即可

void SListPopBack(SListNode** pphead)
{
	if (*pphead == NULL)
	{
		printf("链表为空\n");
	}
	SListNode*cur = (*pphead)->next;
	SListNode*perv = *pphead;
	if (cur == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (cur->next)
		{
			if (cur->next != NULL)
			{
				cur = cur->next;
				perv = perv->next;
			}
		}
		perv->next = NULL;
		free(cur);
	}
}

演示效果:
在这里插入图片描述

头删

头删说来也很简单,原本传进来的地址放着的是第一个结构体的地址,我们可以直接将第二个结构体的位置传给起始地址即可:
在这里插入图片描述

void SListPopFront(SListNode** pphead)
{
	if (*pphead == NULL)
	{
		printf("链表为空\n");
	}
	else
	{
		SListNode*cur = (*pphead)->next;  //cur指针记录第二个链表的位置
		free(*pphead);
		*pphead = cur;					//将第二链表的位置作为头指针	
	}
}

我们直接拿到第二个结构体的地址,然后销毁第一个结构体,随后将地址重新赋值即可,看效果
在这里插入图片描述

删除指定位置的值

这个实现其实就是一个复合,我们先找到位置,这里可以参考指定位置插入,然后删除结构体,删除我们也可以参考尾删,我们将其结合:

void SListEraseAfter(SListNode**pphead, size_t pos)
{
	if (*pphead == NULL)
	{
		printf("链表为空\n");
	}
	SListNode*cur = *pphead;
	if (pos)
	{
		while (pos--)
		{
			if (cur->next == NULL)
			{
				printf("位置错误\n");
			}
			else
			{
				cur = cur->next;
			}
		}
		SListNode*nex = cur->next;
		cur->next = nex->next;
		free(nex);
	}
	else
	{
		*pphead = (*pphead)->next;
		free(cur);
	}
}

同样,一进来我们就可以判断是否为空指针,如果为空我们打印错误,如果不为空,我们就创建一个结构体变量来帮助我们找指定位置的数据,找到了数据之后我们在删除之前要先借助它找到它之后结构体的地址,所以我们才有

SListNode*nex = cur->next; //创建一个指针保存当前cur指针指向的下一个节点的位置
cur->next = nex->next; //使当前指针指向新创建的指针的下一个节点的位置,也就是以当前节点cur的后两个节点位置
free(nex);

因为一旦你先删除,那这之后的所有结构体就再也找不到了,一定要先留下地址,再删除。

如果要删除0地址,那我们直接将第二个结构体的地址给地起始地址即可,别忘了释放创建的临时结构体。

查找

查找可能是最简单的,我们直接遍历,如果找到了数据我们就返回,没找到就返回-1,看代码:

SListNode* SListFind(SListNode* phead, SLTDateType x)  //遍历循环一直找到数据为X的节点
{
	if (phead == NULL)
	{
		printf("空链表\n");
	}
	SListNode* cur = phead;   
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return -1;
}

销毁

void SListDestory(SListNode* phead)
{
	SListNode*cur = phead;
	SListNode*nex = phead;
	while (cur)
	{
		cur = cur->next;
		free(nex);
		nex = cur;
	}
}

我们先创建两个结构体,第一个指向第二个数据,第二个指向第一个数据,我们先删除第一个数据,然后将第一个的值赋给第二个,再销毁第二个,循环到最后为空指针就可以结束了。

修改

修改函数,能将指定位置的值修改,原理就是找到位置然后修改,原理没变:

void SListrevise(SListNode* phead, size_t pos, int x)
{
	if (phead == NULL)
	{
		printf("链表为空\n");
	}
	else
	{
		SListNode*cur = phead;
		while (--pos)
		{
			if (cur == NULL)
			{
				printf("位置错误\n");
			}
			else
			{
				cur = cur->next;
			}
		}
		cur->data = x;
	}
}

效果:
在这里插入图片描述
这里是pos–,可以看到修改的是第二个值。
在这里插入图片描述
同样的代码,这里是–pos,修改的是第一个值,注意一下就好
在这里插入图片描述

带头双向循环链表、

事前准备:

我们需要一个创建一个结构体类型,并将里面的一些类型重定义名字,如下:

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

我们还要知道双向带头循环链表是什么样的,遍历到最后一个元素后,自动返回头节点:
在这里插入图片描述
如何开辟新的节点和初始化链表
我们知道,在链表中尾插,或者头插,你都必须先创建一个结构体,因为是双向带头循环的链表,所以这个链表必须有以下几条:

1、我们存放的内容

2、下一个结构体的地址

3、上一个结构体的地址

4、如果链表仅有表头,那么表头的下一个元素和上一个元素必须指向自己

了解了条件,我们就来初始化我们链表:

void ListInit(ListNode** pphead)
{
	assert(pphead);
	*pphead = BuyLTNode(0);
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;

}
然后我们来看看如何创建新的节点:

跟单链表一样,我们开辟一块空间,将里面的值改成我们需要插入的值,然后再将其指针置为NULL,返回即可:

ListNode* BuyListNode(int x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		printf("malloc false\n");
		exit(-1);
	}
		newnode->_data = x;
		newnode->prev = NULL;
		newnode->next = NULL;
		return newnode;
}

要改变什么,我们就传什么。

这里需不需要传二级指针过去呢?

我们会不会修改到这个头节点呢?

不会,我们只需要修改的是头节点里面指向下一个元素和上一个元素的地址,我们返回的还是头节点的位置,所以我们可以不用传二级指针过去。

所以我们可以将初始化函数这么写:

ListNode* ListCreate()
{
	ListNode* newnode = BuyListNode(0);
	newnode->prev = newnode;
	newnode->next = newnode;
	return newnode;
}

双向链表的打印

我们如何打印这个链表?

跟单链表一样,单链表我们什么时候停下来?

当遇到空,也就是说链表走完了一遍就可以停下来,那双向带头循环链表呢?

我们从表头的下一个元素走,比方说:
在这里插入图片描述

我们让一个指针cur开从表头往后走,每遇到一个值就打印,正因为循环,所以它到 5 的时候会回到表头,也就是 0 的位置,然后我们判断,如果 cur == 表头,那我们就可以停下来。
在这里插入图片描述

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur!= pHead)
	{
		printf("%d->", cur->_data);
		cur = cur->next;
	}
	printf("end\n");
}

双向链表尾插

我们知道这是一个双向循环链表,我们尾插的时候还需要注意几点;

1、最后的结构体的下一个指针应该指向表头

2、表头的上一个结构体指针应该指向最后一个结构体

我们尾插的时候,先要找到没修改前的最后一个结构体,然后得到它的地址,再让它的下一个元素指向新的结构体地址,然后再将新结构体的上一个结构体指针指向它,再修改新节点的next,头节点的prev。

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = BuyListNode(x);
	ListNode* prev = pHead->prev;
	prev->next = newnode;
	newnode->prev = prev;
 
	newnode->next = pHead;
	pHead->prev = newnode;
}

具体文字操作解释:

我们先让prev指向头节点的上一个元素,这里正是我们尾节点的位置,然后我们原本尾节点的下一个指针指向我们新节点,我们新节点的上一个指针指向我们原本的尾节点。

我们新节点的下一个指针应该指向表头,也就是pHead,而pHead表头的上一个结构体应该是尾节点,我们同样赋值过去。

双向链表尾删

单链表的尾删我们拿两个指针遍历数组找最后一个元素,双向链表就不需要遍历了,因为表头的上一个结构体正是我们的尾节点,我们拿一个指针记录尾戒点的上一个结构体,然后释放尾节点,再将尾节点释放,代码如下:

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	ListNode* prev = pHead->prev->prev;
	ListNode* cur = pHead->prev;
	if (pHead->next == cur)
	{
		printf("NULL");
		return;
	}
	else
	{
		prev->next = pHead;
		pHead->prev = prev;
		free(cur);
		cur = NULL;
	}
}

具体文字解释:

两个指针,一个指向尾节点,一个指向尾节点的上一个结构体,因为靠尾节点我们才找得到倒数第二个。

先判断链表有没有值,没有就返回,有就继续,我们让倒数第二个节点的下一个指针指向我们的表头, next = pHead,再将表头的上一个结构体指针指向倒数第二个元素,也就是pHead->prev = prev,不需要返回值。

双向链表头插

就跟单链表的头插一样,只不过我们多加了个prev(链表的上一个结构体指针)而已,多赋一个值即可,但是要注意顺序,别死循环了

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = BuyListNode(x);
	ListNode* next = pHead->next;
	pHead->next = newnode;
	newnode->prev = pHead;
	newnode->next = next;
	return;
}

具体文字解释:

先创建一个指针指向原本第一个结构体,因为要在它之前插入新的结构体,所以地址先存一下

新结构体的下一个结构体指针指向原本的第一个结构体

表头的下一个结构体指针指向新开辟的节点

新开辟的节点的下一个结构体指针指向原本的第一个结构体.

双向链表头删

既然你知道头插,也知道尾删,那头删就已经掌握了大半,看代码:

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("NULL\n");
		return;
	}
	ListNode* cur = pHead->next;
	ListNode* next = cur->next;
	free(cur);
	cur = NULL;
	pHead->next = next;
	next->prev = pHead;
	return;
}

具体文字解释:

新创建的结构体指针cur指向原本的第一个结构体,

next指向第二个结构体

释放cur(第一个结构体)

头节点的下一个结构体指针指向next(原本的第二个元素)

next的上一个结构体指针指向头节点.

双向链表查找

跟单链表一样,我们找链表里面的值判断,如果不是就继续走,如果走到了表头,我们就停下。看代码:

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	if (pHead->next == pHead)
	{
		printf("NULL\n");
		return NULL;
	}
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->_data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

具体文字解释:

新创建的节点cur指向表头的下一个结构体

遍历整个链表判断,如果cur不等于pHead,也就是说链表还有元素,就执行

我们比较cur里面的值,是就返回当前结构体的地址,不是就指向下一个结构体

如果遍历完还没找到,那就返回空指针

双向链表在pos的前面进行插入

创建新的节点,再将这个节点与之前的节点链接起来即可:

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
 
	newnode->prev = prev;
	pos->prev = newnode;
	newnode->next = pos;
	prev->next = newnode;
	return;
}

具体文字解释:

新创建的节点prev指向我们插入节点的前一个元素

我们先让新节点的上一个元素指向prev

再将插入节点位置的pos的上一个指针指向新节点

新节点的下一个指针指向pos

prev的下一个指针指向新节点

删除指定节点

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	free(pos);
	pos = NULL;
	prev->next = next;
	next->prev = prev;
	return;
}

文字详细解释:

prev指向删除节点的前一个元素

next指向删除节点的后一个元素

释放删除节点

链接prev和next

销毁链表

void ListDestory(ListNode* pHead)
{
	ListNode*cur = pHead;
	ListNode*nex = pHead;
	while (cur)
	{
		cur = cur->next;
		free(nex);
		nex = cur;
![请添加图片描述](https://img-blog.csdnimg.cn/direct/fb2206ac6b664964b7b2d680835013c2.jpeg)
	}
 
}

跟单链表一样,每次删除一个并保留下一个的地址,如果为空就删完了。

.

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值