数据结构 - 链表

准备重启尘封一年的博客作为学习笔记,看看自己能坚持多久。
最近会记录做过的算法题,语言描述只用于会意,仅供参考。

文章目录

  • 0.从尾到头获取链表的值(不是反转链表)
  • 1.寻找/删除单链表倒数第k个节点
  • 3.寻找单链表的中点
  • 4.判断链表是否成环 / 求成环链表的起点
  • 5.判断两个链表是否相交
  • 6.反转整个链表
    • 迭代法
    • 递归法**
  • 7.反转链表的从第left到第right个节点
    • 迭代法
    • 递归法
  • 8.K个一组反转列表
  • 9.判断回文链表
  • 10.复制复杂的链表
  • 11 链表加法

0.从尾到头获取链表的值(不是反转链表)

解答思路

  • 最直观的想法是,遍历链表求出长度,创建空数组,再遍历链表将每个节点的值反向填进数组
  • 递归解法很简洁,思路类似于树的后根遍历(先根->递归过程自顶向下,后根->递归过程自底向上),从想象一棵单叉的树(其实就是链表),叶节点向根节点前进,就能得到反向的顺序
# -*- coding:utf-8 -*-
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:# 返回从尾部到头部的列表值序列,例如[1,2,3]def printListFromTailToHead(self, listNode):if listNode is None:return []return self.printListFromTailToHead(listNode.next) + [listNode.val]

1.寻找/删除单链表倒数第k个节点

  • 使用两个指针(forward backward),起始时都指向头节点,首先让forward指针向前k步,到达第k+1个节点,然后让两指针以相同速度前进,直到forward为None,此时的backward指向的就是倒数第k个节点。
  • 对于删除问题,需要额外引入一个指针,指向backward之前的节点。此时会出现特殊情况:如果倒数第k个节点是链表的头节点,则需要特殊处理。
/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode removeNthFromEnd(ListNode head, int n) {// 特判if (head.next == null && n == 1) {return null;}ListNode a = head;ListNode b = head;ListNode pre_b = null;// 先走afor (int i=0; i<n; i++) {a = a.next;}// ab同时走,直到a到达末尾while (a != null) {pre_b = b;a = a.next;b = b.next;}// 得到b是待删除结点// 如果b是首个元素,返回b.nextif (pre_b == null) {return b.next;}// b非首个元素pre_b.next = b.next;return head;}
}

3.寻找单链表的中点

  • 使用快慢指针法。每次步进fast走两步,slow走一步,当fast或fast.next指向None时,slow即为中点。如果链表长度为偶数,则slow指向中间并靠后的节点
  • 注意循环判断条件 fast != null && fast.next != null
/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode middleNode(ListNode head) {ListNode slow = head;ListNode fast = head;while (fast != null && fast.next != null) {  // 注意判断条件slow = slow.next;fast = fast.next.next;}return slow;}
}

4.判断链表是否成环 / 求成环链表的起点

  • 快慢指针。如果fast最终能遇见None,则不成环。如果slow最终能遇见fast,则成环。
/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public boolean hasCycle(ListNode head) {ListNode fast = head;ListNode slow = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (fast == slow) {return true;}}return false;}
}

要求成环链表的起点,需要在fast和slow相遇时,将一个指针指向链表头,再让两指针以1步长前进直到相遇,相遇点即为环起点。
假设首次相遇时,fast比slow多走了k步,则这k步一定是在环内绕圈,即k是环长度的整数倍。此时让一个指针回到头节点,共同前进k-m步后一定会在环起点相遇。
在这里插入图片描述

/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public ListNode detectCycle(ListNode head) {ListNode fast = head;ListNode slow = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next;if (fast != null) {fast = fast.next;}// 有环if (fast == slow) {fast = head;} else {continue;}while (fast != slow) {fast = fast.next;slow = slow.next;}return fast;}// 无环return null;}
}

5.判断两个链表是否相交

  • 关键在于解决两个链表的对齐问题。解决办法是,将链表A的末尾指向链表B的头节点,同理B的末尾指向A的头节点。使用两个指针a、b分别从A和B的头节点开始步进,直到遇见相同节点。
  • 直观思路:
    对于a,走过A的独有部分->相交部分->B的独有部分->相交部分
    对于b,走过B的独有部分->相交部分->A的独有部分->相交部分
  • 如果两个链表不相交,则 pa 和 pb 在对方末尾的 None 相等,返回 None
