手撕单链表(单向,不循环,不带头结点)的基本操作

!!‧✧̣̥̇‧✦‧✧̣̥̇‧✦ ‧✧̣̥̇:Solitary-walk

      ⸝⋆   ━━━┓
     - 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━  ➴ ⷯ

本人座右铭 :   欲达高峰,必忍其痛;欲戴王冠,必承其重。

 


     希望在看完我的此篇博客后可以对你有帮助哟

  此外,希望各位大佬们在看完后,可以互赞互关一下,看到必回

 目录:

前言:对于单链表的基本操作重在考验大家对C语言指针的底子

一:传值传参区别

二:尾插

三:头插

四:尾删

五:头删

六:指定数据的查找

七:指定位置之前的删除

八:指定位置之后的删除

九:任意位置之前的插入

十:任意位置之后的插入

结语


 一:传值传参区别

这里就拿一个比较经典的问题来引入吧!

   写一个函数实现2个数 的交换

 手撕单链表(单向,不循环,不带头结点)的基本操作_第1张图片

对于刚刚接触编程的铁子们,对这个结果 可能存在很大的疑惑

手撕单链表(单向,不循环,不带头结点)的基本操作_第2张图片

不慌不忙,接下来我慢慢给大家解释 

int a = 1, b = 2;
    int* p = &a;
    *p = 3;

想必大家对这个代码应该不会很陌生吧。

此时我们对指针p进行解引用拿到的就是变量 a 

也就是说,此时我们通过借助指针实现了对a   的改变

同理,这里我在调用Swap( )这个函数的时候,是不是进行传地址就可以实现对2个数的交换?

话不多说,接下来我们代码实现

 手撕单链表(单向,不循环,不带头结点)的基本操作_第3张图片

 是滴,此时确实实现了2个数的交换

分析:

1)传参的本质:形参是对实参的一份临时拷贝,对形参的临时修改不会影响实参

2)所以说:当需要对变量进行改变的时候,我们就需要传对应的地址就可以

如何理解“ 传对应的地址”

比如说:

      改变int 类型的变量,这时就需要传int*的指针(地址)    

     改变int *类型的变量,这时就需要传int**的指针(地址)    

     改变结构体类型的变量,这时就需要传结构体的指针(地址)

 二:尾插

分析:

1)首先为要插入进来的数据开辟结点

2)链表不为空的时候:注意此时要改变的是结构体

         首先 先找到尾结点( 链表最后一个结点的next为空)

         其次进行尾插

3)链表为空:注意此时改变的是结构体类型的指针(头节点为空)

         直接进行插入即可

草图如下:

对非空的链表插入前:

手撕单链表(单向,不循环,不带头结点)的基本操作_第4张图片

 插入后:

手撕单链表(单向,不循环,不带头结点)的基本操作_第5张图片

   接下来可是重头戏,好好看,一不仔细,就错失了,那可就不好理解了,避免这个“瓜”没有吃到,反而懊恼不已

 1)开辟结点为插入的数据:
因为之后插入需要频繁开辟结点,所以这里写成了一个函数
SLNode* BuyNode(DataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		return NULL;
	}
	// 对开辟的结点进行初始化
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
2)找尾结点:这里就依次遍历即可

注意啦:看看这样写对不???

SLNode* ptail = phead;
    while (ptail)
    {
        ptail = ptail->next;
    }
    ptail->next = newnode;

NO,NO,NO

乍一看,看不出啥问题,这就是“码图”结合了

这里以逻辑结构(人类的思维,为了形象化的理解)来解释

逻辑图:

 手撕单链表(单向,不循环,不带头结点)的基本操作_第6张图片

   当ptail这个指针指向3这个结点的时候,是不为空的,所以就继续进入我的循环里,此时的尾结点就变成了NULL这个对应当结点,下面在进行插入自然也是不能把新的结点和我原来链表进行有效的连接起来

找尾结点的正确代码:

 while (ptail->next)
    {
        ptail = ptail->next;
    }
    ptail->next = newnode;

 3)当链表为空的时候我们发现以上代码不可取:

因为此时实参plist  传给我的phead这个形参就是一份临时拷贝,我对phead(结构体指针)改变不影响plist(结构体指针)的变化

