数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解

目录

1.与顺序表的对比

2.链表

2.2链表的概念和结构

 3.2 链表的分类

 3.3接口以及实现

LinkList.h

LinkList.c

3.3.1链表的打印

3.3.2获取一个新节点

3.3.3获取的链表长度

3.3.4单链表查找

3.3.4销毁链表

3.3.5链表删除

尾删

头删

pos位置之后的值

3.3.6链表增加

尾插

头插

pos之后插入

pos之前插入


1.与顺序表的对比

书接上回!我们修炼了顺序表的内功,但是在很多使用场景,他都很受限制!

缺点:

  1. 插入删除的时候,还记得吗!我们都要搬运元素到指定位置,时间复杂度是O(N).
  2. 你的顺序表容量是提前给的,扩容的时候还要拷贝数据,开辟新空间,释放旧空间,会有很大的性能消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。若每次扩容扩少一点,则会导致频繁扩容,频繁扩容同样也会有性能消耗。

优点:

  1. 存储空间是一段连续的内存空间,我们可以通过下标随机访问

优劣结合,这本功法会有很大的内耗!顺序表不能按需申请和释放空间,他是连续的内存空间!他的删除也是会在搬运元素,无法做到存一个数据申请一块空间,删除一个数据释放一块空间。

2.链表

我向师傅询问这顺序表的内耗是用何许方法解决的?师傅敲敲我的头指向了另外的一本功法《C语言程序设计》 是指针! 我要加指针!

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第1张图片

2.2链表的概念和结构

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

链表的结构很像大手拉小手,我的大手就相当于上一个节点指针域,小手就是下一个节点的地址。
图解

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第2张图片

代码解释 

void Text() {
	Node* LinkList = NULL;
	Node* Node1 = malloc(sizeof(Node));
	Node* Node2 = malloc(sizeof(Node));
	Node* Node3 = malloc(sizeof(Node));
	Node1->data = 1;
	Node2->data = 2;
	Node3->data = 3;
	Node1->next = Node2;
	Node2->next = Node3;
	Node3->next = NULL;
	LinkList = Node1;
	return 0;
}

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第3张图片

这个时候链表第一个节点的指针域还没有指向下一个节点

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第4张图片

就把值赋过去了,大手拉小手!从上图可以看出链表在物理上不是连续的,在逻辑上是连续的。

节点都是malloc从堆上申请出来的。

 3.2 链表的分类

1. 单向或者双向

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第5张图片

2. 带头或者不带头

 

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第6张图片

 3. 循环或者非循环

数据结构 严薇敏 单链表(无头结点)的实现(增 删 改 查)及其使用方法详解_第7张图片

 3.3接口以及实现

直接上代码

LinkList.h

#pragma once
#include
#include
#include

typedef int Datatype;

typedef struct LinkList {
	struct LinkList* next;
	Datatype data;
}Node;

//打印链表
void PrintLinkList(Node* plist);

//获取一个新节点
Node* LinkListCreat(Datatype data);

//单链表尾插
void LinkListPushBack(Node** pplist, Datatype data);

//单链表的尾删
void LinkListPopBack(Node** pplist);

//单链表的头插
void LinkListPushFront(Node** pplist, Datatype data);

//单链表头删
void LinkListPopFront(Node** pplist);

//单链表查找
Node* LinkListFind(Node* plist, Datatype data);

//单链表在pos位置之前插入x
void LinkListInsertBefore(Node** pplist, Node* pos, Datatype data);

//单链表在pos位置之后插入x
void LinkListInsertAfter(Node* pos, Datatype data);

//单链表删除pos位置之后的值
void LinkListEraseAfter(Node* pos);

//获取的链表长度
int LinkListSize(Node* plist);

//销毁链表
void LinkListDestroy(Node** pplist);

LinkList.c