class Solution(object):def getIntersectionNode(self, headA, headB):""":type head1, head1: ListNode:rtype: ListNode"""pa = headApb = headBwhile (pa != pb):pa = pa.next if pa is not None else headBpb = pb.next if pb is not None else headAreturn pa

6.反转整个链表

迭代法

对于每个节点分为三个步骤:

temp = node.next  // 记录当前节点的下个节点,防止修改后找不到
node.next = pre  // 反转
pre = node  // 步进
node = temp  // 步进
/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode reverseList(ListNode head) {// 迭代ListNode temp = head;  // 从head开始处理,head.next最后指向nullListNode pre_temp = null;while (temp != null) {ListNode next_node = temp.next;  // 保存temp.next = pre_temp;  // 更改指针pre_temp = temp;  // 步进temp = next_node;}return pre_temp;}
}

递归法**

  • 来自拉不拉东的算法小抄:写递归算法的关键是,明确函数的定义是什么,“相信定义”,用其推导出最终结果,而不陷入递归的细节。搞清楚根节点该做什么,剩下的交给前中后序遍历
  • 写递归时要有遍历树的思想。下面的代码即为树的后根遍历
  • 反转等操作(类似题1)都能用后根的思想解决
# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):def reverseList(self, head):""":type head: ListNode:rtype: ListNode"""# 递归出口if head is None or head.next is None:return headreversed = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn reversed

7.反转链表的从第left到第right个节点

迭代法

  • 首先定位到第left个节点,记录边界,并执行right-left次反转
# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):def reverseBetween(self, head, left, right):""":type head: ListNode:type left: int:type right: int:rtype: ListNode"""dummy = ListNode(val=-1, next=head)pre_node = dummytemp_node = head# 移动到第left个节点for i in range(left-1):pre_node = temp_nodetemp_node = temp_node.next# 记录左侧未被反转的最后一个节点,和被反转的第一个节点forward_last_node = pre_nodereverse_first_node = temp_node# 从开始反转的第二个节点开始,改变连接关系pre_node = pre_node.nexttemp_node = temp_node.next# 执行反转for i in range(right-left):temp = temp_node.nexttemp_node.next = pre_nodepre_node = temp_nodetemp_node = temp# 连接reverse_first_node.next = temp_nodeforward_last_node.next = pre_nodereturn dummy.next

递归法

首先写一个反转头部前N个节点的函数reverseN

ListNode reverseBetween(ListNode head, int m, int n) {// 递归出口if (m == 1) {return reverseN(head, n);}head.next = reverseBetween(head.next, m - 1, n - 1);return head;
}

8.K个一组反转列表

class Solution:def reverseKGroup(self , head , k ):# write code hereif not head or k == 1:return headdummy = ListNode(-1)node = head last_left = None while (self.enough(node, k)):  # 每次反转k个pre = node cur = node.next# 执行反转for _ in range(k - 1):temp = cur.nextcur.next = pre pre = cur cur = temp if node == head:  # 记录返回结果dummy.next = pre # 处理边界if last_left:last_left.next = pre last_left = node node = cur if last_left:  # 连接最后不到k个不反转的last_left.next = node return dummy.next if last_left else head # 判断从当前节点开始是否够k个def enough(self, node, k):cnt = 0while node:node = node.nextcnt += 1if cnt >= k:return True return False 

就这么做!

class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode prevLeftNode = null;  // 记录上一组反转后的最后节点ListNode res = null;  // 记录结果ListNode prev = head;ListNode curr = head.next;while (this.hasKNodesLeft(prev, k)) {ListNode groupHead = prev;  // 记录当前组反转前的首个节点// 组内反转k-1次for (int i = 0; i < k - 1; i++) {ListNode temp = curr.next;curr.next = prev;prev = curr;curr = temp;}// 更改和前一组的指针if (prevLeftNode != null) {prevLeftNode.next = prev;  // 非第一组,改变指向} else {res = prev;  // 第一组,记录结果}// 更改循环条件prevLeftNode = groupHead;prevLeftNode.next = curr;  // 先把上一组的最后值指向下一组(可能不够k个)的首个位置,如果够k个再修改prev = curr;curr = curr == null ? null : curr.next;  // 避免下一组无元素}return res;}/* 判断是否足够k个节点 */public boolean hasKNodesLeft(ListNode node, int k) {int count = 0;while (node != null && count < k) {count += 1;node = node.next;}return count == k ? true : false;}
}

