Leetcode随机抽题检测
- 160 相交链表
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 206 反转链表
- 一段用于复制的标题
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 234 回文链表
- 未看解答自己编写的青春版
- 重点
- 综上,利用快慢指针找寻链表中间,就按加入虚拟头的方法写。
- 题解的代码
- 日后再次复习重新写
- 141 环形链表
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 142 环形链表 II
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 21 合并两个有序链表
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 2 两数相加
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 19 删除链表的倒数第 N 个结点
- 未看解答自己编写的青春版
- 重点
- 上面的代码其实有些冗余了,这道题只要能想到加入虚拟头,上面说的两种特殊情况都能迎刃而解,用一般的示例来考虑就能通过。卡哥的代码中,是先让 fast 移动 n+1 步,我的是先移动 n 步,都可以,不同的对用体现在while判断条件上。
- 题解的代码
- 日后再次复习重新写
- 24 两两交换链表中的节点
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 25 K 个一组翻转链表
- 未看解答自己编写的青春版
- 重点
- 上面那个评论的方法真厉害,我悟了,第一次接触这种方式来翻转链表的,但是出人意料的很适合这道题!之前做的反转链表的方法,不能直接用到本题上,想使用就要像我那样去写,去谨慎的赋值。这种翻转的思想是:一次交换两个节点,且保持 pre 不变。例如:0--1--2--3--4,pre在0处,不参与翻转,cur在1处。那么第一次操作,是:0--2--1--3--4,此时pre还在0,cur也还在1,继续:0--3--2--1--4,继续:0--4--3--2--1,它是每次都将后面的一个值,插入到pre的后面,pre不移动,cur也不移动,但是cur的相对位置,会随着链表的更改而移动。
- 题解的代码
- 日后再次复习重新写
- 138 复制带随机指针的链表
- 未看解答自己编写的青春版
- 重点
- 通过对上面两种方法的学习,发现了这道题的本质,就是在利用next建立新链表的时候,利用一个map,保存好原节点和新节点的对应关系就好了!本质上是考哈希!
- 另一种非常牛的方法,随机指针复制+拆分
- 这题太牛了,一定要着重复习后面,这种思想第一次接触。
- 题解的代码
- 日后再次复习重新写
- 148 排序链表
- 未看解答自己编写的青春版
- 重点,这道题太重要了太重要了,一定要多次复习。
- 归并排序,是对链表排序最好的方法
- 一定要好好学习归并排序,这是可以作为模板代码来学习的!本质上就是两个操作,merge 和 cut 。从网上搜到的很多归并排序都是用递归写的,不过本题使用循环来写,觉得这种写法很值得学习!
- 归并排序的递归和循环写法,都值得学习!
- 快速排序版本:是交换节点的,并非只交换数值。(暂时没看)
- 题解的代码
- 日后再次复习重新写
- 23 合并 K 个升序链表
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 146 LRU 缓存
- 未看解答自己编写的青春版
- 重点
- 这道双向链表的题目,真的学习了,后面一定要多复习多重写这道题!
- 题解的代码
- 日后再次复习重新写
- 94 二叉树的中序遍历
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 104 二叉树的最大深度
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 226 翻转二叉树
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 101 对称二叉树
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 543 二叉树的直径
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 102 二叉树的层序遍历
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 108 将有序数组转换为二叉搜索树
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 98 验证二叉搜索树
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 230 二叉搜索树中第K小的元素
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 199 二叉树的右视图
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 一段用于复制的标题
- 未看解答自己编写的青春版
- 重点
- 额外空间复杂度为O(1)的思想!这道题我被题目骗了,题目说顺序应该是前序遍历,我就想着递归必须用前序遍历,但是前序遍历的问题是:会提前修改掉后面要进入递归的值。那么,链表,从上到下连接可以,我从下到上,反向连接,也可以啊!而这一点,利用递归的回溯特性,可以很自然地做到。假如 last 承载了下一层递归的返回值,而本层递归的root为last的上一个,只需要赋值:root.right=last,就可以了!
- 所以后序遍历!后序遍历,也就不会有,还未进入递归,值就被修改的问题!这题的思路太妙了!
- 思路很妙,可以多写多复习。
- 题解的代码
- 日后再次复习重新写
- 105 从前序与中序遍历序列构造二叉树
- 未看解答自己编写的青春版
- 厉害!第一次自己尝试,写出了传入左右索引版的代码,在空间占用上,要比上面的代码低不少。
- 重点
- 题解的代码
- 日后再次复习重新写
- 437 路径总和 III
- 未看解答自己编写的青春版
- 重点
- 没来得及体会,日后再看,重点学习这道题。
- 题解的代码
- 日后再次复习重新写
- 236 二叉树的最近公共祖先
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
- 124 二叉树中的最大路径和
- 未看解答自己编写的青春版
- 重点
- 题解的代码
- 日后再次复习重新写
160 相交链表
未看解答自己编写的青春版
class Solution:def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:curA = headAcurB = headBcountA = 0countB = 0while curA :countA += 1curA = curA.nextwhile curB :countB += 1curB = curB.nextif countA < countB :countA,countB = countB,countAheadA,headB = headB,headAdiff = countA - countBwhile diff > 0 :headA = headA.nextdiff -= 1while headA :if headA == headB :return headBelse :headA = headA.nextheadB = headB.nextreturn None
重点
过。
题解的代码
206 反转链表
一段用于复制的标题
未看解答自己编写的青春版
class Solution:def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:pre = Nonecur = headwhile cur :temp = cur.nextcur.next = prepre = curcur = tempreturn pre
重点
过。
题解的代码
日后再次复习重新写
234 回文链表
未看解答自己编写的青春版
class Solution:def isPalindrome(self, head: Optional[ListNode]) -> bool:dummy_head = ListNode(0,head)slow = fast = dummy_head# 这里要注意,快慢指针找中间,要加虚拟头才是对的,不加虚拟头不对!# 自己举例一下就发现了。while fast and fast.next :slow = slow.nextfast = fast.next.nextcur = slow.nextslow.next = Nonepre = Nonewhile cur :temp = cur.nextcur.next = prepre = curcur = tempwhile pre and head :if pre.val != head.val :return Falsepre = pre.nexthead = head.nextreturn True
重点
通过编写这道题的代码,发现了一个很重要的点,如果要用快慢指针法,寻找链表中点,最严格的方式是:要加入虚拟头!
加入虚拟头的代码:
dummy_head = ListNode(0,head)slow = fast = dummy_headwhile fast and fast.next :slow = slow.nextfast = fast.next.nextcur = slow.nextslow.next = None # 切断操作
不加入虚拟头的代码:
slow = fast = headwhile fast and fast.next :slow = slow.nextfast = fast.next.nextcur = slow.nextslow.next = None # 切断操作
下面用A代表慢指针,B代表快指针。
比如:1-2-3-4 ,不加入虚拟头时,一开始A,B均在 1 ,移动一步,A在2,B在3,此时还要继续移动,while不退出,继续移动,A在3,B为None,while退出,此时让A.next = None,做切断操作,这明显是错误的!应该是从 2 切断!加入虚拟头后,就是正确的结果。
加入虚拟头,可以让偶数个数的链表切割正确,奇数个数的链表切割结果不变,因为奇数个数的链表,正确切割结果,就是左边要包括中间节点,然后中间节点的next为None。
那么之前有一道题:143 重排链表,为什么卡哥的代码,也是快慢指针找中间,却没有加入虚拟头?
143 重排链表 卡哥的代码:
class Solution:def reorderList(self, head: Optional[ListNode]) -> None:"""Do not return anything, modify head in-place instead."""fast = slow = head# find mid point which including (first) mid point into the first half linked listwhile fast and fast.next:fast = fast.next.nextslow = slow.next# 下面两句代码别忘了,一个是取右半边,这样取的右半边一定是短的那一方# 这样连接也符合题目要求right = slow.next # 获取后半边的头slow.next = None # 切断!这句话很重要,不然就成环了node = Nonewhile right:temp = right.nextright.next = nodenode = rightright = temphead2 = nodehead1 = headwhile head1 and head2:temp1 = head1.nexttemp2 = head2.nexthead1.next = head2head2.next = temp1head1 = temp1head2 = temp2
因为这道题目的特殊性!可以看出,从2处切割,本题的节点串联逻辑是:1 – 4 – 2 – 3 。从3处切割,本题的节点串联逻辑是:1 – 4 – ( 2 3 ) ,( 2 3 ) 本来就在左子串中,无需改变位置!这是这道题的特殊性!对于偶数个数的链表,中间两个点,切不切割都一样!因为这两个点的顺序不需要颠倒。
综上,利用快慢指针找寻链表中间,就按加入虚拟头的方法写。
题解的代码
日后再次复习重新写
141 环形链表
未看解答自己编写的青春版
class Solution:def hasCycle(self, head: Optional[ListNode]) -> bool:if head == None or head.next == None :return Falseslow = fast = headwhile fast and fast.next : slow = slow.nextfast = fast.next.next# 注意判断的位置,先移动再判断,不然因为初始化都是头结点,会立刻返回Trueif slow == fast :return Truereturn False
重点
在使用快慢指针时,注意 if 逻辑判断的位置。
注意!注意!警告!警告!上面代码中的提前判断,不是必须的。可以不加。
class Solution:def hasCycle(self, head: Optional[ListNode]) -> bool:slow = fast = headwhile fast and fast.next : slow = slow.nextfast = fast.next.next# 注意判断的位置,先移动再判断,不然因为初始化都是头结点,会立刻返回Trueif slow == fast :return Truereturn False
题解的代码
日后再次复习重新写
142 环形链表 II
未看解答自己编写的青春版
class Solution:def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:slow = headfast = headwhile fast and fast.next :slow = slow.nextfast = fast.next.nextif slow == fast :cur = headwhile True :if cur == slow :return curcur = cur.nextslow = slow.nextreturn None
重点
过,这道题的理论基础,去复习,卡哥的解答。
环形链表 II
题解的代码
日后再次复习重新写
21 合并两个有序链表
未看解答自己编写的青春版
class Solution:def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:newhead = ListNode()res = newheadwhile list1 and list2 :if list1.val >= list2.val :newhead.next = list2list2 = list2.next else :newhead.next = list1list1 = list1.nextnewhead = newhead.nextif list1 == None and list2 != None :newhead.next = list2elif list1 != None and list2 == None :newhead.next = list1return res.next
重点
过。
题解的代码
日后再次复习重新写
2 两数相加
未看解答自己编写的青春版
class Solution:def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:reshead = ListNode()cur = resheadpre = 0while l1 or l2 :if l1 == None :temp = pre+l2.valif temp > 9 :pre = 1temp = temp % 10else :pre = 0node = ListNode(temp)l2 = l2.nextelif l2 == None :temp = pre+l1.valif temp > 9 :pre = 1temp = temp % 10else :pre = 0node = ListNode(temp)l1 = l1.nextelse :temp = pre+l2.val+l1.valif temp > 9 :pre = 1temp = temp % 10else :pre = 0node = ListNode(temp)l1 = l1.nextl2 = l2.nextcur.next = nodecur = cur.nextif pre == 1 :node = ListNode(1)cur.next = nodereturn reshead.next
重点
过。
题解的代码
日后再次复习重新写
19 删除链表的倒数第 N 个结点
未看解答自己编写的青春版
class Solution:def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:dummy_head = ListNode(0,head)slow = fast = dummy_headwhile n > 0 :fast = fast.nextn -= 1while fast and fast.next :fast = fast.nextslow = slow.nextslow.next = slow.next.nextreturn dummy_head.next
重点
这道题就明确两点:
1、加入虚拟头。
2、模拟特殊情况检验代码是否正确:删除的是最后一个节点,删除的是第一个节点。
上面的代码其实有些冗余了,这道题只要能想到加入虚拟头,上面说的两种特殊情况都能迎刃而解,用一般的示例来考虑就能通过。卡哥的代码中,是先让 fast 移动 n+1 步,我的是先移动 n 步,都可以,不同的对用体现在while判断条件上。
移动 n+1 步 :
class Solution:def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:dummy_head = ListNode(0,head)slow = fast = dummy_headwhile n > -1 :fast = fast.nextn -= 1while fast :fast = fast.nextslow = slow.nextslow.next = slow.next.nextreturn dummy_head.next
移动 n 步 :
class Solution:def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:dummy_head = ListNode(0,head)slow = fast = dummy_headwhile n > 0 :fast = fast.nextn -= 1while fast.next :fast = fast.nextslow = slow.nextslow.next = slow.next.nextreturn dummy_head.next
题解的代码
日后再次复习重新写
24 两两交换链表中的节点
未看解答自己编写的青春版
class Solution:def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:if head == None or head.next == None :return headdummy_head = ListNode(0,head)pre = dummy_headcur = headwhile cur and cur.next :temp1 = cur.nexttemp2 = cur.next.nextpre.next = temp1temp1.next = curcur.next = temp2pre = curcur = cur.nextreturn dummy_head.next
重点
理清楚while循环中的交换逻辑就好,逻辑弄不清楚,就多搞一个临时变量嘛,两个temp,逻辑不就非常清晰。
这道题为了交换方便,同样也需要用虚拟头。
题解的代码
日后再次复习重新写
25 K 个一组翻转链表
未看解答自己编写的青春版
哈哈哈,独立完成 hard 题 !不过看评论,很多人也做出来了,看来这道题很简单,也就是中等的实际难度。
耗时上也还行,40% 左右。
class Solution:def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:dummy_head = ListNode(0,head)pre = dummy_headbegin = end = dummy_head.nextcount = 1while end :if count < k :end = end.nextcount += 1else :temp = end.nexta,b = self.reverse(begin,end)pre.next = bpre = aa.next = tempbegin = end = tempcount = 1return dummy_head.nextdef reverse(self,head,end):end.next = Nonepre = Nonecur = headwhile cur :temp = cur.nextcur.next = prepre = curcur = tempreturn head,end
重点
评论中的一个解答:
class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode dummy = new ListNode(0), prev = dummy, curr = head, temp;dummy.next = head;int length = 0;while(head != null) {length++;head = head.next;}head = dummy.next;for(int i = 0; i < length / k; i++) {for(int j = 0; j < k - 1; j++) {temp = curr.next;curr.next = temp.next;temp.next = prev.next;prev.next = temp;}prev = curr;curr = prev.next;}return dummy.next;}
}
上面那个评论的方法真厉害,我悟了,第一次接触这种方式来翻转链表的,但是出人意料的很适合这道题!之前做的反转链表的方法,不能直接用到本题上,想使用就要像我那样去写,去谨慎的赋值。这种翻转的思想是:一次交换两个节点,且保持 pre 不变。例如:0–1–2–3–4,pre在0处,不参与翻转,cur在1处。那么第一次操作,是:0–2–1–3–4,此时pre还在0,cur也还在1,继续:0–3–2–1–4,继续:0–4–3–2–1,它是每次都将后面的一个值,插入到pre的后面,pre不移动,cur也不移动,但是cur的相对位置,会随着链表的更改而移动。
想清楚,节点的移动逻辑,再谨慎地编写每次循环中的逻辑就可以了。
class Solution:def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:dummy_head = ListNode(0,head)pre = dummy_headcur = dummy_head.nextaa = dummy_head.nextcount = 0while aa :count += 1aa = aa.nextfor i in range(count//k):for j in range(k-1):# 这里的四个赋值,顺序非常有讲究!temp = cur.nextcur.next = temp.nexttemp.next = pre.nextpre.next = temppre = curcur = cur.nextreturn dummy_head.next
题解的代码
日后再次复习重新写
138 复制带随机指针的链表
未看解答自己编写的青春版
思想很朴素,每次都从头搜索,random节点,所以耗时也只打败了6%。去学习一下评论区。
class Solution:def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':reshead = Node(0)copycur = resheadcur = headwhile cur :node = Node(cur.val)copycur.next = nodecopycur = copycur.nextcur = cur.nextcopycur = reshead.nextcur = headwhile cur :if cur.random == None :copycur.random == None else :temp_org = headtemp_copy = reshead.nextwhile temp_org != cur.random :temp_org = temp_org.nexttemp_copy = temp_copy.nextcopycur.random = temp_copycur = cur.nextcopycur = copycur.nextreturn reshead.next
重点
评论中的一个,递归的解法:很有意思。当然这种方法相当于记录了该链表的所有节点,空间复杂度是O(n),虽然是递归,但是这道题的时间复杂度也是O(n)。
class Solution:def copyRandomList(self, head):def copyNode(node, res):if not node: return Noneif node in res: return res[node]copy = Node(node.val, None, None)res[node] = copycopy.next = copyNode(node.next, res)copy.random = copyNode(node.random, res)return copyreturn copyNode(head, {})
另一种解法,哈希,利用字典去记录,原节点和新节点的映射关系。使用hash存储原结点和克隆结点的映射关系,通过映射关系处理克隆结点的random指针。
时间复杂度和空间复杂度,均为O(n)
class Solution {public Node copyRandomList(Node head) {if(head == null){return head;}// map方法,空间复杂度O(n)Node node = head;// 使用hash表存储旧结点和新结点的映射Map<Node,Node> map = new HashMap<>();while(node != null){Node clone = new Node(node.val,null,null);map.put(node,clone);node = node.next;}node = head;while(node != null){map.get(node).next = map.get(node.next);map.get(node).random = map.get(node.random);node = node.next;}return map.get(head);}
}
通过对上面两种方法的学习,发现了这道题的本质,就是在利用next建立新链表的时候,利用一个map,保存好原节点和新节点的对应关系就好了!本质上是考哈希!
另一种非常牛的方法,随机指针复制+拆分
原地处理,将克隆结点放在原结点后面,在原链表上处理克隆结点的random指针,最后分离两个链表,空间复杂度O(1)。
class Solution {public Node copyRandomList(Node head) {if(head == null){return head;}// 空间复杂度O(1),将克隆结点放在原结点后面Node node = head;// 1->2->3 ==> 1->1'->2->2'->3->3'while(node != null){Node clone = new Node(node.val,node.next,null);Node temp = node.next;node.next = clone;node = temp;}// 处理random指针node = head;while(node != null){// !!node.next.random = node.random == null ? null : node.random.next;node = node.next.next;}// 还原原始链表,即分离原链表和克隆链表node = head;Node cloneHead = head.next;while(node.next != null){Node temp = node.next;node.next = node.next.next;node = temp;}return cloneHead;}
}
一篇有助于理解的题解链接:
清楚的题解
这题太牛了,一定要着重复习后面,这种思想第一次接触。
题解的代码
日后再次复习重新写
哈希方法复写:
class Solution:def copyRandomList(self, head):reshead = Node(0)copycur = resheadcur = headtable = {}while cur :node = Node(cur.val)copycur.next = nodecopycur = copycur.nexttable[cur] = copycurcur = cur.nextcopycur = reshead.nextcur = headwhile cur :if cur.random == None :copycur.random == None else :copycur.random = table[cur.random]cur = cur.nextcopycur = copycur.nextreturn reshead.next
148 排序链表
未看解答自己编写的青春版
先遍历得到数组,再排序,用一个字典存储:数组排序后的下标(key)和链表中的节点(value)的映射,然后按照key,从小到大去索引原节点,来构造新链表。这是时间复杂度为O(n logn),空间复杂度为O(n)的做法。
速度很快,打败95%,但是空间上只打败了6%
class Solution:def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:table = {}nums = []cur = headwhile cur :# 处理值相同的情况if cur.val in table :# 这里要直接 append, 不能采用赋值操作:table[cur.val] = table[cur.val].append(cur)# 因为 table[cur.val].append(cur) 的返回值是None# 下面的 pop 操作同理table[cur.val].append(cur)else :table[cur.val] = [cur]nums.append(cur.val)cur = cur.nextnums.sort()reshead = ListNode()cur = resheadfor i in nums:# 注意这里,要从最末尾开始取,因为pop就是丢掉末尾的node = table[i][-1] table[i].pop()cur.next = nodecur = cur.nextcur.next = Nonereturn reshead.next
要使用O(1)的空间复杂度,必然是对链表直接进行排序操作,对链表的排序还从来没写过,采用哪种方式好也不清楚,冒泡肯定不行,是O(n^2),看题解说是,归并排序,归并排序是最适合链表这种数据结构的排序方式。快速排序也可以。
重点,这道题太重要了太重要了,一定要多次复习。
归并排序,是对链表排序最好的方法
伪代码:
current = dummy.next;
tail = dummy;
for (step = 1; step < length; step *= 2) {while (current) {// left->@->@->@->@->@->@->nullleft = current;// left->@->@->null right->@->@->@->@->nullright = cut(current, step); // 将 current 切掉前 step 个头切下来。// left->@->@->null right->@->@->null current->@->@->nullcurrent = cut(right, step); // 将 right 切掉前 step 个头切下来。// dummy.next -> @->@->@->@->null,最后一个节点是 tail,始终记录// ^// tailtail.next = merge(left, right);while (tail->next) tail = tail->next; // 保持 tail 为尾部}
}
正式代码:好像是C ?
class Solution {
public:ListNode* sortList(ListNode* head) {ListNode dummyHead(0);dummyHead.next = head;auto p = head;int length = 0;while (p) {++length;p = p->next;}for (int size = 1; size < length; size <<= 1) {auto cur = dummyHead.next;auto tail = &dummyHead;while (cur) {auto left = cur;auto right = cut(left, size); // left->@->@ right->@->@->@...cur = cut(right, size); // left->@->@ right->@->@ cur->@->...tail->next = merge(left, right);while (tail->next) {tail = tail->next;}}}return dummyHead.next;}ListNode* cut(ListNode* head, int n) {auto p = head;while (--n && p) {p = p->next;}if (!p) return nullptr;auto next = p->next;p->next = nullptr;return next;}ListNode* merge(ListNode* l1, ListNode* l2) {ListNode dummyHead(0);auto p = &dummyHead;while (l1 && l2) {if (l1->val < l2->val) {p->next = l1;p = l1;l1 = l1->next; } else {p->next = l2;p = l2;l2 = l2->next;}}p->next = l1 ? l1 : l2;return dummyHead.next;}
};
一定要好好学习归并排序,这是可以作为模板代码来学习的!本质上就是两个操作,merge 和 cut 。从网上搜到的很多归并排序都是用递归写的,不过本题使用循环来写,觉得这种写法很值得学习!
归并排序的递归和循环写法,都值得学习!
快速排序版本:是交换节点的,并非只交换数值。(暂时没看)
class Solution {
public ListNode sortList(ListNode head) {if(head==null||head.next==null) return head;// 没有条件,创造条件。自己添加头节点,最后返回时去掉即可。ListNode newHead=new ListNode(-1);newHead.next=head;return quickSort(newHead,null);}// 带头结点的链表快速排序private ListNode quickSort(ListNode head,ListNode end){if (head==end||head.next==end||head.next.next==end) return head;// 将小于划分点的值存储在临时链表中ListNode tmpHead=new ListNode(-1);// partition为划分点,p为链表指针,tp为临时链表指针ListNode partition=head.next,p=partition,tp=tmpHead;// 将小于划分点的结点放到临时链表中while (p.next!=end){if (p.next.val<partition.val){tp.next=p.next;tp=tp.next;p.next=p.next.next;}else {p=p.next;}}// 合并临时链表和原链表,将原链表接到临时链表后面即可tp.next=head.next;// 将临时链表插回原链表,注意是插回!(不做这一步在对右半部分处理时就断链了)head.next=tmpHead.next;quickSort(head,partition);quickSort(partition,end);// 题目要求不带头节点,返回结果时去除return head.next;}
}
题解的代码
日后再次复习重新写
自己复写的归并排序:好好理解!
class Solution:def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:dummy_head = ListNode(0,head)cur = headcount = 0while cur :count += 1cur = cur.nextstep = 1while step < count :# 这里cur从头结点head开始也是必须的,而不能从虚拟头开始cur = dummy_head.nextpre = dummy_headwhile cur :left = curright = self.cut(left,step)cur = self.cut(right,step)pre.next = self.merge(left,right)while pre.next :pre = pre.nextstep = step << 1return dummy_head.nextdef merge(self,head1,head2):dummy_head = ListNode()cur = dummy_headwhile head1 and head2 :if head1.val >= head2.val :cur.next = head2head2 = head2.nextcur = cur.nextelse :cur.next = head1head1 = head1.nextcur = cur.nextif head1 == None :cur.next = head2else :cur.next = head1return dummy_head.next# 注意,cut操作是,返回切断后,后半部分的链表头def cut(self,head,n):cur = head# 注意这里,一定是n>1 ,因为left是从头结点head开始的# 那么如果step是1的话,指针应该不移动,这样才能仅cut掉当前节点while n > 1 and cur != None :cur = cur.nextn -= 1if cur == None :return Noneres = cur.next# cut 操作要在最后结尾处截断,赋值为 Nonecur.next = Nonereturn res
23 合并 K 个升序链表
未看解答自己编写的青春版
有了上一题的铺垫,这道题就显得较为简单了,但是效率高不高就不清楚了。倒序归并排序,执行 n-1 次,因为是每次合并两个,然后 pop 出这两个,然后将结果 append 进去。
class Solution:def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:n = len(lists) if lists == []:return Nonetemp = lists[0]while len(lists) > 1 :head1 = lists[-1]head2 = lists[-2]temp = self.merge(head1,head2)lists.pop()lists.pop()lists.append(temp)return tempdef merge(self,head1,head2):dummy_head = ListNode()cur = dummy_headwhile head1 and head2 :if head1.val >= head2.val :cur.next = head2head2 = head2.nextcur = cur.nextelse :cur.next = head1head1 = head1.nextcur = cur.nextif head1 == None :cur.next = head2else :cur.next = head1return dummy_head.next
重点
这题的评论区里,一堆妖魔鬼怪的方法,作为初学者的我,就找一个最朴素的思路吧,分治法。
可以学习一下,直接没怎么接触过?
其实就是递归,只不过之前的递归,在获得 L1 和 L2 处,要收获结果了,都是一个类似于:加和,append的操作,本题变成了一个两个链表的merge函数。
class Solution:def mergeKLists(self, lists: List[ListNode]) -> ListNode:n = len(lists)def merge(left, right):if left > right:returnif left == right:return lists[left]mid = (left + right) // 2l1 = merge(left, mid)l2 = merge(mid + 1, right)return mergeTwoLists(l1, l2)def mergeTwoLists(l1, l2):if not l1 or not l2:return l1 or l2if l1.val < l2.val:l1.next = mergeTwoLists(l1.next, l2)return l1else:l2.next = mergeTwoLists(l1, l2.next)return l2return merge(0, n - 1)
题解的代码
日后再次复习重新写
分治法复写:将原本代码中,归并两个有序链表的 mergeTwoLists 函数的递归写法,改为了一般的循环写法,这样看上去就更好理解一些了,本质上就是递归!
class Solution:def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:n = len(lists)def merge(left, right):if left > right:returnif left == right:return lists[left]mid = (left + right) // 2l1 = merge(left, mid)l2 = merge(mid + 1, right)return self.mergeTwoLists(l1, l2)return merge(0, n - 1)def mergeTwoLists(self,head1,head2):dummy_head = ListNode()cur = dummy_headwhile head1 and head2 :if head1.val >= head2.val :cur.next = head2head2 = head2.nextcur = cur.nextelse :cur.next = head1head1 = head1.nextcur = cur.nextif head1 == None :cur.next = head2else :cur.next = head1return dummy_head.next
146 LRU 缓存
未看解答自己编写的青春版
没见过这种类型的题,也不知道应该用什么数据结构。
重点
首先要明确本题的两个要点。
1、LRU 的功能可以使用双向链表实现,访问到的节点移动到头部,超出容量的从尾部删除。
2、要实现O(1)得使用HaspMap,里面储存 key 与 链表节点即可,这样可以快速定位节点,然后删除它,将它移动到链表头部。
这道双向链表的题目,真的学习了,后面一定要多复习多重写这道题!
题解的代码
class ListNode:def __init__(self, key=None, value=None):self.key = keyself.value = valueself.prev = Noneself.next = Noneclass LRUCache:def __init__(self, capacity: int):self.capacity = capacityself.hashmap = {}# 新建两个节点 head 和 tailself.head = ListNode()self.tail = ListNode()# 初始化链表为 head <-> tailself.head.next = self.tailself.tail.prev = self.head# 因为get与put操作都可能需要将双向链表中的某个节点移到末尾,所以定义一个方法def move_node_to_tail(self, key):# 先将哈希表key指向的节点拎出来,为了简洁起名node# hashmap[key] hashmap[key]# | |# V --> V# prev <-> node <-> next pre <-> next ... nodenode = self.hashmap[key]node.prev.next = node.nextnode.next.prev = node.prev# 之后将node插入到尾节点前# hashmap[key] hashmap[key]# | |# V --> V# prev <-> tail ... node prev <-> node <-> tailnode.prev = self.tail.prevnode.next = self.tailself.tail.prev.next = nodeself.tail.prev = nodedef get(self, key: int) -> int:if key in self.hashmap:# 如果已经在链表中了久把它移到末尾(变成最新访问的)self.move_node_to_tail(key)res = self.hashmap.get(key, -1)if res == -1:return reselse:return res.valuedef put(self, key: int, value: int) -> None:if key in self.hashmap:# 如果key本身已经在哈希表中了就不需要在链表中加入新的节点# 但是需要更新字典该值对应节点的valueself.hashmap[key].value = value# 之后将该节点移到末尾self.move_node_to_tail(key)else:if len(self.hashmap) == self.capacity:# 去掉哈希表对应项self.hashmap.pop(self.head.next.key)# 去掉最久没有被访问过的节点,即头节点之后的节点self.head.next = self.head.next.nextself.head.next.prev = self.head# 如果不在的话就插入到尾节点前new = ListNode(key, value)self.hashmap[key] = newnew.prev = self.tail.prevnew.next = self.tailself.tail.prev.next = newself.tail.prev = new
日后再次复习重新写
94 二叉树的中序遍历
未看解答自己编写的青春版
class Solution:def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:if root == None :return []left = self.inorderTraversal(root.left)middle = root.valright = self.inorderTraversal(root.right)return left+[middle]+right
重点
过。本次刷题旨在把题做出来,不考虑,递归法怎么写,迭代法怎么写,其他方法有没有这种事情了。
题解的代码
日后再次复习重新写
104 二叉树的最大深度
未看解答自己编写的青春版
class Solution:def maxDepth(self, root: Optional[TreeNode]) -> int:if root == None :return 0left = self.maxDepth(root.left)right = self.maxDepth(root.right)return 1 + max(left,right)
重点
最大深度 = 根节点的最大高度,直接用高度的定义去递归,简单!
题解的代码
日后再次复习重新写
226 翻转二叉树
未看解答自己编写的青春版
正确可以AC的代码:
class Solution:def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:if root == None :return Noneleft = self.invertTree(root.left)right = self.invertTree(root.right)root.left , root.right = right,leftreturn root
要引以为戒的错误代码:错误原因,在未完全递归完成之前,就改变了当前正在递归节点的左右子树。
class Solution:def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:if root == None :return Noneroot.left = self.invertTree(root.right)# 上一行代码的赋值操作,导致改变了root.left,下面的递归就不对了!root.right = self.invertTree(root.left)return root
重点
题解的代码
日后再次复习重新写
101 对称二叉树
未看解答自己编写的青春版
这道题没法在原函数上进行递归判断了,必须新建一个函数,因为对称的判断需要左右两棵子树的头结点。
class Solution: def isSymmetric(self, root: Optional[TreeNode]) -> bool:if root == None :return Truereturn self.judge_symmetric(root.left,root.right)def judge_symmetric(self,left,right):if left == None and right == None :return Trueelif left == None and right != None :return Falseelif left != None and right == None :return Falseelse :flag1 = self.judge_symmetric(left.left,right.right)flag2 = self.judge_symmetric(left.right,right.left)if left.val == right.val :flag3 = Trueelse :flag3 = Falsereturn flag1 and flag2 and flag3
怎么感觉我这次写的这个代码,有点复杂呢。
重点
之前的代码也差不多,可以通过更改顺序,稍微优化一下。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution: def isSymmetric(self, root: Optional[TreeNode]) -> bool:if root == None :return Truereturn self.digui(root.left,root.right)def digui(self,p,q):if p == None and q == None :return Trueelif p == None and q != None :return Falseelif p != None and q == None :return Falseelse :if p.val != q.val :return Falseelse :left = self.digui(p.left,q.right)if left :right = self.digui(p.right,q.left)return left and right
题解的代码
日后再次复习重新写
543 二叉树的直径
未看解答自己编写的青春版
嘿嘿,无敌。虽然这道题是简单题,但是我觉得这道题如何保存最长的路径,以及怎么处理每个节点的返回值,还蛮需要考虑清楚的。
class Solution: def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:if root == None :return 0self.res = 0self.track_road(root)return self.resdef track_road(self,root):if root == None :return 0left = self.track_road(root.left)right = self.track_road(root.right)self.res = max(self.res,left+right)return 1 + max(left,right)
重点
我自认为重点有两个:
1、最长路径不一定经过根节点,比如根节点的左子树很深,而且是完全二叉树,而根节点的右子树只有一个节点,那么最长路径一定出现在左子树中。
2、本题要处理的值,和节点的返回值不一致,不是像之前做过的题目,所求结果就是根节点的返回值!在当前节点,最长路径是左右子树加起来,但是如果要返回到上一层,只能选一个max的返回!
题解的代码
日后再次复习重新写
102 二叉树的层序遍历
未看解答自己编写的青春版
from collections import deque
class Solution:def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:if root == None :return []dq = deque()dq.append(root)res = []while dq :size = len(dq)level = []for i in range(size):node = dq.popleft()level.append(node.val)if node.left :dq.append(node.left)if node.right :dq.append(node.right)res.append(level)return res
重点
层序遍历,模板题。
题解的代码
日后再次复习重新写
108 将有序数组转换为二叉搜索树
未看解答自己编写的青春版
递归构造就行了。
class Solution:def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:if nums == [] :return Nonen = len(nums)mid = n // 2node = TreeNode(nums[mid])node.left = self.sortedArrayToBST(nums[0:mid])node.right = self.sortedArrayToBST(nums[mid+1:])return node
重点
题解的代码
日后再次复习重新写
98 验证二叉搜索树
未看解答自己编写的青春版
中序遍历:左中右,所有的中间节点处理逻辑都要放在中间。
class Solution:def __init__(self):self.pre = Nonedef isValidBST(self, root: Optional[TreeNode]) -> bool:if root == None :return Trueleft = self.isValidBST(root.left)# 中序遍历:左中右,所有的中间节点处理逻辑都要放在中间if self.pre :if self.pre.val >= root.val :return False# 这句话一定要放在中间self.pre = rootright = self.isValidBST(root.right) return left and right
赋值语句 ( self.pre = root ) ,位置错误,导致的错误代码:
class Solution:def __init__(self):self.pre = Nonedef isValidBST(self, root: Optional[TreeNode]) -> bool:if root == None :return Trueleft = self.isValidBST(root.left)# 中序遍历:左中右,所有的中间节点处理逻辑都要放在中间if self.pre :if self.pre.val >= root.val :return Falseright = self.isValidBST(root.right) self.pre = rootreturn left and right
中序遍历迭代法:
class Solution:def isValidBST(self, root: Optional[TreeNode]) -> bool:stack = []cur = rootpre = Nonewhile stack or cur :if cur :stack.append(cur)cur = cur.leftelse :node = stack.pop()if pre :if pre.val >= node.val :return Falsepre = nodecur = node.rightreturn True
上面从stack里 pop 出来的值,可以直接用 cur 承接,更顺眼一些。
class Solution:def isValidBST(self, root: Optional[TreeNode]) -> bool:stack = []cur = rootpre = Nonewhile stack or cur :if cur :stack.append(cur)cur = cur.leftelse :cur = stack.pop()if pre :if pre.val >= cur.val :return Falsepre = curcur = cur.rightreturn True
重点
题解的代码
日后再次复习重新写
230 二叉搜索树中第K小的元素
未看解答自己编写的青春版
中序遍历的迭代法模板,直接秒了,但是时间上只打败了7% ?
class Solution:def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:count = 0stack = []cur = rootwhile stack or cur :if cur :stack.append(cur)cur = cur.leftelse :cur = stack.pop()count += 1if count == k:return cur.valcur = cur.rightreturn 0
重点
从网上看了一些评论,也基本上和我的方法一致。
另一种思路:通过计算节点个数来找寻第K个数,查找左子树节点个数为 leftN , 如果 K<=leftN ,则所查找节点在左子树上,若 K=leftN+1 , 则所查找节点为根节点,若 K>leftN+1 , 则所查找节点在右子树上, 按照同样方法查找右子树第 K-leftN 个节点。
但是在时间上还是打败 7% , 其他解法怎么这么快的?
class Solution:def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:left = self.count(root.left)if left + 1 == k:return root.valelif left >= k :return self.kthSmallest(root.left,k)else :return self.kthSmallest(root.right,k-left-1)def count(self,root):if root == None :return 0return self.count(root.left)+self.count(root.right)+1
题解的代码
力扣的示例代码:
class Solution:def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: res = []def dfs(node):if not node:returnif len(res)>=k:returndfs(node.left)res.append(node.val)dfs(node.right)dfs(root)return res[k-1]
原来就是递归,储存所有遍历过的节点,当结果列表长度大于等于 k 时,返回。
看起来因为使用的是递归,所以应该是比我的中序遍历迭代法要少遍历一些节点。不过这个代码,有时候90%,有时候也是7%,不纠结这道题的耗时统计了!
日后再次复习重新写
199 二叉树的右视图
未看解答自己编写的青春版
又是层序遍历模板题。
from collections import deque
class Solution:def rightSideView(self, root: Optional[TreeNode]) -> List[int]:if root == None :return []dq = deque()dq.append(root)res = []while dq :size = len(dq)for i in range(size):node = dq.popleft()if node.left :dq.append(node.left)if node.right:dq.append(node.right)res.append(node.val)return res
重点
题解的代码
日后再次复习重新写
一段用于复制的标题
未看解答自己编写的青春版
一开始总想着用递归做,但是发现,在递归中,无法时刻保存当前需要赋值的节点,因为本题要求不能返回任意值,也就是要操作原节点,所以递归函数返回值,赋值,这些操作都是无效的,所以应该用前序遍历的迭代法。
class Solution:def flatten(self, root: Optional[TreeNode]) -> None:"""Do not return anything, modify root in-place instead."""if root == None :return Nonestack = [root]cur = rootwhile stack :node = stack.pop()if node == root :passelse :cur.right = nodecur.left = Nonecur = nodeif node.right :stack.append(node.right)if node.left :stack.append(node.left)
但是上述是:额外空间复杂度为O(n),因为额外申请了一个堆栈。
重点
额外空间复杂度为O(1)的思想!这道题我被题目骗了,题目说顺序应该是前序遍历,我就想着递归必须用前序遍历,但是前序遍历的问题是:会提前修改掉后面要进入递归的值。那么,链表,从上到下连接可以,我从下到上,反向连接,也可以啊!而这一点,利用递归的回溯特性,可以很自然地做到。假如 last 承载了下一层递归的返回值,而本层递归的root为last的上一个,只需要赋值:root.right=last,就可以了!
所以后序遍历!后序遍历,也就不会有,还未进入递归,值就被修改的问题!这题的思路太妙了!
class Solution:def __init__(self):self.last = Nonedef flatten(self, root: Optional[TreeNode]) -> None:"""Do not return anything, modify root in-place instead."""if root == None :return Noneself.flatten(root.right)self.flatten(root.left)root.right = self.lastroot.left = Noneself.last = root
思路很妙,可以多写多复习。
题解的代码
日后再次复习重新写
105 从前序与中序遍历序列构造二叉树
未看解答自己编写的青春版
递归,数组切片方法:
class Solution:def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:if preorder == []:return Nonemiddle = preorder[0]index = inorder.index(middle)node = TreeNode(middle)node.left = self.buildTree(preorder[1:index+1],inorder[:index])node.right = self.buildTree(preorder[index+1:],inorder[index+1:])return node
厉害!第一次自己尝试,写出了传入左右索引版的代码,在空间占用上,要比上面的代码低不少。
class Solution:def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:def build(pleft,pright,ileft,iright):# 左闭右开,不包括right,所以才能有等于号if pleft >= pright :return Nonemiddle = preorder[pleft]index = inorder.index(middle)node = TreeNode(middle)# 举例: ileft = 3 , middle = 4 , 意味着左子树只有一个节点# 那么对于前序数组,起始位置 pleft+1 , 要想让左子树有一个点,区间应该为# [pleft+1,pleft+2) 注意循环不变量,左闭右开node.left = build(pleft+1,pleft+1+index-ileft,ileft,index)node.right = build(pleft+1+index-ileft,pright,index+1,iright)return noden = len(preorder)return build(0,n,0,n)
重点
过。可以体会下,传入左右区间的参数的版本,怎样能把每个区间都写对。
题解的代码
日后再次复习重新写
437 路径总和 III
未看解答自己编写的青春版
没做出来。
重点
主要是两种方法:双重递归 ; 前缀和。
力扣官方题解
没来得及体会,日后再看,重点学习这道题。
题解的代码
双重递归:
class Solution:def pathSum(self, root: TreeNode, targetSum: int) -> int:def rootSum(root, targetSum):if root is None:return 0ret = 0if root.val == targetSum:ret += 1ret += rootSum(root.left, targetSum - root.val)ret += rootSum(root.right, targetSum - root.val)return retif root is None:return 0ret = rootSum(root, targetSum)ret += self.pathSum(root.left, targetSum)ret += self.pathSum(root.right, targetSum)return ret
前缀和:
class Solution:def pathSum(self, root: TreeNode, targetSum: int) -> int:prefix = collections.defaultdict(int)prefix[0] = 1def dfs(root, curr):if not root:return 0ret = 0curr += root.valret += prefix[curr - targetSum]prefix[curr] += 1ret += dfs(root.left, curr)ret += dfs(root.right, curr)prefix[curr] -= 1return retreturn dfs(root, 0)
日后再次复习重新写
236 二叉树的最近公共祖先
未看解答自己编写的青春版
搞懂,左右公共祖先,怎么从后序遍历中,层层回溯到根节点的。
class Solution:def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':self.res = Noneself.digui(root,p,q)return self.resdef digui(self,root,p,q):if root == None :return Falseif root.val == p.val or root.val == q.val :self.res = rootreturn True left = self.digui(root.left,p,q)right = self.digui(root.right,p,q)if left and right :self.res = rootreturn left or right
重点
参考卡哥的解答。
二叉树的最近公共祖先
题目拓展:如果树是一个二叉搜索树呢?前序遍历树中节点的值就可以了,当节点值第一次出现在在区间
[ p.val , q.val ] 时,这个节点就是最近公共祖先,可以用反证法证明,再走一步就不符合条件了。
题解的代码
日后再次复习重新写
124 二叉树中的最大路径和
未看解答自己编写的青春版
小小 hard 。
本题只需要考虑清楚:对于在当前节点收获结果的逻辑:左和右,分别都有两个状态,取或不取,一共四种情况就好了;递归函数在返回时,同前面做过的一道题,返回的时候,没有左右都考虑的情况。
class Solution:def maxPathSum(self, root: Optional[TreeNode]) -> int:self.res = -infself.digui(root)return self.resdef digui(self,root):if root == None :return 0if root.left == None and root.right == None :self.res = max(self.res,root.val)return root.valleft = self.digui(root.left)right = self.digui(root.right)# 只要想明白这里,左和右,分别都有两个状态,取或不取,一共四种情况就好了self.res = max(self.res,left+right+root.val,root.val,right+root.val,left+root.val)# 同前面做过的一道题,返回的时候,没有左右都考虑的情况return max(left,right,0)+root.val