#include "LinkList.h"
//打印链表
void PrintLinkList(Node* plist)
{
	Node* cur = plist;
	//遍历链表输出
	while (cur)
	{
		printf("%d-->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
//获取一个新节点
Node* LinkListCreat(Datatype data)
{
	//开辟空间获取一个节点的大小
	Node* Newnode = (Node*)malloc(sizeof(Node));
	//malloc之后要判空是否申请成功
	if (Newnode == NULL)
	{
		assert(0);
		return NULL;
	}
	//新节点的下一个还没链接那就置空
	Newnode->next = NULL;
	Newnode->data = data;
	return Newnode;
}

// 单链表尾插
void LinkListPushBack(Node** pplist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//我们的遍历指针既然是要尾插那我们就要先找到最后那个
	Node* cur = *pplist;
	//用刚刚写的那个创建节点的方法取到新节点的地址
	Node* NewNode= LinkListCreat(data);
	//一个节点都没有
	//这个NewNode给到操作链表的指针
	if (NULL == *pplist) {
		*pplist = NewNode;
		return;
	}
	//里面就有节点我们直接找到最后一个节点
	//把新节点的地址交给最后一个节点的next
	else
	{
		while (cur->next) 
		{
			cur = cur->next;
		}
		cur->next = NewNode;
	}
	
}

// 单链表的尾删
void LinkListPopBack(Node** pplist)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//遍历指针
	Node* cur = *pplist;
	//这个用来存上一个节点地址
	Node* prev = NULL;
	//空链表情况
	if (NULL == *pplist)
	{
		printf("你的链表为空!");
		return;
	}
	//链表只有有个节点的情况
	else if (NULL == *(pplist)->next)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//开始遍历找到尾节点
	else
	{
		while (cur) 
		{
			//把这个节点先存储下来
			prev = cur;
			cur = cur->next;
		}
		free(cur);
		//让他下一个节点为空
		prev->next = NULL;
	}
}
// 单链表的头插
void LinkListPushFront(Node** pplist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//创建一个新的节点准备插入
	Node* NewNode = LinkListCreat(data);
	//链表为空直接插入
	if (*pplist == NULL)
	{
		*pplist = NewNode;
		return;
	}
	//新节点的next链接原来链表的头
	//原来指向链表头的指针指向新节点
	else {
		NewNode->next = *pplist;
		*pplist = NewNode;
	}
	//还可以这样写
	//assert(pplist);
	//Node* NewNode = LinkListCreat(data);
	//NewNode->next = *pplist;
	//*pplist = NewNode;
}
// 单链表头删
void LinkListPopFront(Node** pplist)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//设置一个指针指向要删除的节点
	Node* DelNode = NULL;
	if (NULL == *pplist)
	{
		return;
	}
	//删的是第一个节点那就指向表头
	DelNode = *pplist;
	//表头指针指向被删除的下一个节点
	*pplist = DelNode->next;
	free(DelNode);
}
// 单链表查找
Node* LinkListFind(Node* plist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(plist);
	//找到对应节点 赋给Find
	Node* FindNoed = plist;
	Node* cur = plist;
	//空链表就直接返回了
	if (NULL == plist)
	{
		printf("你的链表为空!");
		return;
	}
	//循环查找
	while (cur)
	{
		if (cur->data == data);
		{
			//找到了返回
			FindNoed = cur;
			return FindNoed;
		}
		cur = cur->next;
	}
	
}
//任意位置之前插入
void LinkListInsertBefore(Node** pplist, Node* pos, Datatype data)
{
	assert(pplist);
	//找到要插入的位置
	Node* Prior = *pplist;
	Node* NewNode = NULL;
	//链表为空我就调用我尾插这个方法就好了
	if (pos == *pplist) 
	{
		LinkListPushFront(pos, data);
	}
	else if (NULL == pos)
	{
		return;
	}

	else {
		NewNode = LinkListCreat(data);
		while (Prior->next != pos)
		{
			Prior = Prior->next;
		}
		Prior->next = NewNode;
		NewNode->next = pos;
	}
}
//单链表在pos位置之后插入x
//为啥只能是在后面插入呢因为这是链表不是顺序表那种连续的
//不能访问上一个节点 循环链表和双向链表可以解决
//亦或是再加一个指针找到前一个
void LinkListInsertAfter(Node* pos, Datatype data)
{
	Node* NewNode = NULL;
	if (NULL == pos)
	{
		return;
	}
	// 将新节点插入到pos之后
	NewNode = LinkListCreat(data);
	NewNode->next = pos->next;
	pos->next = NewNode;
}

// 单链表删除pos位置之后的值
void LinkListEraseAfter(Node* pos)
{
	Node* DelNode = NULL;
	//校验pos的合法性
	if (NULL == pos || NULL == pos->next)
	{
		return;
	}
	//找到删的节点
	DelNode = pos->next;
	//pos位置跟删除的下一个结点连起来
	pos->next = DelNode->next;
	free(DelNode);
}

int LinkListSize(Node* plist)
{
	assert(plist);
	//遍历链表就完事了
	Node* cur = plist;
	//给个遍历指针 指向链表第一个节点
	//因为不改变链表的值,我们传入
	int count = 0;
	while (cur) {
		//count ++ 计数器
		count++;
		cur = cur->next;
		
	}
	return count;
}

void LinkListDestroy(Node** pplist)
{
	//断言链表是否存在
	assert(pplist);
	//两个指针一个标记删除节点一个遍历
	Node* DelNode = *pplist;
	while (DelNode) 
	{
		//遍历这个指针先指向下一个节点
		//要不以后释放之后下一个节点的地址就不见了
		*pplist = DelNode->next;
		//释放这个要删除的节点
		free(DelNode);
		//这个删掉了该下一个了
		//标记要删除的节点用遍历指针给到刚刚保存了下一个节点地址
		DelNode = *pplist;
	}
}
int main()
{

}

3.3.1链表的打印

void PrintLinkList(Node* plist)
{
	Node* cur = plist;
	//遍历链表输出
	while (cur)
	{
		printf("%d-->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

定义一个遍历指针把链表遍历一遍,每次把对应的结点通过遍历指针访问节点数据域来取出 ,为啥先说打印呢,因为打印可以让我们更好的理解遍历链表!

那怎么循环起来遍历的呢,还是那就话大手拉小手,怎么拉?我的这个节点的指针域放的是下一个一个节点的地址!

那思路我们有了怎么用代码实现?我们可以吧循环展开

cur = cur->next;

这句其实就是cur ->next->next->next.....我们顺着第一个节点就把全部的节点就找到了!但是有个缺陷就是我现在指针已经在N个节点这块了那我能不能对指针--看第N个节点?不可以!对指针进行加加减减是对指针指向的目标的大小进行加加减减,是我们是加加减减了一个节点的大小!那也不能访问到,物理上不连续!

3.3.2获取一个新节点

//获取一个新节点
Node* LinkListCreat(Datatype data)
{
	//开辟空间获取一个节点的大小
	Node* Newnode = (Node*)malloc(sizeof(Node));
	//malloc之后要判空是否申请成功
	if (Newnode == NULL)
	{
		assert(0);
		return NULL;
	}
	//新节点的下一个还没链接那就置空
	Newnode->next = NULL;
	Newnode->data = data;
	return Newnode;
}

直接malloc个一个Node大小的节点,malloc是否申请成功要判断一下,然后返回这个新节点的地址。

3.3.3获取的链表长度

//获取的链表长度
int LinkListSize(Node* plist)
{
	assert(plist);
	//遍历链表就完事了
	Node* cur = plist;
	//给个遍历指针 指向链表第一个节点
	//因为不改变链表的值,我们传入
	int count = 0;
	while (cur) {
		//count ++ 计数器
		count++;
		cur = cur->next;
		
	}
	return count;
}

给个遍历指针,来个计数器还有比这个更简单的了吗?

3.3.4单链表查找

// 单链表查找
Node* LinkListFind(Node* plist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(plist);
	//找到对应节点 赋给Find
	Node* FindNoed = plist;
	Node* cur = plist;
	//空链表就直接返回了
	if (NULL == plist)
	{
		printf("你的链表为空!");
		return;
	}
	//循环查找
	while (cur)
	{
		if (cur->data == data);
		{
			//找到了返回
			FindNoed = cur;
			return FindNoed;
		}
		cur = cur->next;
	}
	
}

老规矩进来先看看这个货是不是空的要是空那就是说链表不存在!plist这个指针指的是链表第一个节点的地址,他都是空就说明你的链表就不存在!那就不找了。纯在就更简单了if判断找得到了返回地址。

3.3.4销毁链表

void LinkListDestroy(Node** pplist)
{
	//断言链表是否存在
	assert(pplist);
	//两个指针一个标记删除节点一个遍历
	Node* DelNode = *pplist;
	while (DelNode) 
	{
		//遍历这个指针先指向下一个节点
		//要不以后释放之后下一个节点的地址就不见了
		*pplist = DelNode->next;
		//释放这个要删除的节点
		free(DelNode);
		//这个删掉了该下一个了
		//标记要删除的节点用遍历指针给到刚刚保存了下一个节点地址
		DelNode = *pplist;
	}
}

为啥要用这个二级指针传参呢?一级指针不行吗?

我们翻开c的秘籍在复习一下吧!

void swap(int a,int b) {
	int temp = a;
	a = b;
	b = temp;

}
int main()
{
	int a = 2;
	int b = 3;
	swap(a, b);
	printf("a = %d\n", a);
	printf("b = %d\n", b);
}

a和b的值能交换吗?是不能的!形参是实参的一份临时拷贝,在函数调用时会发生这个临时拷贝,函数调用结束这个临时拷贝就会释放,你的交换其实只是在函数内部进行交换了,并没有把实际的结果传回来,形参和实参分别占有不同内存块,对形参的修改不会影响实参。那我怎么办?传址传参就可以,在c++中还可以用引用(引用是指针常量)

void swap(int* a,int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;

}
int main()
{
	int a = 2;
	int b = 3;
	swap(&a,&b);
	printf("a = %d\n", a);
	printf("b = %d\n", b);
}

传址调用是把函数外部创建变量的内存地址,传递给函数参数,直接通过地址修改这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
就是实际参数传到函数里了,可以通过形式参数修改实际参数。

回到我们的plist那他不是也是指针吗?他说你节点的地址,你这个函数调用后这个是不是也释放了,我们是要通过这个plist操控链表呀,想让他不被改变,那就应该存plist的地址,也就是指针变量的地址,也就是二级指针变量。

void LinkListDestroy(Node** pplist)
{
	//断言链表是否存在
	assert(pplist);
	//两个指针一个标记删除节点一个遍历
	Node* DelNode = *pplist;
	while (DelNode) 
	{
		//遍历这个指针先指向下一个节点
		//要不以后释放之后下一个节点的地址就不见了
		*pplist = DelNode->next;
		//释放这个要删除的节点
		free(DelNode);
		//这个删掉了该下一个了
		//标记要删除的节点用遍历指针给到刚刚保存了下一个节点地址
		DelNode = *pplist;
	}
}

这个断言是必须要有的,你pplist都是空说明你链表就是有问题,都没有创建!给一个删除指针专门指向删除的那个节点,先移动我们的标记指针pplist让他指向下一个去,在释放前一个,在吧这个删除指针指向下一个节点地址,那有人会说了我要这个删除指针干嘛呀?我直接free(*pplist)不就完事了,你free掉了你的大手拉小手的链接也断了因为当前节点存的下一个节点的地址!你free就没啦!

3.3.5链表删除

尾删

// 单链表的尾删
void LinkListPopBack(Node** pplist)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//遍历指针
	Node* cur = *pplist;
	//这个用来存上一个节点地址
	Node* prev = NULL;
	//空链表情况
	if (NULL == *pplist)
	{
		printf("你的链表为空!");
		return;
	}
	//链表只有有个节点的情况
	else if (NULL == *(pplist)->next)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//开始遍历找到尾节点
	else
	{
		while (cur) 
		{
			//把这个节点先存储下来
			prev = cur;
			cur = cur->next;
		}
		free(cur);
		//让他下一个节点为空
		prev->next = NULL;
	}
}

尾删你就要考虑许多情况,首先链表是空的,解引用pplist是否为空,空就直接返回不操作了,空荡荡删啥呢?然后就是只有个节点咱就看这个节点的下一个是不是空的也就是解引用pplist看看你的next是撒捏?嚯好家伙空!这不简单直接释放,变空链表解引用pplist给我指向空去,接下来就是展现操作的时候,先遍历找到最后一个,用个prve把上个节点保存一下让他的next指向空!然后我们释放最后一个就好了。

这样好麻烦呀!还要分情况那有没有别的方法。

	assert(pplist);
	Node* cur = *pplist;
	while (cur->next->next != NULL)
	{
		cur = cur->next;
	}
	free(cur->next);
	cur->next = NULL;

assert处理了空还有没有节点的情况,然后我也不需要刚刚那个prev了只是要要next的next就好了,最后把最后一个一删除就完事了。

头删

// 单链表头删
void LinkListPopFront(Node** pplist)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//设置一个指针指向要删除的节点
	Node* DelNode = NULL;
	if (NULL == *pplist)
	{
		return;
	}
	//删的是第一个节点那就指向表头
	DelNode = *pplist;
	//表头指针指向被删除的下一个节点
	*pplist = DelNode->next;
	free(DelNode);
}

标记一个删除指针指向你要删除的节点,看看链表是不是空的,不是就开始删,删除指针指向表头,让我们的链表操作指针指向下一个去,free掉这个DelNode,为啥要这个删除指针呢?保存下一个节点的地址呀,要不然你直接释放了,你的表头指针就寄了,你大手拉不上小手了。

pos位置之后的值

// 单链表删除pos位置之后的值
void LinkListEraseAfter(Node* pos)
{
	Node* DelNode = NULL;
	//校验pos的合法性
	if (NULL == pos || NULL == pos->next)
	{
		return;
	}
	//找到删的节点
	DelNode = pos->next;
	//pos位置跟删除的下一个结点连起来
	pos->next = DelNode->next;
	free(DelNode);
}

校验pos的合法性,找到删的节点,pos位置跟删除的下一个结点连起来,这个删除节点也是一个储存下一个节点的地址信息的。

pos->next = DelNode->next;
//其实就是
pos->next=pos->next->next;

3.3.6链表增加

尾插

// 单链表尾插
void LinkListPushBack(Node** pplist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//我们的遍历指针既然是要尾插那我们就要先找到最后那个
	Node* cur = *pplist;
	//用刚刚写的那个创建节点的方法取到新节点的地址
	Node* NewNode= LinkListCreat(data);
	//一个节点都没有
	//这个NewNode给到操作链表的指针
	if (NULL == *pplist) {
		*pplist = NewNode;
		return;
	}
	//里面就有节点我们直接找到最后一个节点
	//把新节点的地址交给最后一个节点的next
	else
	{
		while (cur->next) 
		{
			cur = cur->next;
		}
		cur->next = NewNode;
	}
	
}

还是先assret老规矩进来先看看这个货是不是空的要是空那就是说链表不存在!我们的遍历指针既然是要尾插那我们就要先找到最后那个,用刚刚写的那个创建节点的方法取到新节点的地址,然后开始分情况,一个节点都没有,这个NewNode给到操作链表的指针,里面就有节点我们直接找到最后一个节点,把新节点的地址交给最后一个节点的next。

头插

// 单链表的头插
void LinkListPushFront(Node** pplist, Datatype data)
{
	//老规矩进来先看看这个货是不是空的
	//要是空那就是说链表不存在!
	assert(pplist);
	//创建一个新的节点准备插入
	Node* NewNode = LinkListCreat(data);
	//链表为空直接插入
	if (*pplist == NULL)
	{
		*pplist = NewNode;
		return;
	}
	//新节点的next链接原来链表的头
	//原来指向链表头的指针指向新节点
	else {
		NewNode->next = *pplist;
		*pplist = NewNode;
	}
	//还可以这样写
	//assert(pplist);
	//Node* NewNode = LinkListCreat(data);
	//NewNode->next = *pplist;
	//*pplist = NewNode;
}

开辟一个新的节点把他的地址给到NewNode,要是我们的链表为空直接把解引用pplist指向新节点的地址,新节点的next链接原来链表的头,原来指向链表头的指针指向新节点。

pos之后插入

//单链表在pos位置之后插入x
//为啥只能是在后面插入呢因为这是链表不是顺序表那种连续的
//不能访问上一个节点 循环链表和双向链表可以解决
//亦或是再加一个指针找到前一个
void LinkListInsertAfter(Node* pos, Datatype data)
{
	Node* NewNode = NULL;
	if (NULL == pos)
	{
		return;
	}
	// 将新节点插入到pos之后
	NewNode = LinkListCreat(data);
	NewNode->next = pos->next;
	pos->next = NewNode;
}

创建一个指针指向的是新开辟节点的地址,pos位置下一个节点的地址给到新节点指针域连接起来。最后将pos指针域放的是新节点的地址。

pos之前插入

//任意位置之前插入
void LinkListInsertBefore(Node** pplist, Node* pos, Datatype data)
{
	assert(pplist);
	//找到要插入的位置
	Node* Prior = *pplist;
	Node* NewNode = NULL;
	//链表为空我就调用我尾插这个方法就好了
	if (pos == *pplist) 
	{
		LinkListPushFront(pos, data);
	}
	else if (NULL == pos)
	{
		return;
	}

	else {
		NewNode = LinkListCreat(data);
		while (Prior->next != pos)
		{
			Prior = Prior->next;
		}
		Prior->next = NewNode;
		NewNode->next = pos;
	}
}

先assert看一下链表存在不,定一个指针最后就是指向pos前面,还是分为那几个情况,链表为空,pos是*pplist直接就前插就好了,然后就是多个节点就是用这个prior来操作了,找到这个prior给他尾插一下,在连接起来。

希望本文对你有帮助,欢迎大佬来指指点点!

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