初阶数据结构(C语言实现)——3.3单向非循环链表详解(定义、增、删、查、改)+链表面试题

目录

学习链表之前,建议先学习下顺序表,这是博主的顺序表文章,欢迎学习初阶数据结构(C语言实现)——3顺序表

前置思考

为什么有了顺序表,还要有链表?

  • 顺序表存在一些问题
  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
    再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
  • 为了更好的解决上面的问题,我们引入了链表

3.链表

3.1 链表的概念及结构

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

链表的物理结构
在这里插入图片描述

1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

链表的逻辑结构(为了方便理解,想象出来的)
在这里插入图片描述

注意:

  1. 现实中的结点一般都是从堆上申请出来的
  2. 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

3.2 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

  1. 单向或者双向

在这里插入图片描述

  1. 带头或者不带头(哨兵位的头结点,不存储有效数据)

在这里插入图片描述

  1. 循环或者非循环

在这里插入图片描述

链表总共有8种
在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

在这里插入图片描述

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

3.3 无头+单向+非循环链表增删查改接口实现

3.3.0 单链表的实现

在这里插入图片描述

typedef int SLTDateType;
//定义单链表节点
typedef struct SListNode
{
	SLTDateType data; //数据域
	struct SListNode* next;//指针域
}SListNode;//重命名为SListNode,方便我们后面定义结构体变量。就不需要每次写 struct SListNode 这么一长串了
  • 实现接口

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pphead);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);

3.3.1 动态申请节点和释放销毁节点

动态申请一个结点

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)//申请完,检查是否开辟成功
	{
		perror("BuySListNode::mallo fail!");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

释放(销毁)所有节点

//销毁(释放)所有节点
void SLListDistory(SListNode** pphead)
{
	assert(pphead);
	SListNode*  cur = *pphead;
	while (cur->next != NULL)//遍历链表
	{
		SListNode* next = cur->next;//保存cur的下一个节点
		free(cur);//释放结点
		cur = next;//cur一直往后走
	}
	*pphead = NULL;//最后释放我们的指针
}

3.3.2 单链表打印

void SListPrint(SListNode* phead)
{
	SListNode *cur = phead;
	while (cur)//遍历链表只要不为空,就打印
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.3.3 单链表尾插

  • 先来看一种错误写法

传一级指针的值,用一级指针接收
在这里插入图片描述

  • 指针传值,相当于把plist指针变量的值拷贝给phead,phead=newnode,phead的变化并不会影响plist.
  • 形参只是实参的一份临时拷贝
  • 这样写的结果是什么?

当链表为空的时候,plist指向NULL,phead也指向NULL
我们现在要尾插一个结点,我们在函数内部,令形参phead指向了新结点,但是我们的实参plist,并没有改变
在这里插入图片描述

  • 如何解决

list 是指向第一个节点的指针,想要在函数中改变 plist 的值(指向),必须要把 plist指针的地址 作为实参传过去,形参用 二级指针 接收,这样在函数中对二级指针解引用得到 plist 的值,就可以改变 plist 的值了

在函数里面要改变 int,则要传 int* ,要改变 int* ,则要传 int**

  • 尾插正确写法

图解尾插思路>在这里插入图片描述
在这里插入图片描述

void SListPushBack(SListNode** pphead, SLTDateType x)
{
    assert(pphead);  //检查参数是否传错
	SListNode* newnode = BuySListNode(x);//动态申请一个结点
	
	if (*pphead == NULL)//当单链表中没有结点时
	{
		*pphead = newnode;//让plist指向新结点
	}
	else//当单链表中已有结点时
	{
		
		SListNode* tail =*pphead;
		while (tail->next != NULL)//找尾,找到单链表中的最后一个结点
		{
			tail = tail->next;
		}
		tail->next = newnode;//另最后一个结点的next域指向新结点
	}
}
  • 功能测试

在这里插入图片描述

3.3.4 单链表的头插

  • 图解头插思路

在这里插入图片描述

  • 代码实现
void SListPushFront(SListNode** pphead, SLTDateType x)
{
    assert(pphead);  //检查参数是否传错
	SListNode* newnode = BuySListNode(x);//动态申请一个节点
	newnode->next = *pphead; //新节点的next指针指向plist指向的位置
	*pphead = newnode;//plist指向头插的新节点
}

  • 功能测试

在这里插入图片描述

3.3.5 单链表的尾删

  • 图解尾删思路

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 尾删代码实现
  1. 单链表只有一个节点时,删除节点,plist 指向 NULL;
  2. 单链表有多个节点时,先找到单链表尾节点的上一个节点,删除尾节点,然后将该节点的next指向 NULL;
  3. 因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。
void SListPopBack(SListNode** pphead)
{
	assert(pphead);   //检查参数是否传错
	assert(*pphead);  //断言,链表不能为空
	
	// 1、只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);//删除节点
		*pphead = NULL;//plist置空
	}
	else
	{
		// 2、多个节点
		//找尾
		SListNode* prev = NULL;
		SListNode* tail = *pphead;
		while (tail->next != NULL)//找到链表的尾节点和它的上一个节点
		{
			prev = tail;
			tail = tail->next;
		}

		free(tail);//删除尾节点
		tail = NULL;
		prev->next = NULL;//置空
	}
}
  • 尾删功能验证

在这里插入图片描述

3.3.6 单链表头删

  • 图解单链表头删思路

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 单链表头删代码实现
void SListPopFront(SListNode** pphead)
{
	assert(pphead);   //检查参数是否传错
	assert(*pphead);  //链表不能为空
	SListNode* first = *pphead; //保存头节点的地址
	*pphead = first->next;//plist指向头节点的下一个节点
	free(first); //删除头节点
	first = NULL;
}
  • 单链表头删功能验证

在这里插入图片描述

3.3.7 单链表查找

  • 思路比较简单,如果找到就返回找到的结点,没找到就返回NULL
SListNode* SListFind(SListNode* phead, SLTDateType x)
{
	SListNode* cur = phead;
	//遍历链表
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;//找到了,返回该节点的地址
		}
		cur = cur->next;
	}
	return NULL; //未找到,返回NULL
}
  • 查找函数功能验证

