【数据结构】不带头+单向+不循环链表 增删查改(超详细)

1.链表

1.1链表的定义及结构

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

链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
类比
每个车厢就相当于每个结点,结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量),也就是数据域和指针域。
图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。
为什么还需要指针变量来保存下⼀个结点的位置?
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去堆上申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
结点

注意:4个结点的地址并不是连续的,链表在物理结构上不一定是线性的,而在逻辑结构上是线性的

1.2链表的分类

要搞清楚为什么链表有八大类型,就要先搞清楚链表的三大"性状".

在这里插入图片描述

链表总共有2x2x2有8大类型

在这里插入图片描述

不带头结点+单向+不循环链表
不带头结点+双向+不循环链表
不带头结点+单向+循环链表
不带头结点+双向+循环链表
带头结点+单向+不循环链表
带头结点+双向+不循环链表
带头结点+单向+循环链表
带头结点+双向+循环链表

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
在这里插入图片描述

  1. 不带头单向不循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而更简单。
    我们今天要实现的就是第一种不带头+单向+不循环链表。

2.不带头+单向+不循环链表接口实现

新建一个工程:

SList.h(单链表的类型定义、接口函数声明、引用的头文件)
SList.c(单链表接口函数的实现)
test.c(主函数、测试顺序表各个接口功能)

完整的代码放在后面(包括测试代码),这里就不会展示测试的效果图。大家可以自己别敲边按测试代码测试。图解会写的很详细的,么么😙

2.1 单链表打印

在这里插入图片描述

//单链表打印
void SLNPrint(SLNode* phead)
{
	//空链表可以打印,可以不用断言
	SLNode* pcur = phead;//定义一个新指针去遍历
	while (pcur)//打印链表
	{
		printf("%d ->", pcur->val);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

2.2 动态申请一个结点

// 动态申请一个结点
SLNode* BuySLNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)//检查开辟是否成功
	{
		perror("malloc");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
}

2.3 单链表尾插

这里需要用二级指针才能改变外面plist的指向,如果用一级指针我们只是plist的拷贝,自身的改变是不会影响plist。它还是指向NULL;所以这里我们要把plist的地址传过来。如果有这个疑惑,说明对传值和传址还有一些疑问,从函数栈帧的角度来看,如果传递的是一级指针,那么会在栈帧内创建一个指针形参,而这个指针形参并不会在结束后返回到函数实参中,而是会随着函数的结束而随之销毁,因此这里要引入的是二级指针,运用二级指针的目的就是使得传参的一级指针被函数体中的操作改变,才能输出合适的结果。如图所示:
在这里插入图片描述

假设在SLNTest1中函数传参传的是plist,那么在传递的就是一份plist的拷贝,随着SLPushBack的结束,形参也随之被销毁,此时plist还是指向NULL,那么后续对于plist的操作就不可能成功了
但如果传递的是地址,那这里的pphead就用来管理SLNTest1函数中的plist,pphead有资格对SLNTest1函数中对plist进行操作,进而使SLNTest1函数中的plist发生改变,因此在这里我们把newnode的地址给了*pphead,实际上就是把newnode的值给了plist,那么此时plist不再指向NULL,它有了新的指向,于是就完成了链表的插入.

要一个函数内修改在另一个函数的值,就需要传址才能完成修改,同理要修改指针的指向,则需要二级指针去修改它的地址

如果对指针还是不太了解,那先读懂下图再去深入了解
在这里插入图片描述
在这里插入图片描述

//单链表尾插
void SLNPushBack(SLNode** pphead, SLNDataType x)
{
	assert(pphead);//断言防止传空指针进来
	SLNode* newnode = BuySLNode(x);
	//没有结点,就是空链表情况下
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	SLNode* pcur = *pphead;
	//有结点
	while (pcur->next != NULL)
	{
		pcur = pcur->next;
	}
	pcur->next = newnode;

}

2.4 单链表头插

在这里插入图片描述

// 单链表头插
void SLNPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);//断言防止传空指针进来
	SLNode* node = BuySLNode(x);
	node->next = *pphead;
	*pphead = node;
}

2.5 单链表尾删

在这里插入图片描述

