【数据结构初阶】3. 单链表及OJ题

1. 链表的概念和结构

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

2. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
在这里插入图片描述

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

3. 单链表的代码实现

对链表进行分析:

  1. 链表不像顺序表需要初始化空间,再不断realloc进行调整空间大小。
  2. 链表的各节点都是孤立的(在物理空间上并不连续),再通过指针存储下一节点的地址来进行链接,所以称之为链表。

3.1 声明结点类型

结点分析:首先是结构体类型的 要:1.存储数据 2.存储下一结点地址

// 单链表的数据类型为int
typedef int SLLDataType;
// 单链表结点
typedef struct Single_Linked_List_Node
{
	// 数据
	SLLDataType data;
	// 下一结点指针
	struct Single_Linked_List_Node* next;
}SLLNode,*PSLLNode;

//其中SLLNode 是struct Single_Linked_List_Node 类型    结构体

3.2 打印链表 SSLPrint

先实现打印模块,进一步认知链表
遍历链表中的每一个数据,通常不会对链表的头指针进行移动,不然移动之后找不到链表起始位置。
所以要创建中间变量来进行遍历
在这里插入图片描述
在这里插入图片描述
提问 为啥SSLPrint 打印链表之前不加上assert 断言呢?
因为SSLPrint 中phead 指针可能指向NULL ,也就是空链表的情况 ,所以不需要断言

3.3 创建结点 BuySLLNode

因为在链表创建的过程中,结点的创建是非常频繁的,所以将其封装成小模块,需要时调用即可。

// 这里设计成SLLNode 是有意义的
// 新结点创建出来,是孤立的结点,若为NULL 则找不到该结点
SLLNode* BuySLLNode(SLLDataType x)
{
	// malloc出1块SLLNode 的空间 单链表结点
	SLLNode* newnode = (SLLNode*)malloc(sizeof(SLLNode));
	// 判断是否开辟成功
	if (newnode == NULL)
	{
		perror("malloc fail");
		// 既然结点无法创建,直接结束程序 - 无法实现单链表
		exit(-1);
	}
	// 开辟成功
	newnode->data = x;
	newnode->next = NULL;
	// 返回创建出来的结点  
	return newnode;
}

3.4 头插接口 SLLPushFront

3.4.1 错误版头插

// 头插
void SLLPushFront(SLLNode* phead, SLLDataType x)
{
	// 创建结点
	SLLNode* newnode  = BuySLLNode(x);
	// 要将新结点插入到头结点之前,成为新的头节点
	// 新结点的指针域本来是NULL,改为phead 之前的头结点的地址(phead)
	newnode->next = phead;
	// 将头结点改为newnode
	phead = newnode;
}

既然头插接口实现了,那接下来对头插进行测试:
在这里插入图片描述
因为传参时采用的时传值调用,改变形参并不会影响实参
那么到这里可能许多小伙伴不理解了,plist不是SLLNode *类型的指针吗,这不是传址调用吗?
画个图就明白了
在这里插入图片描述

3.4.2 正确版头插

&plist plist 本身就是SSLNode* 类型(一级指针类型) ,那么phead 就需要用二级指针接收

