【PythonCode】力扣Leetcode21~25题Python版
前言
力扣Leetcode是一个集学习、刷题、竞赛等功能于一体的编程学习平台,很多计算机相关专业的学生、编程自学者、IT从业者在上面学习和刷题。
在Leetcode上刷题,可以选择各种主流的编程语言,如C++、JAVA、Python、Go等。还可以在线编程,实时执行代码,如果代码通过了平台准备的测试用例,就可以通过题目。
本系列中的文章从Leetcode的第1题开始,记录我用Python语言提交的代码和思路,供Python学习参考。
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
代码实现:
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
listm = ListNode(0, next=None)
cur = listm
while list1 and list2:
if list1.val <= list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
cur.next = list1 if list1 is not None else list2
return listm.next
解题思路:要将两个升序的链表合并成一个新的升序链表,可以从两个链表的头节点开始,依次比较节点的大小,将更小的节点添加到新的链表中。
先新建一个链表,在这个链表中初始化一个值为0的节点,作为前置节点。将待合并的节点依次添加到此链表中,合并完成后返回此链表的第二个节点至末尾,就是需要的结果。
当两个待合并的链表都不为空时,取两个链表的头节点进行比较,并将更小的节点添加到新链表中。节点被添加后,对应链表的头指向下一个节点,相当于将已合并的节点删除。链表头指向的节点不断往后移动,就是两个指针在移动,不断更新链表的头,比较大小时就只需要关注头节点。
当其中一个链表的节点被合并完成后,另一个链表中剩余节点的值都更大,此时,不需要再比较,直接将这些剩余的节点添加到新链表的后面。
在往新链表中添加节点时,也是用一个指针,指针一直都指向新链表的最后一个节点,要添加的节点直接加在指针后面。(链表基础,参考:Python实现单向链表)
22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
提示:
1 <= n <= 8
代码实现:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
result = []
def backtrack(temp, left, right):
if len(temp) == 2 * n:
result.append(temp)
return
if left < n:
temp += '('
backtrack(temp, left+1, right)
temp = temp[:-1]
if right < left:
temp += ')'
backtrack(temp, left, right+1)
temp = temp[:-1]
backtrack('', 0, 0)
return result
解题思路:本题需要生成n对有效的小括号,小括号由左括号和右括号两个部分组成,最终生成的结果长度为2n,包含n个左括号和n个右括号。在生成有效括号的过程中,右括号的个数不能多于左括号的个数,否则括号是无效的。
本题求的是所有可能的括号组合,可以用回溯法,通过不断的回溯,穷举出所有的组合可能性。参考:循序渐进,搞懂什么是回溯算法。(前面的力扣17题也用了回溯算法,可以结合一起看)
按照回溯法的求解步骤,先定义回溯的解空间,本题是求所有可能的括号组合,所以结果是一个字符串数组。回溯时,搜索树的高度最高为2n,深度优先遍历时每次往字符串中添加一个左括号或右括号。当左括号的个数小于n时,可以往字符串中添加左括号,当字符串中的右括号个数小于左括号个数时,可以往字符串中添加右括号。这样可以保证括号组合一直是有效的,当字符串长度达到2n时,得到一种可能的组合,将组合添加到结果列表中。往回回溯时,去掉字符串的最后一个字符,通过回溯穷举所有的可能性,最后得到所有可能的括号组合。
结合代码,首先初始化结果数组result,回溯时的括号字符串temp。然后定义回溯函数backtrack(),在回溯函数中,如果括号字符串长度达到2n,就找到一种组合,把当前的括号字符串temp保存到result中,如果长度没有达到2n,就继续按括号有效的规则往字符串中添加左括号或右括号,往回回溯时,将上一个添加的括号从temp中移除,也就是将temp中的最后一个字符移除。最后调用回溯函数backtrack(),从左括号和右括号个数都为0个开始,执行回溯函数。
23. 合并 K 个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:
链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
代码实现:
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
return self.merge(lists, 0, len(lists)-1)
def merge(self, lists, left, right):
if left == right:
return lists[left]
if left > right:
return
mid = (left + right) // 2
return self.mergeTwoLists(self.merge(lists, left, mid), self.merge(lists, mid+1, right))
def mergeTwoLists(self, list1, list2):
listm = ListNode(0, next=None)
cur = listm
while list1 and list2:
if list1.val <= list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
cur.next = list1 if list1 is not None else list2
return listm.next
解题思路:合并k个升序链表是21题合并两个升序链表的升级版,因此我们可以调用合并两个链表的代码,来重复合并操作。最简单的方式就是从第一个和第二个链表开始依次合并,直到合并完k个链表。但这种方式需要合并k-1次,前面的链表被反复遍历,时间复杂度太高,提交代码会超时。
要将时间复杂度降低,就要减少合并的次数,所以考虑的合并策略是二分法合并。用倒推的思路,将k个链表分成两份,如果两部分都合并完成,则将这两个链表合并在一起就完成了,这两个部分再分别递归地往下二分,直到最后分的链表剩一个或两个。
如果从前往后依次合并,假设每个链表的长度为n,第一个和第二个链表合并需要遍历两个链表,时间复杂度为2n,随着每次合并,其中一个链表的长度逐渐变成2n,3n,…,子链表的长度越来越长,需要合并k-1次,最后一次合并时,前k-1个链表合并成的子链表长度为(k-1)*n,与第k个链表合并时间复杂度为k*n,所以整体的时间复杂度为2n+3n+…+kn=(k/2)*k*n(约等于),时间复杂度为O(k2*n)。
使用二分法合并时,最后一次合并两个链表的长度都为(k/2)*n,时间复杂度为k*n。往前推时每次合并的时间复杂度依次分别为(k/2)*n,(k/4)*n,…,并且时间复杂度为(k/2)*n的操作需要做两次,时间复杂度为(k/4)*n的操作需要做四次,以此类推,所以每一轮的合并时间复杂度都为k*n。k个链表二分法合并需要进行logk轮的合并,所以时间复杂度为O(k*logk*n)。O(k*logk*n)的时间复杂度优于O(k2*n)。
最后看一下代码,两个链表合并的代码直接复用,二分法合并时,使用递归的方式对k个链表进行二分,分到剩一个或两个的情况,代码往回递归,完成合并。
24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
代码实现:
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = ListNode(0, head)
temp = prev
while temp.next and temp.next.next:
nodea = temp.next
nodeb = temp.next.next
nodea.next = nodeb.next # 第一步
temp.next = nodeb # 第二步
nodeb.next = nodea # 第三步
temp = nodea
return prev.next
解题思路:题目要求两两交换链表的节点,也就是将链表的第一个节点与第二个节点交换位置,第三个节点与第四个节点交换位置,… 以此类推,不能修改节点内部的值,也就是不能将第一个节点的值改成第二个节点的值,第二个节点的值改成第一个节点的值。
要满足节点的交换,就是要修改节点的next指向关系,为了方便说明,可以参考下图:
创建一个前置节点,将前置节点的next指向链表的头,创建一个临时指针指向前置节点,创建两个指针指向临时节点后面的两个节点,这两个节点就是需要交换位置的节点。
交换需要执行三次next指针修改,分为三个步骤:
第一步,将第一个节点的next指向第二个节点的next(也就是指向第三个节点,可能为空)。
第二步,将前置节点的next指向第二个节点。
第三步,将第二个节点的next指向第一个节点。
修改next指针后,原来的指针“断掉”了,此时,看链表新的指向关系(图中红线),第一个节点和第二个节点的位置已经交换了。
要继续交换链表后面的节点(如第三个和第四个),将临时指针指向第三个节点的前一个节点,也就是节点1,并更新临时指针后两个指针指向的节点。如此循环,直到链表中的节点剩余0个或1个,则交换完成。返回前置节点的后一个节点就是结果。
25. K 个一组翻转链表
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
提示: 链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
代码实现:
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
prev = ListNode(0, head)
temp = prev
k_head = head
while k_head:
node = []
cur = temp
for _ in range(k):
node.append(cur.next)
cur = cur.next
if not cur:
return prev.next
k_head = cur.next
node[0].next = k_head # 第一步
temp.next = node[-1] # 第二步
for i in range(1, k): # 第三步
node[i].next = node[i-1]
temp = node[0]
return prev.next
解题思路:本题是上一题24题的升级版,上一题是两两翻转节点的位置,本题是k个一组翻转(所以要先看懂上一题再看本题)。翻转思路与两两翻转的思路相通,在两两翻转的时候,每一组的节点是两个。
k个节点一组翻转时,翻转方法也可以分为三个步骤,为了方便说明,可以参考下图:
首先还是创建一个前置节点,将前置节点的next指向链表的头,创建一个临时指针指向前置节点。不同的是这里创建一个k_head指针指向k个结点的头,以便代码中找到这k个结点。
第一步,将这组节点中的第一个节点的next指向最后一个节点的next。
第二步,将临时指针的next指向这组节点的最后一个节点。
第三步,在这组节点内部,将除第一个节点的next都指向它的前一个节点。
执行这三步后,就完成了一组节点的翻转。此时将临时指针指向组内的第一个节点,该节点就是下一组待翻转节点的前置节点,并更新k_head指针,如此循环,当链表中剩余的节点数量不足k个时,翻转完成,返回前置节点的后一个节点,就是本题的结果。
相关阅读
【PythonCode】力扣Leetcode16~20题Python版
📢欢迎 点赞👍 收藏⭐ 评论📝 关注❤ 如有错误敬请指正!
☟ 学Python,点击下方名片关注我。☟