在这里插入图片描述

3.3.8 单链表在pos位置之后插入x

  • 图解单链表在pos位置之后插入思路

在这里插入图片描述

  • 思考:为什么不在pos位置之前插入?
  1. 单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点
  2. 单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多
  3. C++官方库里面单链表给的也是在之后插入
  • 代码实现
//单链表在指定pos位置之后插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);//给的pos位置不能为空
	SListNode* newnode = BuySListNode(x);//动态申请一个节点
	newnode->next = pos->next;//新节点的next指针指向pos位置后一个节点
	pos->next = newnode;  //pos位置的next指向新节点
}

3.3.9 单链表删除pos位置之后的值

//单链表删除指定pos位置之后的节点
void SListEraseAfter(SListNode* pos)
{
	assert(pos);//给的pos位置不能为空
	assert(pos->next); //给的pos位置不能是尾节点

	SListNode* del = pos->next;//保存pos位置的后一个节点
	pos->next = del->next;
	free(del); //释放pos位置的后一个节点
	del = NULL;
}

3.3.10 求单链表长度

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

3.3.11 判断单链表是否为空

//单链表判空
bool SListEmpty(SListNode* phead)
{
	//plist为空,返回1(true),非空,返回0(false)
	return phead == NULL;
	
	/*写法2:
	return phead == NULL ? true : false;
	*/
}

3.4 链表面试题

1. 删除链表中等于给定值 val 的所有节点。

删除节点链接

2. 反转一个单链表。

反转一个单链表

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

返回中间结点

4. 输入一个链表,输出该链表中倒数第k个结点。

返回倒数第K个节点

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

合并两个有序链表

6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

链表分割

7. 链表的回文结构。

链表回文

8. 输入两个链表,找出它们的第一个公共结点。

相交链表

9. 给定一个链表,判断链表中是否有环。

环形链表1

10. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL

环形链表2

11. 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝。

12. 其他 。

力扣链表OJ链接+牛客链表OJ链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值