void SLLPushFront(SLLNode** pphead, SLLDataType x)
{
	// 为啥这里可以用assert,之前却不行
	assert(pphead);

	SLLNode* newnode = BuySLLNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

因为pphead 是&plist plist在TestSLList1函数中已经创建,即使plist 指向NULL;但是plist的地址是有效的
而之前的phead 是plist 那么当plist指向NULL时,phead也是NULL 一旦断言就会报错
在这里插入图片描述
经过测试发现头插实现成功

3.5 尾插接口 SLLPushBack

要实现尾插要首先找到链表的尾结点
在这里插入图片描述
在这里插入图片描述

// 尾插
void SLLPushBack(SLLNode** pphead, SLLDataType x)
{
	assert(pphead);

	SLLNode* newnode = BuySLLNode(x);
	// 链表为NULL
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	// 链表中有结点
	else
	{
		//找尾
		SLLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//tail->next =NULL
		tail->next = newnode;
	}
}

在这里插入图片描述

3.6 头删接口 SLLPopFront

// 头删
void SLLPopFront(SLLNode** pphead)
{
	assert(pphead);
	// 如果*pphead 也就是plist为NULL 也就意味着链表中没有结点,直接报assert错误
	assert(*pphead != NULL);
	/*
	//只是单纯的这样可以吗?
	*pphead = (*pphead)->next;
	// 答案是不行的,链表中的结点都是通过malloc来的,也就意味这对应的free
	// 虽然这样是将头结点从链表当中移除,但是空间仍然存在,需要释放
	*/

	SLLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;
}

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

3.7 尾删接口 SLLPopBack

在这里插入图片描述

// 尾删
void SLLPopBack(SLLNode** pphead)
{
	assert(pphead);
	/*
	// 暴力,温柔二选一
	// 温柔的检查
	if (*pphead == NULL)
	{
		return;
	}
	*/
	// 暴力检查
	assert(*pphead != NULL);
	// 1、一个节点
	// 2、多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 找尾
		/*SLLNode* prev = NULL;
		SLLNode* tail = *pphead;
		while (tail->next != NULL)
		{
		prev = tail;
		tail = tail->next;
		}
		prev->next = NULL;
		free(tail);
		tail = NULL;*/

		SLLNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

3.8 销毁接口 SLLDestroy

在这里插入图片描述

3.9 在pos位置前插入结点 SLLInsert

在这里插入图片描述

// 链表在pos位置之前插入(pos 不是下标 pos SLLNode* 类型 指向结点的指针)
void SListInsert(SLLNode** pphead, SLLNode* pos, SLLDataType x)
{
	assert(pphead);
	assert(pos);
	//分情况讨论
	// pos指向的是首结点 调用头插接口
	if (pos == *pphead)
	{
		SLLPushFront(pphead,x);
	}
	else
	{
		// 不是首结点 那么就需要找到pos之前的结点 
		// 又因为是单链表形式,只能往后找结点,所以需要重新创建一个指针,指向pos前结点的位置
		SLLNode* prev = *pphead;
		//指向pos前结点的位置
		while (prev->next != pos)
		{
			prev = prev->next;

			// 暴力检查,pos不在链表中.prev为空,还没有找到pos,说明pos传错了(防止pos传递过来的是随机值)
			assert(prev);
		}
		SLLNode* newnode = BuySLLNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

3.10 在pos位置后插入结点 SLLInsertAfter

// 链表在pos位置之后插入 
void SLLInsertAfter(SLLNode* pos, SLLDataType x)
{
	assert(pos);
	SLLNode* newnode = BuySLLNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

在pos后面插入甚至不需要传pphead,因为不需要分情况讨论而且单链表是可以找到后面元素的(但是找不到前面的元素)

3.11 查找元素 SLLFind

SLLNode* SLLFind(SLLNode* phead, SLLDataType x)
{
	assert(phead);
	SLLNode* cur = phead;
	// 遍历一遍链表即可
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		// 往后找结点
		cur = cur->next;
	}
	return NULL;
}

测试查找、pos前插入、pos后插入
在这里插入图片描述
在这里插入图片描述

3.12 删除pos位置结点 SLLErase

在这里插入图片描述

void SLLErase(SLLNode** pphead, SLLNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		// 头删 调用头删接口
		SLLPopFront(pphead);
	}
	else
	{
		SLLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
			// 检查pos不是链表中节点,参数传错了
			assert(prev);
		}
		prev->next = pos->next;
		free(pos);
		// pos = NULL; 这里的pos置为NULL是无效的
		
		// pos 是形参 是SLLNode* 类型的 接受pos的参数也是SLLNode*类型
		// 要改变SLLNode* 类型的变量,要传SLLNode**类型 二级指针

		// 要改变一级指针变量,要传一级指针的地址 也就是二级指针
	}
}

3.13 删除pos位置后的结点 SLLEraseAfter

void SSLEraseAfter(SLLNode* pos)
{
	assert(pos);
	//该接口无法实现删除尾结点
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLLNode* next = pos->next;
		pos->next = next->next;
		free(next);
	}
}

此接口还是存在一定问题,尾结点因为next==NULL 无法指向下一节点

测试SLLErase 、SLLEraseAfter接口
在这里插入图片描述
在这里插入图片描述
到这里,所有的接口都实现完成!(●’◡’●) 单链表到这里就结束喽🤭

4. 单链表的缺陷

经过单链表的接口实现,我们发现单链表对于头部的操作很便捷,头插头删 时间复杂度为O(1)
而对于尾部来说非常麻烦 时间复杂度为O(N) 要遍历整个链表才能找到尾部
只是单纯的解决了顺序表中不适合头插头删,开辟大空间造成浪费的问题,仍然存在缺陷。
这就引出了更加高级的数据结构 双向循环链表。

5. 链表OJ题

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

5.1 移除链表元素

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
guard(哨兵) 不存储有效数据,只是用来指向头结点。
带哨兵位头结点:
在这里插入图片描述
之前实现的单链表采用的是二级指针的形式
经过OJ题的练习发现,要改变单链表当中的结点还可以采用返回值的形式:1. 返回新的链表头结点 2. 设计成带哨兵位的链表

5.2 合并有序链表

在这里插入图片描述
不带哨兵位:
在这里插入图片描述
带哨兵位头结点:
在这里插入图片描述
在这里插入图片描述

5.3 反转链表

在这里插入图片描述
两种思路:
第一种:取结点头插到新链表
在这里插入图片描述
第二种:倒过来连接链表结点
在这里插入图片描述

5.4 返回链表中间结点

在这里插入图片描述
如果只是单纯的找到中间结点只需遍历一遍,计算出有多少个结点,在取一半遍历即可
但是如果加个附加条件,只能遍历链表1次,那该如何操作呢?
思路:
中间结点和链表的长度的关系是一半,那么我们可以采用两个指针(快慢指针),快指针每次移动两个结点,当快指针走到NULL时,慢指针正好走到中间结点
在这里插入图片描述

5.5 倒数第k个结点

在这里插入图片描述
思路:同样是两个指针,快指针先走k步,再让快慢指针同时走 快指针为NULL结束 慢指针指向的就是倒数第k个结点
在这里插入图片描述
在这里插入图片描述
当然也可以采用–k的形式:
在这里插入图片描述
在这里插入图片描述

5.6 链表分割

在这里插入图片描述
分析过程:
在这里插入图片描述
在这里插入图片描述

5.7 回文链表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
前两个模块都是使用上述反转和返回中间结点的代码

5.8 相交链表

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

5.9 环形链表

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

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast&&fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return NULL;
}