// 单链表尾删
void SLNPopBack(SLNode** pphead)
{
	assert(pphead);//断言防止传空指针进来
	assert(*pphead);//断言 *pphead就是第一个结点的地址,不能删空了继续删。
	//一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;

	}
	//多个结点
	else 
	{
		SLNode* prev = NULL;
		SLNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = ptail->next;
		free(ptail);
		ptail = NULL;
		//方法二:
		//SLNode* ptail = *pphead;
		//while (ptail->next->next!=NULL)
		//{
		//	ptail = ptail->next;
		//}
		//free(ptail->next);
		//ptail->next = NULL;
		//
	}

}

2.6 单链表头删

在这里插入图片描述

// 单链表头删
void SLNPopFront(SLNode** pphead)
{
	assert(pphead);//断言防止传空指针进来
	assert(*pphead);//断言 *pphead就是第一个结点的地址,不能删空了继续删。
	SLNode* tmp = (*pphead)->next;
	free(*pphead);
	*pphead = tmp;

}

2.7 查找x的结点

//查找x的结点
SLNode* SLNFind(SLNode* phead, SLNDataType x)
{
	SLNode* pcur = phead;
	while (pcur)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		else
		{
			pcur = pcur->next;
		}
	}

	return NULL;
}

2.8 在pos位置之前插入数据

在这里插入图片描述

这个其实单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点,单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多。
C++官方库里面单链表给的也是在之后插入,我这里也实现一下

//在pos位置之前插入数据
void SLNInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	//严格限定pos一定是链表里面的一个有效结点
	assert(*pphead);
	assert(pos);
	assert(pphead);
	两种断言方式,下面这种更严格一点
	//assert(pphead);
	要么都为空,要么都不为空
	//assert((!pos) && (!*pphead) || pos && *pphead);
	SLNode* newnode = BuySLNode(x);
	//头插
	if (pos == *pphead)
	{
		newnode->next = *pphead;
		*pphead = newnode;
		return;
	}

	SLNode* pcur = *pphead;
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	newnode->next = pos;
	pcur->next = newnode;

}

2.9 在pos位置之后插入数据

在这里插入图片描述

//在pos位置之后插入数据
void SLNInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* node = BuySLNode(x);
	node->next = pos->next;
	pos->next = node;

}

2.10 删除pos结点

在这里插入图片描述

//删除pos结点
void SLNErase(SLNode** pphead, SLNode* pos)
{
	//严格限定pos一定是链表里面的一个有效结点
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (pos == *pphead)
	{
		*pphead = (*pphead)->next;
		free(pos);
		return;
		//直接调用头删
		//SLNPopFront(pphead);
		//return;
	}
	SLNode* pcur = *pphead;
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	pcur->next = pos->next;
	free(pos);
	pos = NULL;
}

2.11 删除pos之后的结点

在这里插入图片描述

//删除pos之后的结点
void SLNEraseAfter(SLNode* pos)
{
	assert(pos && pos->next);//确保pos结点有效且后一位存在
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

2.12 求单链表长度

//求单链表长度
int SLNSize(SLNode* phead)
{
		int size = 0;
		SLNode* pcur = phead;
		while (pcur != NULL)  //遍历链表
		{
			size++;
			pcur = pcur->next;
		}
		return size;
}

2.13 判断单链表是否为空

//判断单链表是否为空
int SLNEmpty(SLNode* phead)
{
	return phead == NULL ? 0: 1;//为空返回0,非空返回1;
}

2.14 单链表销毁

//单链表销毁
void SLNDestory(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)//遍历链表
	{
		SLNode* next = pcur->next;//存储pcur下一个位置
		free(pcur);
		pcur = next;

	}
	*pphead = NULL;
}

完整代码实现(包含测试代码)

SList.h

#pragma once


// 不带头+单向+不循环链表增删查改实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>


typedef int SLNDataType;
//Single List
//定义链表结点的结构
typedef struct SListNode
{
	SLNDataType val;//要保存的数据
	struct SListNode* next;
}SLNode;