9.判断回文链表

方法一,使用递归,使用树的后根遍历思想,实现栈“后进先出”的功能。

// 左侧指针
ListNode left;boolean isPalindrome(ListNode head) {left = head;return traverse(head);
}boolean traverse(ListNode right) {if (right == null) return true;boolean res = traverse(right.next);// 后序遍历res = res && (right.val == left.val);left = left.next;return res;
}

方法二,利用快慢指针寻找中点,反转中点以后的链表并进行比较,推荐

class Solution(object):def isPalindrome(self , head ):if not head:return True slow, fast = head, head while (fast):slow = slow.next fast = fast.next if fast:fast = fast.next # 反转后半部分post = self.reverse(slow) # 逐个比较pre = head while (pre and post):if pre.val != post.val:return False pre = pre.next post = post.next return True def reverse(self, node):if not node:return Noneelif not node.next:return node r = self.reverse(node.next)node.next.next = node node.next = None return r

10.复制复杂的链表

链表的每个节点都具有val, next, random属性。关键点是random如何获取——下标如何获取——借用list / 哈希表

"""
class Solution:# 返回 RandomListNodedef Clone(self, pHead):if not pHead:return pHead dummy = RandomListNode(-1)head = RandomListNode(pHead.label)dummy.next = head # 先复制原链表,用两个数组分别记录新老链表的节点olist = [pHead]nlist = [head]p = pHead.next while (p):head.next = RandomListNode(p.label)head = head.next nlist.append(head)olist.append(p)p = p.next # 连接randomfor i in range(len(olist)):target = olist[i].random if not target:continue idx = olist.index(target)nlist[i].random = nlist[idx]return dummy.next 

更优的解法

class Solution {public Node copyRandomList(Node head) {if (head == null) {return null;}List<Node> nodes = new ArrayList<>();  // 存放复制的节点Map<Node, Integer> mapping = new HashMap<>();  // 用哈希表记录节点到下标的映射// 复制值,并记录下标Node node = head;int index = 0;while (node != null) {nodes.add(new Node(node.val));mapping.put(node, index);index++;node = node.next;}// 执行连接node = head;for (int i = 0; i < nodes.size(); i++) {// 1.连接randomNode temp = nodes.get(i);if (node.random != null) {int randomIndex = mapping.get(node.random);temp.random = nodes.get(randomIndex);}// 2.连接nextif (i < nodes.size() - 1) {temp.next = nodes.get(i + 1);}node = node.next;}return nodes.get(0);}
}

11 链表加法

在这里插入图片描述

  • 借助栈实现后端对齐
  • 处理最后一步的进位问题
class Solution:def addInList(self , head1 , head2 ):# write code here# 用栈s1, s2 = [], []while head1:s1.append(head1)head1 = head1.next while head2:s2.append(head2)head2 = head2.next post = None plus = 0while (s1 and s2):n1, n2 = s1.pop(), s2.pop()val = n1.val + n2.val + plus plus = 1 if val >= 10 else 0val -= 10 if val >= 10 else 0node = ListNode(val)node.next = post post = node if s1:while s1:n1 = s1.pop()val = n1.val + plus plus = 1 if val >= 10 else 0val -= 10 if val >= 10 else 0node = ListNode(val)node.next = post post = node elif s2:while s2:n2 = s2.pop()val = n2.val + plus plus = 1 if val >= 10 else 0val -= 10 if val >= 10 else 0node = ListNode(val)node.next = post post = nodeif plus == 1:res = ListNode(1)res.next = post return res return post 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307200.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[读书笔记] 《修炼之道:.NET 开发要点精讲》

《修炼之道:.NET 开发要点精讲》目录《修炼之道:.NET 开发要点精讲》第 1 章 另辟蹊径&#xff1a;解读.NET1.7 本章思考 > 位置 465第 2 章 高屋建瓴&#xff1a;梳理编程约定2.2 方法与线程的关系 > 位置 5192.7 线程安全 > 位置 5952.8 调用与回调 > 位置 6612.…

ASP.NET Core 使用 gRPC 初探

&#xff08;RPC通讯示意图&#xff09;为什么突然说到gRPC呢&#xff0c;其实以前就想说一说这个东西&#xff0c;也想尝试使用一下&#xff0c;一直没有机会&#xff0c;一直看我公众号的小伙伴肯定都知道&#xff0c;这几天一直在录制一个《eShopOnContainer微服务架构》系列…

源码都没调试过,怎么能说熟悉 redis 呢?

一&#xff1a;背景 1. 讲故事记得在很久之前给初学的朋友们录制 redis 视频课程&#xff0c;当时结合了不少源码进行解读&#xff0c;自以为讲的还算可以&#xff0c;但还是有一个非常核心的点没被分享到&#xff0c;那就是源码级调试&#xff0c; 对&#xff0c;读源码还远远…

算法 - DFS/BFS

写DFS函数的时候首先确定当前位置是否已经加入路径 DFS函数大概率会传递“位置信息”&#xff0c;根据位置信息获取下一步的选择&#xff0c;&#xff08;大部分是在循环中&#xff09;选择、执行、回退 在哪做选择&#xff0c;就在哪退出选择&#xff0c;参考题9 def DFS()…

你想象中的Task后续,很简单?

【导读】前不久&#xff0c;写过一篇关于Task的简短文章&#xff0c;通过评论和转载得到好评&#xff0c;刚好我昨晚又写了一篇实现简单的消息队列也提到了Task&#xff0c;难道不应该是看具体执行什么操作&#xff0c;再考虑最佳方案&#xff1f;本文我们再次通过简短内容谈谈…

5G在工业互联网应用的机遇与挑战

移动通讯经过十年一代的发展&#xff0c;已经从1G发展到了5G&#xff0c;峰值速率实现十年千倍的增长&#xff0c;1G到4G是面向个人的&#xff0c;而5G是面向产业互联网和智慧城市服务。5G是一个颠覆性的技术&#xff0c;低时延&#xff08;每秒钟下载一部高清电影&#xff09;…

[C#.NET 拾遗补漏]10:理解 volatile 关键字

要理解 C# 中的 volatile 关键字&#xff0c;就要先知道编译器背后的一个基本优化原理。比如对于下面这段代码&#xff1a;public class Example {public int x;public void DoWork(){x 5;var y x 10;Debug.WriteLine("x " x ", y " y);} }在 Releas…

跟我一起学.NetCore之MediatR好像有点火

前言随着微服务的流行&#xff0c;而DDD(领域驱动设计)也光速般兴起&#xff0c;CRQS(Command Query Responsibility Seperation--命令查询职责分离)、领域事件名词是不是经常在耳边环绕&#xff0c;而MediatR组件经常用来对其技术的落地&#xff0c;凭这&#xff0c;小伙伴们说…

不想写脚本清理 mongodb 中的垃圾数据,ttlIndex 能帮到你!

mongodb一直都在不断的更新&#xff0c;不断的发展&#xff0c;那些非常好玩也非常实用的功能都逐步加入到了mongodb中&#xff0c;这不就有了本篇对ttlindex的介绍&#xff0c;刚好我们的生产业务场景中就有一个案例。。。一&#xff1a;案例分析 生产的推荐系统要给用户发送短…

后端学习 - 基础 《Java编程的逻辑》读书笔记

文章目录一 基础概念1 有关Java2 JVM / JDK / JRE3 与C的联系和区别4 各类型数据占用空间大小5 和 equals() 的区别、hashCode() 方法6 包装类型7 final 关键字8 参数传递机制&#xff1a;值传递9 String 的内存情况10 访问修饰符11 引用拷贝、浅拷贝与深拷贝三 面向对象1 面向…

cheatengine找不到数值_彩票中奖500万,领了还不到一半?这些问题不解决,钱都拿不走...

长期以来&#xff0c;“一夜暴富”是很多人梦寐以求的梦想&#xff0c;而作为最能让人“一夜暴富”的方式要数我国的福利彩票了&#xff0c;这也是很多人最容易活动暴富的机会&#xff0c;不少彩民长久以来一直买彩票的梦想就是“一夜暴富”。而突然暴富是很多人的梦想&#xf…

一站式Web开发套件BeetleX.WebFamily

BeetleX.WebFamily是一款前后端分离的Web开发套件&#xff0c;但它并不依赖于nodejs/npm/webpack等相关工具&#xff1b;而使用自身实现的方式来完成前后端分离的Web应用开发&#xff1b;套件以组件的方式发布&#xff0c;只需要在项目引用相关组件即可实现前后端分离开发&…

.NET架构小技巧(2)——访问修饰符正确姿势

在C#中&#xff0c;访问修饰符是使用频率很高的一组关键字&#xff0c;一共四个单词六个组合&#xff1a;public,internal,protected internal,protected,private protected,private&#xff0c;如果你对这些关键字非常清楚&#xff0c;请跳过&#xff0c;节省时间&#xff1b;…

能源36号文解读_IDC报告预测:今年中国新能源汽车销量将达116万辆,未来五年复合增长率36%_详细解读_最新资讯_热点事件...

编者按&#xff1a;本文来自36氪「 未来汽车日报」&#xff0c;(微信公众号ID&#xff1a;auto-time)&#xff0c;作者&#xff1a;秦章勇。 来源&#xff1a;IDC作者 | 秦章勇编辑 | 周游12月3日&#xff0c;在2020世界智能汽车大会上&#xff0c;IDG亚洲(国际数据(亚洲)集团)…

后端学习 - 容器

文章目录一 简介二 底层数据结构总结1 List2 Set3 Queue4 Map三 Collection 的子接口 List1 ArrayList 与 Vector2 ArrayList 与 LinkedList3 ArrayList 的 JDK 7/8 差异4 ArrayList 的构造方法与扩容机制*四 Collection 的子接口 Set1 HashSet、LinkedHashSet 和 TreeSet2 Has…

简单聊聊AspNetCore的启动流程

首先&#xff0c;得和大家达成一个共识&#xff0c;即AspNetCore项目其实就是一个控制台项目。可以简单的理解&#xff0c;AspNetCore就是将一个Web服务器宿主在一个进程(即控制台)中&#xff0c;然后它在这个进程中进行http请求的监听处理。AspNetCore中默认使用kestrel作为we…

共聚焦图片怎么加标尺_聚焦扶贫政策,打造小康生活

导语&#xff1a;农村独栋小楼、整洁的水泥路……扶贫产业蓬勃发展&#xff0c;我省结合实际&#xff0c;狠抓特色产业&#xff0c;助力脱贫攻坚&#xff0c;实现乡村振兴。武宁县&#xff1a;“四个聚焦”巩固脱贫成果2020年是全面建成小康社会目标实现之年&#xff0c;是全面…

后端学习 - 并发编程

文章目录零 基本概念1 CAS、ABA 问题和原子变量2 this 引用逸出3 不变性 immutable4 同步、异步、阻塞、非阻塞5 JMM6 同步方案演示&#xff1a;计数器 demo*一 进程与线程1 区别与联系2 Java内存区域3 线程组4 线程的上下文切换5 并发与并行6 线程的生命周期与状态二 线程间的…

打造跨平台.NET Core后台服务

续之前讲的在TopShelf上部署ASP.NET Core程序&#xff0c;作为后台服务运行&#xff0c;自从.NET Core 3.0出现以后&#xff0c;出现了自带的Generic Host&#xff0c;使得自托管服务变为可能。这种方式和TopShelf方式一样&#xff0c;可以直接F5进行服务的调试&#xff0c;也为…

iphone桌面横屏设置在哪里_我和我各司其职的桌面们

作者&#xff1a;旭彦兮沐桌面是只属于我们自己一个人的舞台&#xff0c;是与我们独处的好伙伴。好好布置一下自己的桌面&#xff0c;能在很大程度上保持我们心情的愉悦和做事情的效率&#xff0c;让我们保持专注当下的沉浸感。我最早了解到「桌面文化」其实是很早之前了&#…