有了前面那个传值传参的引入,想必大家此时应该有了见解了吧

没错!

就是实参传结构体指针的地址,形参用二级指针

 对应完整代码:

void SLPushBack(SLNode* *phead, DataType x)
{
	/*
	1:开辟结点
	2:判读是否为空的链表
	3:非空:找到尾结点;此时改变的是结构体,需要传结构体的地址
	4:为空:直接插入:  因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址,涉及到二级指针
	*/
	SLNode* newnode = BuyNode(x);
	if (*phead == NULL)  //为空
	{
		newnode->next = *phead;// 对*phead解引用 就是plist这个实参
		*phead = newnode;
		return;
	}
	//非空
	/*  找尾结点:err
	SLNode* ptail = phead;
	while (ptail)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;

	*/
	SLNode* ptail = *phead;
	while (ptail->next)  //找尾结点
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
	/*
	当链表为空的时候,以上代码有问题
	为空的时候需要对头节点进行改变,注意头节点是结构体类型指针所以需要传地址
	*/

}

 三:头插

分析:

1) 首先为插入数据开辟结点

2)因为此时改变的是结构体指针(plist),所以需要传入结构体指针的地址

void SLPushFront(SLNode** phead, DataType x)
{
	/*
	1:为x开辟结点
	2:更新头节点
	3:因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址
	传参的本质是:拷贝:形参是对实参的一份临时拷贝,对形参的修改不会影响我实参的变化
	*/
	SLNode* newnode = BuyNode(x);
	newnode->next = *phead;// 对*phead解引用 就是plist这个实参
	*phead = newnode;
}

四:尾删

分析:

1)首先判断链表是否为空;为空不需删除

2)其次:判断链表是否为一个结点;因为此时改变的头节点(结构体指针);那就涉及到了传结构体指针的地址

3)最后就是多个结点的情况:

      先找尾结点

       删除尾结点

 1)先从正常情况说起(多个结点)

 找尾结点:这里需要找到尾结点的前一个结点,避免free(ptail)时找不到新的尾结点

手撕单链表(单向,不循环,不带头结点)的基本操作_第7张图片

 尾删后:

手撕单链表(单向,不循环,不带头结点)的基本操作_第8张图片

SLNode* ptail = *phead;
	SLNode* pre =* phead;
	while (ptail->next)  //找尾结点
	{
		pre = ptail;//保存尾结点的前一个结点
		ptail = ptail->next;
	}
	free(ptail);
	ptail = pre;//尾结点更新
	ptail->next = NULL;//不要忘了置空
2)只有一个结点

注意这里需要传入结构体指针的地址

if ((*phead)->next == NULL)  //一个结点,注意*与->优先级
	{
		free(*phead);
		*phead = NULL;
		return;
	}
 3)判空

直接暴力检查即可:

assert(*phead);

 对应完整代码:

void SLPopBack(SLNode** phead)
{
	/*
	1:判断是否为空
	2:判断是否为一个结点:因为此时改变的是头节点(结构体指针)
	3:找到尾结点,此时尾结点的前一个结点成为新的结点
	4:  *phead 就是头指针  plist
	*/
	//为空:
	assert(*phead);
	
	if ((*phead)->next == NULL)  //一个结点,注意*与->优先级
	{
		free(*phead);
		*phead = NULL;
		return;
	}
	// 非空
	SLNode* ptail = *phead;
	SLNode* pre =* phead;
	while (ptail->next)  //找尾结点
	{
		pre = ptail;//保存尾结点的前一个结点
		ptail = ptail->next;
	}
	free(ptail);
	ptail = pre;//尾结点更新
	ptail->next = NULL;//不要忘了置空


}

五:头删

相信有了前面的尾删,我们对头删那便是轻轻松拿捏了

1)判空

2)非空

 1)判空

assert(*phead);  //直接暴力检查

2)非空:  删除头节点之前需要保存一下

对应完整代码:

void SLPopFront(SLNode** phead)
{
	/*
	1:判是否为空 
	2:非空:删除头节点之前需要保存一下第二个结点
	*/
	assert(*phead);//为空
	SLNode* psec = (*phead)->next;//保存第二个结点
	free(*phead);
	*phead = psec;//更新


}
六:指定数据查找

