当前位置: 首页 > news >正文

链表经典算法题

单链表作为数据结构中最基础的线性结构之一,其灵活的动态内存管理特性使其在算法设计和实际项目开发中有着广泛应用。本文将详细介绍单链表的经典算法 OJ 题目,展示单链表在实际开发中的具体应用。

1 移除链表元素

1.1 题目链接

203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/description/

1.2 问题要求

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

1.3 解决方案

思路1

遍历原链表,将值为 val 节点释放掉。

……

以此类推

 思路2

创建新链表,找值不为 val 的节点,尾插到新的链表中。

1.4 代码

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {ListNode *newHead, *newTail;newHead = newTail = NULL;// 遍历原链表ListNode* pcur = head;while (pcur) {// 找不为 valu 的节点if (pcur->val != val) {// 链表为空if (newHead == NULL) {newHead = newTail = pcur;}// 链表不为空else {newTail->next = pcur;newTail = newTail->next;}}pcur = pcur->next;}if(newTail){newTail->next = NULL;}return newHead;
}

2 反转链表

2.1 题目链接

206. 反转链表 - 力扣(LeetCode)https://leetcode.cn/problems/reverse-linked-list/description/

2.2 问题要求

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

2.3 解决方案

思路1:

创建新链表,遍历原链表,将原链表的节点,依次头插在新的链表中。

思路二:

创建三个指针,完成原链表的翻转。

创建指针

让 n2 的 next 不指向 n3 ,指向 n1

而 n1 指向 n2 后:(圈为调整后的位置,箭头表示调整的参照)

n2 指向 n3后:(圈为调整后的位置,箭头表示调整的参照)

n3 指向 n3 的下一个后:(圈为调整后的位置,箭头表示调整的参照)

调整完成:

再次调整 n2 的指针方向:

继续重复上述步骤。

直到 n2 n3 为空时,跳出循环,此时 n1 为新链表的头节点。

2.4 代码

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{if(head == NULL){return head;}// 创建指针ListNode *n1, *n2, *n3;n1 = NULL, n2 = head, n3 = n2->next;while(n2) {n2->next = n1;n1 = n2;n2 = n3;if (n3) {n3 = n3->next;}}return n1;
}

3 合并两个有序链表

3.1 题目链接

21. 合并两个有序链表 - 力扣(LeetCode)https://leetcode.cn/problems/merge-two-sorted-lists/description/

3.2 问题要求

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

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

3.3 解决方案

思路1:

定义两个指针,依次遍历链表,比较大小。

创建新的空链表,遍历原链表,将节点值小的节点拿出来放在新链表中进行尾插操作。

以此类推:

 

遍历的结果,有两种情况:

l1 要么先为空,要么l2

3.4 代码

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{// 处理空链表情况if(list1 == NULL){return list2;}if(list2 == NULL){    return list1;}// 创建指针遍历两个链表ListNode* l1 = list1;ListNode* l2 = list2;// 创建合并后链表的头节点和尾节点指针ListNode *newHead = NULL, *newTail = NULL;// 遍历两个链表,按值大小合并while (l1 != NULL && l2 != NULL) {if (l1->val < l2->val) {// 取l1的节点插入新链表if (newHead == NULL) {// 新链表为空时,头和尾都指向当前节点newHead = newTail = l1;} else {// 新链表非空时,尾插法添加节点newTail->next = l1;newTail = newTail->next;}l1 = l1->next; // 移动l1指针} else {// 取l2的节点插入新链表(逻辑同上)if(newHead == NULL){newHead = newTail = l2;}else{newTail->next = l2;newTail = newTail->next;}l2 = l2->next; // 移动l2指针}}// 处理剩余节点(其中一个链表已遍历完)if (l1 != NULL) {newTail->next = l1; // 剩余节点接在新链表尾部}if (l2 != NULL) {newTail->next = l2;}return newHead; // 返回合并后链表的头节点(关键修复)
}

3.5 扩展

存在重复代码:

如何优化?

重复原因:

新链表中存在空链表和非空链表两种情况。