思维拓展:
在这里插入图片描述

5.10 环形链表 II

在这里插入图片描述
返回入口点
在这里插入图片描述
在这里插入图片描述
另一种思路就是:转换成相交问题
在这里插入图片描述

5.11 复制带随机指针的链表

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

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 一本通 OJ 库的测试数据,通常是用来验证提交的代码在各种情况下的正确性。测试数据可以分为两种类型,手动和自动。 手动测试数据是由目的出人根据意和数据范围设计的一组数据,用来检测程序的正确性和运行效率。手动测试数据的优点是能够涵盖各种情况,但缺点是数量相对较少,不足以覆盖所有可能的情况。 自动测试数据是由程序自动生成的一组数据,可以生成大量的数据以检测程序的健壮性和效率。自动测试数据的优点是数量大且可以自动生成,但缺点是可能无法覆盖某些特殊情况,导致漏洞。 对于提交的代码,一本通 OJ 库会对其进行编译和运行,然后与测试数据进行比较,判断代码的正确性和效率。如果代码通过了测试数据,就会被判定为正确,否则会被判定为错误,并给出具体的错误信息,供用户进行调试和改进。 综上所述,一本通 OJ 库的测试数据是一个重要的组成部分,它可以帮助用户测试代码的正确性和运行效率,提高用户的编程技能,同时也可以帮助出人设计更好的目,并保证目的质量和难度。 ### 回答2: 一本通 oj库是一个在线的程序设计竞赛平台,提供了丰富的编程目和测试数据。测试数据是用于对程序进行测评的输入和输出数据集合。在目描述中,会对问进行详细的解释和要求,并提供多组测试数据作为样例,让程序员运行他们的代码,并得到程序的输出结果。 测试数据通常包括正向测试数据和反向测试数据。正向测试数据是指符合目条件的测试数据,覆盖了大多数情况,测试程序是否正确;而反向测试数据则是用于测试程序是否能够正确处理异常情况。 在使用一本通 oj库时,程序员不仅需要通过编写算法和程序的方式解决问,还需要通过分析测试数据来判断自己的代码是否正确。而一本通 oj库的丰富数据集合为程序员提供了充足的测试数据,帮助程序员准确地检测代码中存在的漏洞和错误。 总之,一本通 oj库提供了全面的测试数据来测试程序员的代码是否满足目描述和要求,是程序员进行程序设计竞赛、算法练习和编程学习的良好平台。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值