1:若是当前数据存在,则返回对应的结点;否则返回NULL

2:依次遍历

相信有了前面的基础,我们对这个区区查找的代码轻轻松拿下

SLNode* SLFind(SLNode* phead, DataType x)
{
	/*
	若是找到返回该节点
	循环遍历
	*/
	SLNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;//返回节点
		else
			pcur = pcur->next;//更新
	}
	return NULL;
}
七:指定位置之前的删除 

分析:

假设对pos这个位置之前的进行删除

1:pos若是为头节点,则不需要删除

2:pos为第二个结点,其实就是进行头删的操作,注意此时改变的是头节点(结构体指针),所以需要传二级指针

3:正常情况:找到pos前一个结点

 1:pos为头节点

直接暴力断言,就像当你作业还没有写完,但你依然再玩游戏此时你的父亲突然过来问你,作业写完了吗,你回答到:没有。你父亲直接就是一顿说,此时你就乖乖去写作业了

assert(*phead != pos);

2:pos 为第二个结点

if ((*phead)->next == pos)//pos为第二个结点
    {
        free(*phead);
        *phead = pos;//pos是新的头节点
    }

3:正常情况
SLNode* pre = *phead;
		while (pre->next->next != pos)
		{
			pre = pre->next;
		}
		free(pre->next);
		pre->next = pos;

对应完整代码: 

void SLEarseBefore(SLNode** phead, SLNode* pos)
{
	/*
	1:pos为头节点是不可以删除的
	2:pos为第二个结点,此时要删除的是头节点,改变的是结构体指针(phead),需要二级指针
	3:正常情况,找到pos前一个结点
	*/
	assert(pos != *phead);//保证pos不为头节点
	if ((*phead)->next == pos)//pos为第二个结点
	{
		free(*phead);
		*phead = pos;//pos是新的头节点
	}
	else
	{
		SLNode* pre = *phead;
		while (pre->next->next != pos)
		{
			pre = pre->next;
		}
		free(pre->next);
		pre->next = pos;
	}
}
八:指定位置之后的删除

分析:假设要删除的位置是pos

    1:pos为最后一个结点;没有必要删除
    2:pos不为最后一个结点

对应代码:

void SLEarseAfter(SLNode** phead, SLNode* pos)
{
	/*
	1:pos为最后一个结点;没有必要删除
	2:pos不为最后一个结点
	*/
	assert(pos->next != NULL);//暴力判断是否为最后一个
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
九:任意位置之前的插入

假设任意位置为pos

1:pos为头节点:可以借助头插的函数进行,注意此时改变的是头节点(结构体指针),要传结构体指针的地址

2:pos不是头节点:需要找到pos的前一个结点

3:为要插入的数据开辟结点

 对应代码:

void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x)
{
	/*
	*1:开辟结点
	2:找到pos前面的结点(pos不是头节点)
	3:pos是头节点此时变成头插
	*/
	SLNode* newnode = BuyNode(x);
	if (pos == *phead)
	{
		SLPushFront(phead, x);
		return;
	}
	else
	{
		SLNode* pre = *phead;
		while (pre->next != pos)
		{
			pre = pre->next;
		}
		//插入
		pre->next = newnode;
		newnode->next = pos;
	}
}
十:任意位置之后的插入

分析:假设此位置是pos

1:开辟结点

2:保存pos后面的那个结点SLNode* p =  pos->next

3: 直接插入

void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x)
{
	/*
	1:开辟结点
	2:保存一下pos后面的那结点(否则会连不上)
	3:直接插入
	*/
	SLNode* newnode = BuyNode(x);
	SLNode* p = pos->next;//保存pos后的结点
	//插入
	pos->next = newnode;
	newnode->next = p;
}

 整个单链表完整代码:

SList.h
#pragma once
#include
#include
#include

typedef int DataType;
typedef struct SListNode
{
	DataType data;//数据域
	struct SListNode* next;//指针域
}SLNode;