//单链表打印
void SLNPrint(SLNode* phead);
// 动态申请一个结点
SLNode* BuySLNode(SLNDataType x);
//单链表尾插
void SLNPushBack(SLNode** pphead, SLNDataType x);
//单链表头插
void SLNPushFront(SLNode** pphead, SLNDataType x);
//单链表尾删
void SLNPopBack(SLNode** pphead);
//单链表头删
void SLNPopFront(SLNode** pphead);
//查找x的结点
SLNode* SLNFind(SLNode* phead, SLNDataType x);
//在pos位置之前插入数据
void SLNInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
//在pos位置之后插入数据
void SLNInsertAfter(SLNode* pos, SLNDataType x);
//删除pos结点
void SLNErase(SLNode** pphead, SLNode* pos);
//删除pos之后的结点
void SLNEraseAfter(SLNode* pos);
//求单链表长度
int SLNSize(SLNode* phead);
//判断单链表是否为空
int SLNEmpty(SLNode* phead);
//单链表销毁
void SLNDestory(SLNode** pphead);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"




//单链表打印
void SLNPrint(SLNode* phead)
{
	//空链表可以打印,可以不用断言
	SLNode* pcur = phead;//定义一个新指针去遍历
	while (pcur)//打印链表
	{
		printf("%d ->", pcur->val);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

// 动态申请一个结点
SLNode* BuySLNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)//检查开辟是否成功
	{
		perror("malloc");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
}

//单链表尾插
void SLNPushBack(SLNode** pphead, SLNDataType x)
{
	assert(pphead);//断言防止传空指针进来
	SLNode* newnode = BuySLNode(x);
	//没有结点,就是空链表情况下
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	SLNode* pcur = *pphead;
	//有结点
	while (pcur->next != NULL)
	{
		pcur = pcur->next;
	}
	pcur->next = newnode;

}

// 单链表头插
void SLNPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);//断言防止传空指针进来
	SLNode* node = BuySLNode(x);
	node->next = *pphead;
	*pphead = node;
}

// 单链表尾删
void SLNPopBack(SLNode** pphead)
{
	assert(pphead);//断言防止传空指针进来
	assert(*pphead);//断言 *pphead就是第一个结点的地址,不能删空了继续删。
	//一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;

	}
	//多个结点
	else
	{
		SLNode* prev = NULL;
		SLNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = ptail->next;
		free(ptail);
		ptail = NULL;
		//方法二:
		//SLNode* ptail = *pphead;
		//while (ptail->next->next!=NULL)
		//{
		//	ptail = ptail->next;
		//}
		//free(ptail->next);
		//ptail->next = NULL;
		//
	}

}

// 单链表头删
void SLNPopFront(SLNode** pphead)
{
	assert(pphead);//断言防止传空指针进来
	assert(*pphead);//断言 *pphead就是第一个结点的地址,不能删空了继续删。
	SLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;
	//方法二:
	//SLNode* tmp = (*pphead)->next;
	//free(*pphead);
	//*pphead = tmp;

}

//查找x的结点
SLNode* SLNFind(SLNode* phead, SLNDataType x)
{
	SLNode* pcur = phead;
	while (pcur)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		else
		{
			pcur = pcur->next;
		}
	}

	return NULL;
}

//在pos位置之前插入数据
void SLNInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	//严格限定pos一定是链表里面的一个有效结点
	assert(*pphead);
	assert(pos);
	assert(pphead);
	//两种断言方式,下面这种更严格一点
	//assert(pphead);
	//要么都为空,要么都不为空
		//assert((!pos) && (!*pphead) || pos && *pphead);
		SLNode* newnode = BuySLNode(x);
	//头插
	if (pos == *pphead)
	{
		newnode->next = *pphead;
		*pphead = newnode;
		return;
	}

	SLNode* pcur = *pphead;
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	newnode->next = pos;
	pcur->next = newnode;

}

//在pos位置之后插入数据
void SLNInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* node = BuySLNode(x);
	node->next = pos->next;
	pos->next = node;

}

