算法随想录
数组
704. 二分查找
区间一般可以定义为两种:左闭右闭 [left, right],或者左闭右开 [left, right]。
这里采用左闭右闭进行代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution (object ): def search (self, nums, target ): """ :type nums: List[int] :type target: int :rtype: int """ left = 0 right = len (nums) - 1 while left <= right: middle = (left + right) // 2 if nums[middle] > target: right = middle - 1 elif nums[middle] < target: left = middle + 1 else : return middle return -1
27. 移除元素
双指针法:通过一个快指针和一个慢指针在一个 for 循环下完成两个 for 循环的工作。
快指针:寻找新数组的元素,新数组就是不含有目标元素的数组
慢指针:指向更新新数组下标的位置
这里不采用暴力解法,使用双指针实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def removeElement (self, nums, val ): """ :type nums: List[int] :type val: int :rtype: int """ slow = 0 fast = 0 size = len (nums) while fast < size: if nums[fast] != val: nums[slow] = nums[fast] slow += 1 fast+= 1 return slow
977. 有序数组的平方
数组是有序的,但负数平方之后可能成为最大数了。所以最大值不是在最左边就是在最右边,不可能是中间。
可以考虑双指针法,left指向最左边,right 指向最右边。
定义一个新的数组,和 A 数组一样大小,让 k 指向 res 数组终止位置。
如果 A[left] * A[left] < A[right] * A[right],那么 res[k–] = A[right] * A[right]
如果 A[left] * A[left] >= A[right] * A[right],那么 res[k–] = A[left] * A[left]
以下是代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def sortedSquares (self, nums ): """ :type nums: List[int] :rtype: List[int] """ left = 0 right = len (nums) - 1 k = len (nums) - 1 res = [float ('inf' )] * len (nums) while left <= right: if nums[left] ** 2 > nums[right] ** 2 : res[k] = nums[left] ** 2 left += 1 else : res[k] = nums[right] ** 2 right -= 1 k -= 1 return res
209. 长度最小的子数组
不断的调节子序列的起始位置和终止位置
窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?
窗口:满足其和 >= target 的长度最小的连续数组
起始位置如何移动:如果当前窗口的值大于等于 target 了,窗口就要向前移动了(也就是该缩小了)。
结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是 for 循环里的索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def minSubArrayLen (self, target, nums ): """ :type target: int :type nums: List[int] :rtype: int """ l = len (nums) res = 999999 start = 0 end = 0 total = 0 while end < l: total += nums[end] while total >= target: res = min (res, end - start + 1 ) total -= nums[start] start += 1 end += 1 return 0 if res == 999999 else res
59. 螺旋矩阵
初始化一个 n * n 大小的矩阵 matrix,然后模拟整个内向环绕的填入过程。
模拟顺时针画矩阵的过程
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution (object ): def generateMatrix (self, n ): """ :type n: int :rtype: List[List[int]] """ l, r, t, b = 0 , n - 1 , 0 , n - 1 matrix = [[0 ] * n for _ in range (n)] num, target = 1 , n * n while num <= target: for i in range (l, r + 1 ): matrix[t][i] = num num += 1 t += 1 for i in range (t, b + 1 ): matrix[i][r] = num num += 1 r -= 1 for i in range (r, l - 1 , -1 ): matrix[b][i] = num num += 1 b -= 1 for i in range (b, t - 1 , -1 ): matrix[i][l] = num num += 1 l += 1 return matrix
链表
203. 移除链表元素
链表操作两种方式:
直接使用原来的链表来进行删除操作
设置一个虚拟头节点在进行删除操作
推荐设置虚拟头节点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def removeElements (self, head, val ): """ :type head: Optional[ListNode] :type val: int :rtype: Optional[ListNode] """ current = dummy_head = ListNode(next = head) while current.next : if current.next .val == val: current.next = current.next .next else : current = current.next return dummy_head.next
707. 设计链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 class ListNode : def __init__ (self, val=0 , next =None ): self.val = val self.next = next class MyLinkedList (object ): def __init__ (self ): self.dummy_head = ListNode() self.size = 0 def get (self, index ): """ :type index: int :rtype: int """ if index < 0 or index >= self.size: return -1 current = self.dummy_head.next for i in range (index): current = current.next return current.val def addAtHead (self, val ): """ :type val: int :rtype: None """ self.dummy_head.next = ListNode(val, self.dummy_head.next ) self.size += 1 def addAtTail (self, val ): """ :type val: int :rtype: None """ current = self.dummy_head while current.next : current = current.next current.next = ListNode(val) self.size += 1 def addAtIndex (self, index, val ): """ :type index: int :type val: int :rtype: None """ if index < 0 or index > self.size: return current = self.dummy_head for i in range (index): current = current.next current.next = ListNode(val, current.next ) self.size += 1 def deleteAtIndex (self, index ): """ :type index: int :rtype: None """ if index < 0 or index >= self.size: return current = self.dummy_head for i in range (index): current = current.next current.next = current.next .next self.size -= 1
206. 反转链表
本题采用双指针写法:
首先定义一个 current 指针,指向头节点, 再定义一个 previous 指针,初始化 null。
然后开始反转,首先把 current -> next 节点用 temp 指针保存一下,接下来将 current -> next 指向 previous,此时已经反转了第一个节点了。
接下来循环,继续移动 previous 和 current 指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def reverseList (self, head ): """ :type head: Optional[ListNode] :rtype: Optional[ListNode] """ current, previous = head, None while current: temp = current.next current.next = previous previous = current current = temp return previous
24. 两两交换链表中的节点
建议使用虚拟头节点,这样会方便很多,接下来就是交换相邻两个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def swapPairs (self, head ): """ :type head: Optional[ListNode] :rtype: Optional[ListNode] """ dummy_head = ListNode(next =head) current = dummy_head while current.next and current.next .next : temp1 = current.next temp2 = current.next .next .next current.next = current.next .next current.next .next = temp1 temp1.next = temp2 current = current.next .next return dummy_head.next
19. 删除链表的倒数第 N 个节点
采用双指针,如果要删除倒数第 n 个节点,让 fast 移动 n 步。然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的节点就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution (object ): def removeNthFromEnd (self, head, n ): """ :type head: Optional[ListNode] :type n: int :rtype: Optional[ListNode] """ dummy_head = ListNode(0 , head) slow = fast = dummy_head for i in range (n + 1 ): fast = fast.next while fast: fast = fast.next slow = slow.next slow.next = slow.next .next return dummy_head.next
142. 环形链表Ⅱ
主要考察两知识点:
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头节点出发,fast 指针每次移动两个节点,slow 指针每次移动一个节点,如果 fast 指针和 slow 指针在途中相遇,说明这个链表有环。
假设从头节点到环形入口节点的节点数为 x,环形入口节点到 fast 指针与 slow 指针相遇节点节点数为 y,从相遇节点再到环形入口节点数为 z。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution (object ): def detectCycle (self, head ): """ :type head: ListNode :rtype: ListNode """ slow = head fast = head while fast and fast.next : fast = fast.next .next slow = slow.next if fast == slow: index1 = fast index2 = head while index1 != index2: index1 = index1.next index2 = index2.next return index1 return None
哈希表
242. 有效的字母异位词
字符 a 到字符 z 的 ASCII 是 26 个连续的数值,所以字符 a 映射为下标 0,相应的字符 z 映射为下标 25。
record 数组中如果有的元素不为 0,说明字符串 s 和 t 一定是谁多了字符或谁少了字符,return False。
如果 record 数组所有元素都为 0,说明字符串 s 和 t 是字母异位词,return True。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution (object ): def isAnagram (self, s, t ): """ :type s: str :type t: str :rtype: bool """ record = [0 ] * 26 for i in s: record[ord (i) - ord ("a" )] += 1 for i in t: record[ord (i) - ord ("a" )] -= 1 for i in range (26 ): if (record[i] != 0 ): return False return True
349. 两个数组的交集
输出结果中的每个元素一定是唯一的,也就是说输出的结果是去重的,同时可以不考虑输出结果的顺序。
使用集合方式:
1 2 3 4 5 6 7 8 class Solution (object ): def intersection (self, nums1, nums2 ): """ :type nums1: List[int] :type nums2: List[int] :rtype: List[int] """ return list (set (nums1) & set (nums2))
使用数组方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def intersection (self, nums1, nums2 ): """ :type nums1: List[int] :type nums2: List[int] :rtype: List[int] """ record = [0 ] * 1001 result = [] for i in nums1: record[i] = 1 for i in nums2: if (record[i] == 1 ): result.append(i) record[i] = 0 return result
1. 两数之和
此题采用 map 映射方法
map 的目的用来存放访问过的元素,因为遍历数组的时候,需要记录之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的。
map 中的存储结构为 {key:数据元素,value:数组元素对应的下标}
在遍历数组的时候,只需要向 map 去查询是否有和目前遍历数组元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进 map 中,因为 map 存放的就是访问过的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution (object ): def twoSum (self, nums, target ): """ :type nums: List[int] :type target: int :rtype: List[int] """ record = dict () for index, value in enumerate (nums): if target - value in record: return [record[target - value], index] record[value] = index return []
454. 四数相加Ⅱ
四个独立的数组,只要找到 A[i] + B[j] + C[k] + D[i] = 0 就可以,不用考虑有重复的四个元素相加等于 0 的情况。
首先定义一个字典,key 存放 num1 和 num2 两数之和,value 存放 num1 和 num2 两数之和出现的次数
遍历 num1 和 num2 数组,统计两个数组元素之和,和出现的次数,放到 map 中
定义变量 count,用来统计 num1 + num2 + num3 + num4 = 0 出现的次数
再遍历 num3 和 num4 数组,找到如果 0 - (num3 + num4) 在 map 中出现过的话,就用 count 把 map 中 key 对应的 value 也就是统计次数统计出来
最后返回统计值 count
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def fourSumCount (self, nums1, nums2, nums3, nums4 ): """ :type nums1: List[int] :type nums2: List[int] :type nums3: List[int] :type nums4: List[int] :rtype: int """ hashmap = dict () for n1 in nums1: for n2 in nums2: if n1 + n2 in hashmap: hashmap[n1 + n2] += 1 else : hashmap[n1 + n2] = 1 count = 0 for n3 in nums3: for n4 in nums4: target = 0 - (n3 + n4) if target in hashmap: count += hashmap[target] return count
15. 三数之和
使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意。
采用双指针法,比哈希法高效一些
首先将数组排序,然后有一层 for 循环,i 从下标 0 的地方开始,同时定义一个下标 left 定义在 i + 1 的位置上,定义下标 right 在数组结尾的位置上。
在数组中找到 abc 使得 a + b + c = 0,这里相当于 a = nums[i],b = nums[left],c = nums[right]。
如果 nums[i] + nums[left] + nums[right] > 0 说明此时三数之和大了,因为数组是排序后了,所以 right 下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明此时三数之和小了,left 就向右移动,才能让三数之和大一些,直到 left 与 right 相遇为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution (object ): def threeSum (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ result = [] nums.sort() for i in range (len (nums)): if nums[i] > 0 : break if i > 0 and nums[i] == nums[i - 1 ]: continue left = i + 1 right = len (nums) - 1 while right > left: total = nums[i] + nums[left] + nums[right] if total > 0 : right -= 1 elif total < 0 : left += 1 else : result.append([nums[i], nums[left], nums[right]]) while right > left and nums[right] == nums[right - 1 ]: right -= 1 while right > left and nums[left] == nums[left + 1 ]: left += 1 right -= 1 left += 1 return result
18. 四数之和
基本解法就是在三数之和的基础上再套一层 for 循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution (object ): def fourSum (self, nums, target ): """ :type nums: List[int] :type target: int :rtype: List[List[int]] """ result = [] nums.sort() n = len (nums) for i in range (n): if nums[i] > target and nums[i] > 0 and target > 0 : break if i > 0 and nums[i] == nums[i - 1 ]: continue for j in range (i + 1 , n): if nums[i] + nums[j] > target and target > 0 : break if j > i + 1 and nums[j] == nums[j - 1 ]: continue left, right = j + 1 , n - 1 while left < right: total = nums[i] + nums[j] + nums[left] + nums[right] if total == target: result.append([nums[i], nums[j], nums[left], nums[right]]) while left < right and nums[left] == nums[left + 1 ]: left += 1 while left < right and nums[right] == nums[right - 1 ]: right -= 1 left += 1 right -= 1 elif total < target: left += 1 else : right -= 1 return result
字符串
344. 反转字符串
采用双指针方法
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution (object ): def reverseString (self, s ): """ :type s: List[str] :rtype: None Do not return anything, modify s in-place instead. """ left = 0 right = len (s) - 1 while left < right: s[left], s[right] = s[right], s[left] left += 1 right -= 1
541. 反转字符串Ⅱ
在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def reverseStr (self, s, k ): """ :type s: str :type k: int :rtype: str """ def reverse_substring (text ): left = 0 right = len (text) - 1 while left < right: text[left], text[right] = text[right], text[left] left += 1 right -= 1 return text result = list (s) for i in range (0 , len (s), 2 * k): result[i: i + k] = reverse_substring(result[i: i + k]) return '' .join(result)
151. 反转字符串中的单词
采用双指针方法
解题思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def reverseWords (self, s ): """ :type s: str :rtype: str """ words = s.split() left, right = 0 , len (words) - 1 while left < right: words[left], words[right] = words[right], words[left] left += 1 right -= 1 return ' ' .join(words)
栈与队列
232. 用栈实现队列
需要两个栈,一个输入栈,一个输出栈
void push(int x)
将元素 x 推到队列的末尾
int pop()
从队列的开头移除并返回元素
int peek()
返回队列开头的元素
boolean empty()
如果队列为空,返回 true
;否则,返回 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class MyQueue (object ): def __init__ (self ): self.stack_in = [] self.stack_out = [] def push (self, x ): """ :type x: int :rtype: None """ self.stack_in.append(x) def pop (self ): """ :rtype: int """ if self.empty(): return None if self.stack_out: return self.stack_out.pop() else : for i in range (len (self.stack_in)): self.stack_out.append(self.stack_in.pop()) return self.stack_out.pop() def peek (self ): """ :rtype: int """ ans = self.pop() self.stack_out.append(ans) return ans def empty (self ): """ :rtype: bool """ return not (self.stack_in or self.stack_out)
225. 用队列实现栈
队列模拟栈,一个队列就够了。
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class MyStack (object ): def __init__ (self ): self.queue = deque() def push (self, x ): """ :type x: int :rtype: None """ self.queue.append(x) def pop (self ): """ :rtype: int """ if self.empty(): return None for i in range (len (self.queue) - 1 ): self.queue.append(self.queue.popleft()) return self.queue.popleft() def top (self ): """ :rtype: int """ if self.empty(): return None for i in range (len (self.queue) - 1 ): self.queue.append(self.queue.popleft()) ans = self.queue.popleft() self.queue.append(ans) return ans def empty (self ): """ :rtype: bool """ return not self.queue
20. 有效的括号
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以 return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以 return false
第三章情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
当字符串全部遍历完之后,栈是空的,说明全部匹配了。
还有一些技巧,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈实现要简单很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def isValid (self, s ): """ :type s: str :rtype: bool """ stack = [] for item in s: if item == '(' : stack.append(')' ) elif item == '[' : stack.append(']' ) elif item == '{' : stack.append('}' ) elif not stack or stack[-1 ] != item: return False else : stack.pop() return True if not stack else False
1047. 删除字符串中的所有相邻重复项
使用栈来解决
在删除相邻重复项的时候,应该知道当前遍历的这个元素,在前一位是不是遍历过一样数值的元素。
栈的目的就是存放遍历过的元素,当遍历当前的这个元素时,去栈里看看是不是遍历过相同数值的相邻元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def removeDuplicates (self, s ): """ :type s: str :rtype: str """ res = list () for item in s: if res and res[-1 ] == item: res.pop() else : res.append(item) return "" .join(res)
150. 逆波兰表达式求值
栈与递归之间在某种程度上是可以相互转换的。
逆波兰表达式相当于是二叉树中的后序遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def evalRPN (self, tokens ): """ :type tokens: List[str] :rtype: int """ stack = [] for token in tokens: try : stack.append(int (token)) except : num2 = stack.pop() num1 = stack.pop() stack.append(self.calculate(num1, num2, token)) return stack[0 ] def calculate (self, num1, num2, op ): if op == "+" : return num1 + num2 elif op == "-" : return num1 - num2 elif op == "*" : return num1 * num2 elif op == "/" : return int (num1 / float (num2))
239. 滑动窗口最大值
使用单调队列
队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
设计单调队列的时候,pop 和 push 操作要保持如下规则:
pop(value):如果窗口移除的元素 value 等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作。
push(value):如果 push 的元素 value 大于入口元素的数值,那么就将队列入口的元素弹出,直到 push 元素的数值小于等于队列入口元素的数值为止。
本题采用双端队列实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def maxSlidingWindow (self, nums, k ): """ :type nums: List[int] :type k: int :rtype: List[int] """ res = [] queue = collections.deque() for i, num in enumerate (nums): if queue and queue[0 ] == i - k: queue.popleft() while queue and nums[queue[-1 ]] < num: queue.pop() queue.append(i) if i >= k - 1 : res.append(nums[queue[0 ]]) return res
347. 前 k个高频元素
要统计元素出现频率
对频率排序
找出前 K 个高频元素
首先统计元素出现的频率,这一类的问题可以使用 map 来进行统计。
然后是对频率进行排序,可以使用一种容器适配器就是优先级队列 。
使用小顶堆,因为要统计最大前 k 个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前 k 个最大元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def topKFrequent (self, nums, k ): """ :type nums: List[int] :type k: int :rtype: List[int] """ dictionary = collections.Counter(nums) heap, res = [],[] for i in dictionary: heapq.heappush(heap, (-dictionary[i], i)) for _ in range (k): res.append(heapq.heappop(heap)[1 ]) return res
二叉树
确定递归函数的参数和返回值 :确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
确定终止条件 :写完了递归算法,运行时经常遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑 :确定每一层递归需要处理的信息。
144. 二叉树的前序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def preorderTraversal (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ res = [] def dfs (node ): if node is None : return res.append(node.val) dfs(node.left) dfs(node.right) dfs(root) return res
145. 二叉树的后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def postorderTraversal (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ res = [] def dfs (node ): if node is None : return dfs(node.left) dfs(node.right) res.append(node.val) dfs(root) return res
94. 二叉树的中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def inorderTraversal (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ res = [] def dfs (node ): if node is None : return dfs(node.left) res.append(node.val) dfs(node.right) dfs(root) return res
144. 前序遍历(迭代遍历)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution (object ): def preorderTraversal (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ if not root: return [] stack = [root] res = [] while stack: node = stack.pop() res.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return res
145. 后序遍历(迭代遍历)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution (object ): def postorderTraversal (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ if not root: return [] stack = [root] res = [] while stack: node = stack.pop() res.append(node.val) if node.left: stack.append(node.left) if node.right: stack.append(node.right) return res[::-1 ]
102. 二叉树的层序遍历
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过应用在二叉树上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def levelOrder (self, root ): """ :type root: Optional[TreeNode] :rtype: List[List[int]] """ if not root: return [] queue = [root] res = [] while queue: level = [] for _ in range (len (queue)): cur = queue.pop(0 ) level.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) res.append(level) return res
226. 翻转二叉树
只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。
使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转两次。
递归法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def invertTree (self, root ): """ :type root: Optional[TreeNode] :rtype: Optional[TreeNode] """ if not root: return root.left, root.right = root.right, root.left self.invertTree(root.left) self.invertTree(root.right) return root
迭代法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def invertTree (self, root ): """ :type root: Optional[TreeNode] :rtype: Optional[TreeNode] """ if not root: return stack = [root] while stack: node = stack.pop(0 ) node.left, node.right = node.right, node.left if node.left: stack.append(node.left) if node.right: stack.append(node.right) return root
101. 对称二叉树
判断对称二叉树要比较的是哪两个节点,不是左右节点。
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的。
将根节点的左子树记做 left,右子树记做 right。比较 left 是否等于 right,不等的话直接返回就可以了。
如果相当,比较 left 的左节点和 right 的右节点,再比较 left 的右节点和 right 的左节点。
递归法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution (object ): def isSymmetric (self, root ): """ :type root: Optional[TreeNode] :rtype: bool """ if not root: return def dfs (left, right ): if not (left or right): return True if not (left and right): return False if left.val != right.val: return False return dfs(left.left, right.right) and dfs(left.right, right.left) return dfs(root.left, root.right)
迭代法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution (object ): def isSymmetric (self, root ): """ :type root: Optional[TreeNode] :rtype: bool """ if not root: return stack = [root.left, root.right] while stack: left = stack.pop(0 ) right = stack.pop(0 ) if not (left or right): continue if not (left and right): return False if left.val != right.val: return False stack.append(left.left) stack.append(right.right) stack.append(left.right) stack.append(right.left) return True
104. 二叉树的最大深度
可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从 0 开始还是从 1 开始)
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从 0 开始还是从 1 开始)
而根节点的高度就是二叉树的最大深度。
递归法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def maxDepth (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ return self.getDepth(root) def getDepth (self, node ): if not node: return 0 leftHeight = self.getDepth(node.left) rightHeight = self.getDepth(node.right) height = 1 + max (leftHeight, rightHeight) return height
迭代法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution (object ): def maxDepth (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ if not root: return 0 depth = 0 queue = [root] while queue: depth += 1 for _ in range (len (queue)): node = queue.pop(0 ) if node.left: queue.append(node.left) if node.right: queue.append(node.right) return depth
111. 二叉树的最小深度
递归法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution (object ): def minDepth (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ return self.getDepth(root) def getDepth (self, node ): if node is None : return 0 leftDepth = self.getDepth(node.left) rightDepth = self.getDepth(node.right) if node.left is None and node.right is not None : return 1 + rightDepth if node.left is not None and node.right is None : return 1 + leftDepth height = 1 + min (leftDepth, rightDepth) return height
迭代法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution (object ): def minDepth (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ if not root: return 0 depth = 0 queue = [root] while queue: depth += 1 for _ in range (len (queue)): node = queue.pop(0 ) if not node.left and not node.right: return depth if node.left: queue.append(node.left) if node.right: queue.append(node.right) return depth
110. 平衡二叉树
概念:
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def isBalanced (self, root ): """ :type root: Optional[TreeNode] :rtype: bool """ if not root: return True return abs (self.height(root.right) - self.height(root.left)) < 2 and self.isBalanced(root.left) and self.isBalanced(root.right) def height (self, node ): if not node: return 0 return 1 + max (self.height(node.right), self.height(node.left))
257. 二叉树的所有路径
要求从根节点到叶子的路径,所以需要前序遍历,方便让父节点指向孩子节点,找到对应的路径。
这道题目涉及到回溯,因为要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution (object ): def binaryTreePaths (self, root ): """ :type root: Optional[TreeNode] :rtype: List[str] """ res = [] path = [] if not root: return res self.traversal(root, path, res) return res def traversal (self, temp, path, res ): path.append(temp.val) if not temp.left and not temp.right: sPath = '->' .join(map (str , path)) res.append(sPath) return if temp.left: self.traversal(temp.left, path, res) path.pop() if temp.right: self.traversal(temp.right, path, res) path.pop()
404. 左叶子之和
首先要注意是判断左叶子,不是二叉树左侧节点。
左叶子的明确定义:节点 A 的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么 A 节点的左孩子为左叶子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def sumOfLeftLeaves (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ if root is None : return 0 if root.left is None and root.right is None : return 0 leftValue = self.sumOfLeftLeaves(root.left) if root.left and not root.left.left and not root.left.right: leftValue = root.left.val rightValue = self.sumOfLeftLeaves(root.right) sumValue = leftValue + rightValue return sumValue
222. 完全二叉树的节点个数
在完全二叉树中,除了最底层节点可能没填满,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~2^(h-1) 个节点。
递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def countNodes (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ return self.getNodesNum(root) def getNodesNum (self, temp ): if not temp: return 0 leftNum = self.getNodesNum(temp.left) rightNum = self.getNodesNum(temp.right) treeNum = leftNum + rightNum + 1 return treeNum
完全二叉树写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def countNodes (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ if not root: return 0 left = root.left right = root.right leftDepth = 0 rightDepth = 0 while left: left = left.left leftDepth += 1 while right: right = right.right rightDepth += 1 if leftDepth == rightDepth: return (2 << leftDepth) - 1 return self.countNodes(root.left) + self.countNodes(root.right) + 1
513. 找树左下角的值
采用递归方法实现:
确定递归函数的参数和返回值
确定终止条件
当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度
确定单层递归的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def findBottomLeftValue (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ if not root: return 0 queue = deque() queue.append(root) res = 0 while queue: size = len (queue) for i in range (size): node = queue.popleft() if i == 0 : res = node.val if node.left: queue.append(node.left) if node.right: queue.append(node.right) return res
112. 路径总和
可以使用深度优先遍历的方式来遍历二叉树
递归函数什么时候需要返回值?
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径就要及时返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution (object ): def hasPathSum (self, root, targetSum ): """ :type root: Optional[TreeNode] :type targetSum: int :rtype: bool """ if not root: return False if not root.left and not root.right and root.val == targetSum: return True leftPath = self.hasPathSum(root.left, targetSum - root.val) rightPath = self.hasPathSum(root.right, targetSum - root.val) return leftPath or rightPath
106. 从中序与后序遍历序列构造二叉树
一层一层切割,应该想到递归。
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组,切成后序左数组和后序右数组
第六步:递归处理左区间和右区间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution (object ): def buildTree (self, inorder, postorder ): """ :type inorder: List[int] :type postorder: List[int] :rtype: Optional[TreeNode] """ if not postorder: return None root_val = postorder[-1 ] root = TreeNode(root_val) separator_idx = inorder.index(root_val) inorder_left = inorder[:separator_idx] inorder_right = inorder[separator_idx + 1 :] postorder_left = postorder[:len (inorder_left)] postorder_right = postorder[len (inorder_left): -1 ] root.left = self.buildTree(inorder_left, postorder_left) root.right = self.buildTree(inorder_right, postorder_right) return root
654. 最大二叉树
以此数组构建的最大二叉树定义如下:
二叉树的根是数组中的最大元素
左子树是通过数组中最大值左边部分构造出的最大二叉树
右子树是通过数组中最大值右边部分构造出的最大二叉树
构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution (object ): def constructMaximumBinaryTree (self, nums ): """ :type nums: List[int] :rtype: Optional[TreeNode] """ if len (nums) == 1 : return TreeNode(nums[0 ]) node = TreeNode(0 ) maxValue = 0 maxValueIndex = 0 for i in range (len (nums)): if nums[i] > maxValue: maxValue = nums[i] maxValueIndex = i node.val = maxValue if maxValueIndex > 0 : new_list = nums[:maxValueIndex] node.left = self.constructMaximumBinaryTree(new_list) if maxValueIndex < len (nums) - 1 : new_list = nums[maxValueIndex + 1 :] node.right = self.constructMaximumBinaryTree(new_list) return node
617. 合并二叉树
和遍历一个树逻辑一样,只不过传入两个树的节点,同时操作。
迭代法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution (object ): def mergeTrees (self, root1, root2 ): """ :type root1: Optional[TreeNode] :type root2: Optional[TreeNode] :rtype: Optional[TreeNode] """ if not root1: return root2 if not root2: return root1 queue = deque() queue.append(root1) queue.append(root2) while queue: node1 = queue.popleft() node2 = queue.popleft() if node1.left and node2.left: queue.append(node1.left) queue.append(node2.left) if node1.right and node2.right: queue.append(node1.right) queue.append(node2.right) node1.val += node2.val if not node1.left and node2.left: node1.left = node2.left if not node1.right and node2.right: node1.right = node2.right return root1
700. 二叉搜索树中的搜索
二叉搜索树是一个有序树:
若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值
若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
它的左、右子树也分别为二叉搜索树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def searchBST (self, root, val ): """ :type root: Optional[TreeNode] :type val: int :rtype: Optional[TreeNode] """ while root: if val < root.val: root = root.left elif val > root.val: root = root.right else : return root return None
98. 验证二叉搜索树
中序遍历下,输出的二叉搜索树节点的数值是有序序列。
迭代法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution (object ): def isValidBST (self, root ): """ :type root: Optional[TreeNode] :rtype: bool """ stack = [] cur = root pre = None while cur is not None or len (stack) > 0 : if cur is not None : stack.append(cur) cur = cur.left else : cur = stack.pop() if pre is not None and cur.val <= pre.val: return False pre = cur cur = cur.right return True
530. 二叉搜索树的最小绝对差
二叉搜索树是有序的。
想象成在一个有序数组上求最值,求差值。
getMinimumDifference
方法首先用中序遍历将 BST 的节点值存入 res
列表,这样列表 res
中的值是有序的。
然后计算 res
中相邻元素的最小绝对差,并返回该最小值。
inorderTraversal
方法使用递归实现中序遍历,保证节点值按升序排列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution (object ): def getMinimumDifference (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ res = [] minRes = float ('inf' ) self.inorderTraversal(root, res) for i in range (len (res) - 1 ): minRes = min (abs (res[i] - res[i + 1 ]), minRes) return minRes def inorderTraversal (self, root, res ): if root == None : return self.inorderTraversal(root.left, res) res.append(root.val) self.inorderTraversal(root.right, res)
501. 二叉搜索树中的众数
使用迭代法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Solution (object ): def findMode (self, root ): """ :type root: Optional[TreeNode] :rtype: List[int] """ stack = [] pre = None count = 0 maxCount = 0 res = [] while root or stack: if root: stack.append(root) root = root.left else : cur = stack.pop() if pre == None : count = 1 elif pre.val == cur.val: count += 1 else : count = 1 if count == maxCount: res.append(cur.val) if count > maxCount: maxCount = count res = [cur.val] pre = cur root = cur.right return res
236. 二叉树的最近公共祖先
首先想到自底向上查找,这样就可以找到公共祖先了。
二叉树回溯的过程就是自底向上。
后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
求最小公共祖先,需要从底向上遍历,二叉树只能通过后序遍历实现从底向上的遍历方式。
在回溯的过程中,必然要遍历整颗二叉树,即使已经找到结果,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的 left 和 right)做逻辑判断。
如果返回值 left 为空,right 不为空返回 right,可以用返回 right 传给上一层结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution (object ): def lowestCommonAncestor (self, root, p, q ): """ :type root: TreeNode :type p: TreeNode :type q: TreeNode :rtype: TreeNode """ if root == p or root == q or root is None : return root left = self.lowestCommonAncestor(root.left, p, q) right = self.lowestCommonAncestor(root.right, p, q) if left is not None and right is not None : return root if left is None and right is not None : return right elif left is not None and right is None : return left else : return None
235. 二叉搜索树的最近公共祖先
可以利用二叉搜索树有序的特点
使用迭代法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def lowestCommonAncestor (self, root, p, q ): """ :type root: TreeNode :type p: TreeNode :type q: TreeNode :rtype: TreeNode """ while root: if root.val > p.val and root.val > q.val: root = root.left elif root.val < p.val and root.val < q.val: root = root.right else : return root return None
701. 二叉搜索树中的插入操作
只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。
采用递归方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def insertIntoBST (self, root, val ): """ :type root: Optional[TreeNode] :type val: int :rtype: Optional[TreeNode] """ if not root: node = TreeNode(val) return node if root.val > val: root.left = self.insertIntoBST(root.left, val) if root.val < val: root.right = self.insertIntoBST(root.right, val) return root
450. 删除二叉搜索树中的节点
搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑。
第一种情况:没找到删除的节点,遍历到空节点直接返回了。
找到删除的节点:
第二种情况:左右孩子都为空(叶子节点),直接删除节点,返回 NULL 为根节点。
第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点。
第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点。
第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution (object ): def deleteNode (self, root, key ): """ :type root: Optional[TreeNode] :type key: int :rtype: Optional[TreeNode] """ if not root: return root if root.val == key: if root.left is None and root.right is None : return None elif root.left is None : return root.right elif root.right is None : return root.left else : cur = root.right while cur.left is not None : cur = cur.left cur.left = root.left return root.right if root.val > key: root.left = self.deleteNode(root.left, key) if root.val < key: root.right = self.deleteNode(root.right, key) return root
669. 修剪二叉树
删除节点时,将删除节点的右孩子直接赋给父节点的左孩子就可以。
使用递归法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def trimBST (self, root, low, high ): """ :type root: Optional[TreeNode] :type low: int :type high: int :rtype: Optional[TreeNode] """ if not root: return None if root.val < low: return self.trimBST(root.right, low, high) if root.val > high: return self.trimBST(root.left, low, high) root.left = self.trimBST(root.left, low, high) root.right = self.trimBST(root.right, low, high) return root
108. 将有序数组转换为二叉搜索树
数组构造二叉树,构成平衡树时自然而然地事情,默认都是从数组中间位置取值作为节点元素。
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
如果数组长度为偶数,中间节点有两个,取哪一个都可以,只不过构成了不同的平衡二叉树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def sortedArrayToBST (self, nums ): """ :type nums: List[int] :rtype: Optional[TreeNode] """ root = self.traversal(nums, 0 , len (nums) - 1 ) return root def traversal (self, nums, left, right ): if left > right: return None mid = left + (right - left) // 2 root = TreeNode(nums[mid]) root.left = self.traversal(nums, left, mid - 1 ) root.right = self.traversal(nums, mid + 1 , right) return root
538. 把二叉搜索树转换为累加树
从树中可以看出累加的顺序是右中左,所以需要反中序遍历这个二叉树,然后顺序累加就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def convertBST (self, root ): """ :type root: Optional[TreeNode] :rtype: Optional[TreeNode] """ self.pre = 0 self.traversal(root) return root def traversal (self, cur ): if cur is None : return self.traversal(cur.right) cur.val += self.pre self.pre = cur.val self.traversal(cur.left)
回溯算法
回溯法也叫做回溯搜索法,是一种搜索方式。
回溯是递归的副产品,只要有递归就会有回溯。
回溯的本质是穷举,穷举所有可能,然后选出想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
回溯法一般可以解决如下几种问题:
组合问题:N 个数里面按一定规则找出 k 个数的集合
切割问题:一个字符按一定规则有几种切割方式
子集问题:一个 N 个数的集合里有多少符合条件的子集
排列问题:N 个数按一定规则全排列,有几种排列方式
棋盘问题:N 皇后,解数独等等
77. 组合
未剪枝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def combine (self, n, k ): """ :type n: int :type k: int :rtype: List[List[int]] """ result = [] self.backtracking(n, k, 1 , [], result) return result def backtracking (self, n, k, startIndex, path, result ): if len (path) == k: result.append(path[:]) return for i in range (startIndex, n + 1 ): path.append(i) self.backtracking(n, k, i + 1 , path, result) path.pop()
剪枝优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def combine (self, n, k ): """ :type n: int :type k: int :rtype: List[List[int]] """ result = [] self.backtracking(n, k, 1 , [], result) return result def backtracking (self, n, k, startIndex, path, result ): if len (path) == k: result.append(path[:]) return for i in range (startIndex, n - (k - len (path)) + 2 ): path.append(i) self.backtracking(n, k, i + 1 , path, result) path.pop()
216. 组合总和Ⅲ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def combinationSum3 (self, k, n ): """ :type k: int :type n: int :rtype: List[List[int]] """ result = [] self.backtracking(n, k, 0 , 1 , [], result) return result def backtracking (self, tragetSum, k, currentSum, startIndex, path, result ): if currentSum > tragetSum: return if len (path) == k: if currentSum == tragetSum: result.append(path[:]) return for i in range (startIndex, 9 - (k - len (path)) + 2 ): currentSum += i path.append(i) self.backtracking(tragetSum, k, currentSum, i + 1 , path, result) currentSum -= i path.pop()
17. 电话号码的字母组合
数字与字母如何映射
两个字母就两个 for 循环,三个字母三个 for 循环,以此类推,无法写出代码
输入 1 * # 按键等异常情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution (object ): def __init__ (self ): self.letterMap = [ "" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" ] self.result = [] self.s = "" def letterCombinations (self, digits ): """ :type digits: str :rtype: List[str] """ if len (digits) == 0 : return self.result self.backtracking(digits, 0 ) return self.result def backtracking (self, digits, index ): if index == len (digits): self.result.append(self.s) return digit = int (digits[index]) letters = self.letterMap[digit] for i in range (len (letters)): self.s += letters[i] self.backtracking(digits, index + 1 ) self.s = self.s[:-1 ]
官方解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution (object ): def letterCombinations (self, digits ): """ :type digits: str :rtype: List[str] """ if not digits: return [] phoneMap = { "2" : "abc" , "3" : "def" , "4" : "ghi" , "5" : "jkl" , "6" : "mno" , "7" : "pqrs" , "8" : "tuv" , "9" : "wxyz" , } def backtrack (index ): if index == len (digits): combinations.append("" .join(combination)) else : digit = digits[index] for letter in phoneMap[digit]: combination.append(letter) backtrack(index + 1 ) combination.pop() combination = list () combinations = list () backtrack(0 ) return combinations
39. 组合总和
注意叶子结点的返回条件,本题没有组合要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过 target,就直接返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def combinationSum (self, candidates, target ): """ :type candidates: List[int] :type target: int :rtype: List[List[int]] """ res = [] self.backtrack(candidates, target, 0 , 0 , [], res) return res def backtrack (self, candidates, target, total, startIndex, path, res ): if total > target: return if total == target: res.append(path[:]) return for i in range (startIndex, len (candidates)): total += candidates[i] path.append(candidates[i]) self.backtrack(candidates, target, total, i, path, res) total -= candidates[i] path.pop()
40. 组合总和Ⅱ
集合(camdidates)中有重复元素,但还不能有重复的组合。
把所有组合求出来,再用 set 或者 map 去重,这样容易超时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def combinationSum2 (self, candidates, target ): """ :type candidates: List[int] :type target: int :rtype: List[List[int]] """ res = [] candidates.sort() self.backtrack(candidates, target, 0 , 0 , [], res) return res def backtrack (self, candidates, target, total, startIndex, path, res ): if total == target: res.append(path[:]) return for i in range (startIndex, len (candidates)): if i > startIndex and candidates[i] == candidates[i - 1 ]: continue if total + candidates[i] > target: break total += candidates[i] path.append(candidates[i]) self.backtrack(candidates, target, total, i + 1 , path, res) total -= candidates[i] path.pop()
131. 分割回文串
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def partition (self, s ): """ :type s: str :rtype: List[List[str]] """ res = [] self.backtrack(s, 0 , [], res) return res def backtrack (self, s, startIndex, path, res ): if startIndex == len (s): res.append(path[:]) return for i in range (startIndex, len (s)): if s[startIndex: i + 1 ] == s[startIndex: i + 1 ][::-1 ]: path.append(s[startIndex:i + 1 ]) self.backtrack(s, i + 1 , path, res) path.pop()
99. 复原 IP 地址
回溯三部曲
判断字串是否合法
主要考虑如下三点:
段位以 0 为开头的数字不合法
段位里有非正整数字符不合法
段位如果大于 255 不合法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution (object ): def restoreIpAddresses (self, s ): """ :type s: str :rtype: List[str] """ res = [] self.backtrack(s, 0 , 0 , "" , res) return res def backtrack (self, s, start_index, point_num, current, res ): if point_num == 3 : if self.is_valid(s, start_index, len (s) - 1 ): current += s[start_index:] res.append(current) return for i in range (start_index, len (s)): if self.is_valid(s, start_index, i): sub = s[start_index:i+1 ] self.backtrack(s, i + 1 , point_num + 1 , current + sub + '.' , res) else : break def is_valid (self, s, start, end ): if start > end: return False if s[start] == '0' and start != end: return False num = 0 for i in range (start, end + 1 ): if not s[i].isdigit(): return False num = num * 10 + int (s[i]) if num > 255 : return False return True
78. 子集
如果把子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点 。
子集也是一种组合问题,因为它的集合是无序的,子集 {1,2} 和子集 {2,1} 是一样的。
既然是无序,取过的元素不会重复取,写回溯算法的时候,for 就要从 startIndex 开始,而不是从 0 开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def subsets (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ res = [] path = [] self.backtrack(nums, 0 , path, res) return res def backtrack (self, nums, startIndex, path, res ): res.append(path[:]) if startIndex >= len (nums): return for i in range (startIndex, len (nums)): path.append(nums[i]) self.backtrack(nums, i + 1 , path, res) path.pop()
90. 子集Ⅱ
利用集合去重:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def subsetsWithDup (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ res = [] path = [] nums.sort() self.backtrack(nums, 0 , path, res) return res def backtrack (self, nums, startIndex, path, res ): res.append(path[:]) uset = set () for i in range (startIndex, len (nums)): if nums[i] in uset: continue uset.add(nums[i]) path.append(nums[i]) self.backtrack(nums, i + 1 , path, res) path.pop()
491. 递增子序列
回溯(利用 set 去重)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def findSubsequences (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ res = [] path = [] self.backtrack(nums, 0 , path, res) return res def backtrack (self, nums, startIndex, path, res ): if len (path) > 1 : res.append(path[:]) uset = set () for i in range (startIndex, len (nums)): if (path and nums[i] < path[-1 ]) or nums[i] in uset: continue uset.add(nums[i]) path.append(nums[i]) self.backtrack(nums, i + 1 , path, res) path.pop()
46. 全排列
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合有不同之处。
使用 used
布尔列表,表示每个元素是否已被使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def permute (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ res = [] path = [] self.backtrack(nums, path, [False ] * len (nums), res) return res def backtrack (self, nums, path, used, res ): if len (path) == len (nums): res.append(path[:]) return for i in range (len (nums)): if used[i]: continue used[i] = True path.append(nums[i]) self.backtrack(nums, path, used, res) path.pop() used[i] = False
47. 全排列Ⅱ
和上一题的区别在于给定一个可包含重复数字的序列,要返回所有不重复的全排列 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution (object ): def permuteUnique (self, nums ): """ :type nums: List[int] :rtype: List[List[int]] """ nums.sort() res = [] path = [] self.backtrack(nums, path, [False ] * len (nums), res) return res def backtrack (self, nums, path, used, res ): if len (path) == len (nums): res.append(path[:]) return for i in range (len (nums)): if (i > 0 and nums[i] == nums[i - 1 ] and not used[i - 1 ]) or used[i]: continue used[i] = True path.append(nums[i]) self.backtrack(nums, path, used, res) path.pop() used[i] = False
51. N 皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n x n 的棋盘上,并且使皇后彼此之间不能相互攻击。
皇后们的约束条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Solution (object ): def solveNQueens (self, n ): """ :type n: int :rtype: List[List[str]] """ res = [] chessboard = ['.' * n for _ in range (n)] self.backtrack(n, 0 , chessboard, res) return [['' .join(row) for row in solution] for solution in res] def backtrack (self, n, row, chessboard, res ): if row == n: res.append(chessboard[:]) return for col in range (n): if self.isValid(row, col, chessboard): chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1 :] self.backtrack(n, row + 1 , chessboard, res) chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1 :] def isValid (self, row, col, chessboard ): for i in range (row): if chessboard[i][col] == 'Q' : return False i, j = row - 1 , col - 1 while i >= 0 and j >= 0 : if chessboard[i][j] == 'Q' : return False i -= 1 j -= 1 i, j = row - 1 , col + 1 while i >= 0 and j < len (chessboard): if chessboard[i][j] == 'Q' : return False i -= 1 j += 1 return True
37. 解数独
判断棋盘是否合法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution (object ): def solveSudoku (self, board ): """ :type board: List[List[str]] :rtype: None Do not return anything, modify board in-place instead. """ self.backtrack(board) def backtrack (self, board ): for i in range (len (board)): for j in range (len (board[0 ])): if board[i][j] != '.' : continue for k in range (1 , 10 ): if self.isValid(i, j, k, board): board[i][j] = str (k) if self.backtrack(board): return True board[i][j] = '.' return False return True def isValid (self, row, col, val, board ): for i in range (9 ): if board[row][i] == str (val): return False for j in range (9 ): if board[j][col] == str (val): return False start_row = (row // 3 ) * 3 start_col = (col // 3 ) * 3 for i in range (start_row, start_row + 3 ): for j in range (start_col, start_col + 3 ): if board[i][j] == str (val): return False return True
贪心算法
一般分为如下四步:
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解
455. 分发饼干
为了满足更多的小孩,就不要造成饼干尺寸的浪费。
这里的局部最优解就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def findContentChildren (self, g, s ): """ :type g: List[int] :type s: List[int] :rtype: int """ g.sort() s.sort() index = len (s) - 1 res = 0 for i in range (len (g) - 1 , -1 , -1 ): if index >= 0 and s[index] >= g[i]: res += 1 index -= 1 return res
376. 摆动序列
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
实际操作上,其实连删除操作都不用做,题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)。
这就是使用贪心的地方,让峰值尽可能地保持峰值,然后删除单一坡度上的节点。
需要考虑三种情况:
上下坡中有平坡
数组首尾两端
单调坡中有平坡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution (object ): def wiggleMaxLength (self, nums ): """ :type nums: List[int] :rtype: int """ if len (nums) <= 1 : return len (nums) curDiff = 0 preDiff = 0 res = 1 for i in range (len (nums) - 1 ): curDiff = nums[i + 1 ] - nums[i] if (preDiff <= 0 and curDiff > 0 ) or (preDiff >= 0 and curDiff < 0 ): res += 1 preDiff = curDiff return res
53. 最大子序和
局部最优:当前 “连续和” 为负数的时候立刻放弃,从下一个元素重新计算 “连续和”,因为负数加上下一个元素 “连续和” 只会越来越小。
全局最优:选取最大 “连续和”。
局部最优的情况下,并记录最大的 “连续和”,可以推出全局最优。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def maxSubArray (self, nums ): """ :type nums: List[int] :rtype: int """ res = float ("-inf" ) count = 0 for i in range (len (nums)): count += nums[i] if count > res: res = count if count <= 0 : count = 0 return res
122. 买卖股票的最佳时机Ⅱ
首先要清楚两点:
想获得利润至少要两天为一个交易单元。
只需要收集每天的正利润就可以,收集正利润的区间,就是股票买卖的区间,而只需要关注最终利润,不需要记录区间。
局部最优:收集每天的正利润,全局最优:求的最大利润。
1 2 3 4 5 6 7 8 9 10 class Solution (object ): def maxProfit (self, prices ): """ :type prices: List[int] :rtype: int """ res = 0 for i in range (1 , len (prices)): res += max (prices[i] - prices[i - 1 ], 0 ) return res
55. 跳跃游戏
局部最优:每次取最大跳跃步数(取最大覆盖范围)。
整体最优:最后得到整体最大覆盖范围,看是否能到终点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def canJump (self, nums ): """ :type nums: List[int] :rtype: bool """ cover = 0 if len (nums) == 1 : return True for i in range (len (nums)): if i <= cover: cover = max (i + nums[i], cover) if cover >= len (nums) - 1 : return True return False
45. 跳跃游戏Ⅱ
局部最优:当前可移动距离尽可能多走,如果还没到终点,步数加一。
整体最优:一步尽可能多走,从而达到最少步数。
要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def jump (self, nums ): """ :type nums: List[int] :rtype: int """ cur_dis = 0 res = 0 next_dis = 0 for i in range (len (nums) - 1 ): next_dis = max (i + nums[i], next_dis) if i == cur_dis: cur_dis = next_dis res += 1 return res
1005. K 次取反后最大化的数组和
局部最优:让绝对值大的负数变为正数,当前数值达到最大。
整体最优:整个数组和达到最大。
将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小 。
从前向后遍历,遇到负数将其变为正数,同时 K–。
如果 K 还大于 0,那么反复转变数值最小的元素,将 K 用完。
求和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def largestSumAfterKNegations (self, nums, k ): """ :type nums: List[int] :type k: int :rtype: int """ nums.sort(key=lambda x: abs (x), reverse=True ) for i in range (len (nums)): if nums[i] < 0 and k > 0 : nums[i] *= -1 k -= 1 if k % 2 == 1 : nums[-1 ] *= -1 res = sum (nums) return res
134. 加油站
如果总油量减去总消耗大于等于零,那么一定可以跑完一圈,说明各个站点的加油站剩油量 rest[i] 相加一定是大于等于零的。
每个加油站的剩余量 res[i] 为 gas[i] - cost[i]。
局部最优:当前累加 rest[i] 的和 curSum 一旦小于 0,起始位置至少是 i + 1,因为之前从 i 之前开始一定不行。
全局最优:找到可以跑一圈的起始位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def canCompleteCircuit (self, gas, cost ): """ :type gas: List[int] :type cost: List[int] :rtype: int """ curSum = 0 totalSum = 0 start = 0 for i in range (len (gas)): curSum += gas[i] - cost[i] totalSum += gas[i] - cost[i] if curSum < 0 : start = i + 1 curSum = 0 if totalSum < 0 : return -1 return start
135. 分发糖果
一定要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。
先确定右边评分大于左边的情况(也就是从前向后遍历)
局部最优:只要右边评分比左边大,右边的孩子就多一颗糖果。
全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def candy (self, ratings ): """ :type ratings: List[int] :rtype: int """ candy = [1 ] * len (ratings) for i in range (1 , len (ratings)): if ratings[i] > ratings[i - 1 ]: candy[i] = candy[i - 1 ] + 1 for i in range (len (ratings) - 2 , -1 , -1 ): if ratings[i] > ratings[i + 1 ]: candy[i] = max (candy[i], candy[i + 1 ] + 1 ) res = sum (candy) return res
860. 柠檬水找零
只需要维护三种金额的数量,5、10 和 20。
有如下三种情况:
账单是 5,直接收下。
账单是 10,消耗一个 5,增加一个 10。
账单是 20,优先消耗一个 10 和一个 5,如果不够,再消耗三个 5。
局部最优:遇到账单 20,优先消耗美元 10,完成本次找零。
全局最优:完成全部账单的找零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution (object ): def lemonadeChange (self, bills ): """ :type bills: List[int] :rtype: bool """ five = 0 ten = 0 twenty = 0 for bill in bills: if bill == 5 : five += 1 if bill == 10 : if five <= 0 : return False ten += 1 five -= 1 if bill == 20 : if five > 0 and ten > 0 : five -= 1 ten -= 1 elif five >= 3 : five -= 3 else : return False return True
406. 根据身高重建队列
如果按照 k 来从小到大排序,排完之后会发现 k 的排列并不符合条件,身高也不符合条件,两个维度都没确定。
按照身高 h 来排序,身高一定是从大到小排(身高相同的话则 k 小的站前面),让高个子在前面。
此时可以确定一个维度了,就是身高,前面的节点一定都比本节点高!
局部最优:优先按身高高的 people 的 k 来插入,插入操作过后的 people 满足队列属性。
全局最优:最后都做完插入操作,整个队列满足题目队列属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def reconstructQueue (self, people ): """ :type people: List[List[int]] :rtype: List[List[int]] """ people.sort(key=lambda x: (-x[0 ], x[1 ])) queue = [] for p in people: queue.insert(p[1 ], p) return queue
452. 用最小数量的箭引爆气球
局部最优:当气球出现重叠,一起射,所用弓箭最少。
全局最优:把所有气球射爆所用弓箭最少。
为了让气球尽可能重叠,需要对数组进行排序。
按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。
如果气球重叠了,重叠气球中右边边界的最小值之间的区间一定需要一个弓箭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def findMinArrowShots (self, points ): """ :type points: List[List[int]] :rtype: int """ if len (points) == 0 : return 0 points.sort(key = lambda x: x[0 ]) cur_min_right = points[0 ][1 ] count = 1 for i in points: if i[0 ] > cur_min_right: count += 1 cur_min_right = i[1 ] else : cur_min_right = min (cur_min_right, i[1 ]) return count
435. 无重叠区间
按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def eraseOverlapIntervals (self, intervals ): """ :type intervals: List[List[int]] :rtype: int """ if not intervals: return 0 intervals.sort(key = lambda x: x[1 ]) result = 1 for i in range (1 , len (intervals)): if intervals[i][0 ] >= intervals[0 ][1 ]: result += 1 intervals[0 ][1 ] = intervals[i][1 ] return len (intervals) - result
763. 划分字母区间
在遍历的过程中相当于是要找每一个字母的边界,**如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。**此时前面出现过所有字母,最远也就到这个边界了。
统计每一个字符最后出现的位置。
从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def partitionLabels (self, s ): """ :type s: str :rtype: List[int] """ last_occur = {} for i, char in enumerate (s): last_occur[char] = i res = [] start = 0 end = 0 for i, char in enumerate (s): end = max (end, last_occur[char]) if i == end: res.append(end - start + 1 ) start = i + 1 return res
56. 合并区间
先排序,让所有的相邻区间尽可能的重叠在一起,按左边界或者右边界排序都可以,处理逻辑稍有不同。
模拟合并区间,用合并区间后左边界和右边界,作为一个新的区间,加入到 result 数组里就可以了。如果没有合并就把原区间加入到 result 数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def merge (self, intervals ): """ :type intervals: List[List[int]] :rtype: List[List[int]] """ res = [] if not intervals: return res intervals.sort(key = lambda x: x[0 ]) res.append(intervals[0 ]) for i in range (1 , len (intervals)): if res[-1 ][1 ] >= intervals[i][0 ]: res[-1 ][1 ] = max (res[-1 ][1 ], intervals[i][1 ]) else : res.append(intervals[i]) return res
738. 单调递增的数字
举例:数字 332 从前向后遍历的话,变成 329,此时 2 又小于了第一位的 3,真正的结果应该是 299。
那么从后向前遍历,就可以重复利用上次比较得出的结果,从后向前遍历 332 的数值变化为:332 -> 329 -> 299。
采用从后向前遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def monotoneIncreasingDigits (self, n ): """ :type n: int :rtype: int """ strNum = list (str (n)) for i in range (len (strNum) - 1 , 0 , -1 ): if strNum[i - 1 ] > strNum[i]: strNum[i - 1 ] = str (int (strNum[i - 1 ]) - 1 ) for j in range (i, len (strNum)): strNum[j] = '9' return int ('' .join(strNum))
986. 监控二叉树
摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费了一层的覆盖。
所以把摄像头放在叶子节点的父节点的位置,才能充分利用摄像头的覆盖面积。
局部最优 :让叶子节点的父节点安摄像头,所用摄像头最少。
全局最优 :全部摄像头数量所用最少。
可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。
每个节点可能有几种状态:
情况 1:左右节点都有覆盖
左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该是无覆盖的状态。
情况 2:左右节点至少有一个无覆盖的情况
如果是以下情况,则中间节点(父节点)应该放摄像头:
left == 0 && right == 0 左右节点无覆盖
left == 1 && right == 0 左节点有摄像头,右节点无覆盖
left == 0 && right == 1 左节点无覆盖,右节点摄像头
left == 0 && right == 2 左节点无覆盖,右节点覆盖
left == 2 && right == 0 左节点覆盖,右节点无覆盖
情况 3:左右节点至少有一个摄像头
如果是以下情况,其实就是左右孩子节点有一个摄像头了,那么其父节点就应该是 2(覆盖的状态)
left == 1 && right == 2 左节点有摄像头,右节点有覆盖
left == 2 && right == 1 左节点有覆盖,右节点有摄像头
left == 1 && right == 1 左右节点都有摄像头
情况 4:头结点没有覆盖
以上都处理完了,递归结束之后,可能头结点还有一个无覆盖的情况。
贪心 + 二叉树代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution (object ): def minCameraCover (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ res = [0 ] if self.traversal(root, res) == 0 : res[0 ] += 1 return res[0 ] def traversal (self, cur, res ): if not cur: return 2 left = self.traversal(cur.left, res) right = self.traversal(cur.right, res) if left == 2 and right == 2 : return 0 if left == 0 or right == 0 : res[0 ] += 1 return 1 if left == 1 or right == 1 : return 2
动态规划
如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点区分于贪心 ,贪心没有状态推导,而是从局部直接选取最优的。
动态规划五部曲:
确定 dp 数组(dp table)以及下标的含义
确定递推公式
dp 数组如何初始化
确定遍历顺序
举例推导 dp 数组
灵魂三问?
举例推导状态转移公式了吗?
打印 dp 数组的日志了吗?
打印出来的 dp 数组和我想的一样吗?
509. 斐波那契数
确定 dp 数组以及下标的含义
dp[i] 的定义为:第 i 个数的斐波那契数值是 dp[i]
确定递推公式
状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
dp 数组如何初始化
dp[0] = 0, dp[1] = 1
确定遍历顺序
dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
举例推导 dp 数组
当 N 为 10 的时候,dp 数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def fib (self, n ): """ :type n: int :rtype: int """ if n == 0 : return 0 dp = [0 ] * (n + 1 ) dp[0 ] = 0 dp[1 ] = 1 for i in range (2 , n + 1 ): dp[i] = dp[i - 1 ] + dp[i - 2 ] return dp[n]
70. 爬楼梯
确定 dp 数组以及下标的含义
dp[i]:爬到第 i 层楼梯,有 dp[i] 种方法
确定递推公式
状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
dp 数组如何初始化
dp[1] = 1, dp[2] = 2
确定遍历顺序
dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
举例推导 dp 数组
当 N 为 5 的时候,dp 数组应该是如下的数列:
1 2 3 5 8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def climbStairs (self, n ): """ :type n: int :rtype: int """ if n <= 1 : return n dp = [0 ] * (n + 1 ) dp[1 ] = 1 dp[2 ] = 2 for i in range (3 , n + 1 ): dp[i] = dp[i - 1 ] + dp[i - 2 ] return dp[n]
746. 使用最小花费爬楼梯
确定 dp 数组以及下标的含义
dp[i] 的定义:到达第 i 台阶所花费的最少体力为 dp[i]
确定递推公式
状态转移方程 dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
dp 数组如何初始化
dp[0] = 0, dp[1] = 0
确定遍历顺序
dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
举例推导 dp 数组
cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1],dp 数组应该是如下的数列:
0 0 1 2 2 3 3 4 4 5 6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def minCostClimbingStairs (self, cost ): """ :type cost: List[int] :rtype: int """ dp = [0 ] * (len (cost) + 1 ) dp[0 ] = 0 dp[1 ] = 0 for i in range (2 , len (cost) + 1 ): dp[i] = min (dp[i - 1 ] + cost[i - 1 ], dp[i - 2 ] + cost[i - 2 ]) return dp[len (cost)]
62. 不同路径
最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。机器人每次只能向下或向右移动一步,其实机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点。
动态规划实现
确定 dp 数组以及下标的含义
dp[i][j]
:表示从(0,0)出发,到(i, j)有 dp[i][j]
条不同的路径。
确定递推公式
状态转移方程 dp[i][j]
= dp[i - 1][j]
+ dp[i][j - 1]
,因为 dp[i][j]
只有这两个方向过来。
dp 数组如何初始化
dp[i][0]
= 1, dp[0][j]
= 1
确定遍历顺序
dp[i][j]
都是从其上方和左方推导而来,那么从左到右一层层遍历即可。
举例推导 dp 数组
1
1
1
1
1
1
1
1
2
3
4
5
6
7
1
3
6
10
15
21
28
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def uniquePaths (self, m, n ): """ :type m: int :type n: int :rtype: int """ dp = [[0 ] * n for _ in range (m)] for i in range (m): dp[i][0 ] = 1 for j in range (n): dp[0 ][j] = 1 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[m - 1 ][n - 1 ]
63. 不同路径Ⅱ
确定 dp 数组以及下标的含义
dp[i][j]
:表示从(0,0)出发,到(i, j)有 dp[i][j]
条不同的路径。
确定递推公式
状态转移方程 dp[i][j]
= dp[i - 1][j]
+ dp[i][j - 1]
,但因为有了障碍,(i, j)如果就是障碍的话应该保持初始状态(初始状态为 0)。
dp 数组如何初始化
dp[i][0]
= 1, dp[0][j]
= 1
确定遍历顺序
dp[i][j]
都是从其上方和左方推导而来,那么从左到右一层层遍历即可。
举例推导 dp 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution (object ): def uniquePathsWithObstacles (self, obstacleGrid ): """ :type obstacleGrid: List[List[int]] :rtype: int """ m = len (obstacleGrid) n = len (obstacleGrid[0 ]) if obstacleGrid[m - 1 ][n - 1 ] == 1 or obstacleGrid[0 ][0 ] == 1 : return 0 dp = [[0 ] * n for _ in range (m)] dp[0 ][0 ] = 1 if obstacleGrid[0 ][0 ] == 0 else 0 for i in range (1 , m): if obstacleGrid[i][0 ] == 0 : dp[i][0 ] = dp[i - 1 ][0 ] for j in range (1 , n): if obstacleGrid[0 ][j] == 0 : dp[0 ][j] = dp[0 ][j - 1 ] for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i][j] == 1 : continue dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m - 1 ][n - 1 ]
416. 分割等和子集
首先,要求集合里能否出现总和为 sum / 2 的子集。
动规五部曲:
确定 dp 数组以及下标的含义
dp[j] 表示:容量(所能装的重量)为 j 的背包,所背的物品价值最大可以为 dp[j]。
确定递推公式
递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
dp 数组如何初始化
dp[0] = 0
题目中只包含正整数的非空数组,所以非 0 下标的元素初始化为 0 即可。
确定遍历顺序
如果使用一维 dp 数组,物品遍历的 for 循环放在外层,遍历背包的 for 循环放在内层,且内层 for 循环倒序遍历。
举例推导 dp 数组
dp[j] 的数值一定是小于等于 j 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution (object ): def canPartition (self, nums ): """ :type nums: List[int] :rtype: bool """ total = 0 dp = [0 ] * 10001 for num in nums: total += num if total % 2 == 1 : return False target = total // 2 for num in nums: for j in range (target, num - 1 , -1 ): dp[j] = max (dp[j], dp[j - num] + num) if dp[target] == target: return True return False
1049. 最后一块石头的重量Ⅱ
尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。
一堆的石头重量是 sum,尽可能拼成重量为 sum / 2 的石头堆。
动规五部曲:
确定 dp 数组以及下标的含义
dp[j] 表示容量(其实就是重量)为 j 的背包,最多可以背最大重量为 dp[j]。
确定递推公式
递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
dp 数组如何初始化
dp[j] = 0
确定遍历顺序
如果使用一维 dp 数组,物品遍历的 for 循环放在外层,遍历背包的 for 循环放在内层,且内层 for 循环倒序遍历。
举例推导 dp 数组
输入:[2, 4, 1, 1],此时 target = (2 + 4 + 1 + 1) / 2 = 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def lastStoneWeightII (self, stones ): """ :type stones: List[int] :rtype: int """ dp = [0 ] * 15001 total_sum = sum (stones) target = total_sum // 2 for stone in stones: for j in range (target, stone - 1 , -1 ): dp[j] = max (dp[j], dp[j - stone] + stone) return total_sum - dp[target] - dp[target]
494. 目标和
如何使表达式结果为 target?
既然为 target,那么一定有 left 组合 - right 组合 = target。
left + right = sum,而 sum 使固定的。right = sum - left。
left - (sum - left) = target 推导出 left = (target + sum) / 2。
target 是固定的,sum 是固定的,left 就可以求出来。
此时问题就是在集合 nums 中找出和为 left 的组合。
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][j]
:使用下标为 [0, i] 的 nums[i] 能够凑满 j(包括 j)这么大容量的包,有 dp[i][j]
种方法。
确定递推公式
不放物品 i:即背包容量为 j,里面不放物品 i,装满有 dp[i - 1][j]
种方法。
放物品 i:即空出物品 i 的容量,背包容量为(j - 物品 i 容量),放满背包有 dp[i - 1][j - 物品 i 容量]
种方法。
递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
dp 数组如何初始化
dp[i][0]
= 1
确定遍历顺序
从上到下,从左到右。只有这样,才能基于之前的数值做推导。
举例推导 dp 数组
输入:nums: [1, 1, 1, 1, 1],target:3
bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def findTargetSumWays (self, nums, target ): """ :type nums: List[int] :type target: int :rtype: int """ total_sum = sum (nums) if abs (target) > total_sum: return 0 if (target + total_sum) % 2 == 1 : return 0 target_sum = (target + total_sum) // 2 dp = [0 ] * (target_sum + 1 ) dp[0 ] = 1 for num in nums: for j in range (target_sum, num - 1 , -1 ): dp[j] += dp[j - num] return dp[target_sum]
474. 一和零
本题中 strs 数组里的元素就是物品,每个物品都是一个!而 m 和 n 相当于是一个背包,两个维度的背包。
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][j]
:最多有 i 个 0 和 j 个 1 的 strs 的最大子集的大小为 dp[i][j]
。
确定递推公式
不放物品 i:即背包容量为 j,里面不放物品 i,装满有 dp[i - 1][j]
种方法。
放物品 i:即空出物品 i 的容量,背包容量为(j - 物品 i 容量),放满背包有 dp[i - 1][j - 物品 i 容量]
种方法。
递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
dp 数组如何初始化
因为物品价值不会是负数,初始为 0,保证递推的时候 dp[i][j]
不会被初始值覆盖。
确定遍历顺序
外层 for 循环遍历物品,内层 for 循环遍历背包容量且从后向前遍历。
本题也是,物品就是 strs 里的字符串,背包容量就是题目描述中的 m 和 n。
举例推导 dp 数组
输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3
0
1
2
3
0
1
1
1
1
2
2
2
1
2
3
3
1
2
3
3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def findMaxForm (self, strs, m, n ): """ :type strs: List[str] :type m: int :type n: int :rtype: int """ dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for s in strs: zeroNum = s.count('0' ) oneNum = len (s) - zeroNum for i in range (m, zeroNum - 1 , -1 ): for j in range (n, oneNum - 1 , -1 ): dp[i][j] = max (dp[i][j], dp[i - zeroNum][j - oneNum] + 1 ) return dp[m][n]
518. 零钱兑换Ⅱ
确定 dp 数组以及下标的含义
定义二维 dp 数组 dp[i][j]
:使用下标为 [0, i] 的 coins[i] 能够凑满 j 这么大容量的包,有 dp[i][j]
种组合方法。
确定递推公式
递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]
dp 数组如何初始化
dp[i][0]
= 1
确定遍历顺序
二维 DP 数组的完全背包的两个 for 循环先后顺序是无所谓的。
先遍历背包,还是先遍历物品都是可以的。
打印 dp 数组
以 amount 为 5,coins 为:[2,3,5]
dp数组应该是这样的:
1 2 3 1 0 1 0 1 0 1 0 1 1 1 1 1 0 1 1 1 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def change (self, amount, coins ): """ :type amount: int :type coins: List[int] :rtype: int """ dp = [0 ] * (amount + 1 ) dp[0 ] = 1 for i in range (len (coins)): for j in range (coins[i], amount + 1 ): dp[j] += dp[j - coins[i]] return dp[amount]
377. 组合总和Ⅳ
描述说是求组合,但元素相同顺序不同的组合算两个组合,其实就是求排列 。
动规五部曲:
确定 dp 数组以及下标的含义
dp[i]:凑成目标正整数为 i 的排列个数为 dp[i]
确定递推公式
递推公式:dp[i] += dp[i - nums[j]]
dp 数组如何初始化
dp[0] = 1
确定遍历顺序
个数可以不限使用,说明这是一个完全背包。
得到的集合是排列,说明需要考虑元素之间的顺序。
如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包 。
如果求排列数就是外层 for 遍历背包,内层 for 循环遍历物品 。
最终遍历顺序:target(背包)放在外循环,将 nums(物品)放在内循环,内循环从前向后遍历。
举例推导 dp 数组
dp[0] = 1
dp[1] = dp[0] = 1
dp[2] = dp[1] + dp[0] = 2
dp[3] = dp[2] + dp[1] + dp[0] = 4
dp[4] = dp[3] + dp[2] + dp[1] = 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def combinationSum4 (self, nums, target ): """ :type nums: List[int] :type target: int :rtype: int """ dp = [0 ] * (target + 1 ) dp[0 ] = 1 for i in range (1 , target + 1 ): for j in range (len (nums)): if i - nums[j] >= 0 : dp[i] += dp[i - nums[j]] return dp[target]
322. 零钱兑换
动规五部曲:
确定 dp 数组以及下标的含义
dp[j]:凑足金额为 j 所需钱币的最少个数为 dp[j]
确定递推公式
dp[j] = min(dp[j - coins[i]] + 1, dp[j])
dp 数组如何初始化
dp[0] = 0
确定遍历顺序
如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包 。
如果求排列数就是外层 for 遍历背包,内层 for 循环遍历物品 。
本题的两个 for 循环的关系是:外层 for 循环遍历物品,内层 for 遍历背包或者外层 for 遍历背包,内层 for 循环遍历物品都是可以的。
举例推导 dp 数组
以输入:coin = [1, 2, 5] ,amount = 5 为例:
dp[i]:0 1 1 2 2 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution (object ): def coinChange (self, coins, amount ): """ :type coins: List[int] :type amount: int :rtype: int """ dp = [float ('inf' )] * (amount + 1 ) dp[0 ] = 0 for coin in coins: for i in range (coin, amount + 1 ): if dp[i - coin] != float ('inf' ): dp[i] = min (dp[i - coin] + 1 , dp[i]) if dp[amount] == float ('inf' ): return -1 return dp[amount]
279. 完全平方数
动规五部曲:
确定 dp 数组以及下标的含义
dp[j]:和为 j 的完全平方数的最少数量为 dp[j]
确定递推公式
dp[j] = min(dp[j - i * i] + 1, dp[j])
dp 数组如何初始化
dp[0] = 0
确定遍历顺序
如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包 。
如果求排列数就是外层 for 遍历背包,内层 for 循环遍历物品 。
本题的两个 for 循环的关系是:外层 for 循环遍历物品,内层 for 遍历背包或者外层 for 遍历背包,内层 for 循环遍历物品都是可以的。
举例推导 dp 数组
输入 n = 5,
dp[i]:0 1 2 3 1 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def numSquares (self, n ): """ :type n: int :rtype: int """ dp = [float ('inf' )] * (n + 1 ) dp[0 ] = 0 for i in range (1 , n + 1 ): for j in range (1 , int (i ** 0.5 + 1 )): dp[i] = min (dp[i], dp[i - j * j] + 1 ) return dp[n]
139. 单词拆分
单词就是物品,字符串 s 就是背包,单词能否组成字符串 s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包。
动规五部曲:
确定 dp 数组以及下标的含义
dp[i]:字符串长度为 i 的话,dp[i] 为 true,表示可以拆分为一个或多个在字典中出现的单词。
确定递推公式
if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
dp 数组如何初始化
dp[0] = true
确定遍历顺序
先遍历背包,再遍历物品。
举例推导 dp 数组
以输入:s = “leetcode”, wordDict = [“leet”, “code”]为例,dp 状态如图:
dp[i]:1 0 0 0 1 0 0 0 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def wordBreak (self, s, wordDict ): """ :type s: str :type wordDict: List[str] :rtype: bool """ wordSet = set (wordDict) n = len (s) dp = [False ] * (n + 1 ) dp[0 ] = True for i in range (1 , n + 1 ): for j in range (i): if dp[j] and s[j:i] in wordSet: dp[i] = True break return dp[n]
198. 打家劫舍
确定 dp 数组以及下标的含义
dp[i]:考虑下标 i(包括 i)以内的房屋,最多可以偷窃的金额为 dp[i]。
确定递推公式
偷 i:dp[i] = dp[i - 2] + nums[i]
不偷 i:dp[i] = dp[i - 1]
递推公式:dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
dp 数组如何初始化
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
确定遍历顺序
从前向后遍历。
举例推导 dp 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def rob (self, nums ): """ :type nums: List[int] :rtype: int """ if len (nums) == 0 : return 0 if len (nums) == 1 : return nums[0 ] dp = [0 ] * len (nums) dp[0 ] = nums[0 ] dp[1 ] = max (nums[0 ], nums[1 ]) n = len (nums) for i in range (2 , n): dp[i] = max (dp[i - 2 ] + nums[i], dp[i - 1 ]) return dp[-1 ]
213. 打家劫舍Ⅱ
对于一个数组,成环的话主要有如下三种情况:
情况一:考虑不包含首尾元素
情况二:考虑包含首元素,不包含尾元素
情况三:考虑包含尾元素,不包含首元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution (object ): def rob (self, nums ): """ :type nums: List[int] :rtype: int """ if len (nums) == 0 : return 0 if len (nums) == 1 : return nums[0 ] res1 = self.robRange(nums, 0 , len (nums) - 2 ) res2 = self.robRange(nums, 1 , len (nums) - 1 ) return max (res1, res2) def robRange (self, nums, start, end ): if end == start: return nums[start] pre_max = nums[start] cur_max = max (nums[start], nums[start + 1 ]) for i in range (start + 2 , end + 1 ): temp = cur_max cur_max = max (pre_max + nums[i], cur_max) pre_max = temp return cur_max
337. 打家劫舍Ⅲ
对于树的话,首先想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。
本题一定要后序遍历,因为通过递归函数的返回值来做下一步计算 。
如果抢了当前节点,两个孩子就不能动;如果没抢到当前节点,就可以考虑抢左右孩子。
确定 dp 数组以及下标的含义
dp[i]:下标为 0 记录不偷该节点所得到的最大金钱,下标为 1 记录偷该节点所得到的最大金钱。
确定终止条件
在遍历的过程中,如果遇到空节点的话,无论偷还是不偷都是 0。
相当于 dp 数组的初始化。
确定遍历顺序
使用后序遍历。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
确定单层递归的逻辑
如果偷当前节点,那么左右孩子不能偷。
如果不偷当前节点,那么左右孩子可以偷,选最大的。
举例推导 dp 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution (object ): def rob (self, root ): """ :type root: Optional[TreeNode] :rtype: int """ dp = self.traversal(root) return max (dp) def traversal (self, node ): if not node: return (0 , 0 ) left = self.traversal(node.left) right = self.traversal(node.right) val0 = max (left[0 ], left[1 ]) + max (right[0 ], right[1 ]) val1 = node.val + left[0 ] + right[0 ] return (val0, val1)
121. 买卖股票的最佳时机
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][0]
表示第 i 天持有股票所得最多现金。
dp[i][1]
表示第 i 天不持有股票所得最多现金。
确定递推公式
如果第 i 天持有股票:
第 i - 1 天就持有股票,dp[i - 1][0]
第 i 天买入股票,-price[i]
那么 dp[i][0]
应该选所得现金最大的,所以 dp[i][0] = max(dp[i - 1][0], -prices[i])
如果第 i 天不持有股票:
第 i - 1 天不持有股票,dp[i - 1][1]
第 i 天卖出股票,prices[i] + dp[i - 1][0]
同样 dp[i][1]
取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0])
dp 数组如何初始化
dp[0][0] = -price[0]
dp[0][1] = 0
确定遍历顺序
dp[i] 都是由 dp[i - 1] 遍历出来的,那么一定是从前向后遍历。
举例推导 dp 数组
输入:[7, 1, 5, 3, 6, 4]
0 0 4 4 4 5 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def maxProfit (self, prices ): """ :type prices: List[int] :rtype: int """ n = len (prices) if n == 0 : return 0 dp = [[0 ] * 2 for _ in range (n)] dp[0 ][0 ] = - prices[0 ] dp[0 ][1 ] = 0 for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], - prices[i]) dp[i][1 ] = max (dp[i - 1 ][1 ], prices[i] + dp[i - 1 ][0 ]) return dp[-1 ][1 ]
122. 买卖股票的最佳时机Ⅱ
和上一题唯一不同的地方,就是推导 dp[i][0]
时,第 i 天买入股票的情况。
因为一只股票可以买卖多次,所以当第 i 天买入股票的时候,所持有的现金可能有之前买卖过的利润。
第 i 天持有股票即 dp[i][0]
,如果是第 i 天买入股票,所得现金就是昨天不持有股票得所的现金减去今天得股票价格即:dp[i - 1][1] - prices[i]
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def maxProfit (self, prices ): """ :type prices: List[int] :rtype: int """ n = len (prices) if n == 0 : return 0 dp = [[0 ] * 2 for _ in range (n)] dp[0 ][0 ] = - prices[0 ] dp[0 ][1 ] = 0 for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], dp[i - 1 ][1 ] - prices[i]) dp[i][1 ] = max (dp[i - 1 ][1 ], prices[i] + dp[i - 1 ][0 ]) return dp[-1 ][1 ]
123. 买卖股票的最佳时机Ⅲ
动规五部曲:
确定 dp 数组以及下标的含义
一天一共有五个状态:
没有操作
第一次持有股票
第一次不持有股票
第二次持有股票
第二次不持有股票
dp[i][j]
中 i 表示第 i 天,j 为 [0 - 4] 五个状态,dp[i][j]
表示第 i 天状态 j 所剩最大现金。
注意:dp[i][1]
表示得是第 i 天,买入股票的状态,并不是说一定要第 i 天买入股票。
确定递推公式
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
dp[i][3] = max(dp[i - 1][2] - prices[i], dp[i - 1][3])
dp[i][4] = max(dp[i - 1][3] + prices[i], dp[i - 1][4])
dp 数组如何初始化
dp[0][0] = 0
dp[0][1] = - prices[0]
dp[0][2] = 0
dp[0][3] = - prices[0]
dp[0][4] = 0
确定遍历顺序
一定是从前向后遍历
举例推导 dp 数组
0
1
2
3
4
0
-1
0
-1
0
0
-1
1
-1
1
0
-1
2
-1
2
0
-1
3
-1
3
0
-1
4
-1
4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution (object ): def maxProfit (self, prices ): """ :type prices: List[int] :rtype: int """ n = len (prices) if n == 0 : return 0 dp = [[0 ] * 5 for _ in range (n)] dp[0 ][1 ] = - prices[0 ] dp[0 ][3 ] = - prices[0 ] for i in range (1 , n): dp[i][1 ] = max (dp[i - 1 ][0 ] - prices[i], dp[i - 1 ][1 ]) dp[i][2 ] = max (dp[i - 1 ][1 ] + prices[i], dp[i - 1 ][2 ]) dp[i][3 ] = max (dp[i - 1 ][2 ] - prices[i], dp[i - 1 ][3 ]) dp[i][4 ] = max (dp[i - 1 ][3 ] + prices[i], dp[i - 1 ][4 ]) return dp[-1 ][4 ]
188. 买卖股票的最佳时机Ⅳ
动规五部曲:
确定 dp 数组以及下标的含义
使用二维数组 dp[i][j]
:第 i 天的状态为 j,所剩下的最大现金是 dp[i][j]
。
至多有 K 笔交易,那么 j 的范围就定义为 2 * k + 1 即可。
确定递推公式
dp[i][1]
操作一:第 i 天买入股票,dp[i][1] = dp[i - 1][0] - prices[i]
操作二:第 i 天没有操作,沿用前一天买入的状态,dp[i][1] = dp[i - 1][1]
递推公式:dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
dp[i][2]
操作一:第 i 天卖出股票,dp[i][2] = dp[i - 1][1] + prices[i]
操作二:第 i 天没有操作,沿用前一天卖出的状态,dp[i][2] = dp[i - 1][2]
递推公式:dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
dp 数组如何初始化
dp[0][0] = 0
dp[0][1] = - prices[0]
dp[0][3] = - prices[0]
dp[0][4] = 0
同理,可以推出 dp[0][j] 当 j 为奇数的时候都初始化为 -prices[0]
确定遍历顺序
从前向后遍历
举例推导 dp 数组
0
1
2
3
4
0
-1
0
-1
0
0
-1
1
-1
1
0
-1
2
-1
2
0
-1
3
-1
3
0
-1
4
-1
4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution (object ): def maxProfit (self, k, prices ): """ :type k: int :type prices: List[int] :rtype: int """ n = len (prices) if n == 0 : return 0 dp = [[0 ] * (2 * k + 1 ) for _ in range (n)] for j in range (1 , 2 * k, 2 ): dp[0 ][j] = -prices[0 ] for i in range (1 , n): for j in range (0 , 2 * k - 1 , 2 ): dp[i][j + 1 ] = max (dp[i - 1 ][j] - prices[i], dp[i - 1 ][j + 1 ]) dp[i][j + 2 ] = max (dp[i - 1 ][j + 1 ] + prices[i], dp[i - 1 ][j + 2 ]) return dp[-1 ][2 * k]
309. 买卖股票的最佳时机含冷冻期
确定 dp 数组以及下标的含义
dp[i][j]
,第 i 天状态为 j,所剩的最多现金为 dp[i][j]
。
状态一:持有股票
不持有股票:
状态二:保持卖出股票的状态(两天前卖出股票,度过一天冷冻期。或者前一天卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期,但冷冻期不可持续,只有一天
确定递推公式
达到买入股票的状态(状态一),即 dp[i][0]
:
操作一:前一天持有股票(状态一),dp[i][0] = dp[i - 1][0]
操作二:
前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
前一天是卖出股票(状态二),dp[i - 1][1] - prices[i]
dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i])
达到保持卖出股票状态(状态二),即 dp[i][1]
:
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3])
达到今天就卖出股票状态(状态三),即 dp[i][2]
:
昨天一定持有股票,今天买出
dp[i][2] = dp[i - 1][0] + prices[i]
达到冷冻期状态(状态四),即 dp[i][3]
:
昨天卖出了股票
dp[i][3] = dp[i - 1][2]
dp 数组如何初始化
dp[0][0] = -prices[0]
dp[0][1] = 0
dp[0][2] = 0
dp[0][3] = 0
确定遍历顺序
从前向后遍历
举例推导 dp 数组
0
1
2
3
-1
0
0
0
-1
0
1
0
-1
0
2
1
1
1
-1
2
1
2
3
-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def maxProfit (self, prices ): """ :type prices: List[int] :rtype: int """ n = len (prices) if n == 0 : return 0 dp = [[0 ] * 4 for _ in range (n)] dp[0 ][0 ] = -prices[0 ] for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], max (dp[i - 1 ][3 ], dp[i - 1 ][1 ]) - prices[i]) dp[i][1 ] = max (dp[i - 1 ][1 ], dp[i - 1 ][3 ]) dp[i][2 ] = dp[i - 1 ][0 ] + prices[i] dp[i][3 ] = dp[i - 1 ][2 ] return max (dp[n - 1 ][1 ], dp[n - 1 ][2 ], dp[n - 1 ][3 ])
714. 买卖股票的最佳时机含手续费
确定 dp 数组以及下标含义
dp[i][0]
表示第 i 天持有股票所得最多现金。dp[i][1]
表示第 i 天不持有股票所得最多现金。
确定递推公式
如果第 i 天持有股票即 dp[i][0]
第 i - 1 天持有股票,保持现状,所得现金就是昨天持有股票的所得现金,dp[i - 1][0]
。
第 i 天买入股票,所得现金就是昨天不持有股票的所得现金减去今天股票价格,dp[i - 1][1] - prices[i]
。
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])
如果第 i 天不持有股票即 dp[i][1]
第 i - 1天不持有股票,保持现状,所得现金就是昨天不持有股票得所得现金,dp[i - 1][1]
。
第 i 天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,需要计算手续费 ,dp[i - 1][0] + prices[i] - fee
。
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def maxProfit (self, prices, fee ): """ :type prices: List[int] :type fee: int :rtype: int """ n = len (prices) dp = [[0 ] * 2 for _ in range (n)] dp[0 ][0 ] = -prices[0 ] for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], dp[i - 1 ][1 ] - prices[i]) dp[i][1 ] = max (dp[i - 1 ][1 ], dp[i - 1 ][0 ] + prices[i] - fee) return max (dp[-1 ][0 ], dp[-1 ][1 ])
300. 最长递增子序列
动规五部曲:
dp[i] 的定义
dp[i] 表示 i 之前包括 i 的以 nums[i] 结尾的最长递归子序列的长度
状态转移方程
if (nums[i] > nums[j]):
dp[i] = max(dp[i], dp[j] + 1)
这里不是要 dp[i] 与 dp[j] + 1 进行比较,而是要取 dp[j] + 1 的最大值。
dp[i] 的初始化
每一个 i,对应的 dp[i](即最长递增子序列)起始大小至少都是 1。
确定遍历顺序
从前往后遍历
举例推导 dp 数组
0
1
2
3
4
1
2
1
1
1
1
2
1
1
1
1
2
1
3
1
1
2
1
3
3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def lengthOfLIS (self, nums ): """ :type nums: List[int] :rtype: int """ n = len (nums) if n <= 1 : return n dp = [1 ] * n res = 1 for i in range (1 , n): for j in range (0 , i): if nums[i] > nums[j]: dp[i] = max (dp[i], dp[j] + 1 ) res = max (res, dp[i]) return res
674. 最长连续递增序列
动规五部曲:
确定 dp 数组以及下标的含义
dp[i]:以下标 i 为结尾的连续递增的子序列长度为 dp[i]。
确定递推公式
如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增子序列长度一定等于以 i - 1 为结尾的连续递增子序列长度 + 1。
递推公式:dp[i] = dp[i - 1] + 1
dp 数组如何初始化
dp[i] = 1
确定遍历顺序
从前向后遍历
举例推导 dp 数组
0 1 2 3 4
1 2 3 1 2
取 dp[i] 里的最大值,所以 dp[2] 才是结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def findLengthOfLCIS (self, nums ): """ :type nums: List[int] :rtype: int """ if not nums: return 0 max_len = 1 cur_len = 1 for i in range (1 , len (nums)): if nums[i] > nums[i - 1 ]: cur_len += 1 max_len = max (max_len, cur_len) else : cur_len = 1 return max_len
718. 最长重复子数组
用二维数组可以记录两个字符串的所有比较情况。
确定 dp 数组以及下标的含义
dp[i][j]
:以下标为 i - 1 为结尾的 A,以下标为 j - 1 为结尾的 B,最长重复子数组长度为 dp[i][j]
。
dp[i][j]
的定义也就决定了遍历 dp[i][j]
的时候 i 和 j 都要从 1 开始。
确定递推公式:
根据 dp[i][j]
的定义,dp[i][j]
的状态只能由 dp[i - 1][j - 1]
推导出来。
当 A[i - 1] 和 B[j - 1] 相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1
dp 数组如何初始化
dp[i][0]
和 dp[0][j]
初始化为 0。
确定遍历顺序
外层 for 循环遍历 A,内层 for 循环遍历 B。
举例推导 dp 数组
输入:A[1, 2, 3, 2, 1] B[3, 2, 1, 4, 7]
最大长度为 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution (object ): def findLength (self, nums1, nums2 ): """ :type nums1: List[int] :type nums2: List[int] :rtype: int """ dp = [[0 ] * (len (nums2) + 1 ) for _ in range (len (nums1) + 1 )] res = 0 for i in range (1 , len (nums1) + 1 ): for j in range (1 , len (nums2) + 1 ): if nums1[i - 1 ] == nums2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 if dp[i][j] > res: res = dp[i][j] return res
1143. 最长公共子序列
确定 dp 数组以及下标的含义:
dp[i][j]
:长度为 [0, i - 1]
的字符串 text1 与长度为 [0, j - 1]
的字符串 text2 的最长公共子序列为 dp[i][j]
。
确定递推公式
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
dp 数组如何初始化
dp[i][0] = 0
dp[0][j] = 0
确定遍历顺序
从前往后,从上到下遍历这个矩阵
举例推导 dp 数组
输入:text1 = “abcde”,text2 = “ace”
0 0 0 0
0 1 1 1
0 1 1 1
0 1 2 2
0 1 2 2
0 1 2 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def longestCommonSubsequence (self, text1, text2 ): """ :type text1: str :type text2: str :rtype: int """ dp = [[0 ] * (len (text2) + 1 ) for _ in range (len (text1) + 1 )] for i in range (1 , len (text1) + 1 ): for j in range (1 , len (text2) + 1 ): if text1[i - 1 ] == text2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = max (dp[i - 1 ][j], dp[i][j - 1 ]) return dp[len (text1)][len (text2)]
1035. 不相交的线
绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,只要 nums1[i] == nums2[j],且直线不能相交。
直线不能相交,就是说在字符串 nums1 中找到一个字符串 nums2 相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。
本题说是求绘制的最大连接数,其实就是求两个字符串的最长公共子序列的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def maxUncrossedLines (self, nums1, nums2 ): """ :type nums1: List[int] :type nums2: List[int] :rtype: int """ dp = [[0 ] * (len (nums2) + 1 ) for _ in range (len (nums1) + 1 )] for i in range (1 , len (nums1) + 1 ): for j in range (1 , len (nums2) + 1 ): if nums1[i - 1 ] == nums2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = max (dp[i - 1 ][j], dp[i][j - 1 ]) return dp[len (nums1)][len (nums2)]
53. 最大子序和
动规五部曲:
确定 dp 数组以及下标的含义
dp[i]:包括下标 i 的最大连续子序列和为 dp[i]。
确定递推公式
dp[i] 只有两个方向可以推导:
dp[i - 1] + nums[i],即 nums[i] 加入当前连续子序列和。
nums[i],即从头开始计算当前连续子序列和。
递推公式:dp[i] = max(dp[i - 1] + nums[i], nums[i])
dp 数组如何初始化
dp[0] = nums[0]
确定遍历顺序
从前向后遍历
举例推导 dp 数组
输入:[-2, 1, -3, 4, -1, 2, 1, -5, 4]
0
1
2
3
4
5
6
7
8
-2
1
-2
4
3
5
6
1
5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def maxSubArray (self, nums ): """ :type nums: List[int] :rtype: int """ dp = [0 ] * len (nums) dp[0 ] = nums[0 ] res = dp[0 ] for i in range (1 , len (nums)): dp[i] = max (dp[i - 1 ] + nums[i], nums[i]) res = max (res, dp[i]) return res
392. 判断子序列
根据题意可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。
确定 dp 数组以及下标的含义
dp[i][j]
:表示以下标 i - 1 为结尾的字符换 s,和以下标 j - 1 为结尾的字符串 t,相同子序列的长度为 dp[i][j]
。
确定递推公式
dp 数组初始化
dp[i][j]
都是依赖于 dp[i - 1][j - 1]
和 dp[i][j - 1]
,需要初始化。
确定遍历顺序
从上到下,从左到右
举例推导 dp 数组
输入 s = “abc”,t = “ahbgdc”
a
h
b
g
d
c
0
0
0
0
0
0
0
0
1
1
1
1
1
1
0
0
0
2
2
2
2
0
0
0
0
0
0
3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution (object ): def isSubsequence (self, s, t ): """ :type s: str :type t: str :rtype: bool """ dp = [[0 ] * (len (t) + 1 ) for _ in range (len (s) + 1 )] for i in range (1 , len (s) + 1 ): for j in range (1 , len (t) + 1 ): if s[i - 1 ] == t[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = dp[i][j - 1 ] if dp[-1 ][-1 ] == len (s): return True return False
115. 不同的子序列
只有删除操作,不用考虑替换增加。
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][j]
:以 i - 1 为结尾的 s 子序列中出现以 j - 1 为结尾的 t 的个数为 dp[i][j]
。
确定递推公式
基本要分析两种情况:
s[i - 1] 与 t[j - 1] 相等
s[i - 1] 与 t[j - 1] 不相等
当s[i - 1] 与 t[j - 1] 相等时,dp[i][j]
可以有两部分组成。
一部分用 s[i - 1] 来匹配,个数为 dp[i - 1][j - 1]
。即不需要考虑当前 s 字串和 t 字串的最后一位字母,所以只需要 dp[i - 1][j - 1]
。
另一部分是不用 s[i - 1] 来匹配,个数为 dp[i - 1][j]
。
递推公式为:dp[i][j] = dp[i - 1][j]
。
dp 数组如何初始化
dp[0][j] = 0
dp[0][0] = 1
确定遍历顺序
从上到下,从左到右
距离推导 dp 数组
s:“baegg”,t:“bag” 为例,推导 dp 数组状态
b a g
1 0 0 0
b 1 1 0 0
a 1 1 1 0
e 1 1 1 0
g 1 1 1 1
g 1 1 1 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def numDistinct (self, s, t ): """ :type s: str :type t: str :rtype: int """ dp = [[0 ] * (len (t) + 1 ) for _ in range (len (s) + 1 )] for i in range (len (s)): dp[i][0 ] = 1 for j in range (1 , len (t)): dp[0 ][j] = 0 for i in range (1 , len (s) + 1 ): for j in range (1 , len (t) + 1 ): if s[i - 1 ] == t[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + dp[i - 1 ][j] else : dp[i][j] = dp[i - 1 ][j] return dp[-1 ][-1 ]
583. 两个字符串的删除操作
两个字符串可以相互删除。
确定 dp 数组以及下标的含义
dp[i][j]
:以 i - 1 为结尾的字符串 word1,和以 j - 1 为结尾的字符串 word2。想要达到相等,所需要删除元素的最少次数。
确定递推公式
当 word1[i - 1] 与 word2[j - 1] 相同的时候
当 word1[i - 1] 与 word2[j - 1] 不相同的时候
当 word1[i - 1] 与 word2[j - 1] 相同的时候,dp[i][j] = dp[i - 1][j - 1]
当 word1[i - 1] 与 word2[j - 1] 不相同的时候,有三种情况:
删 word1[i - 1],最少操作次数为 dp[i - 1][j] + 1
删 word2[j - 1],最少操作次数为 dp[i][j - 1] + 1
同时删 word1[i - 1] 和 word2[j - 1],操作的最少次数为 dp[i - 1][j - 1] + 2
最后取最小值,所以当 word1[i - 1] 与 word2[j - 1] 不相同的时候,
递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1})
。
可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)
。
dp 数组如何初始化
dp[i][0] = i
dp[0][j] = j
确定遍历顺序
从上到下,从左到右
举例推导 dp 数组
e a t
0 1 2 3
s 1 2 3 4
e 2 1 2 3
a 3 2 1 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def minDistance (self, word1, word2 ): """ :type word1: str :type word2: str :rtype: int """ dp = [[0 ] * (len (word2) + 1 ) for _ in range (len (word1) + 1 )] for i in range (len (word1) + 1 ): dp[i][0 ] = i for j in range (len (word2) + 1 ): dp[0 ][j] = j for i in range (1 , len (word1) + 1 ): for j in range (1 , len (word2) + 1 ): if word1[i - 1 ] == word2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] else : dp[i][j] = min (dp[i - 1 ][j - 1 ] + 2 , dp[i - 1 ][j] + 1 , dp[i][j - 1 ] + 1 ) return dp[-1 ][-1 ]
72. 编辑距离
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][j]
表示以下标 i - 1 为结尾的字符串 word1,和以下标 j - 1 为结尾的字符串 word2,最近编辑距离为 dp[i][j]
。
确定递推公式
1 2 3 4 if word1[i - 1 ] == word2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ]else : dp[i][j] = min (dp[i - 1 ][j - 1 ], dp[i - 1 ][j], dp[i][j - 1 ]) + 1
dp 数组如何初始化
dp[i][j]
表示以下标 i - 1 为结尾的字符串 word1,和以下标 j - 1 为结尾的字符串 word2,最近编辑距离为 dp[i][j]
。
dp[i][0] = i
dp[0][j] = j
确定遍历顺序
从左到右,从上到下
举例推导 dp 数组
输入:word1 = “horse”,word2 = “ros”
0 1 2 3
1 1 2 3
2 2 1 2
3 2 2 2
4 3 3 2
5 4 4 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution (object ): def minDistance (self, word1, word2 ): """ :type word1: str :type word2: str :rtype: int """ dp = [[0 ] * (len (word2) + 1 ) for _ in range (len (word1) + 1 )] for i in range (1 , len (word1) + 1 ): dp[i][0 ] = i for j in range (1 , len (word2) + 1 ): dp[0 ][j] = j for i in range (1 , len (word1) + 1 ): for j in range (1 , len (word2) + 1 ): if word1[i - 1 ] == word2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] else : dp[i][j] = min (dp[i - 1 ][j - 1 ], dp[i - 1 ][j], dp[i][j - 1 ]) + 1 return dp[-1 ][-1 ]
647. 回文子串
动规五部曲:
确定 dp 数组以及下标的含义
布尔类型的 dp[i][j]
:表示区间范围 [i, j](注意是左闭右闭)的子串是否是回文子串,如果是 dp[i][j]
为 true,否则为 false。
确定递推公式
整体上是两种,s[i] 与 s[j] 相等,s[i] 与 s[j] 不相等。
当 s[i] 与 s[j] 不相等,dp[i][j]
一定是 false。
当 s[i] 与 s[j] 相等时,有如下三种情况
情况一:下标 i 与 j 相同,同一个字符例如 a,当然是回文子串
情况二:下标 i 与 j 相差为 1,例如 aa,也是回文子串
情况三:下标 i 与 j 相差大于 1 的时候,例如 cabac,此时 s[i] 与 s[j] 已经相同了,看 i 到 j 区间是不是回文子串就看 aba 是不是回文就可以,那么 aba 的区间就是 i + 1 与 j - 1 区间,是不是回文就看 dp[i + 1][j - 1]
是否为 true。
递推公式:
1 2 3 4 5 6 7 if s[i] == s[j]: if j - i <= 1 : result += 1 dp[i][j] = true elif dp[i + 1 ][j - 1 ]: result += 1 dp[i][j] = true
dp 数组如何初始化
dp[i][j]
初始化为 false。
确定遍历顺序
从上到下,从左到右
举例推导 dp 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution (object ): def countSubstrings (self, s ): """ :type s: str :rtype: int """ dp = [[0 ] * len (s) for _ in range (len (s))] res = 0 for i in range (len (s) - 1 , -1 , -1 ): for j in range (i, len (s)): if s[i] == s[j]: if j - i <= 1 : res += 1 dp[i][j] = True elif dp[i + 1 ][j - 1 ]: res += 1 dp[i][j] = True return res
516. 最长回文子序列
回文字串是要连续的,回文子序列不是连续的!
动规五部曲:
确定 dp 数组以及下标的含义
dp[i][j]
:字符串 s 在 [i, j] 范围内最长的回文子序列的长度为 dp[i][j]
。
确定递推公式
如果 s[i] 与 s[j] 相同,那么 dp[i][j] = dp[i + 1][j - 1] + 2
。
如果 s[i] 与 s[j] 不相同,
加入 s[j] 的回文子序列长度为 dp[i + 1][j]
。
加入 s[i] 的回文子序列长度为 dp[i][j - 1]
。
dp[i][j]
一定取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
。
dp 数组如何初始化
dp[i][i] = 1
确定遍历顺序
从上到下,从左向右
举例推导 dp 数组
c
b
b
d
1
1
2
2
0
1
2
2
0
0
1
1
0
0
0
1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution (object ): def longestPalindromeSubseq (self, s ): """ :type s: str :rtype: int """ dp = [[0 ] * len (s) for _ in range (len (s))] for i in range (len (s)): dp[i][i] = 1 for i in range (len (s) - 1 , -1 , -1 ): for j in range (i + 1 , len (s)): if s[i] == s[j]: dp[i][j] = dp[i + 1 ][j - 1 ] + 2 else : dp[i][j] = max (dp[i + 1 ][j], dp[i][j - 1 ]) return dp[0 ][-1 ]
单调栈
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时就要想到可以用单调栈了。
单调栈的本质是空间换时间 ,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。
直白来说,就是用一个栈来记录遍历过的元素 ,因为在遍历数组的时候,不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以需要用一个容器来记录遍历过的元素。
单调栈里存什么?
单调栈里只需要存放元素的下标 i 就可以了,如果需要使用对应的元素,直接 T[i] 就可以获取。
单调栈里元素是递增?还是递减?
如果求一个元素右边第一个更大元素,单调栈就是递增的;如果求一个元素右边第一个更小元素,单调栈就是递减的。
使用单调栈主要有三个判断条件:
当前遍历的元素 T[i] < 栈顶元素 T[st.top()] 的情况
当前遍历的元素 T[i] = 栈顶元素 T[st.top()] 的情况
当前遍历的元素 T[i] > 栈顶元素 T[st.top()] 的情况
739. 每日温度
定义 result 数组的时候,应该直接初始化为 0,如果 result 没有更新,说明这个元素右边没有更大的元素,也就是为 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution (object ): def dailyTemperatures (self, temperatures ): """ :type temperatures: List[int] :rtype: List[int] """ ans = [0 ] * len (temperatures) stack = [0 ] for i in range (1 , len (temperatures)): if temperatures[i] <= temperatures[stack[-1 ]]: stack.append(i) else : while len (stack) != 0 and temperatures[i] > temperatures[stack[-1 ]]: ans[stack[-1 ]] = i - stack[-1 ] stack.pop() stack.append(i) return ans
496. 下一个更大元素Ⅰ
如果不存在对应位置就输出 -1,所以 result 数组如果某位置没有被赋值,就应该是 -1,所以初始化 -1。
在遍历 nums2 的过程中,要判断 nums2[i] 是否在 num1 中出现过,因为最后要根据 nums1 元素的下标来更新 result 数组。
没有重复元素,就可以用 map 来做映射了。根据数值快速找到下标,还可以判断 nums2[i] 是否在 nums1 中出现过。
栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。
情况一:当前遍历的元素 T[i] < 栈顶元素 T[st.top()] 的情况
此时满足递增栈,所以直接入栈
情况二:当前遍历的元素 T[i] = 栈顶元素 T[st.top()] 的情况
如果相等依然直接入栈,因为要求的是右边第一个比自己大的元素
情况三:当前遍历的元素 T[i] > 栈顶元素 T[st.top()] 的情况
此时如果入栈就不满足递增栈了,判断栈顶元素是否在 nums1 里出现过,如果出现过,开始记录结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution (object ): def nextGreaterElement (self, nums1, nums2 ): """ :type nums1: List[int] :type nums2: List[int] :rtype: List[int] """ res = [-1 ] * len (nums1) stack = [0 ] for i in range (1 , len (nums2)): if nums2[i] <= nums2[stack[-1 ]]: stack.append(i) else : while len (stack) != 0 and nums2[i] > nums2[stack[-1 ]]: if nums2[stack[-1 ]] in nums1: index = nums1.index(nums2[stack[-1 ]]) res[index] = nums2[i] stack.pop() stack.append(i) return res
503. 下一个更大元素Ⅱ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution (object ): def nextGreaterElements (self, nums ): """ :type nums: List[int] :rtype: List[int] """ res = [-1 ] * len (nums) stack = [0 ] for i in range (len (nums) * 2 ): while (len (stack) != 0 and nums[i % len (nums)] > nums[stack[-1 ]]): res[stack[-1 ]] = nums[i % len (nums)] stack.pop() stack.append(i % len (nums)) return res
42. 接雨水
单调栈就是保持栈内元素有序。接雨水正需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。
首先单调栈是按行方向来计算雨水
使用单调栈内元素的顺序
从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
遇到相同高度的柱子怎么办?
遇到相同元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
因为要求宽度的时候,如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
栈里要保存什么数值
使用单调栈,也是通过长 * 宽来计算雨水面积的。
长就是通过柱子的高度来计算,宽就是通过柱子之间的下标来计算。
以下逻辑主要有三种情况:
情况一:当前遍历的元素(柱子)高度 < 栈顶元素的高度 height[i] < height[st.top()]
情况二:当前遍历的元素(柱子)高度 = 栈顶元素的高度 height[i] == height[st.top()]
情况三:当前遍历的元素(柱子)高度 > 栈顶元素的高度 height[i] > height[st.top()]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution (object ): def trap (self, height ): """ :type height: List[int] :rtype: int """ res = 0 stack = [0 ] for i in range (1 , len (height)): if height[i] < height[stack[-1 ]]: stack.append(i) elif height[i] == height[stack[-1 ]]: stack.pop() stack.append(i) else : while stack and height[i] > height[stack[-1 ]]: mid_height = height[stack[-1 ]] stack.pop() if stack: right_height = height[i] left_height = height[stack[-1 ]] h = min (right_height, left_height) - mid_height w = i - stack[-1 ] - 1 res += h * w stack.append(i) return res
84. 柱状图中最大的矩形
本题要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序。
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
本题单调栈的顺序正好与接雨水反过来。
栈顶和栈底的下一个元素以及要入栈的三个元素组成了要求最大面积的高度和宽度 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution (object ): def largestRectangleArea (self, heights ): """ :type heights: List[int] :rtype: int """ res = 0 stack = [0 ] heights.insert(0 , 0 ) heights.append(0 ) for i in range (1 , len (heights)): if heights[i] > heights[stack[-1 ]]: stack.append(i) elif heights[i] == heights[stack[-1 ]]: stack.pop() stack.append(i) else : while stack and heights[i] < heights[stack[-1 ]]: mid_height = stack[-1 ] stack.pop() if stack: left_height = stack[-1 ] right_height = i width = right_height - left_height - 1 height = heights[mid_height] res = max (res, width * height) stack.append(i) return res
背包问题
二维
有 n 件物品和一个最多能背重量为 w 的背包。第i件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次 ,求解将哪些物品装入背包里物品价值总和最大。
动态规划五部曲:
确定 dp 数组以及下标的含义
i 表示物品、j 表示背包容量
dp[i][j]
:表示从下标为 [0 - i] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少。
确定递推公式
不放物品 i:背包容量为 j,里面不放物品 i 的最大价值是 dp[i - 1][j]
。
放物品 i:背包空出物品 i 的容量后,背包容量为 j - weight[i]
,dp[i - 1][j - weight[i]]
为背包容量 j - weight[i]
且不放物品 i 的最大价值,那么 dp[i - 1][j - weight[i]] + value[i]
(物品的价值),就是背包放物品 i 得到的最大价值。
递归公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
dp 数组如何初始化
dp[i][0]
= 0, dp[0][j]
= value[0]
确定遍历顺序
先遍历物品,再遍历背包重量。
举例推导 dp 数组
0
1
2
3
4
0
15
15
15
15
0
15
15
20
15
0
15
15
20
35
一维
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
其实可以发现如果把 dp[i - 1] 那一层拷贝到 dp[i] 上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])
与其把 dp[i - 1] 这一层拷贝到 dp[i] 上,不如只用一个一维数组 (一维数组也可以理解为一个滚动数组)。
动规五部曲:
确定 dp 数组的定义
在一堆 dp 数组中,dp[j] 表示:容量为 j 的背包,所背的物品价值可以最大为 dp[j]。
一维 dp 数组的递推公式
递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
一维 dp 数组如何初始化
关于初始化,一定要和 dp 数组的定义吻合,否则到递推公式的时候就会越来越乱。
假设物品价值都是大于 0 的,所以 dp 数组初始化的时候,都初始化为 0 就可以了。
遍历顺序
二维 dp 遍历的时候,背包容量从小到大,而一维 dp 遍历的时候,背包是从大到小。
举例:物品 0 的重量 weight[0] = 1,价值 value[0] = 15
正序遍历:
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时 dp[2] 就已经是 30 了,意味着物品 0 被放入了两次,所以不能正序遍历。
倒序遍历:
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
举例推导 dp 数组
用物品 0,遍历背包:0 15 15 15 15
用物品 1,遍历背包:0 15 15 20 35
用物品 2,遍历背包:0 15 15 20 35
完全背包
完全背包和 0-1 背包问题唯一不同的地方就是每种物品有无限件。
动规五部曲:
确定 dp 数组的定义
dp[i][j]
表示从下标为 [0-i] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少。
一维 dp 数组的递推公式
不放物品 i:背包容量为 j,里面不放物品 i 的最大价值是 dp[i - 1][j]
放物品 i:背包空出物品 i 的容量后,背包容量为 j - weight[i]
,dp[i - 1][j - weight[i]]
为背包容量 j - weight[i]
且不放物品 i 的最大价值,那么 dp[i - 1][j - weight[i]] + value[i]
(物品的价值),就是背包放物品 i 得到的最大价值。
递推公式为:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
dp 数组如何初始化
关于初始化,一定要和 dp 数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[i][0] = 0
, dp[0][j] = dp[0][j - weight[0]] + value[0]
遍历顺序
既可以先遍历物品再遍历背包,也可以先遍历背包再遍历物品。
举例推导 dp 数组
用物品 0,遍历背包:0 15 30 45 60
用物品 1,遍历背包:0 15 30 45 60
用物品 2,遍历背包:0 15 30 45 60
因为物品 0 的性价比最高,而且在完全背包中,每一类物品都有无限个,所以有无限个物品 0,既然物品 0 性价比最高,当然优先放物品 0。
图论