解决思路:

让新链表不为空。

 

修改这里:

使得空节点变成非空节点,此时,链表不为空,头尾指针都指向了一个有效的地址(节点)。

简化代码:

此时需要注意的是链表变成了:

其中多余申请出来的是链表分类中的一种,叫做带头链表。

头节点(哨兵位) 

 所以打印时不可直接打印:

而是要修改为:

要注意将动态申请的空间及时释放掉:

优化后的代码:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{// 处理空链表情况if(list1 == NULL){return list2;}if(list2 == NULL){    return list1;}// 创建指针遍历两个链表ListNode* l1 = list1;ListNode* l2 = list2;// 创建合并后链表的头节点和尾节点指针ListNode *newHead, *newTail;//动态申请空间newHead=newTail=(ListNode*)malloc(sizeof(ListNode));// 遍历两个链表,按值大小合并while (l1 != NULL && l2 != NULL) {if (l1->val < l2->val) {// 取l1的节点插入新链表newTail->next = l1;newTail = newTail->next;l1 = l1->next; // 移动l1指针} else {// 取l2的节点插入新链表(逻辑同上)newTail->next = l2;newTail = newTail->next;l2 = l2->next; // 移动l2指针}}// 处理剩余节点(其中一个链表已遍历完)if (l1 != NULL) {newTail->next = l1; // 剩余节点接在新链表尾部}if (l2 != NULL) {newTail->next = l2;}//将动态申请的空间及时释放ListNode* ret = newHead->next;free(newHead);newHead = NULL;return ret; // 返回合并后链表的头节点
}

4 链表的中间结点

4.1 题目链接

876. 链表的中间结点 - 力扣(LeetCode)https://leetcode.cn/problems/middle-of-the-linked-list/description/

4.2 问题要求

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

示例 2:

输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

4.3 解决方案

思路1:

依次遍历,利用 count 计数,返回 count / 2 的整数部分,该整数为节点数,其 next 节点为中间结点。

思路2: 

快慢指针(2 * slow = fast)。

创建两个指针,slow 和 fast,初始时都指向头节点:

slow 每次走一步 fast 每次走两步:

重复步骤:

当 fast->next==NULL 时停止。

此时 slow 指向的刚好是中间节点。

偶数个节点,如图所示,同理。(当 fast 为空时,停止,此时slow 指向的刚好是中间节点。)

4.4 代码

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) 
{// 创建快慢指针ListNode* slow = head;ListNode* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow;
}

4.5 扩展

while (fast && fast->next) 可以换为 while (fast->next && fast) 吗?

不可以!

为什么?

如果链表中有偶数个节点时,fast 最后一次会直接走到空,fast->next执行时会报错,所以不可以交换位置。

5 循环链表的经典应用——环形链表的约瑟夫问题

5.1 题目链接

环形链表的约瑟夫问题_牛客题霸_牛客网https://www.nowcoder.com/practice/41c399fdb6004b31a6cbb047c641ed8a

5.2 问题要求

编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。

下一个人继续从 1 开始报数。

n-1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少?

数据范围: 1≤n,m≤100001≤n,m≤10000

进阶:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)

示例1

输入:5,2

返回值:3

说明:

开始5个人 1,2,3,4,5 ,从1开始报数,1->1,2->2编号为2的人离开 1,3,4,5,从3开始报数,3->1,4->2编号为4的人离开 1,3,5,从5开始报数,5->1,1->2编号为1的人离开 3,5,从3开始报数,3->1,5->2编号为5的人离开 最后留下人的编号是3

示例2

输入:1,1

返回值:1

5.3 解决方案

循环链表:

问题分析:

思路1:

环形链表思维。

最后 3 要指向 3 :

创建两个指针 pcur 为1节点,perv 为上一个节点(本题中为 5 节点)

pcur 走前,prev要到pcur, pcur 报 2 时,先将 prev 指向 pcur->next,再释放 pcur,此时,pcur 为野指针,要将 pcur 指向 prev->next 的节点并且数 1,继续向下走报数为 2 时,同上循环。

