目录
83. 删除排序链表中的重复元素:remove-duplicates-from-sorted-list
82. 删除排序链表中的重复元素 II remove-duplicates-from-sorted-list-ii
206. 反转链表:reverse-linked-list
92. 反转链表 II:reverse-linked-list-ii
21. 合并两个有序链表merge-two-sorted-lists
142 环形链表 Ⅱ linked-list-cycle-ii
234. 回文链表palindrome-linked-list
138. 复制带随机指针的链表copy-list-with-random-pointer
算法快速入门
数据结构与算法
数据结构是一种数据的表现形式,如链表、二叉树、栈、队列等都是内存中一段数据表现的形式。 算法是一种通用的解决问题的模板或者思路,大部分数据结构都有一套通用的算法模板,所以掌握这些通用的算法模板即可解决各种算法问题。
后面会分专题讲解各种数据结构、基本的算法模板、和一些高级算法模板,每一个专题都有一些经典练习题,完成所有练习的题后,你对数据结构和算法会有新的收获和体会。
先介绍两个算法题,试试感觉~
示例 1
strStr
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从 0 开始)。如果不存在,则返回 -1。
思路:核心点遍历给定字符串字符,判断以当前字符开头字符串是否等于目标字符串
需要注意点
-
循环时,i 不需要到 len-1
-
如果找到目标字符串,len(needle)==j
示例 2
subsets
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
思路:这是一个典型的应用回溯法的题目,简单来说就是穷尽所有可能性,算法模板如下
通过不停的选择,撤销选择,来穷尽所有可能性,最后将满足条件的结果返回
说明:后面会深入讲解几个典型的回溯算法问题,如果当前不太了解可以暂时先跳过
面试注意点
我们大多数时候,刷算法题可能都是为了准备面试,所以面试的时候需要注意一些点
-
快速定位到题目的知识点,找到知识点的通用模板,可能需要根据题目特殊情况做特殊处理。
-
先去朝一个解决问题的方向!先抛出可行解,而不是最优解!先解决,再优化!
-
代码的风格要统一,熟悉各类语言的代码规范。
-
命名尽量简洁明了,尽量不用数字命名如:i1、node1、a1、b2
-
-
常见错误总结
-
访问下标时,不能访问越界
-
空值 nil 问题 run time error
-
练习
链表
基本技能
链表相关的核心点
-
null/nil 异常处理
-
dummy node 哑巴节点
-
快慢指针
-
插入一个节点到排序链表
-
从一个链表中移除一个节点
-
翻转链表
-
合并两个链表
-
找到链表的中间节点
常见题型
83. 删除排序链表中的重复元素:remove-duplicates-from-sorted-list
83. 删除排序链表中的重复元素:模拟题
,直接遍历链表,遇到重复值的节点删除即可。
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
83. 删除排序链表中的重复元素
#直接法:
这是一个简单的问题,仅测试你操作列表的结点指针的能力。由于输入的列表已排序,
# 因此我们可以通过将结点的值与它之后的结点进行比较来确定它是否为重复结点。
# 如果它是重复的,我们更改当前结点的 next 指针,
# 以便它跳过下一个结点并直接指向下一个结点之后的结点。
解法https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/solution/hua-jie-suan-fa-83-shan-chu-pai-xu-lian-biao-zhong/
图解https://pic.leetcode-cn.com/c61a88b9fe012a9b85b842f4a12a5310c96b462ea4801e6227fc6a04aa140351-frame_00001.png
# Python3
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
cur=head
while cur and cur.next:
if cur.val==cur.next.val:
cur.next=cur.next.next
else:
cur=cur.next
return head
# 输入 输出
# [1,2,2] [1,2]
# [1,1,1] [1]
# 复杂度分析
# 时间复杂度:O(n),因为列表中的每个结点都检查一次以确定它是否重复,
# 所以总运行时间为 O(n),其中 n 是列表中的结点数。
# 空间复杂度:O(1),没有使用额外的空间。
# 此题需要返回一个没有重复元素的新链表,因为没有链表没有下标,
# 如果使用「双指针」(双引用),后面覆盖前面,可行。
# 但返回新链表,实现起来比较复杂。
# 此题可借助链表的特性「指针」(Pointer)来实现。
# 如果当前节点的值等于下一个节点的值,指向下下个节点(跳过下一个节点),
# 引用 head 并后移一位。
82. 删除排序链表中的重复元素 II remove-duplicates-from-sorted-list-ii
82. 删除排序链表中的重复元素 II:模拟题
,遍历链表,若head的节点值与head的next节点值不相等,则pre指向head,也就是不重复节点;若相等,我们需要找到重复值子链表的最后一个节点,然后令pre指向head->next,同时head移动到下一个节点。
思路:链表头结点可能被删除,所以用 dummy node 辅助删除
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
解法二
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/solution/san-chong-jie-fa-duo-tu-zhan-shi-82-shan-chu-pai-x/
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def deleteDuplicates(self, head):
if not (head and head.next):
return head
dummy = ListNode(-1)
dummy.next = head
a = dummy
b = head
while b and b.next:
# 初始化的时a指向的是哑结点,所以比较逻辑应该是a的下一个节点和b的下一个节点
if a.next.val!=b.next.val:
a = a.next
b = b.next
else:
# 如果a、b指向的节点值相等,就不断移动b,直到a、b指向的值不相等
while b and b.next and a.next.val==b.next.val:
b = b.next
a.next = b.next
b = b.next
return dummy.next
206. 反转链表:reverse-linked-list
206. 反转链表:双指针法
,指针pre用来表示前驱节点,指针cur用来遍历链表,每次循环改变将pre->cur
的方向改变为pre<-cur
,直到遍历结束。
反转一个单链表。
思路:用一个 prev 节点保存向前指针,temp 保存向后的临时指针
https://leetcode-cn.com/problems/reverse-linked-list/solution/dong-hua-yan-shi-206-fan-zhuan-lian-biao-by-user74/
双指针迭代
用 O(1) 空间复杂度来实现这道题。
class Solution(object):
def reverseList(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
# 申请两个节点,pre和 cur,pre指向None
pre = None
cur = head
# 遍历链表,while循环里面的内容其实可以写成一行
# 这里只做演示,就不搞那么骚气的写法了
while cur:
# 记录当前节点的下一个节点
tmp = cur.next #相当于赋值
# 然后将当前节点指向pre
cur.next = pre #相当于指向
# pre和cur节点都前进一位
pre = cur
cur = tmp
return pre
递归解法:
class Solution(object):
def reverseList(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
# 递归终止条件是当前为空,或者下一个节点为空
if(head==None or head.next==None):
return head
# 这里的last就是最后一个节点
last = self.reverseList(head.next)
head.next.next = head
# 防止链表循环,需要将head.next设置为空
head.next = None
# 每层递归函数都返回last,也就是最后一个节点
return last
#输入一个节点head,将“以head为起点”的链表反转,并返回反转之后的头结点
def reverse(self,head):
#反转整个链表
if not head or not head.next:return head
last = self.reverse(head.next)
head.next.next = head
head.next = None
return last
反转前N个链表是:
#反转链表前N个节点
def reverseN(self,head,n):
if n == 1:
# 记录第 n + 1 个节点
succeessor=head.next
return head
# 以 head.next 为起点,需要反转前 n - 1 个节点
last = self.reverseN(head.next,n-1)
# 让反转之后的 head 节点和后面的节点连起来
head.next.next = head
head.next = successor
return last
92. 反转链表 II:reverse-linked-list-ii
92. 反转链表 II:双指针法
,指针pre指针指向m的前驱节点,用来将cur的next节点插入到pre后面,指针cur指向位置m起始节点,该节点保持不变,每次需要将cur连接上nxt后边的部分。换句话说,我们要将[m+1,n]的节点每次都要插到位置m之前,这样就完成了反转。
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
思路:先遍历到 m 处,翻转,再拼接后续,注意指针处理
# recurse 递归
# Reverse 反转,颠倒
回溯法(Backtrcking)
读音huí sù,也叫试探法,它是一种系统地搜索问题的解的方法,是一种思想。
有一类问题,我们不知道它明确的计算法则。而是先进行试探,试探到最终状况,发现不满足问题的要求,则回溯到上一个状态继续试探。这种不断试探和回溯的思想,称为回溯法(Backtrcking)
这类问题有求最优解、一组解、求全部解这类问题,例如八皇后问题
【回溯的算法思想】一直往下走,然后再一步步往回走
面对一个问题,每一步有多种做法。先做其中一个做法,然后再此基础上一直做下去,把这个做法的所有可能全部做完,再回来,做第二种做法。
【例子】
深度优先搜索
求一个序列的幂集
八皇后问题
涂色问题
【回溯法实质】它的求解过程实质上是先序遍历一棵“状态树”的过程。
只不过,这棵树不是遍历前预先建立的,而是隐含在遍历过程中。
如果认识到这点,很多问题的递归过程设计也就迎刃而解了。
https://blog.csdn.net/summer_dew/article/details/83921581
解法:
https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/fan-zhuan-lian-biao-ii-by-leetcode/
方法一: 递归
# 说明:
# 1 ≤ m ≤ n ≤ 链表长度。
# 示例:
# 输入: 1->2->3->4->5->NULL, m = 2, n = 4
# 输出: 1->4->3->2->5->NULL
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reverseBetween(self, head, m, n):
#递归
if m == 1:
return self.reverseN(head,n)
head.next = self.reverseBetween(head.next,m-1,n-1)
return head
#反转链表前N个节点
def reverseN(self,head,n):
if n == 1:
# 记录第 n + 1 个节点
succeessor=head.next
return head
# 以 head.next 为起点,需要反转前 n - 1 个节点
last = self.reverseN(head.next,n-1)
# 让反转之后的 head 节点和后面的节点连起来
head.next.next = head
head.next = successor
return last
下面的看不懂
class Solution:
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
"""
:type head: ListNode
:type m: int
:type n: int
:rtype: ListNode
"""
if not head:
return None
left, right = head, head
stop = False
def recurseAndReverse(right, m, n):
nonlocal left, stop
# base case. Don't proceed any further
if n == 1:
return
# Keep moving the right pointer one step forward until (n == 1)
right = right.next
# Keep moving left pointer to the right until we reach the proper node
# from where the reversal is to start.
if m > 1:
left = left.next
# Recurse with m and n reduced.
recurseAndReverse(right, m - 1, n - 1)
# In case both the pointers cross each other or become equal, we
# stop i.e. don't swap data any further. We are done reversing at this
# point.
if left == right or right.next == left:
stop = True
# Until the boolean stop is false, swap data between the two pointers
if not stop:
left.val, right.val = right.val, left.val
# Move left one step to the right.
# The right pointer moves one step back via backtracking.
left = left.next
recurseAndReverse(right, m, n)
return head
方法二: 迭代链接反转:
复杂度分析
时间复杂度: O(N)。考虑包含 N 个结点的链表。对每个节点最多会处理(第 n个结点之后的结点不处理)。
空间复杂度: O(1)。我们仅仅在原有链表的基础上调整了一些指针,只使用了 O(1)的额外存储空间来获得结果。
class Solution:
def reverseBetween(self, head, m, n):
"""
:type head: ListNode
:type m: int
:type n: int
:rtype: ListNode
"""
# Empty list
if not head:
return None
# Move the two pointers until they reach the proper starting point
# in the list.
cur, prev = head, None
while m > 1:
prev = cur
cur = cur.next
m, n = m - 1, n - 1
# The two pointers that will fix the final connections.
tail, con = cur, prev
# Iteratively reverse the nodes until n becomes 0.
while n:
third = cur.next
cur.next = prev
prev = cur
cur = third
n -= 1
# Adjust the final connections as explained in the algorithm
if con:
con.next = prev
else:
head = prev
tail.next = cur
return head
25. K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
# 翻转一个子链表,并且返回新的头与尾
def reverse(self, a :ListNode, b: ListNode):
pre=None
cur=a
while cur != b:
third = cur.next
cur.next = pre
pre = cur
cur = third
return pre
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
if head==None:
return None
a=b=head
for i in range(k):
# 不足 k 个,不需要反转,base case
if b == None:
return head
b = b.next
# 反转前 k 个元素
newhead=self.reverse(a,b)
# 递归反转后续链表并连接起来
a.next=self.reverseKGroup(b,k)
return newhead
21. 合并两个有序链表merge-two-sorted-lists
21. 合并两个有序链表:模拟题
,每次循环比较l1->val
和l2->val
,若l1->val<l2->val
,则在cur后面添加l1
;否则在cur后面添加l2
。
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:通过 dummy node 链表,连接各个元素
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
链接:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/
方法二:迭代
复杂度分析
时间复杂度:O(n + m) ,其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)O(n+m)。
空间复杂度:O(1) 。我们只需要常数的空间存放若干变量。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
prehead = ListNode(-1)
cur = prehead
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
cur.next = l1 if l1 is not None else l2
return prehead.next
方法一:递归
复杂度分析
时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)O(n+m)。
空间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+mn+m 次,因此空间复杂度为 O(n+m)O(n+m)。
class Solution:
def mergeTwoLists(self, l1, l2):
if l1 is None:
return l2
elif l2 is None:
return l1
elif l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
86. 分隔链表:partition-list
86. 分隔链表:双指针法
,
before_head链表存放比x小的节点,after_head链表存放比x大于或等于的节点,我们分别用before和after来前面两个链表添加节点,用head来遍历原始链表。当原始链表遍历完成时,我们需要将before_head链表连接上after_head链表,即before->next=after_head->next;after->next=nullptr;。
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
思路:将大于 x 的节点,放到另外一个链表,最后连接这两个链表
哑巴节点使用场景
当头节点不确定的时候,使用哑巴节点
86. 分隔链表
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
示例:
输入: head = 1->4->3->2->5->2, x = 3
输出: 1->2->2->4->3->5
链接:https://leetcode-cn.com/problems/partition-list/solution/fen-ge-lian-biao-by-leetcode/
复杂度分析
时间复杂度: O(N),其中N是原链表的长度,我们对该链表进行了遍历。
空间复杂度: O(1),我们没有申请任何新空间。值得注意的是,我们只移动了原有的结点,因此没有使用任何额外空间。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def partition(self, head: ListNode, x: int) -> ListNode:
"""
:type head: ListNode
:type x: int
:rtype: ListNode
"""
# before and after are the two pointers used to create two list
# before_head and after_head are used to save the heads of the two lists.
# All of these are initialized with the dummy nodes created.
before = before_head = ListNode(0)
after = after_head = ListNode(0)
while head:
# If the original list node is lesser than the given x,
# assign it to the before list.
if head.val < x:
before.next = head
before = before.next
else:
# If the original list node is greater or equal to the given x,
# assign it to the after list.
after.next = head
after = after.next
# move ahead in the original list
head = head.next
# Last node of "after" list would also be ending node of the reformed list
after.next = None
# Once all the nodes are correctly assigned to the two lists,
# combine them to form a single list which would be returned.
before.next = after_head.next
return before_head.next
148. 排序链表sort-list
没看懂
148. 排序链表:归并排序
,先2个2个的 merge,完成一趟后,再 4个4个的 merge,直到结束。
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
思路:归并排序,找中点和合并操作
注意点
-
快慢指针 判断 fast 及 fast.Next 是否为 nil 值
-
递归 mergeSort 需要断开中间节点
-
递归返回条件为 head 为 nil 或者 head.Next 为 nil
148. 排序链表
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
链接:https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/
class Solution:
def sortList(self, head: ListNode) -> ListNode:
h, length, intv = head, 0, 1
while h:
h, length = h.next, length + 1
res = ListNode(0)
res.next = head
# merge the list in different intv.
while intv < length:
pre, h = res, res.next
while h:
# get the two merge head `h1`, `h2`
h1, i = h, intv
while i and h: h, i = h.next, i - 1
if i: break # no need to merge because the `h2` is None.
h2, i = h, intv
while i and h: h, i = h.next, i - 1
c1, c2 = intv, intv - i # the `c2`: length of `h2` can be small than the `intv`.
# merge the `h1` and `h2`.
while c1 and c2:
if h1.val < h2.val: pre.next, h1, c1 = h1, h1.next, c1 - 1
else: pre.next, h2, c2 = h2, h2.next, c2 - 1
pre = pre.next
pre.next = h1 if c1 else h2
while c1 > 0 or c2 > 0: pre, c1, c2 = pre.next, c1 - 1, c2 - 1
pre.next = h
intv *= 2
return res.next
143. 重排链表reorder-list
143. 重排链表:首尾指针法
,首先将原始链表的每一个节点存放在一个数组中,然后我们取首尾指针向中间遍历,每次循环我们需要将左指针的节点连上右指针的节点,在节点连上之后,我们需要将右指针连上未排序的首节点。
给定一个单链表 L:L→L→…→L__n→L 将其重新排列后变为: L→L__n→L→L__n→L→L__n→…
思路:找到中点断开,翻转后面部分,然后合并前后两个链表
143. 重排链表
给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
示例 2:
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reorderList(self, head: ListNode) -> None:
"""
Do not return anything, modify head in-place instead.
"""
if not head or not head.next: return head
# 1 2 3 4 5
fast = head
pre_mid = head
# 找到中点, 偶数个找到时上界那个
while fast.next and fast.next.next:
pre_mid = pre_mid.next
fast = fast.next.next
# 翻转中点之后的链表,采用是pre, cur双指针方法
pre = None
cur = pre_mid.next
# 1 2 5 4 3
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
# 翻转链表和前面链表拼接
pre_mid.next = pre
# 1 5 2 4 3
# 链表头
p1 = head
# 翻转头
p2 = pre_mid.next
#print(p1.val, p2.val)
while p1 != pre_mid:
# 建议大家这部分画图, 很容易理解
pre_mid.next = p2.next
p2.next = p1.next
p1.next = p2
p1 = p2.next
p2 = pre_mid.next
链接:https://leetcode-cn.com/problems/reorder-list/solution/yong-zhan-fan-zhuan-huo-zhe-zhi-jie-fan-zhuan-by-p/
141. 环形链表linked-list-cycle
141. 环形链表:快慢指针法
,若存在环最终快慢指针会相遇;若不存在环,那么快指针一定会先走到链表尾部。
给定一个链表,判断链表中是否有环。
思路:快慢指针,快慢指针相同则有环,证明:如果有环每走一步快慢指针距离会减 1
141. 环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
如果 pos 是 -1,则在该链表中没有环。
双指针,漫画小灰 5.2节
时间空间复杂度分析:
https://leetcode-cn.com/problems/linked-list-cycle/solution/huan-xing-lian-biao-by-leetcode/
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
slow = fast = head
# if not head: # 没必要这样写可以加入while循环判断更简洁
# return False
while fast and fast.next: # 防止head为空和出现空指针的next的情况
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
作者:gulugulu_go
链接:https://leetcode-cn.com/problems/linked-list-cycle/solution/141-linked-list-cycle_li-jie-by-gulugulu_go/
142 环形链表 Ⅱ linked-list-cycle-ii
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
思路:快慢指针,快慢相遇之后,慢指针回到头,快慢指针步调一致一起移动,相遇点即为入环点
坑点
-
指针比较时直接比较对象,不要用值比较,链表中有可能存在重复值情况
-
第一次相交后,快指针需要从下一个节点开始和头指针一起匀速移动
另外一种方式是 fast=head,slow=head
这两种方式不同点在于,一般用 fast=head.Next 较多,因为这样可以知道中点的上一个节点,可以用来删除等操作。
-
fast 如果初始化为 head.Next 则中点在 slow.Next
-
fast 初始化为 head,则中点在 slow
142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置
(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
漫画小灰,5.2 拓展
class Solution(object):
def detectCycle(self, head):
slow = fast = head
# if not head: # 没必要这样写可以加入while循环判断更简洁
# return False
while True:
if not(fast and fast.next):#无环的问题,走到边界了。
return False
slow = slow.next
fast = fast.next.next
if slow == fast:
break
fast = head
while fast != slow:
slow = slow.next
fast = fast.next
return fast
作者:jyd
链接:https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/linked-list-cycle-ii-kuai-man-zhi-zhen-shuang-zhi-/
234. 回文链表palindrome-linked-list
234. 回文链表:快慢指针法
,快指针走两步,慢指针走一步,找到链表的中点。然后,翻转后半部分。最后从前半部分链表和后半部分链表是否相同。
请判断一个链表是否为回文链表。
234. 回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
方法三:我们可以分为以下几个步骤:
找到前半部分链表的尾节点。
反转后半部分链表。
判断是否为回文。
恢复链表。
返回结果。
链接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode/
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if head is None:
return True
# Find the end of first half and reverse second half.
first_half_end = self.end_of_first_half(head)
second_half_start = self.reverse_list(first_half_end.next)
# Check whether or not there's a palindrome.
result = True
first_position = head
second_position = second_half_start
while result and second_position is not None:
if first_position.val != second_position.val:
result = False
first_position = first_position.next
second_position = second_position.next
# Restore the list and return the result.
first_half_end.next = self.reverse_list(second_half_start)
return result
def end_of_first_half(self, head):
fast = head
slow = head
while fast.next is not None and fast.next.next is not None:
fast = fast.next.next
slow = slow.next
return slow
def reverse_list(self, head):
previous = None
current = head
while current is not None:
next_node = current.next
current.next = previous
previous = current
current = next_node
return previous
138. 复制带随机指针的链表copy-list-with-random-pointer
138. 复制带随机指针的链表:模拟题
,分三步,第一步在原链表的每个节点后面拷贝出一个新的节点,第二步拷贝random,第三步断开链表。
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。 要求返回这个链表的 深拷贝。
思路:1、hash 表存储指针,2、复制节点跟在原节点后面
138. 复制带随机指针的链表
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
解法一
class Solution(object):
def copyRandomList(self, head):
if not head:
return None
p = head
# 第一步,在每个原节点后面创建一个新节点
# 1->1'->2->2'->3->3'
while p:
new_node = Node(p.val,None,None)
new_node.next = p.next
p.next = new_node
p = new_node.next
p = head
# 第二步,设置新节点的随机节点
while p:
if p.random:
p.next.random = p.random.next
p = p.next.next
# 第三步,将两个链表分离
p = head
dummy = Node(-1,None,None)
cur = dummy
while p:
cur.next = p.next
cur = cur.next
p.next = cur.next
p = p.next
return dummy.next
作者:wang_ni_ma
链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/liang-chong-shi-xian-tu-jie-138-fu-zhi-dai-sui-ji-/
总结
链表必须要掌握的一些点,通过下面练习题,基本大部分的链表类的题目都是手到擒来~
-
null/nil 异常处理
-
dummy node 哑巴节点
-
快慢指针
-
插入一个节点到排序链表
-
从一个链表中移除一个节点
-
翻转链表
-
合并两个链表
-
找到链表的中间节点
练习
1. 两数之和
1. 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap = {}
for index, num in enumerate(nums):
another_num = target - num
if another_num in hashmap:
return [hashmap[another_num], index]
#不能直接放前面,有可能两个数相等的情况,比如2+2=4
hashmap[num] = index
return None #错误要有返回值
2. 两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式
存储的,并且它们的每个节点只能存储一位数字。
如果我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
模拟题,由于链表是逆序存放数字的,所以链表数字从左至右低位对低位,高位对高位,
因此我们从左至右遍历两个链表模拟加法运算即可,注意向高位进位。
这个解法里面的图片很形象
https://leetcode-cn.com/problems/add-two-numbers/solution/hua-jie-suan-fa-2-liang-shu-xiang-jia-by-guanpengc/
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
pre = cur = ListNode(None) #保存头结点,返回结果
carry = 0 #每一步的求和暂存变量,进位
while l1 or l2 or carry: #循环条件:l1 或者l2(没有遍历完成),carry(进位)不为0
carry += (l1.val if l1 else 0) + (l2.val if l2 else 0) #这其实是好多代码,我自己写了好多行,但是作者这样写非常简洁,赞
cur.next = ListNode(carry % 10) #构建新的list存储结果,其实用较长的加数链表存也可以,%10:求个位
cur = cur.next
carry //= 10 地板除 #求进位
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
return pre.next
26. 删除排序数组中的重复项
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
26. 删除排序数组中的重复项
# 双指针法:
# 数组完成排序后,我们可以放置两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针。
# 只要 nums[i] = nums[j],我们就增加 j 以跳过重复项。
# 当我们遇到 nums[j] !=nums[i] 时,跳过重复项的运行已经结束,
# 然后递增 i,因此我们必须把它(nums[j])的值复制到 nums[i + 1]。
# 接着我们将再次重复相同的过程,直到j到达数组的末尾为止。
class Solution:
def removeDuplicates(self,nums:list[int])->int:
if not nums:return 0
#if (len(nums) == 0) return 0;
i=0
for j in range(1,len(nums)):
if nums[i]!=nums[j]:
i+=1 #注意这一行,为啥在下一行的前面,她俩顺序不能颠倒。
nums[i]=nums[j]
return i+1 #注意是i+1
nums = [1,1,2]
[1,2]
return 2
nums = [0,0,1,1,1,2,2,3,3,4]
[0,1,2,3,4]
return 5
# 26题,有序数组删除重复元素,可使用「双指针」:
# 不真正删除,找到后面不重复元素,依次覆盖前面元素,最后只返回不重复元素的长度。
# 因为数组有下标,所以「双指针」实现比较简单。
# 复杂度分析
# 时间复杂度:O(n),假设数组的长度是 n,那么 i 和 j 分别最多遍历 n 步。
# 空间复杂度:O(1)。
160 相交链表(easy)
编写一个程序,找到两个单链表相交的起始节点。
方法三:双指针法
创建两个指针 pA 和 pB,分别初始化为链表 A 和 B 的头结点。然后让它们向后逐结点遍历。
当 pA 到达链表的尾部时,将它重定位到链表 B 的头结点 (你没看错,就是链表 B); 类似的,当 pB 到达链表的尾部时,将它重定位到链表 A 的头结点。
若在某一时刻 pA 和 pB 相遇,则 pA/pB 为相交结点。
想弄清楚为什么这样可行, 可以考虑以下两个链表: A={1,3,5,7,9,11} 和 B={2,4,9,11},相交于结点 9。 由于 B.length (=4) < A.length (=6),pB 比 pA 少经过 22 个结点,会先到达尾部。将 pB 重定向到 A 的头结点,pA 重定向到 B 的头结点后,pB 要比 pA 多走 2 个结点。因此,它们会同时到达交点。
如果两个链表存在相交,它们末尾的结点必然相同。因此当 pA/pB 到达链表结尾时,记录下链表 A/B 对应的元素。若最后元素不相同,则两个链表不相交。
复杂度分析
时间复杂度 : O(m+n)。
空间复杂度 : O(1)。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
if not headA or not headB:
return None
nodeA = headA
nodeB = headB
while(nodeA !=nodeB):
nodeA = nodeA.next if nodeA else headB
nodeB = nodeB.next if nodeB else headA
return nodeA
19 删除链表的倒数第N个结点(medium)
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的 n 保证是有效的。
进阶:你能尝试使用一趟扫描实现吗?
快慢指针法
,起始快指针走n步后,若此时快指针已为空,表示我们删除第一个节点,直接返回head->next即可;否则此时快慢指针一起走,也就是慢指针走size-n步到达倒数第N个节点的前驱节点,快指针会到达链表的尾节点,此时我们删除slow->next节点即可。
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/chao-ke-ai-dong-hua-jiao-ni-ru-he-shan-chu-lian-bi/
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if not head:
return head
slownode = ListNode(None)
slownode.next = head
fastnode = slownode
for i in range(n):
fastnode = fastnode.next
while(fastnode.next!=None):
slownode = slownode.next
fastnode = fastnode.next
#注意是指去掉链表表头的那个点,fast已经走到最后一点了,slow未移动
if slownode.next == head:
head = head.next
else:
slownode.next = slownode.next.next
return head
203 移除链表元素(easy)
模拟题
,直接遍历链表确定是否删除节点即可。
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
复杂度分析
- 时间复杂度:O(N),只遍历了一次。
- 空间复杂度:O(1)。
链接:https://leetcode-cn.com/problems/remove-linked-list-elements/solution/yi-chu-lian-biao-yuan-su-by-leetcode/
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
sentinel = ListNode(0)
sentinel.next = head
prev, curr = sentinel, head
while curr:
if curr.val == val:
prev.next = curr.next
else:
prev = curr
curr = curr.next
return sentinel.next
328 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
复杂度分析
时间复杂度: O(n) 。总共有 n 个节点,我们每个遍历一次。
空间复杂度: O(1) 。我们只需要 4 个指针。
class Solution:
def oddEvenList(self, head: ListNode) -> ListNode:
if not head:return head
odd = head
even_head = even = head.next
while odd.next and even.next:
odd.next = odd.next.next
even.next = even.next.next
odd,even = odd.next,even.next
odd.next = even_head
return head
作者:zbzzbz
链接:https://leetcode-cn.com/problems/odd-even-linked-list/solution/zui-po-su-de-xiang-fa-dai-ma-zhu-shi-fei-chang-xia/
430 扁平化多级双向链表
还没看
模拟题
,迭代法,遍历链表,若发现该链表存在child节点那么就将[child,tail]这段子链表插入到当前节点的后面去,然后继续遍历链表。
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。
给你位于列表第一级的头节点,请你扁平化列表,使所有结点出现在单级双链表中。
复杂度分析
- 时间复杂度:O(N)。
- 空间复杂度:O(N)。
"""
# Definition for a Node.
class Node(object):
def __init__(self, val, prev, next, child):
self.val = val
self.prev = prev
self.next = next
self.child = child
"""
class Solution(object):
def flatten(self, head):
if not head:
return
pseudoHead = Node(0,None,head,None)
prev = pseudoHead
stack = []
stack.append(head)
while stack:
curr = stack.pop()
prev.next = curr
curr.prev = prev
if curr.next:
stack.append(curr.next)
if curr.child:
stack.append(curr.child)
# don't forget to remove all child pointers.
curr.child = None
prev = curr
# detach the pseudo head node from the result.
pseudoHead.next.prev = None
return pseudoHead.next
作者:LeetCode
链接:https://leetcode-cn.com/problems/flatten-a-multilevel-doubly-linked-list/solution/bian-ping-hua-duo-ji-shuang-xiang-lian-biao-by-lee/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
61 旋转链表(medium)
模拟题
,先求出链表长度size,若k取余size为空,那么不用旋转了,直接返回head;否则将链表首尾相连形成环形链表,由于k表示尾节点移动k%size位,那么头节点移动size-k%size位。
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
class Solution:
def rotateRight(self, head: 'ListNode', k: 'int') -> 'ListNode':
# base cases
if not head:
return None
if not head.next:
return head
# close the linked list into the ring
old_tail = head
n = 1
while old_tail.next:
old_tail = old_tail.next
n += 1
old_tail.next = head
# find new tail : (n - k % n - 1)th node
# and new head : (n - k % n)th node
new_tail = head
for i in range(n - k % n - 1):
new_tail = new_tail.next
new_head = new_tail.next
# break the ring
new_tail.next = None
return new_head
作者:LeetCode
链接:https://leetcode-cn.com/problems/rotate-list/solution/xuan-zhuan-lian-biao-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
剑指 Offer 24 反转链表
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre=None
cur=head
while cur:
third=cur.next
cur.next=pre
pre=cur
cur=third
return pre
剑指 Offer 18 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
- 题目保证链表中节点的值互不相同
- 若使用 C 或 C++ 语言,你不需要
free
或delete
被删除的节点
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def deleteNode(self, head: ListNode, val: int) -> ListNode:
if not head:
return head
cur=dummy=ListNode(None)
dummy.next=head
while cur.next:
if cur.next.val==val:
cur.next=cur.next.next
break
else:
cur=cur.next
return dummy.next
剑指 Offer 22 链表中倒数第k个节点
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
if not head :
return head
left=right=head
while k:
right=right.next
k-=1
while right :
left=left.next
right=right.next
return left
剑指 Offer 25 合并两个排序的链表
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
cur = dum = ListNode(0)
while l1 and l2:
if l1.val < l2.val:
cur.next, l1 = l1, l1.next
else:
cur.next, l2 = l2, l2.next
cur = cur.next
cur.next = l1 if l1 else l2
return dum.next
剑指 Offer 06 从尾到头打印链表
辅助栈法
class Solution:
def reversePrint(self, head: ListNode) -> List[int]:
stack = []
while head:
stack.append(head.val)
head = head.next
return stack[::-1]
剑指 Offer 35 复杂链表的复制 中等
注意:本题与主站 138 题相同:https://leetcode-cn.com/problems/copy-list-with-random-pointer/