刷题笔记 - 腾讯50题(上)
From: 🐧 腾讯精选练习 50 题 - 力扣(LeetCode),按通过率降序排列
上篇
237. Delete Node in a Linked List
-
参数中的node就是要删除的节点。我们可以把下个节点的值存到当前节点,然后让当前节点的next直接指向下下个节点即可
class Solution: def deleteNode(self, node): node.val = node.next.val node.next = node.next.next
78. Subsets
-
def subset(nums): res = [] for i in range(len(nums)+1): for tmp in itertools.combinations(nums, i): res.append(tmp) return res
-
迭代组合:
def subset(nums): res = [[]] for i in nums: res = res + [[i] + num for num in res] return res
subset([1,2,3]) 迭代过程: i = 1, res = [[]] [[i] + num for num in res] = [[1]] i = 2, res = [[], [1]] [[i] + num for num in res] = [[2], [2, 1]] i = 3, res = [[], [1], [2], [2, 1]] [[i] + num for num in res] = [[3], [3, 1], [3, 2], [3, 2, 1]]
344. Reverse String
-
双指针:
def reverseString(s): j = len(s) - 1 for i in range(len(s)): s[i], s[j] = s[j], s[i] j -= 1 if j <= i: break
46. Permutations
-
from itertools import permutations class Solution: def permute(self, nums: List[int]) -> List[List[int]]: return [list(i) for i in permutations(nums, len(nums))]
-
回溯:
class Solution: def backtrace(self, comb, pool): if len(pool) > 0: for i in pool: new_pool = pool.copy() new_pool.remove(i) self.backtrace(comb + [i], new_pool) else: self.res.append(comb) def permute(self, nums: List[int]) -> List[List[int]]: self.res = [] self.backtrace([], nums) return self.res
104. Maximum Depth of Binary Tree
-
深度优先遍历:
class Solution: def dfs(self, node, depth=1): if node.left: self.dfs(node.left, depth+1) if node.right: self.dfs(node.right, depth+1) if depth > self.max_depth: self.max_depth = depth def maxDepth(self, root: Optional[TreeNode]) -> int: self.max_depth = 0 if root: self.dfs(root) return self.max_depth
230. Kth Smallest Element in a BST
-
BST(二叉搜索树)的性质:“左子树中的所有节点 < 当前节点 < 右子树中的所有节点”。因此解法就是中序遍历(左根右)这颗树,每次处理根节点时对排位进行计数,直到找到目标:
class Solution: def verify(self, node): self.k -= 1 if self.k < 1: self.res = node.val def search(self, node): if node.left and self.res is None: self.search(node.left) if self.res is None: self.verify(node) if node.right and self.res is None: self.search(node.right) def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: self.res = None self.k = k self.search(root) return self.res
-
以迭代的方式进行中序遍历:
class Solution: def kthSmallest(self, root: TreeNode, k: int) -> int: stack = [] while root or stack: while root: # go left first stack.append(root) root = root.left root = stack.pop() # go back and verify k -= 1 if k == 0: return root.val root = root.right
89. Gray Code
-
归纳法(找规律法):
观察:
['0', '1']
['00', '01', '11', '10']
['000', '001', '011', '010', '110', '111', '101', '100']发现:
n位的格雷码可以由n-1位的格雷码序列推导出来,具体方法如下:
- 先遍历n-1位的格雷码序列,在每个元素前补'0',得到n位格雷码的前半段序列
- 倒序遍历n位格雷码的前半段序列,将其中每个元素开头的'0'改成'1',得到后半段序列
- 连接前后两段序列,即得到n位的格雷码序列
class Solution: def grayCode(self, n: int) -> List[int]: res = ['0', '1'] for _ in range(n - 1): part1 = ["0"+i for i in res] # 在n-1位的gray code前补0 part2 = ["1"+i[1:] for i in part1[::-1]] # 倒序遍历part1,把其中每个元素第一位的0改成1 res = part1 + part2 return [int(i, 2) for i in res] # 二进制字串转十进制数字
238. Product of Array Except Self
-
从左往右遍历时,记录每个元素左侧元素的乘积。然后从右往左遍历,记录每个元素右侧元素的乘积。最后遍历乘积列表,得到每个元素左侧元素和右侧元素的乘积的积
- 以[1,2,3,4]为例:先从左往右遍历,得到:[1,1,2,6]。接着从右往左遍历,得到:[24,12,4,1]。接着对[1,1,2,6]和[24,12,4,1]按位相乘,得到最终结果:[24, 12, 8, 6]。至此,一共完成了3次n位数组的遍历,算法时间复杂度为O(3n),空间复杂度为O(3n)
- 实际上,从左往右遍历时,可以通过len(nums)-i得到反向坐标,进而同步计算从右向左的累乘结果,这样我们就节省了一次遍历,算法的时间复杂度可以从O(3n)降到O(2n)
class Solution: def productExceptSelf(self, nums): left = [1] right = [1] for i in range(1, len(nums)): left.append(left[-1]*nums[i-1]) right.append(right[-1]*nums[len(nums)-i]) return [left[i] * right[len(nums)-1-i] for i in range(len(nums))]
557. Reverse Words in a String III
-
基于序列表达式,分割单词,反转拼写,再用空格连接成句:
class Solution: def reverseWords(self, s: str) -> str: return ' '.join([word[::-1] for word in s.split(' ')])
59. Spiral Matrix II
-
就硬写:
class Solution: def get_move_methods(self): go_right = lambda i, j: (i, j+1) go_down = lambda i, j: (i+1, j) go_left = lambda i, j: (i, j-1) go_up = lambda i, j: (i-1, j) return [go_right, go_down, go_left, go_up] def fill(self, res, n): i, j = 0, 0 valid_ij = lambda i, j: (0 <= i < n) and (0 <= j < n) valid_res = lambda i, j: res[i][j] == 0 counter = 1 moves = self.get_move_methods() moves_i = 0 move = moves[moves_i % len(moves)] while valid_res(i, j): res[i][j] = counter counter += 1 next_i, next_j = move(i, j) if valid_ij(next_i, next_j) and valid_res(next_i, next_j): i, j = next_i, next_j else: moves_i += 1 move = moves[moves_i % len(moves)] i, j = move(i, j) return res def generateMatrix(self, n): if n == 1: return [[1]] else: return self.fill([[0 for _ in range(n)] for _ in range(n)], n)
206. Reverse Linked List
-
遍历节点时记录当前节点的前置节点和后置节点, 然后令当前节点的next指向之前保存的前置节点,接着用刚刚保存的后置节点来获得下一个遍历位置:
# class ListNode: # 链表定义 # def __init__(self, val=0, next=None): # self.val = val # self.next = next # def list2linkedlist(data): # 调试用,构造链表 # last_node = None # head = None # for i in data: # node = ListNode(i) # if head is None: # head = node # if last_node: # last_node.next = node # last_node = node # return head # def linkedlist2list(head): # 调试用,解析链表 # cur = head # res = list() # while True: # res.append(cur.val) # if cur.next: # cur = cur.next # else: # break # return res class Solution: def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: curr_node = head prev_node = None while curr_node: next_node = curr_node.next curr_node.next = prev_node prev_node = curr_node curr_node = next_node return prev_node
136. Single Number
-
利用异或运算的4个性质:
- 归零律:a xor a = 0
- 恒等律:a xor 0 = a
- 交换律:a xor b = b xor a
- 结合律:a xor b xor c = a xor (b xor c) = (a xor b) xor c
class Solution: def singleNumber(self, nums: List[int]) -> int: res = nums[0] if len(nums) > 1: for i in nums[1:]: res = res ^ i # 异或运算 return res
122. Best Time to Buy and Sell Stock II
-
贪心策略:由于可以在同一天进行买入和卖出,因此我们可以在遍历数组的同时记录当前的最低价,一旦发现当前价格高于之前记录的低价,就累加利润,然后用当前价格作为新的低价(换句话说,不用等到最高点再卖出,只要发现赚钱了,就马上卖掉然后原地再“买入”,后续如果发现了更低的价格,就用更低的价格作为“买点”)
class Solution: def maxProfit(self, prices: List[int]) -> int: low_price = prices[0] profit = 0 if len(prices) > 1: for cur_price in prices[1:]: if cur_price < low_price: low_price = cur_price if cur_price > low_price: profit += (cur_price - low_price) low_price = cur_price return profit
292. Nim Game
-
从n=1开始枚举,找出先后手在胜负上的规律:
1 2 3 先手必胜
4 后手必胜(任取1-3枚石头,都会进入先手必胜的情形)
5 6 7 先手必胜(取1/2/3枚石头,让对手面对后手必胜的情形)
8 后手必胜...
class Solution: def canWinNim(self, n: int) -> bool: return (n % 4) > 0
236. Lowest Common Ancestor of a Binary Tree
-
DFS
# Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution: def find_answer(self, curr_node, stack): if curr_node.val == self.lca_father_val: self.lca = curr_node self.lca_father_val = stack[-1].val if stack else None if curr_node.val in self.targets: self.targets.remove(curr_node.val) if not self.targets: return True self.lca = curr_node self.lca_father_val = stack[-1].val if stack else None return False def dfs(self, root): stack = [] curr_node = root while curr_node or stack: while curr_node: stack.append(curr_node) curr_node = curr_node.left curr_node = stack.pop() if self.find_answer(curr_node, stack): break curr_node = curr_node.right def lowestCommonAncestor(self, root, p, q): self.targets = {p.val, q.val} self.lca = None self.lca_father_val = None self.dfs(root) return self.lca
235. Lowest Common Ancestor of a Binary Search Tree
-
利用二叉搜索树的性质:
- 树中的每一个节点,其左子树中的值都比该节点值小,右子树中的值都比该节点值大
-
因此,我们可以从根节点开始搜索:
- 如果当前节点的值介于两个目标节点之间,则当前节点为lca
- 如果当前节点的值大于两个目标节点,则继续检索左子树
- 如果当前节点的值小于两个目标节点,则继续检索右子树
class Solution: def lowestCommonAncestor(self, root, p, q): small_val = min([p.val, q.val]) large_val = max([p.val, q.val]) while root: if small_val <= root.val <= large_val: return root elif large_val < root.val: root = root.left else: root = root.right
62. Unique Paths
-
动态规划:
- 状态定义:
dp[i][j]
抵达坐标(i, j)
所需的不同路径数 - 递推关系:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 边界条件:
dp[i][1] = 1, dp[1][j] = 1
class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [[1 for _ in range(n)] for _ in range(m)] for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[-1][-1]
- 状态定义:
169. Majority Element
-
哈希
class Solution: def majorityElement(self, nums: List[int]) -> int: counter = dict() majority_element = nums[-1] max_count = 1 while nums: item = nums.pop() if item in counter: counter[item] += 1 if counter[item] > max_count: majority_element = item max_count = counter[item] else: counter[item] = 1 return majority_element
21. Merge Two Sorted Lists
-
双指针
# class ListNode: # 链表定义 # def __init__(self, val=0, next=None): # self.val = val # self.next = next # def list2linkedlist(data): # 调试用,构造链表 # last_node = None # head = None # for i in data: # node = ListNode(i) # if head is None: # head = node # if last_node: # last_node.next = node # last_node = node # return head # def linkedlist2list(head): # 调试用,解析链表 # cur = head # res = list() # while cur: # res.append(cur.val) # if cur.next: # cur = cur.next # else: # break # return res class Solution: def mergeTwoLists(self, list1, list2): tail_node = None new_head = None while list1 or list2: if list2 is None or (list1 and list1.val <= list2.val): if tail_node: tail_node.next = list1 tail_node = list1 else: tail_node = list1 list1 = list1.next elif list1 is None or (list2 and list2.val <= list1.val): if tail_node: tail_node.next = list2 tail_node = list2 else: tail_node = list2 list2 = list2.next if not new_head: new_head = tail_node return new_head
148. Sort List
-
双指针,一个针对旧链表,一个针对新链表。针对每个旧链表中的节点,遍历新链表,将它插入到合适的位置
- 时间复杂度:\(O(n)\sim O(n^2)\),会超时
class Solution: def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: head_new = ListNode(val=head.val) if head else None cur_old = head.next if head else None while cur_old: cur_new = head_new previous_cur_new = None while cur_new: if cur_old.val < cur_new.val: # insert cur_old between previous_cur_new and cur_new if previous_cur_new: previous_cur_new.next = cur_old next_cur_old = cur_old.next cur_old.next = cur_new if head_new == cur_new: head_new = cur_old cur_old = next_cur_old break else: if not cur_new.next: tail_new = cur_new previous_cur_new = cur_new cur_new = cur_new.next else: tail_new.next = cur_old next_cur_old = cur_old.next cur_old.next = None cur_old = next_cur_old return head_new
-
转成list,然后排序,然后再转回链表
- 时间复杂度:O(2n+nlogn)
class Solution: @staticmethod def list2linkedlist(data): last_node = None head = None for i in data: node = ListNode(i) if head is None: head = node if last_node: last_node.next = node last_node = node return head @staticmethod def linkedlist2list(head): cur = head res = list() while cur: res.append(cur.val) if cur.next: cur = cur.next else: break return res def sortList(self, head): return self.list2linkedlist(sorted(self.linkedlist2list(head)))
215. Kth Largest Element in an Array
- 快速排序原理(时间复杂度nlogn)
- 分解:不断地将数组分成两半,使其一半较小,一半较大
- 这里的划分方法具体为:从数组中任选一个元素作为主元,调整子数组的元素使得左边的元素都小于它,右边的元素都大于它
- 合并:递归调用快速排序,对分成的两半进行排序
- 如果需要将时间复杂度降为O(n),则需要引入随机性,详见《算法导论》9.2:期望为线性的选择算法
- 分解:不断地将数组分成两半,使其一半较小,一半较大
- 优先队列原理
- 普通的队列具有先进先出的特性,元素追加在队尾,如果删除的话,从队头删除。而在优先队列中,队列中的数据被赋予了优先级。当访问元素时,优先级最高的会先被删除。所以说优先队列是最高级数据先出
- 优先队列支持的操作:
- 定义优先级,从而在删除时直接弹出最小(最大)元素
- 插入一个元素
- 优先队列一般通过维护一个堆来实现,堆的原理这里暂时就先不展开了
160. Intersection of Two Linked Lists
-
分别遍历两个链表,将其转化为list,然后反向遍历两个list,比对找出最后一个公用节点
class Solution: @staticmethod def linkedlist2list(head): cur = head res = list() while cur: res.append(cur) if cur.next: cur = cur.next else: break return res def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]: listA = self.linkedlist2list(headA) listB = self.linkedlist2list(headB) intersec_node = None while listA and listB: nodeA = listA.pop() nodeB = listB.pop() if nodeA is nodeB: intersec_node = nodeA else: break return intersec_node
-
另外,看了下题解里的NB思路,感觉很值得学习,所以这里也记录一下:
我先走我的路,再走你的路,你先走你的路,再走我的路,这样咱俩走的路程就一样了,速度一样,那么肯定在咱俩两条路的交叉口相遇,并一起走向终点。
class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: A, B = headA, headB while A != B: A = A.next if A else headB B = B.next if B else headA return A
换一种更有文采的描述(from:sylin):
走到尽头见不到你,于是走过你来时的路,等到相遇时才发现,你也走过我来时的路。
11. Container With Most Water
-
暴力搜索,时间复杂度O(nlogn),超时:
class ForceSearch: def maxArea(self, height): max_v = 0 for i in range(len(height)): for j in range(i+1, len(height)): v = min(height[i], height[j]) * abs(i-j) max_v = v if v > max_v else max_v return max_v
-
双指针:
- 由于水槽面积 = 短板高度×底边长度,因此,当我们让双指针从两端向中间逼近时:
- 如果移动长板,那么下一轮的 短板高度 必然不变或变小,由于底边长度减短了1,因此水槽面积必然缩小
- 如果移动短板,那么下一轮的 短板高度 才有可能变大,因此水槽面积也就有变大的可能
- 因此,我们的策略是:初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出,即可获得最大面积:
class DoublePoint: def maxArea(self, height): max_v = 0 i, j = 0, len(height)-1 while i != j: v = min(height[i], height[j]) * abs(i-j) max_v = v if v > max_v else max_v if height[i] < height[j]: i += 1 else: j -= 1 return max_v
- 由于水槽面积 = 短板高度×底边长度,因此,当我们让双指针从两端向中间逼近时:
155. Min Stack
-
时间复杂度要求严格,但空间复杂度不限。那么思路就应该转到:“以空间换时间”这条路线上:
- 在维护栈时,将栈中的元素由
val
换成(val, min_val)
,其中val是当前插入的数据,min_val是插入当前数据时,栈中数据的最小值
class MinStack: def __init__(self): self.stack = [] def push(self, val: int) -> None: if not self.stack: min_val = val else: min_val = min(val, self.getMin()) self.stack.append((val, min_val)) def pop(self) -> None: val, _ = self.stack.pop() return val def top(self) -> int: val, _ = self.stack[-1] return val def getMin(self) -> int: _, min_val = self.stack[-1] return min_val
- 在维护栈时,将栈中的元素由
121. Best Time to Buy and Sell Stock
-
在遍历数组时,寻找当前最小值之后的极大值,针对每个符合前述条件的极大值,计算当前利润,最后返回过程中得到的最大利润:
class Solution: def maxProfit(self, prices: List[int]) -> int: min_p, max_p = prices[0], prices[0] min_p_i = 0 max_profit = 0 for i in range(1, len(prices)): if prices[i] < min_p: min_p, max_p = prices[i], prices[i] if prices[i] > max_p: max_p = prices[i] profit = max_p - min_p max_profit = profit if profit > max_profit else max_profit return max_profit