如图所示:

以此类推,最终得到:

5.4 代码

/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param n int整型 * @param m int整型 * @return int整型*/#include <stdarg.h>
#include <stdlib.h>
typedef struct ListNode ListNode;
//创建节点
ListNode* buyNode(int x)
{ListNode*node = (ListNode*)malloc(sizeof(ListNode));if(node == NULL){exit(1);}node->val = x;node->next = NULL;return node;
}
//创建带环链表
ListNode* creatCircle(int n)
{//先创建第一个节点ListNode* phead = buyNode(1);ListNode* ptail = phead;for(int i = 2; i <= n; i++){ptail->next = buyNode(i);ptail = ptail->next;}//首尾相连,链表成环ptail->next = phead;return ptail;
}
int ysf(int n, int m ) {// write code here//根据n创建带环链表ListNode* prev = creatCircle(n);ListNode* pcur = prev->next;int count = 1;while(pcur->next != pcur)//只有一个节点时,跳出循环{if(count == m){//销毁 pcur 节点prev->next = pcur->next;free(pcur);pcur = prev->next;count = 1;}else {//销毁 pcur 节点prev = pcur;pcur = pcur->next;count++;}}//此时剩下的一个节点是返回的值return pcur->val;
}

6 分割链表

6.1 题目链接

面试题 02.04. 分割链表 - 力扣(LeetCode)https://leetcode.cn/problems/partition-list-lcci/description/

6.2 问题要求

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你不需要 保留 每个分区中各节点的初始相对位置。

示例 1:

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

示例 2:

输入:head = [2,1], x = 2
输出:[1,2]

6.3 解决方案

思路1:

在原链表上进行修改。

如果 pcur 节点的值小于 x ,往后走。

如果 pcur 节点的值大于或者等于 x,尾插在原链表后,删除旧的节点。

注意当pcur == ptail 时,跳出循环。

具体操作如图:

当遇到时,还要创建一个指针:

将数字,置于新创建的指针后:

prev 要指向 pcur->next :

调整后,释放 pcur 处数字:

移动后,pcur 为野指针,所以要指向 prev->next :

接下来,继续比较,在移动数字,进行尾插操作时,要移动 newPtail 指针:

以此类推,继续向后比较。但是,当遇到原链表的最后一个数字是大于等于的数字时,会非常麻烦。

思路2:

 新建一个链表,进行操作。

依次进行操作:

对于大于等于的数字进行尾插操作:

而小于的数字进行头插操作(头插在 newHand 节点的后面):

依次向后调整:

之后将哨兵位释放即可,题目说“不需要 保留 每个分区中各节点的初始相对位置”,所以只要将大于等于,以及小于的数字分开即可,不需要严格的顺序。

思路3:

 创建小链表和大连表进行操作。

将小于的数尾插在小链表中,将大于等于的数尾插在大链表中:

将小链表的尾节点和大链表的第一个有效节点首尾相连。

之后将哨兵位释放即可。

6.4 代码

初步代码:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x) {//当链表为空时if(head==NULL){return head;}//创建两个带头链表ListNode* lessHead,*lessTail;ListNode* greaterHead,*greaterTail;lessHead=lessTail=(ListNode*)malloc(sizeof(ListNode));greaterHead=greaterTail=(ListNode*)malloc(sizeof(ListNode));//遍历原链表ListNode*pcur=head;while(pcur){if(pcur->val<x){//尾插到小链表lessTail->next=pcur;lessTail=lessTail->next;}else{//尾插大链表greaterTail->next=pcur;greaterTail=greaterTail->next;}pcur=pcur->next;}//小链表的尾节点和大链表的第一个有效节点首尾相连lessTail->next=greaterHead->next;//释放哨兵位ListNode*ret=lessHead->next;free(lessHead);free(greaterHead);lessHead=greaterHead=NULL;return ret;
}

此时:

 

出现死循环。

为什么呢?

因为:

 5 后的指针没有处理,还链接的是 2。