void SLPrint(SLNode* phead);
void SLPushFront(SLNode** phead, DataType x);
void SLPushBack(SLNode** phead, DataType x);
void SLPopBack(SLNode** phead);
void SLPopFront(SLNode** phead); 
SLNode* SLFind(SLNode* phead, DataType x);//对指定数据进行查找
void SLModify(SLNode* phead, SLNode*pos,DataType x);

void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x);//在指定数据之前插入
void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x);//在指定数据之前插入

void SLEarseBefore(SLNode** phead, SLNode* pos);//任意位置之前的删除
void SLEarseAfter(SLNode** phead, SLNode* pos);//任意位置之后的删除


SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

void SLPrint(SLNode* phead)
{
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;//更新
	}
	printf("NULL\n");
}
SLNode* BuyNode(DataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		return NULL;
	}
	// 对开辟的结点进行初始化
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SLPushFront(SLNode** phead, DataType x)
{
	/*
	1:为x开辟结点
	2:更新头节点
	3:因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址
	传参的本质是:拷贝:形参是对实参的一份临时拷贝,对形参的修改不会影响我实参的变化
	*/
	SLNode* newnode = BuyNode(x);
	newnode->next = *phead;// 对*phead解引用 就是plist这个实参
	*phead = newnode;
}
void SLPushBack(SLNode* *phead, DataType x)
{
	/*
	1:开辟结点
	2:判读是否为空的链表
	3:非空:找到尾结点;此时改变的是结构体,需要传结构体的地址
	4:为空:直接插入:  因为改变的是结构体类型的指针,所有需要传结构体类型的指针的地址,涉及到二级指针
	*/
	SLNode* newnode = BuyNode(x);
	if (*phead == NULL)  //为空
	{
		newnode->next = *phead;// 对*phead解引用 就是plist这个实参
		*phead = newnode;
		return;
	}
	//非空
	/*  找尾结点:err
	SLNode* ptail = phead;
	while (ptail)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;

	*/
	SLNode* ptail = *phead;
	while (ptail->next)  //找尾结点
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
	/*
	当链表为空的时候,以上代码有问题
	为空的时候需要对头节点进行改变,注意头节点是结构体类型指针所以需要传地址
	*/

}
void SLPopBack(SLNode** phead)
{
	/*
	1:判断是否为空
	2:判断是否为一个结点:因为此时改变的是头节点(结构体指针)
	3:找到尾结点,此时尾结点的前一个结点成为新的结点
	4:  *phead 就是头指针  plist
	*/
	//为空:
	assert(*phead);
	
	if ((*phead)->next == NULL)  //一个结点,注意*与->优先级
	{
		free(*phead);
		*phead = NULL;
		return;
	}
	// 非空
	SLNode* ptail = *phead;
	SLNode* pre =* phead;
	while (ptail->next)  //找尾结点
	{
		pre = ptail;//保存尾结点的前一个结点
		ptail = ptail->next;
	}
	free(ptail);
	ptail = pre;//尾结点更新
	ptail->next = NULL;//不用忘了置空
	
	//对一个与多个节点的操作可以合并
	//只要找到倒数第二个结点就可以
	/*SLNode* ptail = *phead;

	while (ptail->next->next)
	{
		ptail = ptail->next;
	}
	free(ptail->next);
	ptail->next = NULL;*/

}
void SLPopFront(SLNode** phead)
{
	/*
	1:判是否为空 
	2:非空:删除头节点之前需要保存一下第二个结点
	*/
	assert(*phead);//为空
	SLNode* psec = (*phead)->next;//保存第二个结点
	free(*phead);
	*phead = psec;//更新
}
SLNode* SLFind(SLNode* phead, DataType x)
{
	/*
	若是找到返回该节点
	循环遍历
	*/
	SLNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;//返回节点
		else
			pcur = pcur->next;//更新
	}
	return NULL;
}
void SLInsertBefore(SLNode** phead, SLNode* pos, DataType x)
{
	/*
	*1:开辟结点
	2:找到pos前面的结点(pos不是头节点)
	3:pos是头节点此时变成头插
	*/
	SLNode* newnode = BuyNode(x);
	if (pos == *phead)
	{
		SLPushFront(phead, x);
		return;
	}
	else
	{
		SLNode* pre = *phead;
		while (pre->next != pos)
		{
			pre = pre->next;
		}
		//插入
		pre->next = newnode;
		newnode->next = pos;
	}
}
void SLInsertAfter(SLNode* phead, SLNode* pos, DataType x)
{
	/*
	1:开辟结点
	2:保存一下pos后面的那结点(否则会连不上)
	3:直接插入
	*/
	SLNode* newnode = BuyNode(x);
	SLNode* p = pos->next;//保存pos后的结点
	//插入
	pos->next = newnode;
	newnode->next = p;
}
void SLEarseBefore(SLNode** phead, SLNode* pos)
{
	/*
	1:pos为头节点是不可以删除的
	2:pos为第二个结点,此时要删除的是头节点,改变的是结构体指针(phead),需要二级指针
	3:正常情况,找到pos前一个结点
	*/
	assert(pos != *phead);//保证pos不为头节点
	if ((*phead)->next == pos)//pos为第二个结点
	{
		free(*phead);
		*phead = pos;//pos是新的头节点
	}
	else
	{
		SLNode* pre = *phead;
		while (pre->next->next != pos)
		{
			pre = pre->next;
		}
		free(pre->next);
		pre->next = pos;
		SLNode* pre = *phead;
		while (pre->next->next != pos)
		{
			pre = pre->next;
		}
		free(pre->next);
		pre->next = pos;
	}
}
void SLEarseAfter(SLNode** phead, SLNode* pos)
{
	/*
	1:pos为最后一个结点;没有必要删除
	2:pos不为最后一个结点
	*/
	assert(pos->next != NULL);//暴力判断是否为最后一个
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
void SLModify(SLNode* phead, SLNode* pos, DataType x)
{
	assert(phead);
	pos->data = x;

}


test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

void TestPush()
{
	SLNode* plist = NULL;
	//SLPushFront(&plist,1);
	//SLPushFront(&plist,2);
	//SLPushFront(&plist,3);
	//SLPrint(plist);
	SLPushBack(&plist, 4);
	SLPushBack(&plist, 5);
	SLPrint(plist);

}
void TestPop()
{
	SLNode* plist = NULL;
	SLPushBack(&plist, 4);
	SLPushBack(&plist, 5);
	SLPushBack(&plist, 6);
	SLPushBack(&plist, 7);
	SLPrint(plist);
	SLNode* pos = SLFind(plist, 7);
	if(pos)//避免pos为空
		SLInsertAfter(plist, pos, 44);
	SLPrint(plist);

	/*SLPopBack(&plist);
	SLPrint(plist);

	SLPopBack(&plist);*/
	/*SLPopFront(&plist);
	SLPrint(plist);

	SLPopFront(&plist);
	SLPrint(plist);*/


}
void TestEarse()
{
	SLNode* plist = NULL;
	SLPushBack(&plist, 4);
	SLPushBack(&plist, 5);
	SLPushBack(&plist, 6);
	SLPushBack(&plist, 7);
	SLPrint(plist);
	SLNode* pos = SLFind(plist, 7);
	if (pos)//避免pos为空
		//SLEarseAfter(&plist, pos);
		SLModify(plist, pos, 77);
	SLPrint(plist);
}

void Swap(int *x, int* y)
{
	int tmp = *x;//中间变量
	*x = *y;
	*y = tmp;
}
int main()
{
	TestEarse();
	
	return 0;
	/*
	总结:
	1:是指针不一定必须断言,是否断言取决于你的操作
	2:在函数外面改变变量,需要传地址,想改变谁,就传对应类型的地址
	3:对于链表这块一定注意自己要改变的是结构体还是结构体指针???因为这决定了传的地址类型不一样
	4:找
	*/
}

结语:

对于初学单链表的小白来讲(比如我本人,哈哈哈),这个理解起来确实不是那么顺手,其实重点在指针和结构体的掌握。当然自己也需要反复的体会其中的奥妙,

都看到这里了,屏幕前的你,咱一波关注走起呗,你的支持是我不懈的动力,蟹蟹

你可能感兴趣的:(数据结构,数据结构,链表,c语言)