//删除pos结点
void SLNErase(SLNode** pphead, SLNode* pos)
{
	//严格限定pos一定是链表里面的一个有效结点
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (pos == *pphead)
	{
		*pphead = (*pphead)->next;
		free(pos);
		return;
		//直接调用头删
		//SLNPopFront(pphead);
		//return;
	}
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

//删除pos之后的结点
void SLNEraseAfter(SLNode* pos)
{
	assert(pos && pos->next);//确保pos结点有效且后一位存在
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//求单链表长度
int SLNSize(SLNode* phead)
{
	int size = 0;
	SLNode* pcur = phead;
	while (pcur != NULL)  //遍历链表
	{
		size++;
		pcur = pcur->next;
	}
	return size;
}

//判断单链表是否为空
int SLNEmpty(SLNode* phead)
{
	return phead == NULL ? 0 : 1;//为空返回0,非空返回1;
}

//单链表销毁
void SLNDestory(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)//遍历链表
	{
		SLNode* next = pcur->next;//存储pcur下一个位置
		free(pcur);
		pcur = next;

	}
	*pphead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"SList.h"



void SLNTest1()
{
	SLNode* plist = NULL;
	SLNPushBack(&plist, 1);
	SLNPushBack(&plist, 2);
	SLNPushBack(&plist, 3);
	SLNPushBack(&plist, 4);
	SLNPushBack(&plist, 5);
	SLNPrint(plist);
	SLNPushFront(&plist, 9);
	SLNPrint(plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	//SLNPopBack(&plist);
	SLNPopFront(&plist);
	SLNPopFront(&plist);
	SLNPopFront(&plist);
	SLNPopFront(&plist);
	SLNPopFront(&plist);
	//SLNPopFront(&plist);
	//SLNPopFront(&plist);
	SLNPrint(plist);



	SLNDestory(&plist);
}
void SLNTest2()
{
	SLNode* plist = NULL;
	SLNPushBack(&plist, 1);
	SLNPushBack(&plist, 2);
	SLNPushBack(&plist, 3);
	SLNPushBack(&plist, 4);
	SLNPushBack(&plist, 5);
	SLNPrint(plist);
	SLNode* pos = SLNFind(plist, 3);
	SLNInsert(&plist, pos, 30);
	SLNPrint(plist);
	SLNInsertAfter(pos, 90);
	SLNPrint(plist);


	SLNDestory(&plist);
}

void SLNTest3()
{
	SLNode* plist = NULL;
	SLNPushBack(&plist, 1);
	SLNPushBack(&plist, 2);
	SLNPushBack(&plist, 3);
	SLNPushBack(&plist, 4);
	SLNPushBack(&plist, 5);
	SLNPrint(plist);
	SLNode* pos = SLNFind(plist, 4);
	SLNEraseAfter(pos);
	//SLNErase(&plist, pos);
	SLNPrint(plist);

	SLNDestory(&plist);

}

void SLNTest4()
{
	SLNode* plist = NULL;
	//SLNPushBack(&plist, 1);
	//SLNPushBack(&plist, 2);
	//SLNPushBack(&plist, 3);
	//SLNPushBack(&plist, 4);
	//SLNPushBack(&plist, 5);
	//SLNPrint(plist);
	//int size = SLNSize(plist);
	//printf("%d \n", size);
	int len = SLNEmpty(plist);
	printf("%d \n", len);


	SLNDestory(&plist);

}


int main()
{
	SLNTest4();
	return 0;
}

链表传参问题

1.二级指针

链表phead指向的是结构体,如果用一级指针,改变的只是plist拷贝的形参,这个phead除了和plist地址不一样以外都一样。想要改变plist的指向,就需要二级指针。

2.返回值

因为两者只有地址不一样,所以可以把这个phead当作返回值,把它返回到调用这个函数的函数中,让原来的plist接收一下这个经过函数体的临时拷贝的phead。

这两个方法区别之一就是,如果用二级指针,那么pphead全程都是一个地址,但如果用返回值的方法,phead在内存中的地址会一直变化,因为每调用一次包含返回值的函数就相当于重新创建了一个phead把原来的plist覆盖掉了,进入函数体内的phead在函数体内完成一系列操作后返回出来,把原来的plist覆盖掉,这样就变临时拷贝为永久拷贝,永久的代替了传参前plist的位置。

总结

(一)优点:
1.任意位置插入删除时间复杂度为O(1)。
2.没有增容问题,插入一个开辟一个空间。

(二)缺点:
1.以节点为单位存储,不支持随机访问。

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值