所以链表会形成一个环,出现死循环,进行以下修改 :

但是对于只有一个节点时,代码还是有问题:

进行以下修改:

此时提交成功:

 

因为先前没有将 next 位置的指针初始化,只是给了一个随机的地址,所以会报错。

修正代码:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;struct ListNode* partition(struct ListNode* head, int x) 
{// 当链表为空时if (head == NULL){return head;}// 创建两个带头链表ListNode *lessHead, *lessTail;ListNode *greaterHead, *greaterTail;lessHead = lessTail = (ListNode*)malloc(sizeof(ListNode));greaterHead = greaterTail = (ListNode*)malloc(sizeof(ListNode));// 遍历原链表ListNode* pcur = head;while (pcur) {if (pcur->val < x) {// 尾插到小链表lessTail->next = pcur;lessTail = lessTail->next;} else {// 尾插大链表greaterTail->next = pcur;greaterTail = greaterTail->next;}pcur = pcur->next;}// 避免无限循环// 修改大链表尾节点// next 初始化greaterTail->next = NULL;// 小链表的尾节点和大链表的第一个有效节点首尾相连lessTail->next = greaterHead->next;// 释放哨兵位ListNode* ret = lessHead->next;free(lessHead);free(greaterHead);lessHead = greaterHead = NULL;return ret;
}

7 内容补充:链表的分类

7.1 链表结构

链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

7.2 链表说明

单向:只能从一个方向遍历。

双向:可以从两个方向遍历。

不带头:无哨兵位。

带头:指的是链表中有哨兵位的节点,该哨兵位节点即头节点。(在前面实现链表时,口头上提到的头接点,实际上指的是第一个有效接点,这不是正确的称呼,但是为了更加好的去理解才这样错误的称呼为头节点。实际在链表中,头节点指的是哨兵位。 )

不循环:尾节点的 next 指针指向空。

循环:尾节点的 next 指针不为空。

目前我们最多实现的是:不带头单向不循环链表(简称:单链表) 

其结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。

我们实际中最常用还有:

双向带头循环链表

其结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现(基本上不存在循环的代码)以后会发现结构会带来很多优势,实现反而简了。

形如: 

http://www.dtcms.com/a/292824.html

相关文章:

  • web复习
  • 网络原理 HTTP 和 HTTPS
  • kafka查看消息的具体内容 kafka-dump-log.sh
  • Python笔记完整版
  • 扇形区域拉普拉斯方程傅里叶解法2
  • 一款功能全面的文体场所预约小程序
  • Grails(Groovy)框架抛出NoHandlerFoundException而不是返回404 Not Found
  • 【多线程篇21】:深入浅出理解Java死锁
  • 《Uniapp-Vue 3-TS 实战开发》自定义预约时间段组件
  • 7.22总结mstp,vrrp
  • WebSocket心跳机制实现要点
  • 京东AI投资版图扩张:具身智能与GPU服务器重构科研新范式
  • 小鹏汽车视觉算法面试30问全景精解
  • 学习游戏制作记录(战斗系统简述以及击中效果)7.22
  • 为什么使用扩展坞会降低显示器的最大分辨率和刷新率
  • 智能泵房监控系统:物联网应用与智能管理解决方案
  • 【观察】维谛技术(Vertiv)“全链智算”:重构智算中心基础设施未来演进范式
  • 如何编译RustDesk(Unbuntu 和Android版本)
  • Cookies 详解及其与 Session 的协同工作
  • AWS OpenSearch 搜索排序常见用法
  • 2️⃣tuple(元组)速查表
  • C语言面向对象编程
  • Java函数式编程深度解析:从基础到高阶应用
  • Leetcode题解:209长度最小的子数组,掌握滑动窗口从此开始!!!
  • 光伏电站智能数据采集系统解决方案
  • SpringBoot PO VO BO POJO实战指南
  • 十进制小数转换为二进制表示 ← 除2取余法+乘2取整法
  • csp基础知识——递推
  • SMTP+VRRP实验
  • Markdown 转 PDF API 数据接口