已完成:
TODO:
代码随想录剩下的(相关题目推荐我没做,有缘再巩固)
labuladong
hot150
剑指offer
力扣75
代码随想录 1. 数组 1.1 数组理论基础 数组是存放在连续内存空间上的相同类型数据的集合。
因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动 其他元素的地址。
vector的底层实现是array
在 C++ 中,vector是一种动态数组,它的底层是基于普通数组(array)来实现的。这意味着vector在内存中是以连续的存储单元来存放元素的,就像普通数组一样。这种底层实现方式使得vector能够像数组一样快速地随机访问元素,即可以通过索引快速获取到任意位置的元素。
vector是容器,不是数组
容器的概念 :vector是 C++ 标准模板库(STL)中的一种容器类。容器是一种能够存储和管理其他对象的对象,它提供了一系列的成员函数和操作符,用于方便地对存储的元素进行各种操作,如插入、删除、遍历等。vector作为容器,具有很多方便的功能和特性,比如它可以自动管理内存,根据元素的添加和删除自动调整自身的大小。
与数组的区别 :虽然vector在底层利用了数组的存储方式,但它和普通的 C 风格数组有很多不同之处。普通数组的大小是固定的,在定义时就需要指定其大小,而且在程序运行期间大小不能改变。而vector的大小是可以动态变化的,可以在运行时根据需要添加或删除元素,它会自动分配和释放内存来适应元素数量的变化。
在C++中二维数组是连续分布的,对于int型数组,两个相邻数组元素地址差4个字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void test_arr () { int array[2 ][3 ] = { {0 , 1 , 2 }, {3 , 4 , 5 } }; cout << &array[0 ][0 ] << " " << &array[0 ][1 ] << " " << &array[0 ][2 ] << endl; cout << &array[1 ][0 ] << " " << &array[1 ][1 ] << " " << &array[1 ][2 ] << endl; } int main () { test_arr (); }
像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机,所以看不到每个元素的地址情况。
1 2 3 4 5 6 7 8 public static void test_arr () { int [][] arr = {{1 , 2 , 3 }, {3 , 4 , 5 }, {6 , 7 , 8 }, {9 ,9 ,9 }}; System.out.println(arr[0 ]); System.out.println(arr[1 ]); System.out.println(arr[2 ]); System.out.println(arr[3 ]); }
arr[0] 实际上是一个一维数组对象的引用
输出一个类似 [I@hashcode 的字符串,其中 [I 表示这是一个一维整数数组,@hashcode 是该数组对象在内存中的哈希码,它可以在一定程度上被认为是该数组对象的地址码(但不是真正的物理地址,而是处理过后的数值)
行指针数组在内存中是连续存储的,而每个行所指向的一维数组(即二维数组的每一行)在内存中的存储位置是不连续的
1.2 二分查找 二分法的前提条件:有序数组+无重复元素
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
时间复杂度:O(log n)
空间复杂度:O(1)
这是因为search只使用left、right、middle,在执行过程中额外占用的空间是固定的,与输入数据的规模n无关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int search (int [] nums, int target) { if (target < nums[0 ] || target > nums[nums.length - 1 ]) { return -1 ; } int left = 0 ; int right = nums.length - 1 ; while (left <= right){ int middle = (left + right) / 2 ; if (nums[middle] == target){ return middle; }else if (nums[middle] < target){ left = middle + 1 ; }else { right = middle - 1 ; } } return -1 ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def search (self, nums: List [int ], target: int ) -> int : if nums[0 ] > target or nums[len (nums) - 1 ] < target: return -1 left, right = 0 , len (nums) - 1 while left <= right: mid = left + ((right - left) >> 1 ) if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else : right = mid - 1 return -1
1.3 移除元素 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地 修改输入数组。不需要考虑数组中超出新长度后面的元素。
可以使用暴力解法,发现需要移除的元素,就将数组集体向前移动一位
快慢指针法 双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针:时间复杂度:O(n)、空间复杂度:O(1)。并不改变元素的相对位置。
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新新数组下标的位置
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public int removeElement (int [] nums, int val) { int slow = 0 ; for (int fast = 0 ; fast < nums.length; fast++){ if (nums[fast] != val){ nums[slow] = nums[fast]; slow++; } } return slow; } }
1 2 3 4 5 6 7 8 class Solution : def removeElement (self, nums: List [int ], val: int ) -> int : fast, slow = 0 , 0 for fast in range (len (nums)): if nums[fast] != val: nums[slow] = nums[fast] slow += 1 return slow
相向双指针法 1 2 输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3,_,_,_]
题目描述中的示例告诉我们不一定要按照顺序得到新数组,所以可以另一个指针从后往前
可以让右指针不断地指向不为val的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int removeElement (int [] nums, int val) { int left = 0 , right = nums.length - 1 ; while (right >= 0 && nums[right] == val){ right--; } for (; left <= right; left++){ if (nums[left] == val){ nums[left] = nums[right]; right--; while (right >= 0 && nums[right] == val){ right--; } } } return left; } }
也可以不判断右指针是否为val,先用右侧覆盖左侧再说,后面再检查左指针(最简洁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int removeElement (int [] nums, int val) { int left = 0 , right = nums.length - 1 ; while (left <= right){ if (nums[left] == val){ nums[left] = nums[right]; right--; }else { left++; } } return left; } }
1 2 3 4 5 6 7 8 9 10 class Solution : def removeElement (self, nums: List [int ], val: int ) -> int : left, right = 0 , len (nums) - 1 while left <= right: if nums[left] == val: nums[left] = nums[right] right -= 1 else : left += 1 return left
1.4 有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
暴力法: O(n + nlogn)
双指针法:两侧的数字绝对值大,所以平方也大,所以可以用两侧到中间,不断比较大小。O(n)
注意这道题得创建个新数组,如果在原数组上改的话两个指针不够
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int [] sortedSquares(int [] nums) { int left = 0 , right = nums.length - 1 ; int i = nums.length - 1 ; int [] result = new int [nums.length]; while (left <= right){ if (nums[left] * nums[left] >= nums[right] * nums[right]){ result[i--] = nums[left] * nums[left]; left++; }else { result[i--] = nums[right] * nums[right]; right--; } } return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def sortedSquares (self, nums: List [int ] ) -> List [int ]: l, r = 0 , len (nums) - 1 res = [] while l <= r: if abs (nums[l]) >= abs (nums[r]): res.append(nums[l] ** 2 ) l += 1 else : res.append(nums[r] ** 2 ) r -= 1 return res[::-1 ]
1.5 长度最小的子数组 找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度
暴力法:两层循环,看每个起点连续多少能超过s,比较哪个最短
滑动窗口 :不断调节子序列的起始位置和终止位置,从而得出我们要想的结果
只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。(不然还要一个循环表示起始位置:暴力)
滑动窗口也可以理解为双指针法的一种:主要确定如下三点:
窗口内是什么?就是 满足其和 ≥ s 的长度最小的 连续 子数组。
如何移动窗口的起始位置?如果当前窗口的值大于等于s了,窗口就要向前移动了。(此时已获得一个子序列)
如何移动窗口的结束位置?
精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置
时间复杂度:O(n):因为每个元素在滑动窗后进来操作一次,出去操作一次,其实是O(2n)
空间复杂度:O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public int minSubArrayLen (int target, int [] nums) { int res = Integer.MAX_VALUE; int sum = 0 ; int i = 0 ; for (int j = 0 ; j < nums.length; j++){ sum += nums[j]; while (sum >= target){ res = Math.min(j - i + 1 , res); sum -= nums[i]; i++; } } return res == Integer.MAX_VALUE? 0 : res; } }
1 2 3 4 5 6 7 8 9 10 11 class Solution : def minSubArrayLen (self, target: int , nums: List [int ] ) -> int : l, sum = 0 , 0 res = float ('inf' ) for r in range (len (nums)): sum += nums[r] while sum >= target: res = min (res, r - l + 1 ) sum -= nums[l] l += 1 return res if res != float ('inf' ) else 0
1.6 螺旋矩阵II 模拟行为 :如何坚持循环不变量?确定1-4每次循环的数目为n-loop,然后用左闭右开的区间约束
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 { public int [][] generateMatrix(int n) { int [][] nums = new int [n][n]; int loop = 1 ; int i = 0 , j = 0 ; int count = 1 ; while (loop <= n / 2 ){ for (; j < n - loop; j++){ nums[i][j] = count++; } for (; i < n - loop; i++){ nums[i][j] = count++; } for (; j > loop - 1 ; j--){ nums[i][j] = count++; } for (; i > loop - 1 ; i--){ nums[i][j] = count++; } i++; j++; loop++; } if (n % 2 == 1 ){ nums[i][j] = count; } return nums; } }
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 : def generateMatrix (self, n: int ) -> List [List [int ]]: nums = [[0 ] * n for _ in range (n)] i, j = 0 , 0 loop = 1 count = 1 while loop <= n // 2 : while j < n - loop: nums[i][j] = count count += 1 j += 1 while i < n - loop: nums[i][j] = count count += 1 i += 1 while j > loop - 1 : nums[i][j] = count count += 1 j -= 1 while i > loop - 1 : nums[i][j] = count count += 1 i -= 1 loop += 1 i += 1 j += 1 if n % 2 == 1 : nums[i][j] = count return nums
输出每个指定区间内元素的总和。
前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。
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 import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner scanner = new Scanner (System.in); int n = scanner.nextInt(); int [] vec = new int [n]; int [] p = new int [n]; int presum = 0 ; for (int i = 0 ; i < n; i++){ vec[i] = scanner.nextInt(); presum += vec[i]; p[i] = presum; } while (scanner.hasNextInt()){ int a = scanner.nextInt(); int b = scanner.nextInt(); int res = 0 ; if (a == 0 ){ res = p[b]; }else { res = p[b] - p[a - 1 ]; } System.out.println(res); } scanner.close(); } }
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 import sysinput = sys.stdin.readdef main (): data = input () data = data.split() index = 0 n = int (data[index]) index += 1 vec = [] p = [] presum = 0 for _ in range (n): num = int (data[index]) presum += num vec.append(num) p.append(presum) index += 1 while index < len (data): a, b = int (data[index]), int (data[index + 1 ]) index += 2 res = 0 if a == 0 : res = p[b] else : res = p[b] - p[a - 1 ] print (res) if __name__ == "__main__" : main()
注意只能按行分或按列分:二维前缀和枚举按行分和按列分的所有情况,然后取最小值
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 import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner scanner = new Scanner (System.in); int n = scanner.nextInt(); int m = scanner.nextInt(); int [][] array = new int [n][m]; int [] row = new int [n]; int [] col = new int [m]; for (int i = 0 ; i < n; i++){ for (int j = 0 ; j < m; j++){ array[i][j] = scanner.nextInt(); } } int rowSum = 0 ; for (int i = 0 ; i < n; i++){ for (int j = 0 ; j < m; j++){ rowSum += array[i][j]; } row[i] = rowSum; } int colSum = 0 ; for (int j = 0 ; j < m; j++){ for (int i = 0 ; i < n; i++){ colSum += array[i][j]; } col[j] = colSum; } int rowResult = Main.result(row); int colResult = Main.result(col); System.out.println(Math.min(rowResult, colResult)); } public static int result (int [] array) { int total = array[array.length - 1 ]; int res = Integer.MAX_VALUE; for (int i = 0 ; i < array.length - 1 ; i++){ res = Math.min(res, Math.abs(total - 2 * array[i])); } return res; } }
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 import sysinput = sys.stdin.readdef result (arr ): total = arr[len (arr) - 1 ] res = float ('inf' ) for i in range (len (arr) - 1 ): res = min (res, abs (total - 2 * arr[i])) return res def main (): data = input ().split() index = 0 n, m = int (data[index]), int (data[index + 1 ]) index += 2 array = [[0 ] * m for _ in range (n)] row = [0 ] * n col = [0 ] * m for i in range (n): for j in range (m): array[i][j] = int (data[index]) index += 1 rowsum = 0 for i in range (n): for j in range (m): rowsum += array[i][j] row[i] = rowsum colsum = 0 for j in range (m): for i in range (n): colsum += array[i][j] col[j] = colsum rowresult = result(row) colresult = result(col) print (min (rowresult, colresult)) if __name__ == "__main__" : main()
1.9 总结篇
2. 链表 2.1 链表理论基础 单链表:
双链表:既可以向前查询也可以向后查询。
循环链表:链表首尾相连,可以用来解决约瑟夫环问题(n个人围成圈,报到k出列)。
链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ListNode { int val; ListNode next; public ListNode () { } public ListNode (int val) { this .val = val; } public ListNode (int val, ListNode next) { this .val = val; this .next = next; } }
1 2 3 4 class ListNode : def __init__ (self, val, next =None ): self .val = val self .next = next
删除节点:C++需要手动释放,Java、Python有自己的内存回收机制无需手动释放
就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。
数组在定义的时候长度是固定的,链表长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
这里就涉及如下链表操作的两种方式:
直接使用原来的链表来进行删除操作。
设置一个虚拟头结点在进行删除操作。 dummy会简单些,统一写dummy!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public ListNode removeElements (ListNode head, int val) { ListNode dummy = new ListNode (); dummy.next = head; ListNode curr = dummy; while (curr.next != null ){ if (curr.next.val == val){ curr.next = curr.next.next; }else { curr = curr.next; } } return dummy.next; } }
1 2 3 4 5 6 7 8 9 10 class Solution : def removeElements (self, head: Optional [ListNode], val: int ) -> Optional [ListNode]: dummy = ListNode(next = head) curr = dummy while curr.next : if curr.next .val == val: curr.next = curr.next .next else : curr = curr.next return dummy.next
这道题目设计链表的五个接口:
获取链表第index个节点的数值
在链表的最前面插入一个节点
在链表的最后面插入一个节点
在链表第index个节点前面插入一个节点
删除链表的第index个节点
采用设置一个虚拟头结点
时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1)
空间复杂度: O(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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class MyLinkedList { class ListNode { int val; ListNode next; ListNode(int val) { this .val = val; } } private int size; private ListNode head; public MyLinkedList () { this .size = 0 ; this .head = new ListNode (0 ); } public int get (int index) { if (index < 0 || index >= size) return -1 ; ListNode cur = head; for (int i = 0 ; i <= index; i++){ cur = cur.next; } return cur.val; } public void addAtHead (int val) { ListNode newNode = new ListNode (val); newNode.next = head.next; head.next = newNode; size++; } public void addAtTail (int val) { ListNode cur = head; ListNode newNode = new ListNode (val); while (cur.next != null ){ cur = cur.next; } cur.next = newNode; size++; } public void addAtIndex (int index, int val) { if (index == size){ addAtTail(val); }else if (index >= 0 && index < size){ ListNode newNode = new ListNode (val); ListNode pre = head; for (int i = 0 ; i < index; i++){ pre = pre.next; } ListNode cur = pre.next; pre.next = newNode; newNode.next = cur; size++; } } public void deleteAtIndex (int index) { if (index >= 0 && index < size){ ListNode pre = head; for (int i = 0 ; i < index; i++){ pre = pre.next; } pre.next = pre.next.next; size--; } } }
ListNode 就是一个非静态内部类。它的实例依赖于外部类的实例。内部类 ListNode 可以直接访问 MyLinkedList 的私有属性(如 head 和 size),但在这个例子中并没有直接访问。
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 class MyLinkedList : class ListNode : def __init__ (self, val = 0 , next = None ): self .val = val self .next = next def __init__ (self ): self .head = self .ListNode() self .size = 0 def get (self, index: int ) -> int : if index < 0 or index >= self .size: return -1 cur = self .head for _ in range (index + 1 ): cur = cur.next return cur.val def addAtHead (self, val: int ) -> None : newNode = self .ListNode(val) newNode.next = self .head.next self .head.next = newNode self .size += 1 def addAtTail (self, val: int ) -> None : newNode = self .ListNode(val) cur = self .head while cur.next : cur = cur.next cur.next = newNode self .size += 1 def addAtIndex (self, index: int , val: int ) -> None : if index == self .size: self .addAtTail(val) elif index >= 0 and index < self .size: newNode = self .ListNode(val) pre = self .head for _ in range (index): pre = pre.next newNode.next = pre.next pre.next = newNode self .size += 1 def deleteAtIndex (self, index: int ) -> None : if index >= 0 and index < self .size: pre = self .head for _ in range (index): pre = pre.next pre.next = pre.next .next self .size -= 1
注意python访问实例属性、实例方法、内部类 都要使用 self。但是访问外部类不用。
不需要定义一个新的链表,直接改变链表的next指针的指向即可,记得把 cur->next 节点用tmp指针保存一下即可
用双指针法从前往后翻转指针指向,比较直接好理解(递归感觉这题没啥必要,反而复杂了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public ListNode reverseList (ListNode head) { ListNode pre = null ; ListNode cur = head; ListNode tmp = null ; while (cur != null ){ tmp = cur.next; cur.next = pre; pre = cur; cur = tmp; } return pre; } }
1 2 3 4 5 6 7 8 9 10 class Solution : def reverseList (self, head: Optional [ListNode] ) -> Optional [ListNode]: pre = None cur = head while cur: tmp = cur.next cur.next = pre pre = cur cur = tmp return pre
因为每次循环都会进行垃圾回收,所以tmp定义在循环外部内部感觉区别不大,编译器会复用栈上的同一块内存,创建和销毁都很高效。
重点是每次只交换2个数,不用纠结tmp3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution { public ListNode swapPairs (ListNode head) { ListNode dummy = new ListNode (0 , head); ListNode cur = dummy; while (cur.next != null && cur.next.next != null ){ ListNode tmp1 = cur.next; ListNode tmp2 = tmp1.next; ListNode tmp3 = tmp2.next; cur.next = tmp2; tmp2.next = tmp1; tmp1.next = tmp3; cur = cur.next.next; } return dummy.next; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def swapPairs (self, head: Optional [ListNode] ) -> Optional [ListNode]: dummy = ListNode(0 , head) cur = dummy while cur.next and cur.next .next : tmp1 = cur.next tmp2 = tmp1.next tmp3 = tmp2.next cur.next = tmp2 tmp2.next = tmp1 tmp1.next = tmp3 cur = cur.next .next return dummy.next
扫描两遍很简单,如何只扫描一遍?双指针法!
fast首先走n + 1步,然后fast和slow同时移动,直到fast指向末尾
这样可以让slow正好在待删节点的前一个,原理是间距固定为n+1,fast为空时flow正好指向倒数n+1个节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public ListNode removeNthFromEnd (ListNode head, int n) { ListNode dummy = new ListNode (0 , head); ListNode fast = dummy, slow = dummy; for (int i = 0 ; i <= n; i++){ fast = fast.next; } while (fast != null ){ fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next; } }
1 2 3 4 5 6 7 8 9 10 11 class Solution : def removeNthFromEnd (self, head: Optional [ListNode], n: int ) -> Optional [ListNode]: dummy = ListNode(0 , head) fast = slow = dummy for _ in range (n + 1 ): fast = fast.next while fast: fast = fast.next slow = slow.next slow.next = slow.next .next return dummy.next
注意交点不是数值相等,而是指针相等,可以理解为物理结构就是相交的,先有图这样的物理结构再有题目。所以不要纠结为什么val和next一样但地址不一样,题目比较的就是地址。
引用 指的是链表节点在内存中的地址。当我们说两个链表相交时,意味着它们共享同一个节点(即两个链表中的某个节点的内存地址相同),即引用完全相同。
因为相交后面的节点一定一样,所以先将长一点的链表移动到相同的位置 ,然后同时向后移动cur比较即可。
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 public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode curA = headA; ListNode curB = headB; int lenA = 0 , lenB = 0 ; while (curA != null ) { lenA++; curA = curA.next; } while (curB != null ) { lenB++; curB = curB.next; } int gap = Math.abs(lenA - lenB); curA = headA; curB = headB; if (lenA > lenB){ while (gap != 0 ){ curA = curA.next; gap--; } }else { while (gap != 0 ){ curB = curB.next; gap--; } } while (curA != null ){ if (curA == curB){ return curA; }else { curA = curA.next; curB = curB.next; } } return 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 class Solution : def getIntersectionNode (self, headA: ListNode, headB: ListNode ) -> ListNode: lenA, lenB = 0 , 0 curA = headA curB = headB while curA: curA = curA.next lenA += 1 while curB: curB = curB.next lenB += 1 gap = abs (lenA - lenB) curA = headA curB = headB if lenA > lenB: while gap: curA = curA.next gap -= 1 else : while gap: curB = curB.next gap -= 1 while curA: if curA == curB: return curA else : curA = curA.next curB = curB.next return None
哈希表是直观的思路,时间/空间复杂度是O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Solution { public ListNode detectCycle (ListNode head) { ListNode pos = head; Set<ListNode> visited = new HashSet <ListNode>(); while (pos != null ) { if (visited.contains(pos)) { return pos; } else { visited.add(pos); } pos = pos.next; } return null ; } }
1 2 3 4 5 6 7 8 9 10 11 class Solution : def detectCycle (self, head: Optional [ListNode] ) -> Optional [ListNode]: pos = head visited = set () while pos: if pos in visited: return pos else : visited.add(pos) pos = pos.next return None
进阶:用空间O(1)实现,Floyd判圈算法
判断链表是否环:可以使用快慢指针法 ,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
如果有环,如何找到这个环的入口:首先可以证明第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,然后相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),可以得到x = n (y + z) - y。
从头结点出发一个指针ptr,从相遇节点也出发一个指针slow,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点。如果n=1,则slow没有在环中走多余的路,如果n>1,则相遇前会在环中转n-1圈,但最后会和ptr在入口相遇。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Solution { public ListNode detectCycle (ListNode head) { ListNode fast = head; ListNode slow = head; while (fast != null && fast.next != null ){ slow = slow.next; fast = fast.next.next; if (fast == slow){ ListNode ptr = head; while (ptr != slow){ ptr = ptr.next; slow = slow.next; } return ptr; } } return null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def detectCycle (self, head: Optional [ListNode] ) -> Optional [ListNode]: fast = slow = head while fast and fast.next : fast = fast.next .next slow = slow.next if fast == slow: ptr = head while ptr != slow: ptr = ptr.next slow = slow.next return ptr return None
2.9 总结篇 统一使用虚拟头节点dummy
3. 哈希表 3.1 哈希表理论基础 哈希表是根据关键码的值而直接进行访问的数据结构,用来快速判断一个元素是否出现集合里。
哈希函数:通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
哈希碰撞:小李和小王都映射到了索引下标 1 的位置
拉链法:发生冲突的元素都被存储在链表中。需要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法:一定要保证tableSize大于dataSize,因为需要依靠哈希表中的空位来解决碰撞问题。如向下找一个空位。
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合
底层实现
是否有序
数值是否可以重复
能否更改数值
查询效率
增删效率
std::set
红黑树
有序
否
否
O(log n)
O(log n)
std::multiset
红黑树
有序
是
否
O(logn)
O(logn)
std::unordered_set
哈希表
无序
否
否
O(1)
O(1)
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射
底层实现
是否有序
数值是否可以重复
能否更改数值
查询效率
增删效率
std::map
红黑树
key有序
key不可重复
key不可修改
O(logn)
O(logn)
std::multimap
红黑树
key有序
key可重复
key不可修改
O(log n)
O(log n)
std::unordered_map
哈希表
key无序
key不可重复
key不可修改
O(1)
O(1)
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
虽然std::set和std::multiset 的底层实现基于红黑树而非哈希表,它们通过红黑树来索引和存储数据。不过给我们的使用方式,还是哈希法的使用方式,即依靠键(key)来访问值(value)。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。std::map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
以下是我的补充:
Java:红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
集合
底层实现
是否有序
数值是否可以重复
能否更改数值
查询效率
增删效率
HashSet
哈希表
无序
否
否
O(1)
O(1)
LinkedHashSet
哈希表 + 链表
有序(插入顺序)
否
否
O(1)
O(1)
TreeSet
红黑树
有序(自然顺序或自定义顺序)
否
否
O(log n)
O(log n)
映射
底层实现
是否有序
key是否可以重复
能否更改key
查询效率
增删效率
HashMap
哈希表
无序
否
否
O(1)
O(1)
LinkedHashMap
哈希表 + 链表
有序(插入顺序或访问顺序)
否
否
O(1)
O(1)
TreeMap
红黑树
有序(自然顺序或自定义顺序)
否
否
O(log n)
O(log n)
Python:
集合
底层实现
是否有序
数值是否可以重复
能否更改数值
查询效率
增删效率
set
哈希表
无序
否
否
O(1)
O(1)
frozenset
哈希表
无序
否
否
O(1)
不可变
映射
底层实现
是否有序
key是否可以重复
能否更改key
查询效率
增删效率
dict
哈希表
无序(Python 3.7+ 有序)
否
否
O(1)
O(1)
OrderedDict
哈希表 + 双向链表
有序(插入顺序)
否
否
O(1)
O(1)
当我们要使用集合来解决哈希问题的时候,优先使用HashSet,因为它的查询和增删效率是最优的。基于红黑树而非哈希表,它们通过红黑树来索引和存储数据。不过给我们的使用方式,还是哈希法的使用方式,即依靠键(key)来访问值(value)。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法 。
但是哈希法也是牺牲了空间换取了时间 ,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
排序法时间复杂度O (n logn )
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public boolean isAnagram (String s, String t) { if (s.length() != t.length()) { return false ; } char [] str1 = s.toCharArray(); char [] str2 = t.toCharArray(); Arrays.sort(str1); Arrays.sort(str2); return Arrays.equals(str1, str2); } }
1 2 3 4 5 class Solution : def isAnagram (self, s: str , t: str ) -> bool : if len (s) != len (t): return False return sorted (s) == sorted (t)
数组其实就是一个简单哈希表 ,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。对一个字符串统计+1,对另一个字符串-1,最后看是否有元素不为0,有则不是字母异位词。时间复杂度: O(n)
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 { public boolean isAnagram (String s, String t) { if (s.length() != t.length()) { return false ; } int [] record = new int [26 ]; for (int i = 0 ; i < s.length(); i++){ record[s.charAt(i) - 'a' ]++; } for (int i = 0 ; i < t.length(); i++){ record[t.charAt(i) - 'a' ]--; } for (int i = 0 ; i < 26 ; i++){ if (record[i] != 0 ) return false ; } return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def isAnagram (self, s: str , t: str ) -> bool : if len (s) != len (t): return False record = [0 ] * 26 for char in s: record[ord (char) - ord ('a' )] += 1 for char in t: record[ord (char) - ord ('a' )] -= 1 for count in record: if count != 0 : return False return True
另外python的Counter和defaultdict也可以解题:
1 2 3 class Solution : def isAnagram (self, s: str , t: str ) -> bool : return Counter(s) == Counter(t)
1 2 3 4 5 6 7 8 9 class Solution : def isAnagram (self, s: str , t: str ) -> bool : s_dict = defaultdict(int ) t_dict = defaultdict(int ) for x in s: s_dict[x] += 1 for x in t: t_dict[x] += 1 return s_dict == t_dict
输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序。
使用数组来做哈希的题目,是因为题目都限制了数值的大小,如上一题只需要26位记录次数,但这里没有限制大小,哈希值可能比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。此处就适合使用集合。
那为什么有时哈希问题要用数组?直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。不要小瞧这个耗时,在数据量大的情况,差距是很明显的。
后来此题限制了大小在1000以内,也可以用数组
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 { public int [] intersection(int [] nums1, int [] nums2) { if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0 ) { return new int [0 ]; } Set<Integer> set1 = new HashSet <>(); Set<Integer> resSet = new HashSet <>(); for (int i : nums1) { set1.add(i); } for (int i : nums2) { if (set1.contains(i)) { resSet.add(i); } } return resSet.stream().mapToInt(Integer::intValue).toArray(); } }
1 2 3 class Solution : def intersection (self, nums1: List [int ], nums2: List [int ] ) -> List [int ]: return list (set (nums1) & set (nums2))
1 2 3 4 5 6 7 8 9 class Solution : def intersection (self, nums1: List [int ], nums2: List [int ] ) -> List [int ]: st = set (nums1) ans = [] for x in nums2: if x in st: st.remove(x) ans.append(x) return ans
注意无限循环:如果sum重复出现了,就不可能是快乐数,sum达到1了。所以需要用set存储出现过的sum值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public boolean isHappy (int n) { Set<Integer> record = new HashSet <>(); while (n != 1 && !record.contains(n)){ record.add(n); n = getNextNum(n); } return n == 1 ; } public int getNextNum (int n) { int sum = 0 ; while (n != 0 ){ sum += Math.pow(n % 10 , 2 ); n /= 10 ; } return sum; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def isHappy (self, n: int ) -> bool : record = set () while n not in record: record.add(n) n_str = str (n) sum = 0 for i in n_str: sum += int (i)**2 if sum == 1 : return True else : n = sum return False
当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适 。
再来看一下使用数组和set来做哈希法的局限。
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置 ,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public int [] twoSum(int [] nums, int target) { int [] res = new int [2 ]; if (nums == null || nums.length == 0 ){ return res; } Map<Integer, Integer> map = new HashMap <>(); for (int i = 0 ; i < nums.length; i++){ int tmp = target - nums[i]; if (map.containsKey(tmp)){ res[0 ] = i; res[1 ] = map.get(tmp); break ; } map.put(nums[i], i); } return res; } }
1 2 3 4 5 6 7 8 9 class Solution : def twoSum (self, nums: List [int ], target: int ) -> List [int ]: record = dict () for index, value in enumerate (nums): if target - value in record: return [index, record[target - value]] else : record[value] = index return []
参考前文的两数之和,建立map,先记录A+B数组的元素之和和出现次数,然后遍历C+D,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
时间复杂度: O(n^2)
空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int fourSumCount (int [] nums1, int [] nums2, int [] nums3, int [] nums4) { int count = 0 ; Map<Integer, Integer> map = new HashMap <Integer, Integer>(); for (int i: nums1){ for (int j: nums2){ map.put(i + j, map.getOrDefault(i + j, 0 ) + 1 ); } } for (int i: nums3){ for (int j: nums4){ count += map.getOrDefault(- i - j, 0 ); } } return count; } }
1 2 3 4 5 6 7 8 9 10 11 class Solution : def fourSumCount (self, nums1: List [int ], nums2: List [int ], nums3: List [int ], nums4: List [int ] ) -> int : map = dict () count = 0 for i in nums1: for j in nums2: map [i + j] = map .get(i + j, 0 ) + 1 for i in nums3: for j in nums4: count += map .get( - i - j, 0 ) return count
因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组 来记录magazine里字母出现的次数。然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public boolean canConstruct (String ransomNote, String magazine) { if (ransomNote.length() > magazine.length()) return false ; int [] record = new int [26 ]; for (int i = 0 ; i < magazine.length(); i++){ record[magazine.charAt(i) - 'a' ] += 1 ; } for (int i = 0 ; i < ransomNote.length(); i++){ record[ransomNote.charAt(i) - 'a' ] -= 1 ; if (record[ransomNote.charAt(i) - 'a' ] < 0 ){ return false ; } } return true ; } }
1 2 3 4 5 6 7 8 9 10 11 class Solution : def canConstruct (self, ransomNote: str , magazine: str ) -> bool : record = [0 ] * 26 for c in magazine: record[ord (c) - ord ('a' )] += 1 for c in ransomNote: record[ord (c) - ord ('a' )] -= 1 for i in record: if i < 0 : return False return True
1 2 3 4 5 class Solution : def canConstruct (self, ransomNote: str , magazine: str ) -> bool : return not (Counter(ransomNote) - Counter(magazine))
两层for循环就可以确定 两个数值,可以使用哈希法来确定 第三个数 0-(a+b) 或者 0 - (a + c) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
哈希法用set存b,a是nums[i],c是nums[k](另一层循环,k=i+1…n),过程中要不断对a和c去重,比较繁琐
时间复杂度: O(n^2),第二层循环有时作c有时作b,总之有点难理解,用双指针!
而排序可以去除重复解。拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。时间复杂度:O(n^2)。
去重逻辑的思考
a的去重:不是与nums[i + 1]比较,而是与nums[i - 1]比较,因为不能有重复的三元组,但三元组内的元素是可以重复的
b与c的去重:在三数之和非0时,无需去重bc,因为下一个循环也会校验。等于0时要校验是否重复。
两数之和 就不能使用双指针法,因为1.两数之和 (opens new window) 要求返回的是索引下标, 而双指针法一定要排序 ,一旦排序之后原数组的索引就被改变了(要求返回的是数值的话,就可以使用双指针法了)
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 { public List<List<Integer>> threeSum (int [] nums) { List<List<Integer>> res = new ArrayList <>(); Arrays.sort(nums); for (int i = 0 ; i < nums.length; i++){ if (nums[i] > 0 ){ return res; } if (i > 0 && nums[i] == nums[i - 1 ]){ continue ; } int left = i + 1 ; int right = nums.length - 1 ; while (right > left){ int sum = nums[i] + nums[left] + nums[right]; if (sum > 0 ){ right--; }else if (sum < 0 ){ left++; }else { res.add(Arrays.asList(nums[i], nums[left], nums[right])); while (right > left && nums[right] == nums[right - 1 ]) right--; while (right > left && nums[left] == nums[left + 1 ]) left++; right--; left++; } } } return res; } }
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 : def threeSum (self, nums: List [int ] ) -> List [List [int ]]: res = [] nums.sort() for i in range (len (nums)): if nums[i] > 0 : return res if i > 0 and nums[i] == nums[i - 1 ]: continue left = i + 1 right = len (nums) - 1 while left < right: sum_ = nums[i] + nums[left] + nums[right] if sum_ < 0 : left += 1 elif sum_ > 0 : right -= 1 else : res.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 res
三数之和:一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。
四数之和:两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution { public List<List<Integer>> fourSum (int [] nums, int target) { List<List<Integer>> res = new ArrayList <>(); Arrays.sort(nums); for (int i = 0 ; i < nums.length; i++){ if (nums[i] >= 0 && nums[i] > target){ return res; } if (i > 0 && nums[i] == nums[i - 1 ]){ continue ; } for (int j = i + 1 ; j < nums.length; j++){ if (nums[i] + nums[j] >= 0 && nums[i] + nums[j] > target){ break ; } if (j > i + 1 && nums[j] == nums[j - 1 ]){ continue ; } int left = j + 1 ; int right = nums.length - 1 ; while (left < right){ int sum = nums[i] + nums[j] + nums[left] + nums[right]; if (sum == target){ res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); while (left < right && nums[right] == nums[right - 1 ]) right--; while (left < right && nums[left] == nums[left + 1 ]) left++; right--; left++; }else if (sum > target){ right--; }else { left++; } } } } return res; } }
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 : def fourSum (self, nums: List [int ], target: int ) -> List [List [int ]]: res = [] nums.sort() for i in range (len (nums)): if nums[i] >= 0 and nums[i] > target: return res if i > 0 and nums[i] == nums[i - 1 ]: continue for j in range (i + 1 , len (nums)): if nums[i] + nums[j] >= 0 and nums[i] + nums[j] > target: continue if j > i + 1 and nums[j] == nums[j - 1 ]: continue left = j + 1 right = len (nums) - 1 while left < right: sum_ = nums[i] + nums[j] + nums[left] + nums[right] if sum_ > target: right -= 1 elif sum_ < target: left += 1 else : res.append([nums[i], nums[j], nums[left], nums[right]]) while left < right and nums[right] == nums[right - 1 ]: right -= 1 while left < right and nums[left] == nums[left + 1 ]: left += 1 right -= 1 left += 1 return res
3.10 总结篇 要求只有小写字母时,很合适用数组,因为空间消耗固定
没有限制数值大小时,用set做映射
当需要记录下标时,用map,因为是<key, value>结构(虽然map是万能的,但数组和set当然效率更高)
std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),并不需要key有序时选择std::unordered_map 效率更高
4. 字符串 对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
swap可以有两种实现。
一种就是常见的交换数值:
1 2 3 int tmp = s[i];s[i] = s[j]; s[j] = tmp;
一种就是通过位运算:01,则结果为1;11/00,则结果为0。
1 2 3 s[i] ^= s[j]; s[j] ^= s[i]; s[i] ^= s[j];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public void reverseString (char [] s) { int left = 0 ; int right = s.length - 1 ; while (left < right){ char tmp = s[left]; s[left] = s[right]; s[right] = tmp; left++; right--; } } }
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def reverseString (self, s: List [str ] ) -> 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
在遍历字符串的过程中,只要让 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 class Solution : def reverse_substring (self, s:str , i:int , j:int ) -> str : before = s[:i] reversed_part = s[i:j][::-1 ] after = s[j:] return before + reversed_part + after def reverseStr (self, s: str , k: int ) -> str : n = len (s) // (2 * k) while n > 0 : index = (n - 1 )*2 *k s = self .reverse_substring(s, index, index + k) n -= 1 n = len (s) // (2 * k) last = len (s) - n*2 *k if last > 0 and last < k: s = self .reverse_substring(s, n*2 *k, len (s)) elif last >= k and last < 2 *k: s = self .reverse_substring(s, n*2 *k, n*2 *k + k) return s
改善版:其实每次看2k个,反转前k个就行,最后剩余字符的两种情况也符合
1 2 3 4 5 6 7 8 class Solution : def reverseStr (self, s: str , k: int ) -> str : p = 0 while p < len (s): p2 = p + k s = s[:p] + s[p: p2][::-1 ] + s[p2:] p = p + 2 * k return s
申请新数组的方法:
1 2 3 4 5 6 7 8 9 s = input () result = "" for char in s: if char.isdigit(): result += "number" else : result += char print (result)
正则表达式的方法:
1 2 3 4 5 6 7 import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner sc = new Scanner (System.in); System.out.println(sc.next().replaceAll("\\d" , "number" )); } }
如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java刷题的录友,一定要使用辅助空间,因为Java里的string不能修改)
注意从后往前 填充:从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动。
很多数组填充类的问题,其做法都是先预先给数组扩容到填充后的大小,然后在从后向前进行操作。这么做有两个好处:
不用申请新数组。
从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
1 2 3 4 5 6 class Solution : def reverseWords (self, s: str ) -> str : s_list = s.split() reversed_list = s_list[::-1 ] res = " " .join(reversed_list) return res
1 2 3 4 5 6 7 8 class Solution : def reverseWords (self, s: str ) -> str : s_list = s.split() res = "" for index, word in enumerate (s_list): res = word + " " + res return res.strip()
不要使用辅助空间,空间复杂度要求为O(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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class Solution { public String reverseWords (String s) { StringBuilder sb = removeSpace(s); reverseString(sb, 0 , sb.length() - 1 ); reverseEachWord(sb); return sb.toString(); } private StringBuilder removeSpace (String s) { int start = 0 ; int end = s.length() - 1 ; while (s.charAt(start) == ' ' ) start++; while (s.charAt(end) == ' ' ) end--; StringBuilder sb = new StringBuilder (); while (start <= end) { char c = s.charAt(start); if (c != ' ' || sb.charAt(sb.length() - 1 ) != ' ' ) { sb.append(c); } start++; } return sb; } public void reverseString (StringBuilder sb, int start, int end) { while (start < end) { char temp = sb.charAt(start); sb.setCharAt(start, sb.charAt(end)); sb.setCharAt(end, temp); start++; end--; } } private void reverseEachWord (StringBuilder sb) { int start = 0 ; int end = 1 ; int n = sb.length(); while (start < n) { while (end < n && sb.charAt(end) != ' ' ) { end++; } reverseString(sb, start, end - 1 ); start = end + 1 ; end = start + 1 ; } } }
1 2 3 4 5 k = input () s = input () index = len (s) - int (k) res = s[index:] + s[:index] print (res)
通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,负负得正 ,这样就不影响子串里面字符的顺序了。
这样额外空间只有交换时的O(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 import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner in = new Scanner (System.in); int n = Integer.parseInt(in.nextLine()); String s = in.nextLine(); int len = s.length(); char [] chars = s.toCharArray(); reverseString(chars, 0 , len - 1 ); reverseString(chars, 0 , n - 1 ); reverseString(chars, n, len - 1 ); System.out.println(chars); } public static void reverseString (char [] ch, int start, int end) { while (start < end) { ch[start] ^= ch[end]; ch[end] ^= ch[start]; ch[start] ^= ch[end]; start++; end--; } } }
“mississippi”注意这种用例,不能一直前移haystack,朴素做法:O (m ∗n )
1 2 3 4 5 6 7 8 class Solution : def strStr (self, haystack: str , needle: str ) -> int : if len (haystack) < len (needle): return -1 for i in range (0 , len (haystack) - len (needle) + 1 ): if haystack[i: i+len (needle)] == needle: return i return -1
KMP算法:O (m +n ) 应用于匹配DNA片段、文本搜索
详见本人博客:超简单理解KMP算法(最长公共前后缀next数组、合并主子串、子串偏移法)-CSDN博客
获得next数组:
1 2 3 4 5 6 7 def getNext (self, next , s ): for i in range (1 , len (s)): len_ = next [i - 1 ] while len_ != 0 and s[i] != s[len_]: len_ = next [len_ - 1 ] if s[i] == s[len_]: next [i] = len_ + 1
合并主串子串的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def strStr (self, haystack: str , needle: str ) -> int : n = len (haystack) m = len (needle) if haystack == needle: return 0 s = needle + "#" + haystack next = [0 ] * len (s) for i in range (1 , n + m + 1 ): len_ = next [i - 1 ] while len_ != 0 and s[i] != s[len_]: len_ = next [len_ - 1 ] if s[i] == s[len_]: next [i] = len_ + 1 if next [i] == m: return i - m*2 return -1
子串偏移的做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def getNext (self, next , s ): for i in range (1 , len (s)): len_ = next [i - 1 ] while len_ != 0 and s[i] != s[len_]: len_ = next [len_ - 1 ] if s[i] == s[len_]: next [i] = len_ + 1 def strStr (self, haystack: str , needle: str ) -> int : next = [0 ] * len (needle) self .getNext(next , needle) i = 0 j = 0 for i in range (len (haystack)): while j > 0 and haystack[i] != needle[j]: j = next [j - 1 ] if haystack[i] == needle[j]: j += 1 if j == len (needle): return i - len (needle) + 1 return -1
暴力法:用一个for循环获取子串的终止位置(起始位置肯定是第一个字母,不然不可能重复多次构成,同时这里可以优化:子串至少重复一次,所以终止位置遍历到n/2的中间位置即可),再用一个循环判断子串是否能重复构成字符串。O (n^2)
移动匹配 法:如果将两个s 连在一起,并移除第一个和最后一个字符,那么得到的字符串一定包含s ,即s 是它的一个子串
这种方法只能判断有没有,不能立马得到子串是什么,仅用于此题返回true/false的情况
我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数, 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。也就是4.6中的KMP。
这里构造 s + s 的时间复杂度是 O (n ),在 s + s 中查找 s 的时间复杂度也是 O (n )。所以总的也是O (n )。
1 2 3 class Solution : def repeatedSubstringPattern (self, s: str ) -> bool : return (s + s).find(s, 1 ) != len (s)
KMP法:
充分条件:如果字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串 一定是 s的最小重复子串。
必要条件:如果字符串s的最长相等前后缀不包含的子串 是 s最小重复子串,那么 s是由重复子串组成。
当 最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,那么不包含的子串 就是s的最小重复子串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def getNext (self, next , s ): for i in range (1 , len (s)): len_ = next [i - 1 ] while len_ != 0 and s[i] != s[len_]: len_ = next [len_ - 1 ] if s[i] == s[len_]: next [i] = len_ + 1 def repeatedSubstringPattern (self, s: str ) -> bool : next = [0 ] * len (s) self .getNext(next , s) if next [-1 ] != 0 and len (s) % (len (s) - next [-1 ]) == 0 : return True return False
4.8 总结篇 建议不使用库函数
很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前 进行操作。
双指针法是字符串处理的常客。
5. 双指针法 5.11 总结篇 双指针不能同时存下标和数值
双指针有时是快慢(一个走2,一个走1),有时是从两头向中间逼近,有时是从后往前。虽然惯性思维经常是从前往后
除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为 $O(n)$
6. 栈与队列 6.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 40 41 42 43 class MyQueue : def __init__ (self ): """ in主要负责push,out主要负责pop """ self .stack_in = [] self .stack_out = [] def push (self, x: int ) -> None : """ 有新元素进来,就往in里面push """ self .stack_in.append(x) def pop (self ) -> int : """ Removes the element from in front of queue and returns that element. """ 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 ) -> int : ans = self .pop() self .stack_out.append(ans) return ans def empty (self ) -> bool : """ 只要in或者out有元素,说明队列不为空 """ return not (self .stack_in or self .stack_out)
需要两个栈一个输入栈,一个输出栈 ,在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入) ,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
两个队列:新来的数字入2,然后把1中的数字加入,最后返回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 from collections import dequeclass MyStack : def __init__ (self ): self .q1 = deque() self .q2 = deque() def push (self, x: int ) -> None : self .q2.append(x) while self .q1: self .q2.append(self .q1.popleft()) while self .q2: self .q1.append(self .q2.popleft()) def pop (self ) -> int : return self .q1.popleft() def top (self ) -> int : return self .q1[0 ] def empty (self ) -> bool : return not self .q1
一个队列:模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。O(n)
linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。
这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(..退,就是后面的目录级要出栈)
判断右括号的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def isValid (self, s: str ) -> bool : st = [] for c in s: if c == "]" and st: tmp = st.pop() if tmp != "[" : return False elif c == "}" and st: tmp = st.pop() if tmp != "{" : return False elif c == ")" and st: tmp = st.pop() if tmp != "(" : return False else : st.append(c) return False if st else True
碰到左括号,就把相应的右括号入栈,然后右括号来时对比是否一样即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def isValid (self, s: str ) -> bool : stack = [] mapping = { '(' : ')' , '[' : ']' , '{' : '}' } for item in s: if item in mapping.keys(): stack.append(mapping[item]) elif not stack or stack[-1 ] != item: return False else : stack.pop() return True if not stack else False
这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中 ,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。无限递归会引发调用栈溢出Segmentation fault
而且在企业项目开发中,尽量不要使用递归 !在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
如果不让用栈可以用双指针模拟
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def removeDuplicates (self, s: str ) -> str : st = [] for c in s: if st and c == st[-1 ]: st.pop() else : st.append(c) res = "" for c in st: res += c return res
栈与递归之间在某种程度上是可以转换的!逆波兰表达式相当于是二叉树中的后序遍历。后缀表达式RPN对计算机来说是非常友好的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def evalRPN (self, tokens: List [str ] ) -> int : st = [] sign = ['+' , '-' , '*' , '/' ] for c in tokens: if c in sign: num1 = st.pop() num2 = st.pop() if c == "+" : num = num1 + num2 elif c == "-" : num = num2 - num1 elif c == "*" : num = num1 * num2 elif c == "/" : num = num2 / num1 st.append(int (num)) else : st.append(int (c)) return st[-1 ]
可以学习下面这种调用函数的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from operator import add, sub, muldef div (x, y ): return int (x / y) class Solution (object ): op_map = {'+' : add, '-' : sub, '*' : mul, '/' : div} def evalRPN (self, tokens: List [str ] ) -> int : stack = [] for token in tokens: if token not in {'+' , '-' , '*' , '/' }: stack.append(int (token)) else : op2 = stack.pop() op1 = stack.pop() stack.append(self .op_map[token](op1, op2)) return stack.pop()
不能每k个数都调用一次max,会超时(如k=5000)
每次出一个数,来一个数,和max比较,也会超时
后来我的思路是不需要维护滑动窗口中的每个数,只要维护第一大和第二大即可,这样如果最大数离开了,比较新来的数和第二大数即可
我本来觉得维护第一大和第二大简单,看答案后发现复杂度是一样的,不是非要死板地保证队列长度为2,不出界就行
标答的while和每次都加入巧妙地保证了队列一定单调,且无需重新计算也知道目前窗口里的最值(如果q长度为1就是1,不然长度为3就是top3)
也就是单调队列维护队列递减,不断把新的数与队尾元素比较即可
存下标非常巧妙,因为和滑动窗口的位置息息相关!!这样窗口变更后才知道现有队列中的最值是否出去了,while保证队列里的每个值都被判断,留下来的都是还在滑动窗口里的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from collections import dequefrom typing import List class Solution : def maxSlidingWindow (self, nums: List [int ], k: int ) -> List [int ]: q = deque() result = [] for i in range (len (nums)): while q and q[0 ] < i - k + 1 : q.popleft() while q and nums[i] >= nums[q[-1 ]]: q.pop() q.append(i) if i >= k - 1 : result.append(nums[q[0 ]]) return result
优先级队列:披着队列外衣的堆,看起来就是队列,但内部元素是自动依照元素的权值排列
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,大顶堆就是节点的值不小于左右孩子的值
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。
维护k个有序的序列即可
要用小顶堆,这样每次更新时把最小的元素弹出,留下来的就是前k个最大元素;用大顶堆,每次更新会把最大的元素弹出去,不符合逻辑
所以本题先计算频率,然后把频率放入大小为k的小顶堆中,最后留下的就是前k大的高频元素(对于我的重点是小顶堆怎么建?)
1 2 3 4 5 6 def topKFrequent (nums, k ): count = Counter(nums) return [item for item, freq in heapq.nsmallest(k, count.items(), key=lambda x: (-x[1 ]))]
注意调用heapq库很方便:排序过程的时间复杂度是 O(log k) ,整个算法的时间复杂度是 O(nlog k)。
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 import heapqclass Solution : def topKFrequent (self, nums: List [int ], k: int ) -> List [int ]: map_ = {} for num in nums: map_[num] = map_.get(num, 0 ) + 1 heap = [] for num, freq in map_.items(): if len (heap) < k: heapq.heappush(heap, (freq, num)) else : if freq > heap[0 ][0 ]: heapq.heappushpop(heap, (freq, num)) res = [] while heap: res.append(heapq.heappop(heap)[1 ]) return res
6.9 总结篇 栈里面的元素在内存中是连续分布的么?这个问题有两个陷阱:
陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。(python用list实现栈时是连续的)
陷阱2:缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。
递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中 ,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
7. 二叉树 7.1 理论基础 分类 满二叉树:所以排序的过程的时间复杂度是 $O(\log k)$ ,整个算法的时间复杂度是 $O(n\log k)$ 。深度为k,有2^k-1个节点
完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。堆就是一个完全二叉树。
二叉搜索树:有数值,有序。下面这两棵树都是搜索树
平衡二叉搜索树:AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
如图:
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树 ,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
正是高度差<=1,以及红黑树的规则,保证了O(logn)的复杂度
存储方式 链式存储方式就用指针, 顺序存储的方式就是用数组。顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。一般是用链式表示二叉树,有助于理解。
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
遍历方式
深度优先遍历:先往深走,遇到叶子节点再往回走。
前序遍历(递归法,迭代法)中左右
中序遍历(递归法,迭代法)左中右
后序遍历(递归法,迭代法)左右中
广度优先遍历:一层一层的去遍历。
栈其实就是递归的一种实现结构 ,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
定义 1 2 3 4 5 class TreeNode : def __init__ (self, val, left = None , right = None ): self .val = val self .left = left self .right = right
7.2 二叉树的递归遍历 每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
145. 二叉树的后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def postorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: res = [] def traverse (node: Optional [TreeNode] ): if node is None : return traverse(node.left) traverse(node.right) res.append(node.val) traverse(root) return res
94. 二叉树的中序遍历
1 2 3 4 5 6 7 8 9 10 11 class Solution : def inorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: res = [] def traverse (node: Optional [TreeNode] ): if node is None : return traverse(node.left) res.append(node.val) traverse(node.right) traverse(root) return res
144. 二叉树的前序遍历
1 2 3 4 5 6 7 8 9 10 11 class Solution : def preorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: res = [] def traverse (node: Optional [TreeNode] ): if node is None : return res.append(node.val) traverse(node.left) traverse(node.right) traverse(root) return res
7.3 二叉树的迭代遍历 前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,每次弹出来的加入res,然后将右孩子加入栈,再加入左孩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def preorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: if not root: return [] stack = [root] result = [] while stack: node = stack.pop() result.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return result
中序遍历:需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def inorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: if not root: return [] stack = [] result = [] cur = root while cur or stack: if cur: stack.append(cur) cur = cur.left else : cur = stack.pop() result.append(cur.val) cur = cur.right return result
后序遍历:因为第一个是中比较好处理,所以先得到中右左再反转,跟前序很像,就是先放左节点再放右节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def postorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: if not root: return [] stack = [root] result = [] while stack: n = stack.pop() result.append(n.val) if n.left: stack.append(n.left) if n.right: stack.append(n.right) return result[::-1 ]
此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
7.4 二叉树的统一迭代法 方法一:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法。这样当遇见空指针时,就应该pop出栈+加入result列表。什么时候加空?就是在当前处理的节点(中)后面加None。
先统一把root加进去,然后左中右的顺序因为是栈,反过来加入即可。要处理指只访问了,没有加入到结果集未处理的点。
前序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def preorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: result = [] st= [] if root: st.append(root) while st: node = st.pop() if node != None : if node.right: st.append(node.right) if node.left: st.append(node.left) st.append(node) st.append(None ) else : node = st.pop() result.append(node.val) return result
中序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def inorderTraversal (self, root: TreeNode ) -> List [int ]: result = [] st = [] if root: st.append(root) while st: node = st.pop() if node != None : if node.right: st.append(node.right) st.append(node) st.append(None ) if node.left: st.append(node.left) else : node = st.pop() result.append(node.val) return result
后序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def postorderTraversal (self, root: TreeNode ) -> List [int ]: result = [] st = [] if root: st.append(root) while st: node = st.pop() if node != None : st.append(node) st.append(None ) if node.right: st.append(node.right) if node.left: st.append(node.left) else : node = st.pop() result.append(node.val) return result
方法二:加一个 boolean 值跟随每个节点,false (默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,true 表示该节点的位次之前已经安排过了,可以收割节点了。 这种方法可以叫做boolean 标记法,样例代码见下文C++ 和 Python 的 boolean 标记法。 这种方法更容易理解,在面试中更容易写出来。
还是一开始把root进栈,很好理解的visited数组,如果该节点和两个子节点都安排过了,就可以标为true,准备加入结果集了
前序:中左右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def preorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: result = [] stack = [(root, False )] if root else [] while stack: node, visited = stack.pop() if visited: result.append(node.val) continue if node.right: stack.append((node.right, False )) if node.left: stack.append((node.left, False )) stack.append((node, True )) return result
中序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def inorderTraversal (self, root: TreeNode ) -> List [int ]: result = [] stack = [(root, False )] if root else [] while stack: node, visited = stack.pop() if visited: result.append(node.val) continue if node.right: stack.append((node.right, False )) stack.append((node, True )) if node.left: stack.append((node.left, False )) return result
后序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def postorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: result = [] stack = [(root, False )] if root else [] while stack: node, visited = stack.pop() if visited: result.append(node.val) continue stack.append((node, True )) if node.right: stack.append((node.right, False )) if node.left: stack.append((node.left, False )) return result
7.5 二叉树层序遍历 栈先进后出适合模拟DFS,深度优先
队列先进先出适合模拟BFS,广度优先
难点是如何确定在某一层?用目前队列的长度len!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def levelOrder (self, root: Optional [TreeNode] ) -> List [List [int ]]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level = [] for _ in range (len (queue)): cur = queue.popleft() level.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) result.append(level) return result
自底向上的层序遍历,最后反转一下即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def levelOrderBottom (self, root: Optional [TreeNode] ) -> List [List [int ]]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level = [] for _ in range (len (queue)): cur = queue.popleft() level.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) result.append(level) return result[::-1 ]
取层序遍历的右值,即level[-1],其实不需要像上面一样记录每层完整的level,只需要在i==len(queue)-1时获取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def rightSideView (self, root: Optional [TreeNode] ) -> List [int ]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level_size = len (queue) for i in range (level_size): cur = queue.popleft() if i == level_size - 1 : result.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) return result
就是把每层求累计再求平均
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def averageOfLevels (self, root: Optional [TreeNode] ) -> List [float ]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level_size = len (queue) level_sum = 0 for i in range (level_size): cur = queue.popleft() level_sum += cur.val if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) result.append(level_sum / level_size) return result
从2叉树变为n叉树,遍历每个节点的children即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def levelOrder (self, root: 'Node' ) -> List [List [int ]]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level = [] for _ in range (len (queue)): cur = queue.popleft() level.append(cur.val) for node in cur.children: queue.append(node) result.append(level) return result
定义无穷小值然后去比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def largestValues (self, root: Optional [TreeNode] ) -> List [int ]: if not root: return [] queue = collections.deque([root]) result = [] while queue: max_ = float ('-inf' ) for i in range (len (queue)): cur = queue.popleft() if cur.val > max_: max_ = cur.val if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) result.append(max_) return result
保证是有两个子节点,仍然是遍历level进行指针的排列,最后一个节点指向null
我刚开始是先把同层节点都加入level,再遍历level进行连接,但其实不用额外的空间level存每层节点,遍历时用prev存即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def connect (self, root: 'Optional[Node]' ) -> 'Optional[Node]' : if not root: return root queue = collections.deque([root]) while queue: level = [] for _ in range (len (queue)): cur = queue.popleft() level.append(cur) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) for i in range (len (level)): if i != len (level) - 1 : level[i].next = level[i + 1 ] else : level[i].next = None return root
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def connect (self, root: 'Optional[Node]' ) -> 'Optional[Node]' : if not root: return root queue = collections.deque([root]) while queue: prev = None for _ in range (len (queue)): cur = queue.popleft() if prev: prev.next = cur prev = cur if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) return root
要求只能使用常量级的额外空间,上面的两种方法均符合,时间复杂度为O(n),把树上的每个节点都遍历了
仍然用层序遍历看,最大深度就是二叉树的层数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def maxDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 queue = collections.deque([root]) res = 0 while queue: for i in range (len (queue)): cur = queue.popleft() if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) res += 1 return res
哪一层最先出现叶子节点,该层数就是最小深度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def minDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 queue = collections.deque([root]) res = 0 while queue: res += 1 for i in range (len (queue)): cur = queue.popleft() if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) if not cur.left and not cur.right: return res return res
我使用的方法是迭代法的层序遍历,把每个节点的左右孩子翻转,用前中后序也是可以的
1 2 3 4 5 6 7 8 9 10 11 class Solution : def invertTree (self, root: Optional [TreeNode] ) -> Optional [TreeNode]: if not root: return root queue = collections.deque([root]) while queue: node = queue.popleft() node.left, node.right = node.right, node.left if node.left: queue.append(node.left) if node.right: queue.append(node.right) return root
递归法的前序遍历举例:
1 2 3 4 5 6 7 8 class Solution : def invertTree (self, root: Optional [TreeNode] ) -> Optional [TreeNode]: if not root: return root root.left, root.right = root.right, root.left self .invertTree(root.left) self .invertTree(root.right) return root
7.7 二叉树周末总结 589. N 叉树的前序遍历
1 2 3 4 5 6 7 8 9 10 11 class Solution : def preorder (self, root: 'Node' ) -> List [int ]: res = [] def traverse (root: 'Node' ): if not root: return res.append(root.val) for node in root.children: traverse(node) traverse(root) return res
590. N 叉树的后序遍历
1 2 3 4 5 6 7 8 9 10 11 class Solution : def postorder (self, root: 'Node' ) -> List [int ]: res = [] def traverse (root: 'Node' ): if not root: return for node in root.children: traverse(node) res.append(root.val) traverse(root) return res
在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。
一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。所以要用后序遍历。
递归三部曲
确定递归函数的参数和返回值:bool
确定终止条件:
左节点为空,右节点不为空,不对称,return false
左不为空,右为空,不对称 return false
左右都为空,对称,返回true
左右都不为空,比较节点数值,不相同就return false
确定单层递归的逻辑:
比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
如果左右都对称就返回true ,有一侧不对称就返回false 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def isSymmetric (self, root: Optional [TreeNode] ) -> bool : if not root: return True return self .compare(root.left, root.right) def compare (self, left, right ): if left == None and right != None : return False elif left != None and right == None : return False elif left == None and right == None : return True elif left.val != right.val: return False outside = self .compare(left.left, right.right) inside = self .compare(left.right, right.left) return outside and inside
迭代法:我使用层序遍历获得level判断是否对称,但这样有额外的空间开销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def isSymmetric (self, root: Optional [TreeNode] ) -> bool : if not root: return True queue = collections.deque([root]) while queue: level_size = len (queue) if level_size % 2 != 0 : return False level = [] for i in range (level_size): cur = queue.popleft() if not cur: level.append(None ) else : queue.append(cur.left) queue.append(cur.right) level.append(cur.val) if level != level[::-1 ]: return False return True
不适用level记录,直接根据终止条件获得左右子树节点然后判断的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def isSymmetric (self, root: Optional [TreeNode] ) -> bool : if not root: return True queue = collections.deque() queue.append(root.left) queue.append(root.right) while queue: leftNode = queue.popleft() rightNode = queue.popleft() if not leftNode and not rightNode: continue if not leftNode or not rightNode or leftNode.val != rightNode.val: return False queue.append(leftNode.left) queue.append(rightNode.right) queue.append(leftNode.right) queue.append(rightNode.left) return True
104. 二叉树的最大深度
递归法:左右中:后序遍历,取最大值作为深度
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def maxDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 if not root.left and not root.right: return 1 l, r = 1 , 1 if root.left: l = self .maxDepth(root.left) + 1 if root.right: r = self .maxDepth(root.right) + 1 return max (l, r)
迭代法:用queue,每轮存一层的所有节点,层数就是深度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def maxDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 depth = 0 queue = collections.deque([root]) while queue: depth += 1 for _ in range (len (queue)): n = queue.popleft() if n.left: queue.append(n.left) if n.right: queue.append(n.right) return depth
559. N 叉树的最大深度
递归法:
1 2 3 4 5 6 7 8 9 10 class Solution : def maxDepth (self, root: 'Node' ) -> int : if not root: return 0 if not root.children: return 1 res = 0 for n in root.children: res = max (res, self .maxDepth(n)) return res + 1
迭代法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def maxDepth (self, root: 'Node' ) -> int : if not root: return 0 depth = 0 queue = collections.deque([root]) while queue: depth += 1 for _ in range (len (queue)): n = queue.popleft() if n.children: for node in n.children: queue.append(node) return depth
难点在于左右孩子不为空的逻辑
可以设置正无穷,这样只有一边的节点时不会取默认值0,但测出来效率低
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def minDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 if not root.left and not root.right: return 1 l, r = float ('+inf' ), float ('+inf' ) if root.left: l = self .minDepth(root.left) if root.right: r = self .minDepth(root.right) return min (l, r) + 1
也可以老实写判断逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def minDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 leftDepth = self .minDepth(root.left) rightDepth = self .minDepth(root.right) if root.left is None and root.right is not None : return 1 + rightDepth if root.left is not None and root.right is None : return 1 + leftDepth result = 1 + min (leftDepth, rightDepth) return result
迭代法就是层序遍历到第一个叶子节点就返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def minDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 queue = collections.deque([root]) depth = 0 while queue: depth += 1 for _ in range (len (queue)): n = queue.popleft() if not n.left and not n.right: return depth if n.left: queue.append(n.left) if n.right: queue.append(n.right) return depth
求节点个数:O(n)
1 2 3 4 5 class Solution : def countNodes (self, root: Optional [TreeNode] ) -> int : if not root: return 0 return self .countNodes(root.left) + self .countNodes(root.right) + 1
利用完全二叉树:只有两种情况
一种是满二叉树:节点个数可以直接用2^树深度-1来计算
一种是最后一层叶子节点没有满,但递归到某一深度后一定会有左右孩子为满二叉树,用上式计算
这个情况无需考虑因为不是完全二叉树
别看最后还是后序遍历的递归,但用满二叉树的公式计算左右子树是一种剪枝,节约了许多递归开销
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def countNodes (self, root: Optional [TreeNode] ) -> 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
或者把left/right写在一个while里循环
1 2 3 4 5 6 7 8 9 10 11 class Solution : def countNodes (self, root: Optional [TreeNode] ) -> int : if not root: return 0 count = 0 left = root.left; right = root.right while left and right: count+=1 left = left.left; right = right.right if not left and not right: return (2 <<count)-1 return 1 +self .countNodes(root.left)+self .countNodes(root.right)
平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。
二叉树节点的深度:指从根节点到该节点 的最长简单路径边的条数。用前序遍历,是层数
二叉树节点的高度:指从该节点到叶子节点 的最长简单路径边的条数。用后序遍历,是倒数第几层
我本来的算法把递归重复了两遍,其实没必要,因为getDepth的过程中就可以进行平衡二叉树的判断了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def isBalanced (self, root: Optional [TreeNode] ) -> bool : if not root: return True def getHeight (root ) -> int : if not root: return 0 if not root.left and not root.right: return 1 return max (getHeight(root.left), getHeight(root.right)) + 1 l = getHeight(root.left) + 1 r = getHeight(root.right) + 1 if abs (l - r) > 1 : return False else : return self .isBalanced(root.right) and self .isBalanced(root.left)
定义getHeight依然返回高度,如果不是平衡二叉树就返回-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def isBalanced (self, root: Optional [TreeNode] ) -> bool : if not root: return True def getHeight (root ) -> int : if not root: return 0 if not root.left and not root.right: return 1 l = getHeight(root.left) r = getHeight(root.right) if abs (l - r) > 1 or l == -1 or r == -1 : return -1 else : return max (l, r) + 1 return False if getHeight(root) == -1 else True
此题用迭代法效率很低,虽然理论上所有递归都可以迭代实现,但这里需要遍历每个节点然后计算当前节点左右子树的高度,没细看感觉很麻烦
从根节点到叶子节点:前序遍历(中左右)递归+回溯
递归法+回溯:回溯这里的path.pop()是关键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def binaryTreePaths (self, root: Optional [TreeNode] ) -> List [str ]: def traversal (self, cur, path, result ): path.append(cur.val) if not cur.left and not cur.right: sPath = '->' .join(map (str , path)) result.append(sPath) return if cur.left: self .traversal(cur.left, path, result) path.pop() if cur.right: self .traversal(cur.right, path, result) path.pop() def binaryTreePaths (self, root ): result = [] path = [] if not root: return result self .traversal(root, path, result) return result
迭代法:刚开始我用stack不知道如何实现,发现答案用path_st存储遍历到每个节点的路径,而不是仅用一个res保存当前的访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def binaryTreePaths (self, root: Optional [TreeNode] ) -> List [str ]: if not root: return [] res = [] stack = [root] path_st = [str (root.val)] while stack: n = stack.pop() path = path_st.pop() if not n.left and not n.right: res.append(path) if n.right: stack.append(n.right) path_st.append(path + '->' + str (n.right.val)) if n.left: stack.append(n.left) path_st.append(path + '->' + str (n.left.val)) return res
7.14 二叉树周末总结 1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def isSameTree (self, p: Optional [TreeNode], q: Optional [TreeNode] ) -> bool : def isSame (node1: Optional [TreeNode], node2: Optional [TreeNode] ) -> bool : if node1 is None and node2 is not None : return False if node2 is None and node1 is not None : return False if node1 is None and node2 is None : return True if node1.val != node2.val: return False return isSame(node1.left, node2.left) and isSame(node1.right, node2.right) return isSame(p, q)
注意判断子树的节点和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 class Solution : def isSubtree (self, root: Optional [TreeNode], subRoot: Optional [TreeNode] ) -> bool : def isSame (node1: Optional [TreeNode], node2: Optional [TreeNode] ) -> bool : if not node1 and not node2: return True if not node1 or not node2: return False return ( node1.val == node2.val and isSame(node1.left, node2.left) and isSame(node1.right, node2.right) ) if not subRoot: return True if not root: return False return ( isSame(root, subRoot) or self .isSubtree(root.left, subRoot) or self .isSubtree(root.right, subRoot) )
给定二叉树的根节点 root ,返回所有左叶子之和。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : res = 0 def sumOfLeftLeaves (self, root: Optional [TreeNode] ) -> int : self .res = 0 def sum (node: Optional [TreeNode] ): if node is None : return if node.left and node.left.left is None and node.left.right is None : self .res += node.left.val sum (node.left) sum (node.right) sum (root) return self .res
后面发现这道题也没必要另外定义一个函数
1 2 3 4 5 6 7 8 9 class Solution : res = 0 def sumOfLeftLeaves (self, root: Optional [TreeNode] ) -> int : res = 0 if root is None : return 0 if root.left and root.left.left is None and root.left.right is None : res += root.left.val return res + self .sumOfLeftLeaves(root.left) + self .sumOfLeftLeaves(root.right)
用迭代很好理解,递归没看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def findBottomLeftValue (self, root: Optional [TreeNode] ) -> int : queue = collections.deque([root]) res = 0 while queue: for i in range (len (queue)): 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
判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。也是典型的回溯+dfs
递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window) 中介绍)
如果要搜索其中一条符合条件的路径 ,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
其实不一定要回溯,因为路径是连续的不会断掉:
1 2 3 4 5 6 7 class Solution : def hasPathSum (self, root: Optional [TreeNode], targetSum: int ) -> bool : if not root: return False if not root.left and not root.right: return targetSum == root.val return self .hasPathSum(root.left, targetSum - root.val) or self .hasPathSum(root.right, targetSum - root.val)
回溯:这里要搜索其中一条符合条件的路径 ,所以递归返回的是bool。path+这里体现的就是回溯,因为没有直接改变path
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def hasPathSum (self, root: Optional [TreeNode], targetSum: int ) -> bool : if not root: return False return self .dfs(root, targetSum, [root.val]) def dfs (self, root, target, path ): if not root: return False if sum (path) == target and not root.left and not root.right: return True left_flag, right_flag = False , False if root.left: left_flag = self .dfs(root.left, target, path + [root.left.val]) if root.right: right_flag = self .dfs(root.right, target, path + [root.right.val]) return left_flag or right_flag
也可以写成:看剩余的count,这里没用path作为中间值存储,所以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 class Solution : def traversal (self, cur: TreeNode, count: int ) -> bool : if not cur.left and not cur.right and count == 0 : return True if not cur.left and not cur.right: return False if cur.left: count -= cur.left.val if self .traversal(cur.left, count): return True count += cur.left.val if cur.right: count -= cur.right.val if self .traversal(cur.right, count): return True count += cur.right.val return False def hasPathSum (self, root: Optional [TreeNode], targetSum: int ) -> bool : if root is None : return False return self .traversal(root, targetSum - root.val)
和上面相比就是要把满足条件的记录下来,就不能写简单的递归了,用dfs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def pathSum (self, root: Optional [TreeNode], targetSum: int ) -> List [List [int ]]: if not root: return [] self .res = [] self .traversal(root, targetSum, []) return self .res def traversal (self, root, target, path ): if not root: return path.append(root.val) if sum (path) == target and not root.left and not root.right: self .res.append(path.copy()) self .traversal(root.left, target, path) self .traversal(root.right, target, path) path.pop()
重点是如何切割和找边界值
中序数组用root切割
后序数组用左中序数组的大小切割(因为都是左开始)
得到左子树和右子树的中序和后序后,就可以递归了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def buildTree (self, inorder: List [int ], postorder: List [int ] ) -> Optional [TreeNode]: if not postorder: return None root_val = postorder[-1 ] root = TreeNode(root_val) idx = inorder.index(root_val) inorder_left = inorder[:idx] inorder_right = inorder[idx + 1 :] postorder_left = postorder[:len (inorder_left)] postorder_right = postorder[len (inorder_left):len (postorder) - 1 ] root.left = self .buildTree(inorder_left, postorder_left) root.right = self .buildTree(inorder_right, postorder_right) return root
105. 从前序与中序遍历序列构造二叉树
1 2 3 4 5 6 7 8 9 class Solution : def buildTree (self, preorder: List [int ], inorder: List [int ] ) -> Optional [TreeNode]: if not preorder: return None left_size = inorder.index(preorder[0 ]) left = self .buildTree(preorder[1 : 1 + left_size], inorder[:left_size]) right = self .buildTree(preorder[1 + left_size:], inorder[1 + left_size:]) return TreeNode(preorder[0 ], left, right)
前序和中序、中序和后序可以唯一确定一棵二叉树,但前序和后序不能唯一确定二叉树
使用切片:
1 2 3 4 5 6 7 8 9 class Solution : def constructMaximumBinaryTree (self, nums: List [int ] ) -> Optional [TreeNode]: if not nums: return None root = max (nums) idx = nums.index(root) return TreeNode(root, self .constructMaximumBinaryTree(nums[:idx]), self .constructMaximumBinaryTree(nums[idx + 1 :]))
7.20 二叉树周末总结 切片的题还是比较简单的,我对回溯不太熟练
我的写法:
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def mergeTrees (self, root1: Optional [TreeNode], root2: Optional [TreeNode] ) -> Optional [TreeNode]: if root1 and not root2: return root1 if root2 and not root1: return root2 if not root1 and not root2: return None root = TreeNode(root1.val + root2.val) root.left = self .mergeTrees(root1.left, root2.left) root.right = self .mergeTrees(root1.right, root2.right) return root
简洁写法:
1 2 3 4 5 6 7 8 9 10 class Solution : def mergeTrees (self, root1: TreeNode, root2: TreeNode ) -> TreeNode: if not root1: return root2 if not root2: return root1 root1.val += root2.val root1.left = self .mergeTrees(root1.left, root2.left) root1.right = self .mergeTrees(root1.right, root2.right) return root1
首先是一个我的错误写法:找到目标节点后没有立即返回,如果不加return就相当于遍历整棵树了
1 2 3 4 5 6 7 8 9 class Solution : def searchBST (self, root: Optional [TreeNode], val: int ) -> Optional [TreeNode]: if not root: return None if root.val == val: return root else : self .searchBST(root.left, val) self .searchBST(root.right, val)
利用二叉搜索树右子树大于root,左子树小于root的性质
1 2 3 4 5 6 7 8 9 10 class Solution : def searchBST (self, root: Optional [TreeNode], val: int ) -> Optional [TreeNode]: if not root: return None if root.val == val: return root elif root.val < val: return self .searchBST(root.right, val) else : return self .searchBST(root.left, val)
迭代法也很简单好理解:
1 2 3 4 5 6 7 class Solution : def searchBST (self, root: TreeNode, val: int ) -> TreeNode: while root: if val < root.val: root = root.left elif val > root.val: root = root.right else : return root return None
首先是我的错误写法:只检查了当前节点与其直接子节点的关系 ,而没有保证整个左子树是否都小于当前节点,或者整个右子树是否都大于当前节点
1 2 3 4 5 6 7 8 9 10 11 class Solution : def isValidBST (self, root: Optional [TreeNode] ) -> bool : if not root: return True if not root.left and not root.right: return True if root.left and root.left.val >= root.val: return False if root.right and root.right.val <= root.val: return False return self .isValidBST(root.left) and self .isValidBST(root.right)
正确写法:引入min/max表示整个子树的上界和下界
1 2 3 4 5 6 class Solution : def isValidBST (self, root: Optional [TreeNode], left=-inf, right=inf ) -> bool : if root is None : return True x = root.val return left < x < right and self .isValidBST(root.left, left, x) and self .isValidBST(root.right, x, right)
二叉搜索树一定有左边<root<右边,中序遍历是升序的,最小查值一定出现在中序遍历时相邻的两个数之间,所以用prev记录前一个节点的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def getMinimumDifference (self, root: Optional [TreeNode] ) -> int : prev = None min_diff = float ('inf' ) def inorder (node ): nonlocal prev, min_diff if not node: return inorder(node.left) if prev is not None : min_diff = min (min_diff, abs (node.val - prev)) prev = node.val inorder(node.right) inorder(root) return min_diff
众数:出现频率最高的数
如果不是BST,就遍历,然后用map统计频率,将频率排序,最后返回最高频元素的集合
是BST,那中序遍历就是有序的,用pre和cur计数,然后因为可能不止一个,要返回集合,所以用列表存储
假设可以使用额外空间,简单,用map统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def searchBST (self, cur, freq_map ): if cur is None : return freq_map[cur.val] += 1 self .searchBST(cur.left, freq_map) self .searchBST(cur.right, freq_map) def findMode (self, root: Optional [TreeNode] ) -> List [int ]: freq_map = defaultdict(int ) result = [] if root is None : return result self .searchBST(root, freq_map) max_freq = max (freq_map.values()) for key, freq in freq_map.items(): if freq == max_freq: result.append(key) return result
不适用额外空间:中序遍历,其中技术的步骤在处理当前节点中:
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 : def findMode (self, root: Optional [TreeNode] ) -> List [int ]: self .result = [] self .maxCount = 0 self .count = 0 self .pre = None def searchBST (cur: Optional [TreeNode] ): if cur is None : return searchBST(cur.left) if self .pre is None : self .count = 1 elif self .pre.val == cur.val: self .count += 1 else : self .count = 1 self .pre = cur if self .count == self .maxCount: self .result.append(cur.val) if self .count > self .maxCount: self .maxCount = self .count self .result = [cur.val] searchBST(cur.right) return searchBST(root) return self .result
如果pq不在在同一侧子树中,不然最近公共节点就是root
左右中:后序遍历 ,是天然的回溯,自底向上查找,如果找到就返回
为什么要遍历整棵树?因为如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : if root is None or root == q or root == p: return root left = self .lowestCommonAncestor(root.left, p, q) right = self .lowestCommonAncestor(root.right, p, q) if left and right: return root if left: return left if right: return right return None
7.27 周末总结 BST注意是全局的性质,有时不能只比相邻节点;然后中序遍历是有序数组很重要,结合prev/cur进行中序遍历即可完成一些有序的任务
平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。
平衡二叉树与完全二叉树的区别在于底层节点的位置?
是的,完全二叉树底层必须是从左到右连续的,且次底层是满的(像一层一层铺砖,不能跳过任何位置)
堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树 。
首先把二叉树的答案默写了一遍,当然是能过的,但这里是二叉搜索树,我想到的是可以多剪枝一下:把p/q和root做比较
我写的:
1 2 3 4 5 6 7 8 9 10 class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : if root is None or root == p or root == q: return root if p.val < root.val < q.val or q.val < root.val < p.val: return root if p.val < root.val and q.val < root.val: return self .lowestCommonAncestor(root.left, p, q) if p.val > root.val and q.val > root.val: return self .lowestCommonAncestor(root.right, p, q)
但其实可以观察到除了左右子树的情况其他都是return root,只是我分类讨论了,精简版的代码:
1 2 3 4 5 6 7 class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : if p.val < root.val and q.val < root.val: return self .lowestCommonAncestor(root.left, p, q) if p.val > root.val and q.val > root.val: return self .lowestCommonAncestor(root.right, p, q) return root
把一个节点插入:刚开始想的很复杂,要调整二叉树结构,但其实遍历二叉搜索树,找到空节点 插入元素就可以了。就是找到空位就行,符合大小规律。
1 2 3 4 5 6 7 8 9 class Solution : def insertIntoBST (self, root: Optional [TreeNode], val: int ) -> Optional [TreeNode]: if root is None : return TreeNode(val) if root.val > val: root.left = self .insertIntoBST(root.left, val) if root.val < val: root.right = self .insertIntoBST(root.right, val) return root
如果该节点同时有左右节点:这个比较复杂,要找左子树最大/右子树最小进行覆盖
如果该节点只有一个子节点,那就用那一个子节点覆盖
如果没有子节点,直接删除
这里的递归注意也是和上一题一样赋值给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 class Solution : def deleteNode (self, root: Optional [TreeNode], key: int ) -> Optional [TreeNode]: if not root: return if root.val < key: root.right = self .deleteNode(root.right, key) elif root.val > key: root.left = self .deleteNode(root.left, key) else : if not root.left and not root.right: return None if not root.left: return root.right if not root.right: return root.left cur = root.right while cur.left: cur = cur.left root.val = cur.val root.right = self .deleteNode(root.right, cur.val) return root
不在范围内的节点要删除,有唯一答案
如果root.val>=low,那肯定有root.right.val >= low,只需要看root.right.val <= high
如果root.val < low,那root+整个左子树可以删除,用右子树第一个满足范围的节点替代(这里有错!因为右子树可能还有其他点不满足范围,不能直接就return了,要继续递归)
如果root.val<=high,那肯定有root.left.val <= high,只需要看root.left.val >= low
如果root.val > high,那root+整个右子树可以删除
我的错误写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def trimBST (self, root: Optional [TreeNode], low: int , high: int ) -> Optional [TreeNode]: if not root: return None if low <= root.val <= high: root.right = self .trimBST(root.right, low, high) root.left = self .trimBST(root.left, low, high) if low > root.val: while root and root.val < low: root = root.right return root if high < root.val: while root and root.val > high: root = root.left return root return root
正确写法是继续递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def trimBST (self, root: Optional [TreeNode], low: int , high: int ) -> 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
中间值是root,分割两边的数组,递归
1 2 3 4 5 6 7 8 9 class Solution : def sortedArrayToBST (self, nums: List [int ] ) -> Optional [TreeNode]: if not nums: return None idx = len (nums) // 2 root = TreeNode(nums[idx]) root.left = self .sortedArrayToBST(nums[: idx]) root.right = self .sortedArrayToBST(nums[idx + 1 :]) return root
就是一个有序数组,求从后到前的累加数组 ,累加顺序是右中左(反中序遍历)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def convertBST (self, root: Optional [TreeNode] ) -> Optional [TreeNode]: s = 0 def dfs (node: TreeNode ) -> None : if node is None : return dfs(node.right) nonlocal s s += node.val node.val = s dfs(node.left) dfs(root) return root
7.34 二叉树:总结篇 深度:从上到下 高度:从下到上
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。不过这个类型比较多。
求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。
8. 回溯算法 8.1 回溯算法理论基础 回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案 ,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。并不是很高效。
回溯法,一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
组合是不强调元素顺序的,排列是强调元素顺序 。
回溯法解决的问题都可以抽象为树形结构 (N叉树),因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度 。
for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历 ,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
1 2 3 4 5 6 7 8 9 10 11 12 void backtracking(参数) { if (终止条件) { 存放结果; return ; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
[1,n]中所有可能的k个数的组合
我的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def combine (self, n: int , k: int ) -> List [List [int ]]: self .res = [] self .cur = [] self .num = [] for i in range (1 , n + 1 ): self .num.append(i) def backtracking (k: int , start: int ): if len (self .cur) == k: self .res.append(self .cur.copy()) return for i, n in enumerate (self .num[start:]): self .cur.append(n) backtracking(k, i + start + 1 ) self .cur = self .cur[:-1 ] backtracking(k, 0 ) return self .res
可以优化的地方:
用pop撤销节点
num数组不用创建,用startindex记录当前遍历到哪个数字了即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def combine (self, n: int , k: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (k: int , start: int ): if len (self .cur) == k: self .res.append(self .cur.copy()) return for i in range (start, n + 1 ): self .cur.append(i) backtracking(k, i + 1 ) self .cur.pop() backtracking(k, 1 ) return self .res
8.3 组合(优化) 剪枝:很多情况下继续遍历是没有意义的,因为剩下的数字不足以构成一个长度为k的组合。
k - len(path):还需要选择的数字个数。
n - (k - len(path)) + 1:最大的起始点,确保从i开始至少还有k - len(path)个数字可供选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def combine (self, n: int , k: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (k: int , start: int ): if len (self .cur) == k: self .res.append(self .cur.copy()) return for i in range (start, n - (k - len (self .cur)) + 2 ): self .cur.append(i) backtracking(k, i + 1 ) self .cur.pop() backtracking(k, 1 ) return self .res
注意for循环是惰性对的,只进入for时计算一次,之后即使self.cur在递归中被修改,也不会重复计算
刚开始照着上面的模板写会超时,加了剪枝后通过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def combinationSum3 (self, k: int , n: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (k: int , n: int , start: int ): if sum (self .cur) == n and len (self .cur) == k: self .res.append(self .cur.copy()) return if sum (self .cur) >= n or len (self .cur) >= k: return for i in range (start, 11 - (k - len (self .cur))): self .cur.append(i) backtracking(k, n, i + 1 ) self .cur.pop() backtracking(k, n, 1 ) return self .res
hot100时的答案:简短但省略了添加和回溯撤销的操作,下标也用数组划分代替了
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 : def letterCombinations (self, digits: str ) -> List [str ]: if not digits: return [] phone = {'2' :['a' ,'b' ,'c' ], '3' :['d' ,'e' ,'f' ], '4' :['g' ,'h' ,'i' ], '5' :['j' ,'k' ,'l' ], '6' :['m' ,'n' ,'o' ], '7' :['p' ,'q' ,'r' ,'s' ], '8' :['t' ,'u' ,'v' ], '9' :['w' ,'x' ,'y' ,'z' ]} def backtrack (conbination, nextdigit ): if len (nextdigit) == 0 : res.append(conbination) else : for letter in phone[nextdigit[0 ]]: backtrack(conbination + letter,nextdigit[1 :]) res = [] backtrack('' , digits) return res
更好理解的版本:
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 : def letterCombinations (self, digits: str ) -> List [str ]: if not digits: return [] self .phone = {'2' :['a' ,'b' ,'c' ], '3' :['d' ,'e' ,'f' ], '4' :['g' ,'h' ,'i' ], '5' :['j' ,'k' ,'l' ], '6' :['m' ,'n' ,'o' ], '7' :['p' ,'q' ,'r' ,'s' ], '8' :['t' ,'u' ,'v' ], '9' :['w' ,'x' ,'y' ,'z' ]} self .res = [] self .cur = "" def backtracking (digits: str , start: int ): if len (self .cur) == len (digits): self .res.append(self .cur) return digit = digits[start] letters = self .phone[digit] for letter in letters: self .cur += letter backtracking(digits, start + 1 ) self .cur = self .cur[:-1 ] backtracking(digits, 0 ) return self .res
8.6 回溯周末总结 for循环横向遍历,递归纵向遍历,回溯不断调整结果集 。
首先写了一版代码,但没有去重,会有[3,5],[5,3]结果同时出现在res里。所以在加入res时加了Counter的比较操作,刚开始只比较counter是超时的,后来加了比较len的就不超时了(还跟我每次用sum求和有关,如果用一个变量记录和计算开销会少一些)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def combinationSum (self, candidates: List [int ], target: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (candidates: List [int ], target: int ): if sum (self .cur) == target: for l in self .res: if len (l) == len (self .cur) and Counter(l) == Counter(self .cur): return self .res.append(self .cur.copy()) return if sum (self .cur) > target: return for n in candidates: self .cur.append(n) backtracking(candidates, target) self .cur.pop() backtracking(candidates, target) return self .res
其实应该用startIndex防止重复,不应该从0开始而应该从i开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def combinationSum (self, candidates: List [int ], target: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (candidates: List [int ], target: int , startIndex: int ): if sum (self .cur) > target: return if sum (self .cur) == target: self .res.append(self .cur.copy()) return for i in range (startIndex, len (candidates)): self .cur.append(candidates[i]) backtracking(candidates, target, i) self .cur.pop() backtracking(candidates, target, 0 ) return self .res
进一步剪枝:把candidates排序,这样如果当前循环和超过target了,后面都不用看了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def combinationSum (self, candidates: List [int ], target: int ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (candidates: List [int ], target: int , startIndex: int ): if sum (self .cur) > target: return if sum (self .cur) == target: self .res.append(self .cur.copy()) return for i in range (startIndex, len (candidates)): if sum (self .cur) + candidates[i] > target: return self .cur.append(candidates[i]) backtracking(candidates, target, i) self .cur.pop() candidates.sort() backtracking(candidates, target, 0 ) return self .res
和上一题相比只能使用一次,感觉去重那里没处理好,下面是错误的写法(面对全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 : def combinationSum2 (self, candidates: List [int ], target: int ) -> List [List [int ]]: candidates.sort() self .cur = [] self .res = [] def backtracking (candidates: List [int ], target: int , startIndex: int ): if sum (self .cur) == target: self .res.append(self .cur.copy()) return if sum (self .cur) > target: return for i in range (startIndex, len (candidates)): if candidates[i] + sum (self .cur) > target: return self .cur.append(candidates[i]) backtracking(candidates, target, i + 1 ) self .cur.pop() backtracking(candidates, target, 0 ) unique_arr = [] for sub in self .res: if sub not in unique_arr: unique_arr.append(sub) return unique_arr
理论上应该与前一个数比较,但第一次尝试时是可以重复的,不知道该怎么写
要在搜索过程中去重,同一个树枝无所谓,但同一个树层要去重,方法是用used bool数组,但我觉得不如用i和startIndex比较好理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def combinationSum2 (self, candidates: List [int ], target: int ) -> List [List [int ]]: candidates.sort() self .cur = [] self .res = [] def backtracking (candidates: List [int ], target: int , startIndex: int ): if sum (self .cur) == target: self .res.append(self .cur.copy()) return if sum (self .cur) > target: return for i in range (startIndex, len (candidates)): if i > startIndex and candidates[i] == candidates[i - 1 ]: continue if candidates[i] + sum (self .cur) > target: return self .cur.append(candidates[i]) backtracking(candidates, target, i + 1 ) self .cur.pop() backtracking(candidates, target, 0 ) return self .res
分割的地方有1到n-1种,怎么选不同的分割方式?类似组合问题
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…..。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…..。
终止条件是分割线已经到字符串末尾
截取子串也不简单,注意start/end是开区间还是闭区间
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 : def isHuiWen (self, s: str , start: int , end: int ) -> bool : i = start j = end while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True def partition (self, s: str ) -> List [List [str ]]: self .res = [] self .cur = [] def backtracking (s: str , start: int ): if start == len (s): self .res.append(self .cur.copy()) return for i in range (start, len (s)): if self .isHuiWen(s, start, i): self .cur.append(s[start : i + 1 ]) backtracking(s, i + 1 ) self .cur.pop() backtracking(s, 0 ) return self .res
有效的地址是每位在0-255之间,有四位,顺序不能变,在s中插入’.’
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 : def restoreIpAddresses (self, s: str ) -> List [str ]: self .res = [] self .cur = [] def backtracking (s: str , start: int ): if len (self .cur) == 4 : self .res.append('.' .join(map (str , self .cur))) return for i in range (start, len (s)): if len (self .cur) == 3 : cur_str = s[start: ] i = len (s) else : cur_str = s[start: i + 1 ] if len (cur_str) > 1 and cur_str[0 ] == '0' : return if len (cur_str) <= 3 and 0 <= int (cur_str) <= 255 : self .cur.append(cur_str) backtracking(s, i + 1 ) self .cur.pop() if i == len (s): break backtracking(s, 0 ) return self .res
优化:每个数长度最多为3,可以写作 for i in range(start, min(start + 3, len(s))):
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def subsets (self, nums: List [int ] ) -> List [List [int ]]: self .cur = [] self .res = [[]] def backtracking (nums: List [int ], start: int ): if self .cur: self .res.append(self .cur.copy()) for i in range (start, len (nums)): self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() backtracking(nums, 0 ) return self .res
8.12 回溯周末总结 如果是一个集合来求组合的话,就需要startIndex,如组合问题;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
在求和问题中,排序之后加剪枝是常见的套路!
if i > startIndex and candidates[i] == candidates[i - 1]:#i>startIndex说明第一个数已经选过了,后面就是判断同一数层不能重复了 树层去重是该题的重点
这里也是使用树层去重,同组合总和II(用used数组用过的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def subsetsWithDup (self, nums: List [int ] ) -> List [List [int ]]: self .cur = [] self .res = [[]] nums.sort() def backtracking (nums: List [int ], start: int ): if self .cur: self .res.append(self .cur.copy()) for i in range (start, len (nums)): if i > start: if nums[i] == nums[i - 1 ]: continue self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() backtracking(nums, 0 ) return self .res
用used数组的方法:used数组存的是树层的使用情况,并不是树枝的(树枝的进入backtracking就会更新)不过上面i>start更好懂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def subsetsWithDup (self, nums ): result = [] path = [] nums.sort() self .backtracking(nums, 0 , path, result) return result def backtracking (self, nums, startIndex, path, result ): result.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 .backtracking(nums, i + 1 , path, result) path.pop()
不能改变数组现有的顺序,nums =[1,2,3,1,1,1,1,1]这个用例总是报错,这道题数组没有排序 ,所以不能用if i > start and nums[i] == nums[i - 1]:continue去重,要用set记录用过的数字,不然像[1, 1], [1, 1, 1], [1, 1, 1, 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 class Solution : def findSubsequences (self, nums: List [int ] ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (nums: List [int ], start: int ): if len (self .cur) >= 2 : self .res.append(self .cur.copy()) for i in range (start, len (nums)): if i > start and nums[i] == nums[i - 1 ]: continue if len (self .cur) == 0 : self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() elif nums[i] > self .cur[-1 ]: self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() elif nums[i] == self .cur[-1 ]: if len (self .cur) == 1 : self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() elif self .cur[-1 ] != self .cur[-2 ]: self .cur.append(nums[i]) backtracking(nums, i + 1 ) self .cur.pop() backtracking(nums, 0 ) return self .res
另外子函数(嵌套函数)可以访问父函数(外层函数)的变量 (我上面的题解都写麻烦了,不用传,而且也不用写self),包括父函数传递进来的参数(如nums)。这是因为Python支持闭包(closure) ,即嵌套函数可以访问其外层作用域中的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def findSubsequences (self, nums: List [int ] ) -> List [List [int ]]: self .res = [] self .cur = [] def backtracking (start: int ): if len (self .cur) >= 2 : self .res.append(self .cur.copy()) used = set () for i in range (start, len (nums)): if nums[i] in used: continue if not self .cur or nums[i] >= self .cur[-1 ]: used.add(nums[i]) self .cur.append(nums[i]) backtracking(i + 1 ) self .cur.pop() backtracking(0 ) return self .res
因为每个元素要使用一次,但不按顺序,所以回溯时是从0开始找没用过的数字,把used定义在外面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def permute (self, nums: List [int ] ) -> List [List [int ]]: self .cur = [] self .res = [] self .used = [] def backtracking (start ): if len (self .cur) == len (nums): self .res.append(self .cur.copy()) return for i in range (start, len (nums)): if nums[i] in self .used: continue self .used.append(nums[i]) self .cur.append(nums[i]) backtracking(0 ) self .cur.pop() self .used.pop() backtracking(0 ) return self .res
根据下文的观察self.cur和self.used的操作完全一致,所以self.cur其实就足够表达数字有没有使用过了
因为start也一直从0传,这个也可以省略
比dfs方法要好理解些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def permute (self, nums: List [int ] ) -> List [List [int ]]: self .cur = [] self .res = [] def backtracking (): if len (self .cur) == len (nums): self .res.append(self .cur.copy()) return for i in range (len (nums)): if nums[i] in self .cur: continue self .cur.append(nums[i]) backtracking() self .cur.pop() backtracking() return self .res
区别在于要排序+通过相邻节点判断去重
如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重,用used[i - 1] == true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def permuteUnique (self, nums: List [int ] ) -> List [List [int ]]: self .cur = [] self .res = [] self .used = [False ] * len (nums) nums.sort() def backtracking (start ): if len (self .cur) == len (nums): self .res.append(self .cur.copy()) return for i in range (start, len (nums)): if i> start and nums[i] == nums[i - 1 ] and not self .used[i - 1 ]: continue if self .used[i]: continue self .used[i] = True self .cur.append(nums[i]) backtracking(0 ) self .cur.pop() self .used[i] = False backtracking(0 ) return self .res
8.17 周末总结 排列问题的不同:
每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了
一般说道回溯算法的复杂度,都说是指数级别的时间复杂度
8.18 去重问题的另一种写法 使用set去重的版本相对于used数组的版本效率都要低很多,不仅时间复杂度高了,空间复杂度也高了
从JFK开始一笔画,经过所有路径的字典排序(返回字典序最小的组合)
每个航线是一条线,最后的行程必须包含每条线
下面的写法有一个用例过不了,原始回溯法每次都要排序和尝试所有邻接节点,时间复杂度较高(接近 O(n!))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def findItinerary (self, tickets: List [List [str ]] ) -> List [str ]: ticket_dict = defaultdict(list ) for item in tickets: ticket_dict[item[0 ]].append(item[1 ]) res = ['JFK' ] def backtracking (cur_from ): if len (res) == len (tickets) + 1 : return True ticket_dict[cur_from].sort() for _ in ticket_dict[cur_from]: cur_to = ticket_dict[cur_from].pop(0 ) res.append(cur_to) if backtracking(cur_to): return True res.pop() ticket_dict[cur_from].append(cur_to) return False backtracking('JFK' ) return res
使用Hierholzer算法 来寻找欧拉路径(找到一条路径能够遍历所有的边恰好一次)为什么这样能保证找到欧拉路径?
DFS 确保所有边被访问 :由于每次访问一条边后就会“拆掉”这条边(从邻接表中移除),所以不会重复访问。
后序遍历保证路径连通性 :在 DFS 回溯时才记录节点,可以确保路径是连续的(不会遗漏中间节点)。
反转路径得到正确顺序 :因为 DFS 是后序存储的,所以最终需要反转才能得到从起点到终点的路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def findItinerary (self, tickets: List [List [str ]] ) -> List [str ]: graph = defaultdict(list ) for src, dst in tickets: graph[src].append(dst) for src in graph: graph[src].sort(reverse=True ) itinerary = [] def dfs (node ): while graph[node]: next_node = graph[node].pop() dfs(next_node) itinerary.append(node) dfs("JFK" ) return itinerary[::-1 ]
把n个皇后放置在n*n的棋盘上,不在同一行/同一列/同一斜线,输出所有可能的解法
检查是否不互相攻击:不用同行检查,因为每行只选一个元素;因为后面的行还没选,是从前往后选的,所以检查同一列、45度、135度也只用看前面的行
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 class Solution : def solveNQueens (self, n: int ) -> List [List [str ]]: res = [] chessboard = ['.' * n for _ in range (n)] def isValid (row: int , col: int ): 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 def backtracking (row: int ): if row == n: res.append(chessboard.copy()) return for col in range (n): if isValid(row, col): chessboard[row] = chessboard[row][:col] + "Q" + chessboard[row][col+1 :] backtracking(row + 1 ) chessboard[row] = chessboard[row][:col] + "." + chessboard[row][col+1 :] backtracking(0 ) return res
遍历行和列,遍历1-9,看(i, j)这个位置放k是否合适,如果1-9都不行就说明这个棋盘找不到解决数独问题的解
判断合法:同行、同列是否重复,九宫格里是否重复
题目数据 保证 输入数独仅有一个解,所以只有一种情况会递归成功直到所有空白格已填充
下面的解法通过空间换时间,用row/col/palace让判断合法变得相对简单
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 : def solveSudoku (self, board: List [List [str ]] ) -> None : nums = {"1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" } row = [set () for _ in range (9 )] col = [set () for _ in range (9 )] palace = [[set () for _ in range (3 )] for _ in range (3 )] blank = [] for i in range (9 ): for j in range (9 ): ch = board[i][j] if ch == "." : blank.append((i, j)) else : row[i].add(ch) col[j].add(ch) palace[i//3 ][j//3 ].add(ch) def dfs (n ): if n == len (blank): return True i, j = blank[n] rst = nums - row[i] - col[j] - palace[i//3 ][j//3 ] if not rst: return False for num in rst: board[i][j] = num row[i].add(num) col[j].add(num) palace[i//3 ][j//3 ].add(num) if dfs(n+1 ): return True row[i].remove(num) col[j].remove(num) palace[i//3 ][j//3 ].remove(num) dfs(0 )
8.22 回溯总结 如果是一个集合来求组合的话,就需要startIndex;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
“树层去重”和“树枝去重”
使用set去重的版本相对于used数组的版本效率都要低很多
9. 贪心算法 9.1 贪心算法理论基础 如何能看出局部最优是否能推出整体最优呢?要手动模拟,如果可行就贪心,不可行可能需要动态规划,举不出反例就可以试试贪心
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解
尽可能多的满足孩子,就要从胃口值小的分起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def findContentChildren (self, g: List [int ], s: List [int ] ) -> int : g.sort() s.sort() g_idx = 0 s_idx = 0 res = 0 while g_idx < len (g) and s_idx < len (s): if g[g_idx] <= s[s_idx]: res += 1 g_idx += 1 s_idx += 1 else : s_idx += 1 return res
可以直接在原始数组上删除一些元素,局部最优:删除单调坡度上的节点(不包含两端)。不需要删除,统计数组的峰值数量即可。
但有一些特殊情况,要考虑平坡!所以只在坡度 摆动变化的时候,更新 prediff,不在单调区间有平坡时变化
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def wiggleMaxLength (self, nums: List [int ] ) -> int : if len (nums) == 1 : return 1 max_len = 1 curDiff = 0 preDiff = 0 for i in range (len (nums) - 1 ): curDiff = nums[i + 1 ] - nums[i] if (curDiff < 0 and preDiff >= 0 ) or (curDiff > 0 and preDiff <= 0 ): max_len += 1 preDiff = curDiff return max_len
负数一定会拉低总和,只要连续和为正就对后面的元素起到增大总和的作用
1 2 3 4 5 6 7 8 9 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : max_res = -inf sum = 0 for n in nums: sum += n max_res = max (sum , max_res) sum = max (0 , sum ) return max_res
简洁版dp:
1 2 3 4 5 6 7 8 9 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : ans = -inf dp = 0 for n in nums: dp = max (dp, 0 ) + n ans = max (ans, dp) return ans
9.5 贪心周总结 局部最优和全局最优两个关键点。
不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数
只要差值为正就可以加上,贪心:只要正利润
最终利润是可以分解的,可以把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑。根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1])…..(prices[1] - prices[0])。
1 2 3 4 5 6 class Solution : def maxProfit (self, prices: List [int ] ) -> int : res = 0 for i in range (len (prices) - 1 ): res += max (prices[i + 1 ] - prices[i], 0 ) return res
不太懂怎么跳。其实跳几步无所谓,关键在于可跳的覆盖范围!每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。在移动单位的同时,覆盖的点也都走了,范围也更新了。
1 2 3 4 5 6 7 8 9 10 class Solution : def canJump (self, nums: List [int ] ) -> bool : cover = 0 i = 0 while i <= cover: cover = max (cover, nums[i] + i) if cover >= len (nums) - 1 : return True i += 1 return False
以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点,用修桥理解
难点是维护cur和next,用i表示目前到的位置,用next收集目前可达的位置的最远距离;然后i==cur时说明走不动了,要用next更新,很巧妙
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def jump (self, nums: List [int ] ) -> int : cur_distance = 0 ans = 0 next_distance = 0 for i in range (len (nums) - 1 ): next_distance = max (nums[i] + i, next_distance) if i == cur_distance: cur_distance = next_distance ans += 1 return ans
注意跳跃游戏I和II都要遍历每个i
选择k个数字,替换成相反数(如果有负值就替换为正值,尽量重复替换,如果没有负值就替换最小的正值)
对于[4, 2, 3, -2, 1, -4, -3, -6, -4],按照绝对值排序后结果是[-6, 4, -4, -4, 3, -3, 2, -2, 1]
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def largestSumAfterKNegations (self, nums: List [int ], k: int ) -> int : nums.sort(key=lambda x: abs (x), reverse=True ) for i in range (len (nums)): if k == 0 : break if nums[i] < 0 and k > 0 : nums[i] *= -1 k -= 1 if k > 0 and k % 2 == 1 : nums[-1 ] *= -1 return sum (nums)
9.10 贪心周总结 其实代码都不难,难的是思路,需要反复练习和观察
如何获取数组中所有1的下标:list.index(val)只能获取第一个,如果val不在list里还会报错
1 indices = [i for i, x in enumerate (lst) if x == 1 ]
我的错误做法是找到cost最小的下标,但其实不一定,正确方法如下:i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。这是局部最优的方法,但可以推出全局最优。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def canCompleteCircuit (self, gas: List [int ], cost: List [int ] ) -> int : curSum = 0 start = 0 if sum (cost) > sum (gas): return -1 for i in range (len (gas)): curSum += gas[i] - cost[i] if curSum < 0 : start = i + 1 curSum = 0 return start
规则定义: 设学生 A 和学生 B 左右相邻,A 在 B 左边;
左规则: 当 ratings B > ratings A 时,B 的糖比 A 的糖数量多。
右规则: 当 ratings A > ratings B 时,A 的糖比 B 的糖数量多。
先从左到右遍历ratings得到left,满足左规则;再从右到左遍历得到right,满足右规则,最后取left和right的最大值,这样同时满足左右规则(使用两次贪心,从局部最优推出全局最优,思路还是很巧妙的)
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def candy (self, ratings: List [int ] ) -> int : left = [1 ] * len (ratings) right = left.copy() for i in range (1 , len (ratings)): if ratings[i] > ratings[i - 1 ]: left[i] = left[i - 1 ] + 1 res = left[-1 ] for i in range (len (ratings) - 2 , -1 , -1 ): if ratings[i] > ratings[i + 1 ]: right[i] = right[i + 1 ] + 1 res += max (left[i], right[i]) return res
记录5和10的数量即可,然后用规则找零
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def lemonadeChange (self, bills: List [int ] ) -> bool : num_5 = 0 num_10 = 0 for bill in bills: if bill == 5 : num_5 += 1 elif bill == 10 : if num_5 == 0 : return False num_5 -= 1 num_10 += 1 elif bill == 20 : if num_10 >= 1 and num_5 >= 1 : num_5 -= 1 num_10 -= 1 elif num_5 >= 3 : num_5 -= 3 else : return False return True
我写的版本:如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from collections import defaultdictfrom typing import List class Solution : def reconstructQueue (self, people: List [List [int ]] ) -> List [List [int ]]: people.sort(key=lambda x: x[1 ]) heights = [people[i][0 ] for i in range (len (people))] heights.sort() print (heights) res = [] left_index = list (range (len (people))) print (left_index) for height in heights: for i in range (len (people)): if people[i][0 ] == height and people[i][1 ] != -1 : k = people[i][1 ] res.append([people[i][0 ], left_index[k]]) left_index.remove(left_index[k]) people[i][1 ] = -1 print (res) sol = Solution() sol.reconstructQueue([[7 ,0 ],[4 ,4 ],[7 ,1 ],[5 ,0 ],[6 ,1 ],[5 ,2 ]])
答案按身高高的来排序,这样后续插入的节点不影响
1 2 3 4 5 6 7 class Solution : def reconstructQueue (self, people: List [List [int ]] ) -> List [List [int ]]: people.sort(key=lambda x: (-x[0 ], x[1 ])) res = [] for hi, ki in people: res.insert(ki, [hi, ki]) return res
9.15 贪心周总结 9.16 vector原理 避免vector的底层扩容,尽量在原数组修改,但在原数组肯定要细致点
贪心:局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。
第一个点就放在第一个区间的右端点处,第二个点就放在剩余区间的第一个区间的右端点处。依此类推。
1 2 3 4 5 6 7 8 9 10 class Solution : def findMinArrowShots (self, points: List [List [int ]] ) -> int : points.sort(key=lambda p: p[1 ]) pre = -inf ans = 0 for start, end in points: if start > pre: pre = end ans += 1 return ans
我的写法是移除end大的区间,不断更新end进行比较:
1 2 3 4 5 6 7 8 9 10 11 class Solution : def eraseOverlapIntervals (self, intervals: List [List [int ]] ) -> int : intervals.sort(key=lambda p: p[1 ]) ans = 0 pre_end = intervals[0 ][1 ] for start, end in intervals[1 :]: if start < pre_end: ans += 1 else : pre_end = end return ans
也可以计算不重叠的区间个数,然后减掉,同样是按end排序
1 2 3 4 5 6 7 8 9 10 class Solution : def eraseOverlapIntervals (self, intervals: List [List [int ]] ) -> int : intervals.sort(key=lambda x: x[1 ]) ans = 0 pre_r = -inf for l, r in intervals: if l >= pre_r: ans += 1 pre_r = r return len (intervals) - ans
为了满足同一字母最多出现在一个片段中,选最长的区间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def partitionLabels (self, s: str ) -> List [int ]: dict = defaultdict(int ) for i, c in enumerate (s): dict [c] = i res = [] curr_idx = 0 pre_idx = 0 for i, c in enumerate (s): if i > curr_idx: res.append(curr_idx - pre_idx + 1 ) curr_idx += 1 pre_idx = curr_idx if curr_idx < dict [c]: curr_idx = dict [c] res.append(curr_idx - pre_idx + 1 ) return res
更简洁的写法:用end==i来判断区间该合并:
1 2 3 4 5 6 7 8 9 10 11 class Solution : def partitionLabels (self, s: str ) -> List [int ]: last = {c: i for i, c in enumerate (s)} ans = [] start = end = 0 for i, c in enumerate (s): end = max (end, last[c]) if end == i: ans.append(end - start + 1 ) start = i + 1 return ans
不断比较上一个的end和下一个的start即可
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def merge (self, intervals: List [List [int ]] ) -> List [List [int ]]: intervals.sort(key=lambda x: (x[0 ], x[1 ])) pre_start, pre_end = intervals[0 ] res = [] for start, end in intervals: if start <= pre_end: pre_end = max (end, pre_end) else : res.append([pre_start, pre_end]) pre_start, pre_end = start, end res.append([pre_start, pre_end]) return res
9.21 贪心周总结 区间:画图+排序
10. 动态规划 10.1 动态规划理论基础 动态规划中每一个状态一定是由上一个状态推导出来的
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
1 2 3 4 5 6 7 8 9 class Solution : def fib (self, n: int ) -> int : if n == 0 : return 0 dp = [0 ] * (n + 1 ) dp[1 ] = 1 for i in range (2 , n + 1 ): dp[i] = dp[i - 1 ] + dp[i - 2 ] return dp[n]
1 2 3 4 5 6 7 8 class Solution : def climbStairs (self, n: int ) -> int : dp = [0 ] * (n + 1 ) dp[0 ] = 1 dp[1 ] = 1 for i in range (2 , n + 1 ): dp[i] = dp[i - 1 ] + dp[i - 2 ] return dp[n]
dp[0] 表示“用若干步恰好到达楼顶”的方法数。唯一方法是一步都不走,所以是1。
扩展:每次你可以爬至多m (1 <= m < n)个台阶。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def climbStairs (self, n: int , m: int ) -> int : dp = [0 ] * (n + 1 ) dp[0 ] = 1 dp[1 ] = 1 for i in range (2 , n + 1 ): for j in range (1 , m + 1 ): dp[i] += dp[i - j] return dp[n] n, m = map (int , input ().split()) sol = Solution() print (sol.climbStairs(n, m))
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]) dp表示到达第i个位置支付的最小费用 dp[0] = 0 dp[1]=0 dp[2]=10 dp[3]就是结果
1 2 3 4 5 6 class Solution : def minCostClimbingStairs (self, cost: List [int ] ) -> int : dp = [0 ] * (len (cost) + 1 ) 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)]
还可以优化空间复杂度,用dp0和dp1迭代,不用建数组了
10.5 周总结 debug技巧:检查递推公式,打印dp数组手动推理检查
完全背包问题:对比0-1背包,物品只能选 1 次
物品可以无限次使用 (类比:每次可以爬 1、2、…、m 阶,不限次数)。
求组合数 (类比:爬楼梯的顺序不同,算不同的方法)。
dp[m][n]表示到达(m,n)的路径数,dp[m][n]=dp[m-1][n]+dp[m][n-1].当m=0时,dp[0][n]=1;当n=0时,dp[m][0]=1
1 2 3 4 5 6 7 8 9 10 11 class Solution : def uniquePaths (self, m: int , n: int ) -> int : dp = [[0 ] * n for _ in range (m)] for i in range (m): dp[i][0 ] = 1 for i in range (n): dp[0 ][i] = 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 ]
我看hot100的题解用的是dfs,这里要注意使用@cache
我的错误答案:写了很多判断条件,但还是考虑不周全
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 : def uniquePathsWithObstacles (self, obstacleGrid: List [List [int ]] ) -> int : m = len (obstacleGrid) n = len (obstacleGrid[0 ]) dp = [[0 ] * n for _ in range (m)] if m == 1 : for i in range (n): if obstacleGrid[0 ][i] == 1 : return 0 if n == 1 : for i in range (m): if obstacleGrid[i][0 ] == 1 : return 0 if obstacleGrid[0 ][0 ] == 1 : return 0 for i in range (m): if obstacleGrid[i][0 ] == 1 : dp[i][0 ] = 0 obstacleGrid[i][0 ] = 1 else : dp[i][0 ] = 1 for i in range (n): if obstacleGrid[0 ][i] == 1 : dp[0 ][i] = 0 else : dp[0 ][i] = 1 for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i][j] == 1 : dp[i][j] = 0 elif obstacleGrid[i - 1 ][j] == 1 and obstacleGrid[i][j - 1 ] == 1 : dp[i][j] = 0 obstacleGrid[i][j] = 1 elif obstacleGrid[i - 1 ][j] == 1 : dp[i][j] = dp[i][j - 1 ] elif obstacleGrid[i][j - 1 ] == 1 : dp[i][j] = dp[i - 1 ][j] else : dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m - 1 ][n - 1 ]
后来我决定按规则先遍历obstacleGrid,如果上面和左边同时为1,或者在边界遇见1,则标为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 24 25 26 27 28 class Solution : def uniquePathsWithObstacles (self, obstacleGrid: List [List [int ]] ) -> int : m = len (obstacleGrid) n = len (obstacleGrid[0 ]) dp = [[0 ] * n for _ in range (m)] if obstacleGrid[0 ][0 ] == 1 : return 0 for i in range (1 , m): if obstacleGrid[i - 1 ][0 ] == 1 : obstacleGrid[i][0 ] = 1 for i in range (1 , n): if obstacleGrid[0 ][i - 1 ] == 1 : obstacleGrid[0 ][i] = 1 for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i - 1 ][j] == 1 and obstacleGrid[i][j - 1 ] == 1 : obstacleGrid[i][j] == 1 for i in range (m): if obstacleGrid[i][0 ] != 1 : dp[i][0 ] = 1 for i in range (n): if obstacleGrid[0 ][i] != 1 : dp[0 ][i] = 1 for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i][j] != 1 : dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m - 1 ][n - 1 ]
简洁版:其实不用遍历更新obstacleGrid,因为如果两边dp为0的话,加起来自然为0
关键点是先处理边界时遇到1就停止赋值,剩下的dp均为0,用else break写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def uniquePathsWithObstacles (self, obstacleGrid: List [List [int ]] ) -> int : m = len (obstacleGrid) n = len (obstacleGrid[0 ]) dp = [[0 ] * n for _ in range (m)] if obstacleGrid[0 ][0 ] == 1 or obstacleGrid[m - 1 ][n - 1 ] == 1 : return 0 for i in range (m): if obstacleGrid[i][0 ] != 1 : dp[i][0 ] = 1 else : break for i in range (n): if obstacleGrid[0 ][i] != 1 : dp[0 ][i] = 1 else : break for i in range (1 , m): for j in range (1 , n): if obstacleGrid[i][j] != 1 : dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m - 1 ][n - 1 ]
拆分成两种(j * (i - j))或者拆分成三种及以上(j * dp[i - j])所以dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j})
1 2 3 4 5 6 7 8 class Solution : def integerBreak (self, n: int ) -> int : dp = [0 ] * (n + 1 ) dp[2 ] = 1 for i in range (3 , n + 1 ): for j in range (1 , i // 2 + 1 ): dp[i] = max (dp[i], (i - j) * j, dp[i - j] * j) return dp[n]
我的状态转移方程推错了,dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量 dp[2]*dp[0]
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量 dp[1]*dp[1]
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量 dp[0]*dp[2]
1 2 3 4 5 6 7 8 class Solution : def numTrees (self, n: int ) -> int : dp = [0 ] * (n + 1 ) dp[0 ] = 1 for i in range (1 , n + 1 ): for j in range (1 , i + 1 ): dp[i] += dp[j - 1 ] * dp[i - j] return dp[n]
10.10 周总结 感觉还是trick居多,状态转移方程难,要多练
10.11 0-1背包理论基础(一) 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次 ,求解将哪些物品装入背包里物品价值总和最大。
暴力的解法是取或者不取,回溯搜索
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 不取该物品,取该物品
初始化关注:i=0,j=0的特殊情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 m, n = map (int , input ().split()) weight = list (map (int , input ().split())) value = list (map (int , input ().split())) dp = [[0 ] * (n + 1 ) for _ in range (m)] for j in range (weight[0 ], n + 1 ): dp[0 ][j] = value[0 ] for i in range (1 , m): for j in range (1 , n + 1 ): if weight[i] > j: dp[i][j] = dp[i - 1 ][j] else : dp[i][j] = max (dp[i - 1 ][j], dp[i - 1 ][j - weight[i]] + value[i]) print (dp[m - 1 ][n])
这里的背包和物品遍历顺序可以交换,以及物品一定要从小到大,但容量j无所谓,可以从小到大或者从大到小,因为不管顺序如何dp[i-1][j] 和 dp[i-1][j - weight[i]] 都已在上一轮计算完成。
10.12 动态规划:01背包理论基础(滚动数组) 滚动数组:把二维dp降为一维dp,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
1 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
注意和二维不同的是:
遍历背包要倒序!不然物品会被重复加入!
只能先遍历物品再遍历背包,不然每个dp[j]只会被最后一个物品更新
总结:先物品再背包,背包倒序
1 2 3 4 5 6 7 8 9 10 m, n = map (int , input ().split()) weight = list (map (int , input ().split())) value = list (map (int , input ().split())) dp = [0 ] * (n + 1 ) for i in range (m): for j in range (n, -1 , -1 ): if weight[i] <= j: dp[j] = max (dp[j], dp[j - weight[i]] + value[i]) print (dp[n])
就是01背包,只是背包值是数组总和的一半
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def canPartition (self, nums: List [int ] ) -> bool : sum_ = sum (nums) len_ = len (nums) if sum_ % 2 == 1 : return False total = sum_ // 2 dp = [False ] * (total + 1 ) dp[0 ] = True for i in range (len_): for j in range (total, -1 , -1 ): if nums[i] <= j: dp[j] = dp[j] or dp[j - nums[i]] return dp[total]
我的想法是先排序,然后两两做减法
但这道题其实是01背包的思路,尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。
此时的问题:有一堆石头,每个石头都有自己的重量,是否可以 装满 最大重量为 sum / 2的背包。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def lastStoneWeightII (self, stones: List [int ] ) -> int : len_ = len (stones) sum_ = sum (stones) target = sum_ // 2 dp = [0 ] * (target + 1 ) for stone in stones: for j in range (target, -1 , -1 ): if stone <= j: dp[j] = max (dp[j], dp[j - stone] + stone) return abs (sum_ - 2 * dp[target])
本题其实和416. 分割等和子集 (opens new window) 几乎是一样的,只是最后对dp[target]的处理方式不同。
416. 分割等和子集 (opens new window) 相当于是求背包是否正好装满,而本题是求背包最多能装多少 。
10.15 周总结
返回nums可以得到target的表达式数目
dp[i][j]表示前i个数可以得到j的数目,我本来想的是dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]],但是效率比较低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def findTargetSumWays (self, nums: List [int ], target: int ) -> int : total = sum (nums) if abs (target) > total: return 0 n = len (nums) dp = [[0 ] * (2 * total + 1 ) for _ in range (n + 1 )] dp[0 ][total] = 1 for i in range (1 , n + 1 ): for j in range (2 * total + 1 ): if j - nums[i-1 ] >= 0 : dp[i][j] += dp[i-1 ][j - nums[i-1 ]] if j + nums[i-1 ] <= 2 * total: dp[i][j] += dp[i-1 ][j + nums[i-1 ]] return dp[n][target + total]
另一种方法:因为left-right=target,left+right=sum,所以left=(sum+target)//2,求总和为这个的表达式的数目
注意在这个场景下,left,right>=0,所以要剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def findTargetSumWays (self, nums: List [int ], target: int ) -> int : total = sum (nums) if (total + target) % 2 != 0 or (total + target) < 0 : return 0 left = (total + target) // 2 dp = [0 ] * (left + 1 ) dp[0 ] = 1 for num in nums: for j in range (left, -1 , -1 ): if num <= j: dp[j] += dp[j - num] return dp[left]
统计strs中元素的0/1个数
m和n代表两个背包,dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。创建时还是以m+1,n+1为基础
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1) zero/one相当于物品的重量,字符串个数1相当于物品的价值
外层是物品,正序遍历;内层是背包,倒序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def findMaxForm (self, strs: List [str ], m: int , n: int ) -> int : dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for str in strs: zeroNum, oneNum = 0 , 0 for c in str : if c == '0' : zeroNum += 1 else : oneNum += 1 for i in range (m, -1 , -1 ): for j in range (n, -1 , -1 ): if i >= zeroNum and j >= oneNum: dp[i][j] = max (dp[i][j], dp[i-zeroNum][j-oneNum] + 1 ) return dp[m][n]
10.18 完全背包理论基础 每件物品有无数个,dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
dp[i][j]=max(dp[i][j-weight[i]]+value[i], dp[i-1][j]) 依然是分为不取该物品,取该物品
如果背包容量为0,那总和一定为0
遍历顺序:先遍历物品,或者先遍历背包都可以
往往还是m + 1, n + 1比较好写,不用处理边界条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def totalbeibao (n, v, value, weight ): dp = [[0 ] * (v + 1 ) for _ in range (n + 1 )] for i in range (1 , n + 1 ): for j in range (1 , v + 1 ): if j >= weight[i - 1 ]: dp[i][j] = max (dp[i - 1 ][j], dp[i][j - weight[i - 1 ]] + value[i - 1 ]) else : dp[i][j] = dp[i - 1 ][j] return dp[n][v] n, v = map (int , input ().split()) weight = [] value = [] for _ in range (n): wi, vi = map (int , input ().split()) weight.append(wi) value.append(vi) print (totalbeibao(n, v, value, weight))
dp[i][j]表示用前i个数组成金额为j的组合数 dp[i][j]=max(dp[i-1][j],dp[i][j-coins[i]] + 1)
这里我的递推公式列错了!应该是选当前硬币i或者不选,而且需要注意dp[i][0]=1,这样j-coin才能表示正好选到
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def change (self, amount: int , coins: List [int ] ) -> int : m = len (coins) dp = [[0 ] * (amount + 1 ) for _ in range (m + 1 )] dp[0 ][0 ] = 1 for i in range (1 , m + 1 ): for j in range (amount + 1 ): if j >= coins[i - 1 ]: dp[i][j] = dp[i - 1 ][j] + dp[i][j - coins[i - 1 ]] else : dp[i][j] = dp[i - 1 ][j] return dp[m][amount]
压缩成一维dp:dp[j] += dp[j - coins[i]]
装满背包容量为0 的方法是1,即不放任何物品,dp[0] = 1
1 2 3 4 5 6 7 8 9 class Solution : def change (self, amount: int , coins: List [int ] ) -> int : dp = [0 ] * (amount + 1 ) dp[0 ] = 1 for coin in coins: for j in range (coin, amount + 1 ): if j >= coin: dp[j] += dp[j - coin] return dp[amount]
组合与排列的区别:
组合:硬币的顺序不重要,即 2+2+1 和 2+1+2 是同一种组合。
排列:顺序重要,视为不同的方式。
为了计算组合数,需要将硬币的循环放在外层,金额的循环放在内层。
完全背包: 每种物品可以选无限次,因此内层循环是正序的,这样在计算 dp[j] 时,dp[j - coin] 已经考虑了当前硬币的多次使用。
0-1背包: 每种物品只能选一次,因此内层循环是倒序的,这样在计算 dp[j] 时,dp[j - coin] 还没有被当前硬币更新,确保只使用一次。
如果求组合数就是外层for循环遍历物品,内层for遍历背包 。
如果求排列数就是外层for遍历背包,内层for循环遍历物品 。
10.20 周总结 dp[i][j]表示用0-i数表示j的个数,dp[i][j]=max(dp[i-1][j],dp[i][j-num])
dp[j] += dp[j-num] 顺序不同,说明是排序,要外层是背包内层是数字
1 2 3 4 5 6 7 8 9 class Solution : def combinationSum4 (self, nums: List [int ], target: int ) -> int : dp = [0 ] * (target + 1 ) dp[0 ] = 1 for j in range (target + 1 ): for num in nums: if j >= num: dp[j] += dp[j - num] return dp[target]
10.22 爬楼梯 之前斐波拉契的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def climbStairs (self, n: int , m: int ) -> int : dp = [0 ] * (n + 1 ) dp[0 ] = 1 dp[1 ] = 1 for i in range (2 , n + 1 ): for j in range (1 , m + 1 ): dp[i] += dp[i - j] return dp[n] n, m = map (int , input ().split()) sol = Solution() print (sol.climbStairs(n, m))
dp[i][j]表示可以爬0-i级台阶,不限次数,到达第j级的方法次数,属于排列(这里我最开始弄错了,12/21不同)
dp[i][j]= for stair in 0-i级台阶,dp[i][j-stair]的总和,外层是背包,内层是物品0-i
1 2 3 4 5 6 7 8 9 10 11 def climbStairs (m, n ): dp = [0 ] * (n + 1 ) dp[0 ] = 1 for j in range (1 , n + 1 ): for i in range (1 , m + 1 ): if j >= i: dp[j] += dp[j - i] return dp[n] n, m = map (int , input ().split()) print (climbStairs(m, n))
dp[i][j]=min(dp[i-1][j],dp[i][j-num]+1) # 表示用0-i的硬币组成j需要的最少的硬币个数 本题是要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def coinChange (self, coins: List [int ], amount: int ) -> int : m = len (coins) dp = [[float ("+inf" )]*(amount + 1 ) for _ in range (m + 1 )] for i in range (m + 1 ): dp[i][0 ] = 0 for i in range (1 , m + 1 ): for j in range (amount + 1 ): if j >= coins[i - 1 ]: dp[i][j] = min (dp[i-1 ][j], dp[i][j - coins[i - 1 ]] + 1 ) else : dp[i][j] = dp[i - 1 ][j] return dp[m][amount] if dp[m][amount] != float ("+inf" ) else -1
这里也是可以优化成一维dp的,因为每次更新 dp[j] 用到的 dp[j - coins[i]] 是上一轮尚未被更新的旧值 。dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
问题类型
可否优化为一维
遍历顺序要求
原因说明
完全背包
✅ 可以
j 从小到大
可以重复选,用旧值不会出错
0-1 背包
✅ 可以
j 从大到小
不能重复选,防止用到当前轮新值
多重背包
✅ 可转为 0-1
类似于 0-1 背包
分解成多个 0-1 背包处理
依赖两维状态
❌ 一般不能
无法一维表示
如 dp[i][j] = f(dp[i-1][j-1])
子序列类问题
一般可优化
视转移方向决定顺序
如 LCS、LIS 等,可压缩一维
1 2 3 4 5 6 7 8 9 class Solution : def coinChange (self, coins: List [int ], amount: int ) -> int : dp = [float ("+inf" )]*(amount + 1 ) dp[0 ] = 0 for coin in coins: for j in range (1 , amount + 1 ): if j >= coin: dp[j] = min (dp[j], dp[j - coin] + 1 ) return dp[amount] if dp[amount] != float ("+inf" ) else -1
物品是每个完全平方数,149…背包是n 完全平方数的最大值是根号n
1 2 3 4 5 6 7 8 9 class Solution : def numSquares (self, n: int ) -> int : dp = [float ("+inf" )]*(n + 1 ) dp[0 ] = 0 for i in range (1 , int (math.sqrt(n)) + 1 ): for j in range (1 , n + 1 ): if j >= i ** 2 : dp[j] = min (dp[j], dp[j - i ** 2 ] + 1 ) return dp[n]
这里的遍历顺序依然不重要,可以先物品或者先背包 sqrt也可以用n**0.5代替
10.25 周总结 求最小数不同于求排列数和求组合数,遍历顺序不重要
单词是物品,可以重复使用,字符串是背包。强调顺序,所以是排列,外层是背包,内层是物品
dp[i][j]表示用0-i个单词可以组成字符串的前j位 dp[i][j]=dp[i-1][j] or dp[i][j-len(word)] 且str[j-len(word):j]==word
我的思路太复杂了,用一维数组,dp[i]表示s[0:i]的内容是否可以用字典拼出
方程:dp[i] = True,如果存在 j < i,使得 dp[j] == True 且 s[j:i] 在 wordDict 中
1 2 3 4 5 6 7 8 9 class Solution : def wordBreak (self, s: str , wordDict: List [str ] ) -> bool : dp = [False ] * (len (s) + 1 ) dp[0 ] = True for i in range (1 , len (s) + 1 ): for word in wordDict: if i >= len (word) and dp[i - len (word)] and s[i - len (word): i] in wordDict: dp[i] = True return dp[len (s)]
更为简洁的写法:就是内层遍历物品时遍历左下标就行,然后看截取的内容是否出现在wordset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def wordBreak (self, s: str , wordDict: List [str ] ) -> bool : word_set = 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 word_set: dp[i] = True break return dp[n]
10.27 多重背包理论基础 k[i]个可用,约束了可用的数量上限,和01背包很像,把Mi件摊开,每个物品只能用一次
组合:外层物品,内层背包(01是倒序),dp[c]表示在容量为c的前提下,能获得的最大总价值
0-1的公式dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);多重改写下k即可
1 2 3 4 5 6 7 8 9 10 11 12 C, N = map (int , input ().split()) weight = list (map (int , input ().split())) values = list (map (int , input ().split())) counts = list (map (int , input ().split())) dp = [0 ] * (C + 1 ) for i in range (N): for j in range (C, weight[i] - 1 , -1 ): for k in range (1 , counts[i] + 1 ): if k * weight[i] > j: break dp[j] = max (dp[j], dp[j - weight[i] * k] + values[i] * k) print (dp[C])
上面这样写会超时,因为使用了三重循环,O(N * C * k)
二进制优化:O(N × C × log(k)) 利用的是任何数字可以被表示成二进制数的和,把1/2/4丢进包里,就相当于选了7个,只要不超过k就是对的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 C, N = map (int , input ().split()) weights = list (map (int , input ().split())) values = list (map (int , input ().split())) counts = list (map (int , input ().split())) dp = [0 ] * (C + 1 ) for i in range (N): w, v, k = weights[i], values[i], counts[i] num = 1 while k > 0 : take = min (num, k) k -= take weight = take * w value = take * v for j in range (C, weight - 1 , -1 ): dp[j] = max (dp[j], dp[j - weight] + value) num <<= 1 print (dp[C])
10.28 背包问题总结**
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); nums是物品的重量
问装满背包有几种方法:dp[j] += dp[j - nums[i]] dp[0]=1
dp[i][j]=max(dp[i-1][j],dp[i][j-coins[i]] + 1) 就是选和不选的简略版
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); 不同于组合/排列,顺序不重要
01背包:
二维:先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
一维:只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。因为从小到大的话同一轮的同一物品会被反复使用
完全背包:
纯完全背包的一维:先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
如果求组合数就是外层for循环遍历物品,内层for遍历背包 。组合不看顺序,先遍历物品的话,肯定是1,1,2,不可能出现1,2,1的顺序,顺序是从小到大固定的,先考虑所有用 1 的情况,再在这些基础上加入 2,再加入 3
如果求排列数就是外层for遍历背包,内层for循环遍历物品 。区分顺序,所以先固定容量,再调顺序,这样会出现1,2,1的情况
不能抢劫相邻房屋,问能偷窃到的最大数量
dp[i] 表示0-i-1间能偷窃到的最大数量,dp[i]=max(dp[i-1], dp[i -2] + nums[i])
1 2 3 4 5 6 7 8 9 10 11 class Solution : def rob (self, nums: List [int ] ) -> int : n = len (nums) if n == 1 : return nums[0 ] dp = [0 ] * (n + 1 ) dp[1 ] = nums[0 ] dp[2 ] = max (nums[0 ], nums[1 ]) for i in range (3 , n + 1 ): dp[i] = max (dp[i - 1 ], dp[i - 2 ] + nums[i - 1 ]) return dp[n]
房屋围成一个圈,环状排列意味着第一个房子和最后一个房子中 只能选择一个偷窃
把环拆成两个队列,一个是从0到n-1(不偷窃最后一个房子),另一个是从1到n(不偷窃第一个房子),然后返回两个结果最大的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def rob (self, nums: List [int ] ) -> int : def robRange (start, end ): if end == start: return nums[start] prev = nums[start] curr = max (nums[start], nums[start + 1 ]) for i in range (start + 2 , end + 1 ): tmp = curr curr = max (curr, prev + nums[i]) prev = tmp return curr n = len (nums) if n == 1 : return nums[0 ] return max (robRange(0 , n - 2 ), robRange(1 , n - 1 ))
比较好理解的方法:第一个房间偷或者不偷。偷的话,只能从第一间累积到第n-1间房子;不偷的话,从第二间累积到最后一间。比较两种情况大小即可。
如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子
暴力递归:分为偷父节点和不偷,过程中有重复计算,会超时
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def rob (self, root: Optional [TreeNode] ) -> int : if not root: return 0 val1 = root.val if root.left: val1 += self .rob(root.left.left) + self .rob(root.left.right) if root.right: val1 += self .rob(root.right.left) + self .rob(root.right.right) val2 = self .rob(root.left) + self .rob(root.right) return max (val1, val2)
记忆化递推:用map把结果保存一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : memory = {} def rob (self, root: Optional [TreeNode] ) -> int : if not root: return 0 if self .memory.get(root) is not None : return self .memory[root] val1 = root.val if root.left: val1 += self .rob(root.left.left) + self .rob(root.left.right) if root.right: val1 += self .rob(root.right.left) + self .rob(root.right.right) val2 = self .rob(root.left) + self .rob(root.right) self .memory[root] = max (val1, val2) return self .memory[root]
在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。
而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
树形DP:
确定参数和返回值,每个节点分为偷与不偷,返回值是长度为2的数组,令下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
终止条件:遇到空节点是0
遍历顺序:后序遍历,因为需要子节点的值
递归逻辑:偷当前节点,那左右孩子就不能偷,不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def rob (self, root: Optional [TreeNode] ) -> int : def traversal (node ): if not node: return (0 , 0 ) left = traversal(node.left) right = traversal(node.right) val0 = max (left[0 ], left[1 ]) + max (right[0 ], right[1 ]) val1 = node.val + left[0 ] + right[0 ] return (val0, val1) dp = traversal(root) return max (dp)
循环过程用当前节点值与之前记录的股票最小价格作比较,若小于则更新当前值为最小股票价格,若大于则用当前值减去已记录的最小股票价格,结果与记录的最大利润作比较,若大于则更新为最大利润
这是贪心的写法
1 2 3 4 5 6 7 8 9 10 class Solution : def maxProfit (self, prices: List [int ] ) -> int : res = 0 curr_min = prices[0 ] for i in range (1 , len (prices)): if prices[i] < curr_min: curr_min = prices[i] else : res = max (res, prices[i] - curr_min) return res
上面的思路也可以用dp解释,可解释性更强,dp[i] 表示 从 第0 到 第i 天,必须进行一轮买卖操作,能获得的最大收益。
选择在 前 i-1 天完成一轮买卖操作,则 第 i 天不能做任何操作,问题转化为 dp[i-1]。
选择在 前 i-1 天做买入操作,在第 i 天做卖出操作,即从 第0 到 第i 天,进行一轮买卖操作。在这种情况下,卖出操作的价格一定是 prices[i],为了使收益最大,买入操作的价格应当是 前i-1天中价格的最小值,low = min(prices[0], prices[1], ..., prices[i-1]) 。收益为 prices[i] - low。
dp[i][0]代表第i天过后手上有股票时的最大收益,dp[i][1]代表第i天过后手上无股票时的最大收益
1 2 3 4 5 6 7 8 9 class Solution : def maxProfit (self, prices: List [int ] ) -> int : n = len (prices) dp = [[0 , 0 ] for _ in range (n)] dp[0 ] = [-prices[0 ], 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 ]
10.33 周总结
dp[i][0] 表示第i天持有股票所得现金。
dp[i][1] 表示第i天不持有股票所得最多现金
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def maxProfit (self, prices: List [int ] ) -> int : len_ = len (prices) i = 1 curr_price = prices[0 ] max_profit = 0 while i < len_: if curr_price < prices[i]: max_profit += prices[i] - curr_price curr_price = prices[i] i += 1 return max_profit
持有股票(dp[i][0]) :表示在第 i 天结束时,手中持有一支股票。
这个状态可以由两种方式达到:
在第 i-1 天已经持有股票,第 i 天不进行任何操作(即 dp[i-1][0])。
在第 i-1 天不持有股票,第 i 天买入股票(即 dp[i-1][1] - prices[i])。
因此,dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])。
不持有股票(dp[i][1]) :表示在第 i 天结束时,手中不持有股票。
这个状态也可以由两种方式达到:
在第 i-1 天已经不持有股票,第 i 天不进行任何操作(即 dp[i-1][1])。
在第 i-1 天持有股票,第 i 天卖出股票(即 dp[i-1][0] + prices[i])。
因此,dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i])。
1 2 3 4 5 6 7 8 9 class Solution : def maxProfit (self, prices: List [int ] ) -> int : n = len (prices) dp = [[0 ,0 ] for _ in range (n)] dp[0 ] = [-prices[0 ], 0 ] for i in range (1 , n): dp[i][0 ] = max (dp[i-1 ][0 ], - prices[i] + dp[i-1 ][1 ]) dp[i][1 ] = max (dp[i-1 ][1 ], + prices[i] + dp[i-1 ][0 ]) return dp[-1 ][-1 ]
也可以节约空间,只开两个数组:存以前和当前状态
1 2 3 4 5 6 7 8 9 10 class Solution : def maxProfit (self, prices: List [int ] ) -> int : length = len (prices) dp = [[0 ] * 2 for _ in range (2 )] dp[0 ][0 ] = -prices[0 ] dp[0 ][1 ] = 0 for i in range (1 , length): dp[i % 2 ][0 ] = max (dp[(i-1 ) % 2 ][0 ], dp[(i-1 ) % 2 ][1 ] - prices[i]) dp[i % 2 ][1 ] = max (dp[(i-1 ) % 2 ][1 ], dp[(i-1 ) % 2 ][0 ] + prices[i]) return dp[(length-1 ) % 2 ][1 ]
最多进行两笔交易,可以只进行一笔或不进行
dp[i][0]代表第i天过后第一次买股票时的最大收益,dp[i][1]代表第i天过后第一次卖股票时的最大收益,dp[i][2]代表第i天过后第二次买股票时的最大收益,dp[i][3]代表第i天过后第二次卖股票时的最大收益
注意并不是要在第i天买,0-i天都能买
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def maxProfit (self, prices: List [int ] ) -> int : n = len (prices) dp = [[0 ,0 ,0 ,0 ] for _ in range (n)] dp[0 ] = [-prices[0 ], 0 , -prices[0 ], 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 ], dp[i - 1 ][0 ] + prices[i]) dp[i][2 ] = max (dp[i - 1 ][2 ], dp[i - 1 ][1 ] - prices[i]) dp[i][3 ] = max (dp[i - 1 ][3 ], dp[i - 1 ][2 ] + prices[i]) return dp[-1 ][-1 ]
最多可以买k次,卖k次,比如上一道题k=2
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxProfit (self, k: int , prices: List [int ] ) -> int : n = len (prices) dp = [[0 , 0 ] * k for _ in range (n)] dp[0 ] = [-prices[0 ], 0 ] * k for i in range (1 , n): for j in range (k): dp[i][j * 2 ] = max (dp[i - 1 ][j * 2 ], -prices[i] + (dp[i - 1 ][j * 2 - 1 ] if j != 0 else 0 )) dp[i][j * 2 + 1 ] = max (dp[i - 1 ][j * 2 + 1 ], prices[i] + dp[i - 1 ][j * 2 ]) return dp[-1 ][-1 ]
有一天冷冻期,注意卖没有冷冻期,买才有冷冻期!第一次写时把dp[i][1]的也写成i-2了
1 2 3 4 5 6 7 8 9 class Solution : def maxProfit (self, prices: List [int ] ) -> int : n = len (prices) dp = [[0 , 0 ] for _ in range (n)] dp[0 ] = [-prices[0 ], 0 ] for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], -prices[i] + (dp[i - 2 ][1 ] if i > 1 else 0 )) dp[i][1 ] = max (dp[i - 1 ][1 ], prices[i] + dp[i - 1 ][0 ]) return dp[-1 ][-1 ]
10.38 周总结 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/solutions/2199035/yi-tao-mo-ban-ji-xing-dai-ma-bi-zhao-yan-0ap8/ 看这个题解,最好懂,买是花钱要-prices,卖是赚钱+
注意买入持有并卖出只用付一次手续费,不用重复,统一在卖出时计算即可
1 2 3 4 5 6 7 8 9 class Solution : def maxProfit (self, prices: List [int ], fee: int ) -> int : n = len (prices) dp = [[0 , 0 ] for _ in range (n)] dp[0 ] = [-prices[0 ], 0 ] for i in range (1 , n): dp[i][0 ] = max (dp[i - 1 ][0 ], -prices[i] + (dp[i - 1 ][1 ] if i > 0 else 0 )) dp[i][1 ] = max (dp[i - 1 ][1 ], prices[i] + dp[i - 1 ][0 ] - fee) return dp[-1 ][-1 ]
10.40 股票总结 一共六道题
记住除了0以外,偶数是卖出,奇数是买入,为i天构建二维数组
试了一下dp[n]表示0-n的最长递增子序列的长度,但这样不知道现有的子序列的最大值,而且可能有同一长度多种选择,不知道该怎么写
解决方案是定义dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度 ,这里以nums[i]结尾很重要。有了这个定义我就会写了dp[i] = max(dp[i], dp[j] + 1);,即对比之前的dp值,如果当前nums大就得到了新的递增子序列。注意最大值不一定是dp[n-1]!
1 2 3 4 5 6 7 8 9 10 11 class Solution : def lengthOfLIS (self, nums: List [int ] ) -> int : n = len (nums) dp = [1 ] * n ans = 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 ) ans = max (ans, dp[i]) return ans
不断更新左右区间,因为是连续所以简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def findLengthOfLCIS (self, nums: List [int ] ) -> int : n = len (nums) if n == 1 : return 1 ans = 1 l, r = 0 , 1 while r < n: if nums[r] <= nums[r - 1 ]: ans = max (ans, r - l) l = r r = l + 1 else : r += 1 ans = max (ans, r - l) return ans
如果用dp的定义,仍然是以i结尾的连续递增的子序列长度为dp[i],即dp[i] = dp[i - 1] + 1;
1 2 3 4 5 6 7 8 9 10 class Solution : def findLengthOfLCIS (self, nums: List [int ] ) -> int : n = len (nums) dp = [1 ] * n ans = 1 for i in range (1 , n): if nums[i] > nums[i - 1 ]: dp[i] = dp[i - 1 ] + 1 ans = max (ans, dp[i]) return ans
就是有一小段既要数字相同,也要顺序相同
dp[i][j]中i和j表示结尾都比较奇怪
dp[i][j] 表示以 nums1[i-1] 和 nums2[j-1] 结尾的最长公共子数组的长度(是连续的,因为是连续,所以可以-1,而不是要把前面的0-i全部遍历一遍)
如果 nums1[i-1] == nums2[j-1],那么这两个元素可以接在前一对公共子数组之后: dp[i][j] = dp[i-1][j-1] + 1
否则,这两个元素不相等,无法继续相同子数组:dp[i][j] = 0
注意dp中一般把0空出来 ,也就是定义dp[n + 1],这样不用处理特殊的0,也不容易出现负下表、
1 2 3 4 5 6 7 8 9 10 11 class Solution : def findLength (self, nums1: List [int ], nums2: List [int ] ) -> int : m, n = len (nums1), len (nums2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] ans = 0 for i in range (1 , m + 1 ): for j in range (1 , n + 1 ): if nums1[i - 1 ] == nums2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ans = max (ans, dp[i][j]) return ans
这里最难的是状态转移方程,要理解最长公共子数组的表现是连续的,以及和下标的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def longestCommonSubsequence (self, text1: str , text2: str ) -> int : m, n = len (text1), len (text2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for i in range (1 , m + 1 ): for j in range (1 , n + 1 ): if text1[i - 1 ] == text2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = max (dp[i][j - 1 ], dp[i - 1 ][j]) return dp[m][n]class Solution : def longestCommonSubsequence (self, text1: str , text2: str ) -> int : m, n = len (text1), len (text2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for i in range (1 , m + 1 ): for j in range (1 , n + 1 ): if text1[i - 1 ] == text2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = max (dp[i][j - 1 ], dp[i - 1 ][j]) return dp[m][n]
dp[i][j]是text1[i-1]和text2[j-1],列举相等和不相等的情况,用归纳法很合适
其实就是最大公共子数组的长度,不一定连续,但要按顺序
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxUncrossedLines (self, nums1: List [int ], nums2: List [int ] ) -> int : m, n = len (nums1), len (nums2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for i in range (1 , m + 1 ): for j in range (1 , n + 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[m][n]
dp[i]表示以nums[i-1]结尾的,一定要选
遇到全是负值的,该如何处理?下面是一种写法,但用了max库函数,增加了O(n),并不好
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : m = len (nums) if max (nums) < 0 : return max (nums) dp = [0 ] * (m + 1 ) ans = 0 for i in range (1 , m + 1 ): dp[i] = max (0 , dp[i - 1 ] + nums[i - 1 ]) ans = max (ans, dp[i]) return ans
下面这种更符合题意,因为dp的定义就是必须选nums[i-1],所以有可能是负值
1 2 3 4 5 6 7 8 9 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : m = len (nums) dp = [0 ] * (m + 1 ) ans = -inf for i in range (1 , m + 1 ): dp[i] = max (0 , dp[i - 1 ]) + nums[i - 1 ] ans = max (ans, dp[i]) return ans
我就是用移动下标的方法做这道题:双指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def isSubsequence (self, s: str , t: str ) -> bool : i, j = 0 , 0 m, n = len (s), len (t) while i < m and j < n: if s[i] == t[j]: i += 1 j += 1 else : j += 1 if i == m: return True else : return False
后续挑战:如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10 亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?每个字符串都要遍历t,都需要O(n),太慢了
位置
剩余部分
'a'
'b'
'c'
'd'
'g'
'h'
0
ahbgdc
0
2
5
4
3
1
1
hbgdc
-1
2
5
4
3
1
2
bgdc
-1
2
5
4
3
-1
3
gdc
-1
-1
5
4
3
-1
4
dc
-1
-1
5
4
-1
-1
5
c
-1
-1
5
-1
-1
-1
6
(末尾之后)
-1
-1
-1
-1
-1
-1
last[t[i] - ‘a’] = i这样写会报错,要写成ord,就是把字符写成对应的unicode值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def build_next (self, t: str ): n = len (t) nxt = [[-1 ]*26 for _ in range (n + 1 )] last = [-1 ] * 26 for i in range (n - 1 , -1 , -1 ): last[ord (t[i]) - ord ('a' )] = i for c in range (26 ): nxt[i][c] = last[c] return nxt def isSubsequence (self, s: str , t: str ) -> bool : nxt = self .build_next(t) pos = 0 for c in s: j = nxt[pos][ord (c) - ord ('a' )] if j == -1 : return False pos = j + 1 return True
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 t中一定要出现
一个难点是相等的情况,也可以不选当前s[i-1],另一个难点是初始化的情况,并不是简单的只用设置0,0,这是因为递推公式一开始就涉及[0][1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def numDistinct (self, s: str , t: str ) -> int : m, n = len (s), len (t) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for i in range (1 , m + 1 ): dp[i][0 ] = 1 for j in range (1 , n + 1 ): dp[0 ][j] = 0 dp[0 ][0 ] = 1 for i in range (1 , m + 1 ): for j in range (1 , n + 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[m][n]
dp[i][j]:以word1[i-1]结尾的字符串和以word2[j-1]结尾的字符串相同所需的最小步数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def minDistance (self, word1: str , word2: str ) -> int : m, n = len (word1), len (word2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] dp[0 ][0 ] = 0 for i in range (1 , m + 1 ): dp[i][0 ] = i for j in range (1 , n + 1 ): dp[0 ][j] = j for i in range (1 , m + 1 ): for j in range (1 , n + 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], dp[i][j - 1 ]) + 1 return dp[m][n]
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def minDistance (self, word1: str , word2: str ) -> int : m, n = len (word1), len (word2) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] dp[0 ][0 ] = 0 for i in range (1 , m + 1 ): dp[i][0 ] = i for j in range (1 , n + 1 ): dp[0 ][j] = j for i in range (1 , m + 1 ): for j in range (1 , n + 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], dp[i][j - 1 ], dp[i - 1 ][j - 1 ]) + 1 return dp[m][n]
虽然看提示,说xax是回文,那bxaxb也是回文,但不知道怎么写状态转移方程
还停留在dp[i][j]表示s[i][j]是回文串,不过这个定义是对的,不是定义dp[i]为结尾的字符串有几个回文子串,那就完全写不出来了
虽然这个定义是对的,但不知道i/j该从中间开始遍历吗?感觉很奇怪
5. 最长回文子串 中,找到s中最长的回文子串,就是分别找奇数串和偶数串,以s[i]为中心,i不断变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def longestPalindrome (self, s: str ) -> str : len_ = len (s) start, end = 0 , 0 max_len = 0 for i in range (2 *len_ - 1 ): left, right = i // 2 , (i + 1 ) // 2 while left >= 0 and right < len_ and s[left] == s[right]: left -= 1 right += 1 if right - left - 1 > max_len: max_len = right - left - 1 start, end = left + 1 , right - 1 return s[start: end + 1 ]
简洁的写法是把两个循环合并成一个
在这道题中,也可以用中心扩展法(双指针),来统计回文串的个数
1 2 3 4 5 6 7 8 9 10 11 class Solution : def countSubstrings (self, s: str ) -> int : n = len (s) ans = 0 for i in range (0 , 2 * n - 1 ): l, r = i // 2 , (i + 1 ) // 2 while l >= 0 and r < n and s[l] == s[r]: ans += 1 l -= 1 r += 1 return ans
动态规划:布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
当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。
初始化和遍历顺序:由于依赖i+1,j-1,所以i从大到小,j从小到大(当然本身>=i),i和j的顺序倒无所谓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def countSubstrings (self, s: str ) -> int : n = len (s) dp = [[False ] * n for _ in range (n)] ans = 0 for i in range (n - 1 , -1 , -1 ): for j in range (i, n): if s[i] == s[j]: if j - i <= 1 : dp[i][j] = True else : dp[i][j] = dp[i + 1 ][j - 1 ] if dp[i][j]: ans += 1 return ans
本来想用上面的回文子串做的,然后发现这里的回文子序列可以不连续,所以要用动态规划
dp[i][j]表示s的i-j范围内的最长回文子序列长度
如果s[i]==s[j],则如果i==j,dp=1;j-i=1,dp=2,j - i>1,dp=dp[i + 1][j - 1] + 2
若不等,则dp=max(dp[i + 1][j], dp[i][j - 1])
同样,i从大到小,j从小到大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def longestPalindromeSubseq (self, s: str ) -> int : n = len (s) dp = [[0 ] * n for _ in range (n)] for i in range (n - 1 , -1 , -1 ): for j in range (i, n): if s[i] == s[j]: if j == i: dp[i][j] = 1 elif j - i == 1 : dp[i][j] = 2 else : 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 ][n - 1 ]
10.53 总结 背包问题:遍历顺序很重要
打家劫舍
股票
子序列
关于动规,还有 树形DP(打家劫舍系列里有一道),数位DP,区间DP ,概率型DP,博弈型DP,状态压缩dp等等等,这些我就不去做讲解了,面试中出现的概率非常低。
11. 单调栈 只知道该存索引,但不知道该怎么存,从前往后从后往前的话该存什么
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了 。时间复杂度为O(n)。
存下标i即可,因为元素可以通过t[i]访问获得
本质是空间换时间
从右到左的写法:栈中记录的是下一个更大元素的候选项的下标,发现索引更小、数值更大的元素就开除现有的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def dailyTemperatures (self, temperatures: List [int ] ) -> List [int ]: n = len (temperatures) answer = [0 ] * n st = [] for i in range (n - 1 , -1 , -1 ): t = temperatures[i] while st and t >= temperatures[st[-1 ]]: st.pop() if st: answer[i] = st[-1 ] - i st.append(i) return answer
从左到右的写法:存的是目前还没找到比自己更大的元素的下标
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def dailyTemperatures (self, temperatures: List [int ] ) -> List [int ]: n = len (temperatures) answer = [0 ] * n st = [] for i in range (n): t = temperatures[i] while st and t > temperatures[st[-1 ]]: j = st.pop() answer[j] = i - j st.append(i) return answer
其实就是用单调栈得到nums2每个数字的下一个更大元素
我感觉必须得用一个dict建立nums2中数值和索引的映射,才能让nums1映射上。重点就是这里,直接建立数字->下一个更大元素的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def nextGreaterElement (self, nums1: List [int ], nums2: List [int ] ) -> List [int ]: m, n = len (nums1), len (nums2) ans = [-1 ] * m dict_ = {} st = [] for i in range (n - 1 , -1 , -1 ): num = nums2[i] while st and num >= st[-1 ]: st.pop() if st: dict_[num] = st[-1 ] else : dict_[num] = -1 st.append(num) for i in range (m): num = nums1[i] ans[i] = dict_[num] return ans
方法
用于列表
用于字符串
是否原地修改
返回结果
list.reverse()
✅ 可以
❌ 不可用
✅ 是(原地修改)
返回 None
reversed(x)
✅ 可以
✅ 可以
❌ 否
返回一个反转迭代器
x[::-1]
✅ 可以
✅ 可以
❌ 否
返回反转后的新对象
字符串的反转可以写:''.join(reversed(s))
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def nextGreaterElement (self, nums1: List [int ], nums2: List [int ] ) -> List [int ]: next_greater = {} st = [] for x in reversed (nums2): while st and st[-1 ] <= x: st.pop() next_greater[x] = st[-1 ] if st else -1 st.append(x) return [next_greater[x] for x in nums1]
就是这里的更大是循环的,也就是除了最大值没有更大的元素,其他值都是有更大的元素的
除了nums最后一个数,其他都是和前面的单调栈一样的,最后一个数又需要从nums[0]开始找
这种写法不对,比如[5,4,3,2,1]的结果是[-1,5,5,5,5],而不是[-1,-1,-1,-1,5]
真正的方法就是把数组拼接起来,把数组看两遍,从右往左扫下标 i = 2n-1…0,实际位置用idx = i % n
[5,4,3,2,1,5,4,3,2,1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def nextGreaterElements (self, nums: List [int ] ) -> List [int ]: n = len (nums) st = [] ans = [-1 ] * n for i in range (2 *n - 1 , -1 , -1 ): idx = i % n x = nums[idx] while st and x >= st[-1 ]: st.pop() if st: ans[idx] = st[-1 ] st.append(x) return ans
也可以用单调栈,但反而比较复杂,所以我没看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def trap (self, height: List [int ] ) -> int : n = len (height) l, r = [0 ] * n, [0 ] * n curr_max = 0 for i, h in enumerate (height): curr_max = max (curr_max, h) l[i] = curr_max curr_max = 0 for i in range (n - 1 , -1 , -1 ): curr_max = max (curr_max, height[i]) r[i] = curr_max ans = 0 for i in range (n): ans += min (l[i], r[i]) - height[i] return ans
以每根柱子作为矩形的最低高度 来扩展其左右可延伸的宽度 ,就能得到以这根柱子为“短板”的最大矩形面积。
如果我们知道每根柱子向左 和向右 第一个严格小于 它的柱子位置,就能立刻算出这根柱子能撑起的最大矩形面积:
areai=heighti×(rightLessi−leftLessi−1)
单调栈正是用来一次遍历 就找出每根柱子的“左/右第一个更小”。
和接雨水的区别就在于,接雨水是找大的,这里是找小的,利用现在的高度,然后接雨水不需要第一个大的,但这里需要第一个小的,所以这里要使用单调栈得到l和r数组
这里单调栈是找左右第一个更小的,所以要维护单调递增的栈
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 : def largestRectangleArea (self, heights: List [int ] ) -> int : n = len (heights) l, r = [-1 ] * n, [n] * n st = [] for i in range (n): h = heights[i] while st and heights[st[-1 ]] >= h: st.pop() if st: l[i] = st[-1 ] st.append(i) st = [] for i in range (n - 1 , -1 , -1 ): h = heights[i] while st and heights[st[-1 ]] >= h: st.pop() if st: r[i] = st[-1 ] st.append(i) ans = 0 for i in range (n): width = r[i] - l[i] - 1 area = heights[i] * width ans = max (ans, area) return ans
当然也有一种简单的写法,只需要遍历一次:这种写法是因为k就是当前高度stack[-1]的right下标,而把stack[-1]pop出来后,剩下的stack[-1]就是left的下标,这样就无需记录left和right数组了,在一次遍历O(n)的过程中可以得到res
为什么这里没有>=呢,因为上面是要找第一个严格小于的元素,不能让相等的柱子互当边界,而下面也是为了合并相同高度的矩形,其实都是处理相等的情况,是一个意思
1 2 3 4 5 6 7 8 9 10 11 class Solution : def largestRectangleArea (self, heights: List [int ] ) -> int : res = 0 stack = [-1 ] heights.append(0 ) for k, num in enumerate (heights): while stack and heights[stack[-1 ]] > num: index = stack.pop() res = max (res, heights[index] * (k - stack[-1 ] - 1 )) stack.append(k) return res
12. 图论 12.1 图论基础 度:无向图中有几条边连接该节点
连通图:在无向图中,任何两个节点都是可以到达的
在无向图中的极大连通子图称之为该图的一个连通分量。
在有向图中极大强连通子图称之为该图的强连通分量。
邻接矩阵:对于无向图,就是双向边grid[2][5] = 6,grid[5][2] = 6。缺点是图很稀疏
邻接表:使用数组存节点,使用链表存节点指向的每个点。遍历节点连接的情况容易,但检查任意两个节点之间是否存在边,效率就很低
图论,就是在图(邻接表或邻接矩阵)上进行搜索(BFS或DFS)
12.2 DFS DFS:找到目标后再回头,回头就是回溯
用递归是最方便的,回溯就在递归函数的下面一步
1 2 3 4 5 void dfs (参数) { 处理节点 dfs (图,选择的节点); 回溯,撤销处理结果 }
回溯和dfs差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void backtracking (参数) { if (终止条件) { 存放结果; return ; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking (路径,选择列表); 回溯,撤销处理结果 } } void dfs (参数) { if (终止条件) { 存放结果; return ; } for (选择:本节点所连接的其他节点) { 处理节点; dfs (图,选择的节点); 回溯,撤销处理结果 } }
12.3 可达路径 12.4 BFS 适合于解决两个点之间的最短路径问题,一旦遇到终点,就是最短路
岛屿问题是BFS和DFS都可以解决,因为只要能把相邻且相同属性的节点标记上就行
这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。
其实,我们仅仅需要一个容器,能保存我们要遍历过的元素 就可以,那么用队列,还是用栈,甚至用数组,都是可以的 。
用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针 。
因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历 。
因为栈是先进后出,加入元素和弹出元素的顺序改变了。
BFS本身不需要注意 转圈搜索的顺序,但大家都习惯用队列了,因为搜索顺序不会变
13. 额外题目 想的是先排序,然后找每个数字在正序数组中的位置
排序之后,每个数值的下标就代表有几个比它小的了,用哈希表记录每个数字的下标
如果用nums.sort(),是直接对原数组进行排序,如果用sorted(nums),是产生新对象
1 2 3 4 5 6 7 8 class Solution : def smallerNumbersThanCurrent (self, nums: List [int ] ) -> List [int ]: n = len (nums) d = {} sorted_nums = sorted (nums) for i in range (n - 1 , -1 , -1 ): d[sorted_nums[i]] = i return [d[n] for n in nums]
O(nlogn)
就是有且只有一个最大值,不能出现在开头或结尾,要寻找这个变化点
我没想出来,应该用双指针的方法,保证左边到中间,和右边到中间是递增的
我又犯了一个错误,while中l和r不应该一起移动
还要注意一个细节,递增和递减序列,这样也会得到l==r,要把这种情况排除
1 2 3 4 5 6 7 8 9 10 11 class Solution : def validMountainArray (self, arr: List [int ] ) -> bool : n = len (arr) if n <= 2 : return False l, r = 0 , n - 1 while l < n - 1 and arr[l] < arr[l + 1 ]: l += 1 while r >= 1 and arr[r] < arr[r - 1 ]: r -= 1 return l == r and l != 0 and r != n
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def uniqueOccurrences (self, arr: List [int ] ) -> bool : n = len (arr) d = {} for n in arr: if n not in d: d[n] = 1 else : d[n] += 1 times = d.values() s = set (times) return len (s) == len (times)
注意是出现次数这个数,要独一无二,所以先统计每个数的出现次数,再取set
用Counter库的写法:
1 2 3 4 class Solution : def uniqueOccurrences (self, arr: List [int ] ) -> bool : freq = Counter(arr) return len (freq.values()) == len (set (freq.values()))
我的想法就是遇到0后找第一个非0的数交换,其实就是暴力
1 2 3 4 5 6 7 8 9 class Solution : def moveZeroes (self, nums: List [int ] ) -> None : n = len (nums) for i in range (n): if nums[i] == 0 : for j in range (n - 1 , i, -1 ): if nums[j] != 0 : nums[i], nums[j] = nums[j], nums[i] return nums
但是这种暴力方法效率低,用双指针,slow指向下一个应该放置非零元素的位置,fast用于扫描数组,注意并不是每次交换后0就在最后了(fast是正序的,不能倒序!不然会覆盖还没有扫描到的元素)
1 2 3 4 5 6 7 8 class Solution : def moveZeroes (self, nums: List [int ] ) -> None : slow = 0 for fast in range (len (nums)): if nums[fast] != 0 : nums[slow], nums[fast] = nums[fast], nums[slow] slow += 1 return nums
另一种直观理解的方法是先把所有非零元素填到前面,剩下的都改为0
1 2 3 4 5 6 7 8 9 10 11 class Solution : def moveZeroes (self, nums: List [int ] ) -> None : pos = 0 for num in nums: if num != 0 : nums[pos] = num pos += 1 for i in range (pos, len (nums)): nums[i] = 0 return nums
先把[0,n-k)和[n-k, n)分别进行翻转,再把[0,n)进行翻转
总之记得左旋转、右旋转数组可以分段翻转即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def rotate (self, nums: List [int ], k: int ) -> None : def reverse (start, end ): l, r = start, end - 1 while l <= r: nums[l], nums[r] = nums[r], nums[l] l += 1 r -= 1 n = len (nums) k = k % n reverse(0 , n - k) reverse(n - k, n) reverse(0 , n) return nums
注意左侧和和右侧和是不包括自己的
感觉要记录前缀和数组,这样后缀和可以通过total-前缀-自己得到
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 : def pivotIndex (self, nums: List [int ] ) -> int : n = len (nums) total = sum (nums) pre = [0 ] * n curr = 0 for i, num in enumerate (nums): pre[i] = curr curr += num for i in range (n): if pre[i] == total - pre[i] - nums[i]: return i return -1 class Solution : def pivotIndex (self, nums: List [int ] ) -> int : n = len (nums) total = sum (nums) curr = 0 for i, num in enumerate (nums): if curr == total - curr - num: return i curr += num return -1
递增排列,用二分法,说建议写左闭右开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def searchRange (self, nums: List [int ], target: int ) -> List [int ]: l, r = 0 , len (nums) - 1 while l <= r: idx = (l + r) // 2 if nums[idx] < target: l = idx + 1 elif nums[idx] > target: r = idx - 1 else : start, end = idx, idx while start >= 0 and nums[start] == target: start -= 1 while end <= len (nums) - 1 and nums[end] == target: end += 1 return [start + 1 , end - 1 ] return [-1 , -1 ]
就是把奇数都移动到奇数的下标上,偶数移动到偶数的下标上
不适用额外空间,就是用双指针,一个指向奇数下标,一个指向偶数下标
我下面的写法效率很低,这是因为没有提前判断nums[j] / nums[i] 本身是否已经是正确奇偶,可能会做多余交换,最好把两个错误的奇偶拿来交换,这样就节约交换次数了
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def sortArrayByParityII (self, nums: List [int ] ) -> List [int ]: i, j = 0 , 1 for idx in range (len (nums)): while idx % 2 == 0 and nums[idx] % 2 == 1 and j < len (nums): nums[idx], nums[j] = nums[j], nums[idx] j += 2 while idx % 2 == 1 and nums[idx] % 2 == 0 and i < len (nums): nums[idx], nums[i] = nums[i], nums[idx] i += 2 return nums
更简洁的写法:就是用i和j代表需要交换的位置,不需要另外用for+while的形式,肯定是能交换完的,因为数组特点是一半一半:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def sortArrayByParityII (self, nums: List [int ] ) -> List [int ]: n = len (nums) i, j = 0 , 1 while i < n and j < n: if nums[i] % 2 == 0 : i += 2 elif nums[j] % 2 == 1 : j += 2 else : nums[i], nums[j] = nums[j], nums[i] i += 2 j += 2 return nums
如果出现,就是返回下标,如果没出现,就是插入到升序排列的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def searchInsert (self, nums: List [int ], target: int ) -> int : l, r = 0 , len (nums) while l < r: idx = (l + r) // 2 if target > nums[idx]: if idx == len (nums) - 1 : return idx + 1 elif target < nums[idx + 1 ]: return idx + 1 else : l = idx + 1 elif target < nums[idx]: if idx == 0 : return 0 elif target > nums[idx - 1 ]: return idx else : r = idx else : return idx
简洁版的:
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def searchInsert (self, nums: List [int ], target: int ) -> int : l, r = 0 , len (nums) while l < r: idx = (l + r) // 2 if target > nums[idx]: l = idx + 1 elif target < nums[idx]: r = idx else : return idx return l
我没想出来的地方是1. 创建dummy 2. 移动一步(这个想过,但在移动两步中徘徊)3. 返回dummy.next
0 1 2 3 变成 0 2 1 3 1 3 4变成 1 4 3
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def swapPairs (self, head: Optional [ListNode] ) -> Optional [ListNode]: dummy = ListNode(0 , head) pre, cur = dummy, head while cur and cur.next : nxt = cur.next tmp = nxt.next nxt.next = cur pre.next = nxt cur.next = tmp pre, cur = cur, cur.next return dummy.next
Hot100 我本来的想法是倒序看两个链表,但链表又只能从头开始。看来还是要用数学技巧
指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为: a+(b−c) 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为: b+(a−c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def getIntersectionNode (self, headA: ListNode, headB: ListNode ) -> ListNode: A, B = headA, headB while A != B: A = A.next if A else headB B = B.next if B else headA if A: A = A.next else : A = headB if B: B = B.next else : B = headA return A
深度就是第几层,从根节点到该节点,从上往下
高度是倒数第几层,就像楼层一样从下往上
两个节点 p,q 分为两种情况:
p 和 q 在相同子树中
p 和 q 在不同子树中
从根节点遍历,递归向左右子树查询节点信息
递归终止条件:如果当前节点为空或等于 p 或 q,则返回当前节点
递归遍历左右子树,如果左右子树查到节点都不为空,则表明 p 和 q 分别在左右子树中,在root的两侧,因此,当前节点root即为最近公共祖先;
如果左右子树其中一个不为空,则返回非空节点。
如果left不为空,说明p,q在左子树。
如果right不为空,说明p,q在右子树。
left和right都为空,说明找不到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def lowestCommonAncestor (self, root: 'TreeNode' , p: 'TreeNode' , q: 'TreeNode' ) -> 'TreeNode' : if root is None or root == q or root == p: return root left = self .lowestCommonAncestor(root.left, p, q) right = self .lowestCommonAncestor(root.right, p, q) if left and right: return root if left: return left if right: return right return None
找中间节点:快慢指针
反转链表:中间到最后的反转了
对比两个链表
为什么反转中间而不是一开始就反转全部,当然是为了O(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 class Solution : def middleNode (self, head: Optional [ListNode] ) -> Optional [ListNode]: slow = fast = head while fast and fast.next : slow = slow.next fast = fast.next .next return slow def reverse (self, head: Optional [ListNode] ) -> Optional [ListNode]: pre, cur = None , head while cur: nxt = cur.next cur.next = pre pre = cur cur = nxt return pre def isPalindrome (self, head: Optional [ListNode] ) -> bool : mid = self .middleNode(head) head2 = self .reverse(mid) while head and head2: if head.val != head2.val: return False head = head.next head2 = head2.next return True
单调栈(Monotonic Stack)是一种特殊的栈结构,其元素按照严格递增或严格递减 的顺序排列。它的核心思想是利用栈的单调性快速找到元素的前后边界 ,常用于解决数组中与“相邻元素大小关系”相关的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def dailyTemperatures (self, temperatures: List [int ] ) -> List [int ]: n = len (temperatures) res = [0 ] * n st = [] for i in range (n - 1 , -1 , -1 ): t = temperatures[i] while st and t >= temperatures[st[-1 ]]: st.pop() if st: res[i] = st[-1 ] - i st.append(i) return res
从左到右的方法是TODO list
1 2 3 4 5 6 7 8 9 10 11 class Solution : def dailyTemperatures (self, temperatures: List [int ] ) -> List [int ]: n = len (temperatures) res = [0 ] * n st = [] for i, t in enumerate (temperatures): while st and t > temperatures[st[-1 ]]: j = st.pop() res[j] = i - j st.append(i) return res
普通递推,注意不用定义tmp,直接用=即可进行交换
1 2 3 4 5 6 7 8 class Solution : def invertTree (self, root: Optional [TreeNode] ) -> Optional [TreeNode]: if not root: return root 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 class Solution : def maximalSquare (self, matrix: List [List [str ]] ) -> int : if len (matrix) == 0 or len (matrix[0 ]) == 0 : return 0 maxSide = 0 rows, cols = len (matrix), len (matrix[0 ]) dp = [[0 ] * cols for _ in range (rows)] for i in range (rows): for j in range (cols): if matrix[i][j] == '1' : if i == 0 or j == 0 : dp[i][j] = 1 else : dp[i][j] = min (dp[i - 1 ][j], dp[i][j - 1 ], dp[i - 1 ][j - 1 ]) + 1 maxSide = max (maxSide, dp[i][j]) maxSquare = maxSide * maxSide return maxSquare
快速排序:
这里并没有排序好完整的序列,只是把k所在的地方排序了,节约了时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def findKthLargest (self, nums: List [int ], k: int ) -> int : def quick_select (nums, k ): pivot = random.choice(nums) big, equal, small = [], [], [] for num in nums: if num > pivot: big.append(num) elif num < pivot: small.append(num) else : equal.append(num) if k <= len (big): return quick_select(big, k) if len (nums) - len (small) < k: return quick_select(small, k - len (nums) + len (small)) return pivot return quick_select(nums, k)
插入单词,检索是否存在,看前缀是否存在
创建26叉树,像桶排序一样方便查找;然后用bool表示是否结束
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 class Node : __slots__ = 'son' , 'end' def __init__ (self ): self .son = [None ] * 26 self .end = False class Trie : def __init__ (self ): self .root = Node() def insert (self, word: str ) -> None : cur = self .root for c in word: c = ord (c) - ord ('a' ) if cur.son[c] is None : cur.son[c] = Node() cur = cur.son[c] cur.end = True def find (self, word: str ) -> int : cur = self .root for c in word: c = ord (c) - ord ('a' ) if cur.son[c] is None : return 0 cur = cur.son[c] return 2 if cur.end else 1 def search (self, word: str ) -> bool : return self .find(word) == 2 def startsWith (self, prefix: str ) -> bool : return self .find(prefix) != 0
拓扑排序:给定一个包含 n 个节点的有向图 G,我们给出它的节点编号的一种排列,如果满足:对于图 G 中的任意一条有向边 (u,v),u 在排列中都出现在 v 的前面。如果有有向无环图即可
1 2 3 4 5 6 7 8 9 class Solution : def reverseList (self, head: Optional [ListNode] ) -> Optional [ListNode]: pre, cur = None , head while cur: nxt = cur.next cur.next = pre pre = cur cur = nxt return pre
一旦我们发现 (i,j) 是 1,就从 (i,j) 开始,DFS 这个岛。
每一步可以往左右上下四个方向走,也就是
(i,j−1),(i,j+1),(i−1,j),(i+1,j) 这四个格子。
每次到达一个新的格子,就插上旗子🚩,把 grid[i][j] 改成 2。
如果 (i,j) 出界,或者 (i,j) 是水,或者 (i,j) 已经插上了旗子🚩,就不再继续往下递归。
⚠注意:DFS 的过程中,最重要的是不能重复访问之前访问过的格子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def numIslands (self, grid: List [List [str ]] ) -> int : m, n = len (grid), len (grid[0 ]) def dfs (i: int , j: int ) -> None : if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1' : return grid[i][j] = '2' dfs(i, j - 1 ) dfs(i, j + 1 ) dfs(i - 1 , j) dfs(i + 1 , j) ans = 0 for i, row in enumerate (grid): for j, c in enumerate (row): if c == '1' : dfs(i, j) ans += 1 return ans
1.子问题:从k个房子能偷到的最大金额
2.递推关系:
3.计算顺序:k依赖K-1和k-2,所以从左到右算
1 2 3 4 5 6 7 8 9 10 11 class Solution : def rob (self, nums: List [int ] ) -> int : if len (nums) == 0 : return 0 n = len (nums) dp = [0 ] * (n + 1 ) dp[0 ] = 0 dp[1 ] = nums[0 ] for i in range (2 , n + 1 ): dp[i] = max (dp[i - 1 ], nums[i - 1 ] + dp[i - 2 ]) return dp[n]
空间优化:其实只用两个值迭代,不需要一整个dp,就能推出最后的最大值
1 2 3 4 5 6 class Solution : def rob (self, nums: List [int ] ) -> int : pre, cur = 0 , 0 for i in nums: pre, cur = cur, max (cur, i + pre) return cur
暴力:排序返回中间数字;哈希表统计
最佳方法:摩尔投票(就是抵消原则,一一配对,最后剩下来至少一个该元素)
推论一: 若记 众数 的票数为 +1 ,非众数 的票数为 −1 ,则一定有所有数字的 票数和 >0 。
推论二: 若数组的前 a 个数字的 票数和 =0 ,则 数组剩余 (n−a) 个数字的 票数和一定仍 >0 ,即后 (n−a) 个数字的 众数仍为 x 。
1 2 3 4 5 6 7 8 9 10 class Solution : def majorityElement (self, nums: List [int ] ) -> int : votes, count = 0 , 0 for num in nums: if votes == 0 : x = num votes += 1 if num == x else -1 for num in nums: if num == x: count += 1 return x if count > len (nums) // 2 else 0
分别迭代计算 上三角和下三角的乘积即可。前缀积和后缀积
1 2 3 4 5 6 7 8 9 class Solution : def productExceptSelf (self, nums: List [int ] ) -> List [int ]: ans, tmp = [1 ] * len (nums), 1 for i in range (1 , len (nums)): ans[i] = ans[i - 1 ] * nums[i -1 ] for i in range (len (nums) - 2 , -1 , -1 ): tmp *= nums[i + 1 ] ans[i] *= tmp return ans
为什么不能用一个stack,因为min是O(n),不是常数时间
用额外的空间辅助存最小值,用list的原因是最小值可能不止一个,可以保证数量的正确性
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 MinStack : def __init__ (self ): self .stack = [] self .min_stack = [] def push (self, val: int ) -> None : self .stack.append(val) if not self .min_stack or val <= self .min_stack[-1 ]: self .min_stack.append(val) def pop (self ) -> None : if self .stack.pop() == self .min_stack[-1 ]: self .min_stack.pop() def top (self ) -> int : return self .stack[-1 ] def getMin (self ) -> int : return self .min_stack[-1 ]
要有连续的非空子数组
如果当前的数是正数,要与前面最大的乘积相乘;如果当前的数是负数,要与前面最小的乘积相乘。这样才有可能最大(这是最关键的思路,一般题只考虑最大值,但因为连续,这里要考虑最小值)
可以合并在一起写
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxProduct (self, nums: List [int ] ) -> int : n = len (nums) f_max = [0 ] * n f_min = [0 ] * n f_max[0 ] = f_min[0 ] = nums[0 ] for i in range (1 , n): x = nums[i] f_max[i] = max (f_max[i - 1 ] * x, f_min[i - 1 ] * x, x) f_min[i] = min (f_max[i - 1 ] * x, f_min[i - 1 ] * x, x) return max (f_max)
注意最后返回的是过程中的最大值
空间优化:
1 2 3 4 5 6 7 8 class Solution : def maxProduct (self, nums: List [int ] ) -> int : ans = -inf f_max = f_min = 1 for x in nums: f_max, f_min = max (f_max * x, f_min * x, x), min (f_max * x, f_min * x, x) ans = max (ans, f_max) return ans
归并排序:
分割:不断用快慢指针找到mid,然后mid.next=None把链表切断
终止条件非常重要!当head.next为空时说明只有一个节点,返回该节点即可
合并:把两个有序链表合并,不断比较头节点即可,用dummy更简单
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 : def sortList (self, head: Optional [ListNode] ) -> Optional [ListNode]: if not head or not head.next : return head slow, fast = head, head.next while fast and fast.next : slow = slow.next fast = fast.next .next mid, slow.next = slow.next , None left, right = self .sortList(head), self .sortList(mid) res = ListNode(0 ) h = res while left and right: if left.val < right.val: h.next = left left = left.next else : h.next = right right = right.next h = h.next if left: h.next = left else : h.next = right return res.next
最近最久未使用的缓存会被移除
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
看到题目要我们实现一个可以存储 key-value 形式数据的数据结构,并且可以记录最近访问的 key 值。首先想到的就是用字典来存储 key-value 结构,这样对于查找操作时间复杂度就是 O(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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 class ListNode : def __init__ (self, key=None , value=None ): self .key = key self .value = value self .prev = None self .next = None class LRUCache : def __init__ (self, capacity: int ): self .capacity = capacity self .hashmap = {} self .head = ListNode() self .tail = ListNode() self .head.next = self .tail self .tail.prev = self .head def move_node_to_tail (self, key ): node = self .hashmap[key] node.prev.next = node.next node.next .prev = node.prev node.prev = self .tail.prev node.next = self .tail self .tail.prev.next = node self .tail.prev = node def get (self, key: int ) -> int : if key in self .hashmap: self .move_node_to_tail(key) res = self .hashmap.get(key, -1 ) if res == -1 : return res else : return res.value def put (self, key: int , value: int ) -> None : if key in self .hashmap: self .hashmap[key].value = value self .move_node_to_tail(key) else : if len (self .hashmap) == self .capacity: self .hashmap.pop(self .head.next .key) self .head.next = self .head.next .next self .head.next .prev = self .head new = ListNode(key, value) self .hashmap[key] = new new.prev = self .tail.prev new.next = self .tail self .tail.prev.next = new self .tail.prev = new
第一次相遇后,令fast重新指向头节点
f =2nb ,s =nb ,n是环的周长(根据f =2s 和f =s +nb 求出)
此指针和 slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头节点head。这样可以求出a
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def detectCycle (self, head: Optional [ListNode] ) -> Optional [ListNode]: fast = slow = head while fast and fast.next : slow = slow.next fast = fast.next .next if slow == fast: ptr = head while head != slow: slow = slow.next head = head.next return slow return None
这题简单些,不必用求出环的入口,用快慢指针证明有环即可
1 2 3 4 5 6 7 8 9 class Solution : def hasCycle (self, head: Optional [ListNode] ) -> bool : slow = fast = head while fast and fast.next : slow = slow.next fast = fast.next .next if fast == slow: return True return False
动态规划(回溯暂时没看懂)
dp表示前i位是否可用wordDict表示,返回dp[-1]
trick是如果dp[i]为True,s[i:j]又在worddict中,则dp[j]为true。ij都需要枚举
1 2 3 4 5 6 7 8 9 10 class Solution : def wordBreak (self, s: str , wordDict: List [str ] ) -> bool : n=len (s) dp=[False ]*(n+1 ) dp[0 ]=True for i in range (n): for j in range (i+1 ,n+1 ): if (dp[i] and (s[i:j] in wordDict)): dp[j]=True return dp[-1 ]
异或:n^n=0, n^0=n
1 2 3 4 5 6 class Solution : def singleNumber (self, nums: List [int ] ) -> int : ans = 0 for n in nums: ans ^= n return ans
返回数目
用dp[i][j]表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。该定义j一定>=i,所以dp只填充右上部分
当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 8 9 10 11 12 13 14 class Solution : def countSubstrings (self, s: str ) -> int : dp = [[False ] * len (s) for _ in range (len (s))] result = 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 : result += 1 dp[i][j] = True elif dp[i+1 ][j-1 ]: result += 1 dp[i][j] = True return result
双指针法:一个元素可以作为中心点,两个元素也可以作为中心点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def countSubstrings (self, s: str ) -> int : res = 0 for i in range (len (s)): res += self .extend(s, i, i, len (s)) res += self .extend(s, i, i + 1 , len (s)) return res def extend (self, s, i, j, n ): res = 0 while i >= 0 and j < n and s[i] == s[j]: i -= 1 j += 1 res += 1 return res
这里连续的意思是下一个数字要+1
O(n)所以不能排序
把nums放进集合(去除重复元素,因为重复元素不连续)
如果 x−1 在哈希集合中,则不以 x 为起点。为什么?因为以 x−1 为起点计算出的序列长度,一定比以 x 为起点计算出的序列长度要长!这样可以避免大量重复计算。比如 nums=[3,2,4,5],从 3 开始,我们可以找到 3,4,5 这个连续序列;而从 2 开始,我们可以找到 2,3,4,5 这个连续序列,一定比从 3 开始的序列更长。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def longestConsecutive (self, nums: List [int ] ) -> int : ans = 0 st = set (nums) for x in st: if x - 1 in st: continue y = x + 1 while y in st: y += 1 ans = max (ans, y - x) return ans
各值和,不一定经过根节点
这里dfs算的是链!
注意一定要写nonlocal,如果只是读取外层变量,不用nonlocal,但这里要修改,加入nonlocal用于在嵌套函数中修改外层函数的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def maxPathSum (self, root: Optional [TreeNode] ) -> int : ans = -inf def dfs (node: Optional [TreeNode] ) -> int : if node is None : return 0 l_val = dfs(node.left) r_val = dfs(node.right) nonlocal ans ans = max (l_val + r_val + node.val, ans) return max (max (l_val, r_val) + node.val, 0 ) dfs(root) return ans
背包问题
因为求最小值所以用inf初始化dp
1 2 3 4 5 6 7 8 class Solution : def coinChange (self, coins: List [int ], amount: int ) -> int : dp = [0 ] + [inf] * amount for n in coins: for c in range (n, amount + 1 ): dp[c] = min (dp[c], dp[c - n] + 1 ) ans = dp[amount] return ans if ans < inf else -1
当s-|target|是奇数时直接返回0即可
dfs(i, c)表示用i个数字凑出背包容量的方案数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def findTargetSumWays (self, nums: List [int ], target: int ) -> int : s = sum (nums) - abs (target) if s < 0 or s % 2 : return 0 m = s // 2 @cache def dfs (i: int , c: int ) -> int : if i < 0 : return 1 if c == 0 else 0 if c < nums[i]: return dfs(i - 1 , c) return dfs(i - 1 , c) + dfs(i - 1 , c - nums[i]) return dfs(len (nums) - 1 , m)
异或剩下不为1的数字就对应二进制位不同的位置
1 2 3 4 class Solution : def hammingDistance (self, x: int , y: int ) -> int : return (x ^ y).bit_count()
列表查询的时间为on,集合只用o1,因为list是连续内存存储,set是哈希表
1 2 3 4 5 6 7 8 9 class Solution : def findDisappearedNumbers (self, nums: List [int ] ) -> List [int ]: n = len (nums) nums = set (nums) res = [] for i in range (1 , n + 1 ): if i not in nums: res.append(i) return res
异位词:字母相同顺序不同
暴力法:以右侧下标做对比可以保证左侧数据早已加入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def findAnagrams (self, s: str , p: str ) -> List [int ]: ans = [] cnt_p = Counter(p) cnt_s = Counter() for right, c in enumerate (s): cnt_s[c] += 1 left = right - len (p) + 1 if left < 0 : continue if cnt_s == cnt_p: ans.append(left) cnt_s[s[left]] -= 1 return ans
下面的方法是通过比较右侧值保证[left,right]内有cnt的内容
1.维护一个有条件的滑动窗口; 2.右端点右移,导致窗口扩大,是不满足条件的罪魁祸首; 3.左端点右移目的是为了缩小窗口,重新满足条件
如果出现一个非p的字符,cnt[c]就为-1,left会移到right+1的位置,right下一个循环也是right+1。只有字符和次数都和p正好相同,left才不会移动,right进行右移扩大范围到长度为p
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def findAnagrams (self, s: str , p: str ) -> List [int ]: ans = [] cnt = Counter(p) left = 0 for right, c in enumerate (s): cnt[c] -= 1 while cnt[c] < 0 : cnt[s[left]] += 1 left += 1 if right - left + 1 == len (p): ans.append(left) return ans
路径必须从父节点到子节点
前缀和+回溯/DFS
dfs(node, presum)以node为最后节点的,节点和等于目标和的路径数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def pathSum (self, root: Optional [TreeNode], targetSum: int ) -> int : def dfs (node: Optional [TreeNode], presum: int ) -> int : if not node: return 0 presum += node.val path_cnt = presum_counts.get(presum - targetSum, 0 ) presum_counts[presum] = presum_counts.get(presum, 0 ) + 1 path_cnt += dfs(node.left, presum) + dfs(node.right, presum) presum_counts[presum] -= 1 return path_cnt presum_counts = {0 : 1 } return dfs(root, 0 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def canPartition (self, nums: List [int ] ) -> bool : s = sum (nums) n = len (nums) if s % 2 : return False num = s // 2 dp = [[0 ] * (num + 1 ) for _ in range (n + 1 )] dp[0 ][0 ] = 1 for i in range (1 , n + 1 ): for j in range (num + 1 ): if j >= nums[i - 1 ]: dp[i][j] = dp[i - 1 ][j] + dp[i - 1 ][j - nums[i - 1 ]] else : dp[i][j] = dp[i - 1 ][j] return False if dp[n][num] == 0 else True
people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
要把乱序的数组排列成实际身高的队形
一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。
这里首先按照数对的元素 1 降序排序,按照数对的元素 2 升序排序。保证身高高的在前面,以及同样身高的排在前面的在前面(因为>=的人少),这样也保证在插队时不会有数量错误
1 2 3 4 5 6 7 8 9 10 class Solution : def reconstructQueue (self, people: List [List [int ]] ) -> List [List [int ]]: res = [] people = sorted (people, key = lambda x: (-x[0 ], x[1 ])) for p in people: if len (res) <= p[1 ]: res.append(p) elif len (res) > p[1 ]: res.insert(p[1 ], p) return res
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。
建立过渡变量,用图的形式构造双向边(dict:{}),值作为权重
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 : def calcEquation (self, equations: List [List [str ]], values: List [float ], queries: List [List [str ]] ) -> List [float ]: graph = {} for (x, y), v in zip (equations, values): if x in graph: graph[x][y] = v else : graph[x] = {y: v} if y in graph: graph[y][x] = 1 /v else : graph[y] = {x: 1 /v} def dfs (s, t ) -> int : if s not in graph: return -1 if t == s: return 1 for node in graph[s].keys(): if node == t: return graph[s][node] elif node not in visited: visited.add(node) v = dfs(node, t) if v != -1 : return graph[s][node]*v return -1 res = [] for qs, qt in queries: visited = set () res.append(dfs(qs, qt)) return res
难点在于嵌套括号,以及数字可能也是多位数,用栈!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def decodeString (self, s: str ) -> str : stack, res, multi = [], "" , 0 for c in s: if c == '[' : stack.append([multi, res]) res, multi = "" , 0 elif c == ']' : cur_multi, last_res = stack.pop() res = last_res + cur_multi * res elif '0' <= c <= '9' : multi = multi * 10 + int (c) else : res += c return res
先获得频率,再创建小顶堆。如果比k个数的最小值大,就更换数据
heapq会自动把最小的frequency放在开头,因为封装了最小堆,从小到大排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import heapqclass Solution : def topKFrequent (self, nums: List [int ], k: int ) -> List [int ]: map_ = {} for num in nums: map_[num] = map_.get(num, 0 ) + 1 heap = [] for num, freq in map_.items(): if len (heap) < k: heapq.heappush(heap, (freq, num)) else : if freq > heap[0 ][0 ]: heapq.heappushpop(heap, (freq, num)) res = [] while heap: res.append(heapq.heappop(heap)[1 ]) return res
内置函数就是bin和count(‘1’)
动态规划:某个偶数肯定能由前面的某个数左移一位得到,如十进制6对应的二进制为110,由十进制3对应二进制11左移一位得到。 某个奇数肯定能由前面的某个数左移一位并加上1得到,如十进制7对应的二进制为111,是由十进制3对应二进制11左移一位为110并加一得到。利用位运算的性质
1 2 3 4 5 6 7 8 class Solution : def countBits (self, num: int ) -> List [int ]: dp=[0 ]*(num+1 ) for i in range (num//2 +1 ): dp[i*2 ]=dp[i] if i*2 +1 <=num: dp[i*2 +1 ]=dp[i]+1 return dp
对于选择了根,那么我们就不能选它的儿子了 如果没有选根,我们就可以任意选了(即选最大的那一个)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def dp (self,root ): if not root : return [0 ,0 ] l=self .dp(root.left) r=self .dp(root.right) return [max (l)+max (r), root.val+l[0 ]+r[0 ]] def rob (self, root: TreeNode ) -> int : if not root: return 0 return max (self .dp(root))
1 2 3 4 5 6 7 8 class Solution : def maxProfit (self, prices: List [int ] ) -> int : ans = 0 minPrice = prices[0 ] for p in prices: ans = max (ans, p - minPrice) minPrice = min (minPrice, p) return ans
f [i ][j ] 表示戳破区间 (i ,j ) 内的所有气球能得到的最多硬币数,开区间
如果k是最后一个戳破的气球,那得到的硬币数量是确定的
f[i ][j ]=max(f[i][j],f[i][k]+f[k][j]+arr[i]×arr[k]×arr[j]) k∈(i,j) 引入k,非常巧妙
确定ij的遍历顺序:f[i][j]依赖于f[k][j],k>i,所以i要从大到小;f[i][k]<f[i][j],j要从从小到大
1 2 3 4 5 6 7 8 9 10 class Solution : def maxCoins (self, nums: List [int ] ) -> int : n = len (nums) arr = [1 ] + nums + [1 ] f = [[0 ] * (n + 2 ) for _ in range (n + 2 )] for i in range (n - 1 , -1 , -1 ): for j in range (i + 2 , n + 2 ): for k in range (i + 1 , j): f[i][j] = max (f[i][j], f[i][k] + f[k][j] + arr[i] * arr[k] * arr[j]) return f[0 ][-1 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def maxProfit (self, prices: List [int ] ) -> int : if len (prices) < 2 : return 0 dp0 = 0 dp1 = float ("-inf" ) dp2 = - prices[0 ] for i in range (1 , len (prices)): new_dp0 = max (dp0, dp1) new_dp1 = dp2 + prices[i] new_dp2 = max (dp2, dp0 - prices[i]) dp0, dp1, dp2 = new_dp0, new_dp1, new_dp2 return max (dp0, dp1)
为什么判断cr>cl?如果右比左多,无论后面怎么添加字符,都无法形成有效的括号组合(因为 ')' 必须在 '(' 之后出现)。
但是在遍历过程中,'(' 可以暂时多于 ')'(例如 "(()" 是部分有效的,后面可能补上 ')')。
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 : def removeInvalidParentheses (self, s: str ) -> List [str ]: l = r = 0 for c in s: if c == '(' : l += 1 elif c == ')' : if l: l -= 1 else : r += 1 ans = [] @cache def dfs (idx, cl, cr, dl, dr, path ): if idx == len (s): if not dl and not dr: ans.append(path) return if cr > cl or dl < 0 or dr < 0 : return c = s[idx] if c == '(' : dfs(idx+1 ,cl,cr,dl-1 ,dr, path) elif c == ')' : dfs(idx+1 ,cl,cr,dl,dr-1 , path) dfs(idx+1 ,cl+(c=='(' ),cr+(c==')' ),dl,dr, path+c) dfs(0 , 0 , 0 , l, r, "" ) return ans
为什么可以保证删除最小数量的无效括号?由于 dl 和 dr 的限制,DFS 只会探索恰好删除 l 个 '(' 和 r 个 ')' 的路径。如果有更少的删除的话,最终字符串一定有多余的括号。其实我们是通过合法字符串的规则进行了约束。
dp表示以 nums [i ] 结尾的最长子序列长度。
1 2 3 4 5 6 7 8 class Solution : def lengthOfLIS (self, nums: List [int ] ) -> int : dp = [1 ] * len (nums) for i in range (len (nums)): for j in range (0 , i): if nums[i] > nums[j]: dp[i] = max (dp[i], dp[j] + 1 ) return max (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 class Codec : def serialize (self, root ): if not root: return "[]" queue = deque([root]) res = [] while queue: node = queue.popleft() if node: res.append(str (node.val)) queue.append(node.left) queue.append(node.right) else : res.append("null" ) while res and res[-1 ] == "null" : res.pop() return "[" + "," .join(res) + "]" def deserialize (self, data ): if data == "[]" : return None values = data[1 :-1 ].split("," ) root = TreeNode(int (values[0 ])) queue = deque([root]) i = 1 while queue and i < len (values): node = queue.popleft() if i < len (values) and values[i] != "null" : node.left = TreeNode(int (values[i])) queue.append(node.left) i += 1 if i < len (values) and values[i] != "null" : node.right = TreeNode(int (values[i])) queue.append(node.right) i += 1 return root
已知数字都在[1, n]范围内,有n+1个整数,根据抽屉原理至少存在一个重复的整数
用二分法(知道最大最小界),数在范围里的数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def findDuplicate (self, nums: List [int ] ) -> int : min_val = 1 max_val = len (nums) while min_val < max_val: mid = (min_val + max_val) // 2 cnt = sum (min_val <= num <= mid for num in nums) if cnt > mid - min_val + 1 : max_val = mid else : min_val = mid + 1 return min_val
因为要保持原数组非零元素的顺序,用双指针
将 l 移动到自身右侧第一个元素为 0 的位置,将 r 移动到 l 右侧第一个元素非 0 的位置,然后交换元素
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def moveZeroes (self, nums: List [int ] ) -> None : l = 0 r = 0 while r < len (nums): if r == l or nums[r] == 0 : r += 1 elif nums[l] != 0 : l += 1 else : nums[l] = nums[r] nums[r] = 0
完全平方数满足n^2,用最少的完全平方数使和为n,也是DP
DFS都可以1:1翻译成递推,往往再进行空间优化
1 2 3 4 5 6 7 8 9 N = 10000 f = [0 ] + [inf] * N for i in range (1 , isqrt(N) + 1 ): for j in range (i * i, N + 1 ): f[j] = min (f[j], f[j - i * i] + 1 ) class Solution : def numSquares (self, n: int ) -> int : return f[n]
为什么f写在类的外面而不是函数内就不会超时:作为全局变量不用重复计算
在程序启动时,f 数组被计算一次(时间复杂度 O(N√N)),之后所有测试用例的 numSquares(n) 查询都是 O(1)。
48:253.会议室II 堆 时间间隔问题,按照会议的开始时间进行排序
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 import heapqdef minMeetingRooms (intervals ): if not intervals: return 0 free_rooms = [] intervals.sort(key=lambda x: x[0 ]) heapq.heappush(free_rooms, intervals[0 ][1 ]) for i in intervals[1 :]: if i[0 ] >= free_rooms[0 ]: heapq.heappop(free_rooms) heapq.heappush(free_rooms, i[1 ]) return len (free_rooms)
类似二叉搜索树,3/7这种左下角、右上角元素称为标志数flag,每次比较可以消除一行或一列
若 flag > target ,则 target 一定在 flag 所在 行的上方 ,即 flag 所在行可被消去。
若 flag < target ,则 target 一定在 flag 所在 列的右方 ,即 flag 所在列可被消去。
我们从左下角开始找:
1 2 3 4 5 6 7 8 9 10 11 class Solution : def searchMatrix (self, matrix: List [List [int ]], target: int ) -> bool : i, j = len (matrix) - 1 , 0 while i >= 0 and j < len (matrix[0 ]): if matrix[i][j] > target: i -= 1 elif matrix[i][j] < target: j += 1 else : return True return False
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from collections import dequefrom typing import List class Solution : def maxSlidingWindow (self, nums: List [int ], k: int ) -> List [int ]: q = deque() result = [] for i in range (len (nums)): while q and q[0 ] < i - k + 1 : q.popleft() while q and nums[i] >= nums[q[-1 ]]: q.pop() q.append(i) if i >= k - 1 : result.append(nums[q[0 ]]) return result
n为生成括号的对数
先左括号+让右小于左是为了保证括号组合有效
回溯模板:
1 2 3 4 5 6 7 8 9 10 11 12 def backtrack(路径, 选择列表): if 满足终止条件: 结果.append(路径.copy()) # 注意深拷贝 return for 选择 in 选择列表: if 剪枝条件: # 可选,提前跳过无效选择 continue 做选择 # 将当前选择加入路径 backtrack(路径, 新选择列表) # 递归 撤销选择 # 从路径中移除当前选择
本题注意不等式的细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def generateParenthesis (self, n: int ) -> List [str ]: ans = [] def backtrack (S, left, right ): if len (S) == 2 * n: ans.append('' .join(S)) return if left < n: S.append('(' ) backtrack(S, left+1 , right) S.pop() if right < left: S.append(')' ) backtrack(S, left, right+1 ) S.pop() backtrack([], 0 , 0 ) return ans
把输入数组中的字母异位词组合到一起
把字符串排序后字母异位词应该一样
1 2 3 4 5 6 class Solution : def groupAnagrams (self, strs: List [str ] ) -> List [List [str ]]: d = defaultdict(list ) for s in strs: d['' .join(sorted (s))].append(s) return list (d.values())
原地旋转二维矩阵,不采用额外的空间
如果用拷贝,记得用深拷贝!
1 2 3 4 5 6 7 8 9 class Solution : def rotate (self, matrix: List [List [int ]] ) -> None : n = len (matrix) tmp = copy.deepcopy(matrix) for i in range (n): for j in range (n): matrix[j][n - 1 - i] = tmp[i][j]
原地修改时只用看左上角1/4的部分,就可以实现全局修改
1 2 3 4 5 6 7 8 9 10 class Solution : def rotate (self, matrix: List [List [int ]] ) -> None : n = len (matrix) for i in range (n // 2 ): for j in range ((n + 1 ) // 2 ): tmp = matrix[i][j] matrix[i][j] = matrix[n - 1 - j][i] matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j] matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i] matrix[j][n - 1 - i] = tmp
回溯算法:
终止条件:长度为len-1
递推参数:当前固定位x
递推工作:固定nums[i]作为当前位元素
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def permute (self, nums: List [int ] ) -> List [List [int ]]: def dfs (x ): if x == len (nums) - 1 : res.append(list (nums)) return for i in range (x, len (nums)): nums[i], nums[x] = nums[x], nums[i] dfs(x + 1 ) nums[i], nums[x] = nums[x], nums[i] res = [] dfs(0 ) return res
res[i] = min(l_max[i], r_max[i]) - height[i]:min是木桶效应,决定当前柱子i能承载的最高水位;减去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 class Solution : def trap (self, height: List [int ] ) -> int : if not height: return 0 n = len (height) l_max = [0 ] * n r_max = [0 ] * n l_max[0 ] = height[0 ] for i in range (1 , n): l_max[i] = max (l_max[i-1 ], height[i]) r_max[-1 ] = height[-1 ] for i in range (n-2 , -1 , -1 ): r_max[i] = max (r_max[i+1 ], height[i]) res = 0 for i in range (n): res += min (l_max[i], r_max[i]) - height[i] return res
dfs(i, left) i是当前选择的下标,left是剩余的数
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 : def combinationSum (self, candidates: List [int ], target: int ) -> List [List [int ]]: candidates.sort() ans = [] path = [] def dfs (i: int , left: int ) -> None : if left == 0 : ans.append(path.copy()) return if i == len (candidates) or left < candidates[i]: return dfs(i + 1 , left) path.append(candidates[i]) dfs(i, left - candidates[i]) path.pop() dfs(0 , target) return ans
求左右子树深度值的最大值
注意返回的是子树的链长
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def diameterOfBinaryTree (self, root: Optional [TreeNode] ) -> int : ans = 0 def dfs (node: Optional [TreeNode] ) -> int : if node is None : return -1 l_len = dfs(node.left) + 1 r_len = dfs(node.right) + 1 nonlocal ans ans = max (ans, l_len + r_len) return max (l_len, r_len) dfs(root) return ans
递增的数组
开始位置和结束位置
闭区间的二分写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def lower_bound (self, nums: List [int ], target: int ) -> int : left, right = 0 , len (nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] >= target: right = mid - 1 else : left = mid + 1 return left def searchRange (self, nums: List [int ], target: int ) -> List [int ]: start = self .lower_bound(nums, target) if start == len (nums) or nums[start] != target: return [-1 , -1 ] end = self .lower_bound(nums, target + 1 ) - 1 return [start, end]
排序数组被旋转了,要找目标值
思路:先找排序数组的最小值,知道目标值在哪一段,然后在那一段进行二分查找
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 : def findMin (self, nums: List [int ] ) -> int : left, right = -1 , len (nums) - 1 while left + 1 < right: mid = (left + right) // 2 if nums[mid] < nums[-1 ]: right = mid else : left = mid return right def lower_bound (self, nums: List [int ], left: int , right: int , target: int ) -> int : while left + 1 < right: mid = (left + right) // 2 if nums[mid] >= target: right = mid else : left = mid return right if nums[right] == target else -1 def search (self, nums: List [int ], target: int ) -> int : i = self .findMin(nums) if target > nums[-1 ]: return self .lower_bound(nums, -1 , i, target) return self .lower_bound(nums, i - 1 , len (nums), target)
只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
先遍历一遍字符串,用栈把左右括号进行匹配,然后计算连续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 : def longestValidParentheses (self, s: str ) -> int : stack = [] maxL = 0 n = len (s) tmp = [0 ] * n cur = 0 for i in range (n): if s[i] == '(' : stack.append(i) else : if stack: j = stack.pop() tmp[i], tmp[j] = 1 ,1 for num in tmp: if num: cur += 1 else : maxL = max (cur, maxL) cur = 0 maxL = max (cur,maxL) return maxL
下一个排列是字典序更大的排列
最大的字典序是从左到右依次递减的,下一个是最小字典序
否则从右向左寻找第一个严格递减的位置i;然后从右向左寻找第一个严格小于该位置值的位置j;
将i右侧的所有值翻转:
1 2 1 4 2 i = 2,j=4 1 2 1 2 4 1 然后翻转 得到2 1 2 1 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def nextPermutation (self, nums: List [int ] ) -> None : if nums == sorted (nums, reverse=True ): return nums.sort() n = len (nums) i, j = n - 2 , n - 1 while not nums[i] < nums[i + 1 ]: i -= 1 while not nums[j] > nums[i]: j -= 1 nums[i], nums[j] = nums[j], nums[i] left, right = i + 1 , n - 1 while left < right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1
看不太懂题,但是先递归右边,再赋给中间,再递归左边
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def convertBST (self, root: TreeNode ) -> TreeNode: s = 0 def dfs (node: TreeNode ) -> None : if node is None : return dfs(node.right) nonlocal s s += node.val node.val = s dfs(node.left) dfs(root) return root
最小堆:这可以用最小堆实现。初始把所有链表的头节点入堆,然后不断弹出堆中最小节点 x,如果 x.next 不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来,就得到了答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ListNode.__lt__ = lambda a, b: a.val < b.val class Solution : def mergeKLists (self, lists: List [Optional [ListNode]] ) -> Optional [ListNode]: cur = dummy = ListNode() h = [head for head in lists if head] heapify(h) while h: node = heappop(h) if node.next : heappush(h, node.next ) cur.next = node cur = cur.next return dummy.next
注意要求子数组是数组中连续的非空序列,所以要用前缀和
1 2 3 4 5 6 7 8 9 10 class Solution : def subarraySum (self, nums: List [int ], k: int ) -> int : ans = s = 0 cnt = defaultdict(int ) cnt[0 ] = 1 for x in nums: s += x ans += cnt[s - k] cnt[s] += 1 return ans
1 2 3 4 5 6 7 8 9 10 class Solution : def mergeTwoLists (self, list1: Optional [ListNode], list2: Optional [ListNode] ) -> Optional [ListNode]: if not list1: return list2 if not list2: return list1 if list1.val <= list2.val: list1.next = self .mergeTwoLists(list1.next , list2) return list1 else : list2.next = self .mergeTwoLists(list1, list2.next ) return list2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution : def isValid (self, s: str ) -> bool : st = [] for c in s: if c == "]" and st: tmp = st.pop() if tmp != "[" : return False elif c == "}" and st: tmp = st.pop() if tmp != "{" : return False elif c == ")" and st: tmp = st.pop() if tmp != "(" : return False else : st.append(c) return False if st else True
注意是倒数第n个,而不是正数第n个,所以slow才能找到倒数第一个
1 2 3 4 5 6 7 8 9 10 11 class Solution : def removeNthFromEnd (self, head: Optional [ListNode], n: int ) -> Optional [ListNode]: dummy = ListNode(0 , head) fast = slow = dummy for _ in range (n + 1 ): fast = fast.next while fast: fast = fast.next slow = slow.next slow.next = slow.next .next return dummy.next
符合回溯模板,for遍历选择,+letter写在参数里面了所以没有+ -的添加和移除
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 : def letterCombinations (self, digits: str ) -> List [str ]: if not digits: return [] phone = {'2' :['a' ,'b' ,'c' ], '3' :['d' ,'e' ,'f' ], '4' :['g' ,'h' ,'i' ], '5' :['j' ,'k' ,'l' ], '6' :['m' ,'n' ,'o' ], '7' :['p' ,'q' ,'r' ,'s' ], '8' :['t' ,'u' ,'v' ], '9' :['w' ,'x' ,'y' ,'z' ]} def backtrack (conbination, nextdigit ): if len (nextdigit) == 0 : res.append(conbination) else : for letter in phone[nextdigit[0 ]]: backtrack(conbination + letter,nextdigit[1 :]) res = [] backtrack('' , digits) return res
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 : def threeSum (self, nums: List [int ] ) -> List [List [int ]]: res = [] nums.sort() for i in range (len (nums)): if nums[i] > 0 : return res if i > 0 and nums[i] == nums[i - 1 ]: continue left = i + 1 right = len (nums) - 1 while left < right: sum_ = nums[i] + nums[left] + nums[right] if sum_ < 0 : left += 1 elif sum_ > 0 : right -= 1 else : res.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 res
S (i ,j )=min (h [i ],h [j ])×(j −i )
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxArea (self, height: List [int ] ) -> int : i, j, res = 0 , len (height) - 1 , 0 while i < j: if height[i] < height[j]: res = max (res, height[i] * (j - i)) i += 1 else : res = max (res, height[j] * (j - i)) j -= 1 return res
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def isMatch (self, s: str , p: str ) -> bool : m, n = len (s) + 1 , len (p) + 1 dp = [[False ] * n for _ in range (m)] dp[0 ][0 ] = True for j in range (2 , n, 2 ): dp[0 ][j] = dp[0 ][j - 2 ] and p[j - 1 ] == '*' for i in range (1 , m): for j in range (1 , n): dp[i][j] = dp[i][j - 2 ] or dp[i - 1 ][j] and (s[i - 1 ] == p[j - 2 ] or p[j - 2 ] == '.' ) \ if p[j - 1 ] == '*' else \ dp[i - 1 ][j - 1 ] and (p[j - 1 ] == '.' or s[i - 1 ] == p[j - 1 ]) return dp[-1 ][-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 class Solution : def longestPalindrome (self, s: str ) -> str : if s == '' : return '' n = len (s) start = end = 0 for i in range (n): left = right = i while left >= 0 and right < n and s[left] == s[right]: left -= 1 right += 1 cur_len = right - left - 1 if cur_len > end - start + 1 : start = left + 1 end = right - 1 left, right = i, i+1 while left >= 0 and right < n and s[left] == s[right]: left -= 1 right += 1 cur_len = right - left - 1 if cur_len > end - start + 1 : start = left + 1 end = right - 1 return s[start:end+1 ]
逻辑是第k小的数,要么在nums1的前k个数,要么在nums2的前k个数。利用二分思想,每次可以删k/2个不可能的数,对比当前n1[k/2]和n2[k/2](减去删除过的数index)
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 class Solution : def getKthElement (self, nums1: List [int ], nums2: List [int ], k: int ) -> int : """找到两个已排序数组中的第 k 小的元素""" m, n = len (nums1), len (nums2) index1, index2 = 0 , 0 while True : if index1 == m: return nums2[index2 + k - 1 ] if index2 == n: return nums1[index1 + k - 1 ] if k == 1 : return min (nums1[index1], nums2[index2]) newIndex1 = min (index1 + k // 2 - 1 , m - 1 ) newIndex2 = min (index2 + k // 2 - 1 , n - 1 ) pivot1 = nums1[newIndex1] pivot2 = nums2[newIndex2] if pivot1 <= pivot2: k -= newIndex1 - index1 + 1 index1 = newIndex1 + 1 else : k -= newIndex2 - index2 + 1 index2 = newIndex2 + 1 def findMedianSortedArrays (self, nums1: List [int ], nums2: List [int ] ) -> float : """计算两个已排序数组的中位数""" totalLength = len (nums1) + len (nums2) if totalLength % 2 == 1 : return self .getKthElement(nums1, nums2, (totalLength + 1 ) // 2 ) else : return (self .getKthElement(nums1, nums2, totalLength // 2 ) + self .getKthElement(nums1, nums2, totalLength // 2 + 1 )) / 2.0
子串是连续的
滑动窗口+哈希表
1 2 3 4 5 6 7 8 9 10 11 class Solution : def lengthOfLongestSubstring (self, s: str ) -> int : dic = {} res = 0 i = -1 for j in range (len (s)): if s[j] in dic: i = max (dic[s[j]], i) dic[s[j]] = j res = max (res, j - i) return res
1 2 3 4 5 6 7 8 9 10 class Solution : def addTwoNumbers (self, l1: Optional [ListNode], l2: Optional [ListNode], carry=0 ) -> Optional [ListNode]: if l1 is None and l2 is None : return ListNode(carry) if carry else None if l1 is None : l1, l2 = l2, l1 s = carry + l1.val + (l2.val if l2 else 0 ) l1.val = s % 10 l1.next = self .addTwoNumbers(l1.next , l2.next if l2 else None , s // 10 ) return l1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def exist (self, board: List [List [str ]], word: str ) -> bool : def dfs (i, j, k ): if not 0 <= i < len (board) or not 0 <= j < len (board[0 ]) or board[i][j] != word[k]: return False if k == len (word) - 1 : return True board[i][j] = '' res = dfs(i + 1 , j, k + 1 ) or dfs(i - 1 , j, k + 1 ) or dfs(i, j + 1 , k + 1 ) or dfs(i, j - 1 , k + 1 ) board[i][j] = word[k] return res for i in range (len (board)): for j in range (len (board[0 ])): if dfs(i, j, 0 ): return True return False
链表应该是先序遍历
头插法反过来了:右左中,倒着在插
1 2 3 4 5 6 7 8 9 10 11 class Solution : head = None def flatten (self, root: Optional [TreeNode] ) -> None : if root is None : return self .flatten(root.right) self .flatten(root.left) root.left = None root.right = self .head self .head = root
假如 只有 任务 A 且出现了 freq_A 次,那么执行这些任务的最少时间是:(freq_A - 1) * (n+1) + 1.
假设有多种任务,且需要填充空闲,A 还是出现次数最多的任务。此时 ans = (freq_A-1) * (n+1) + 1 + p p 等于和 A 出现次数一致的数目。
first_freq就是出现次数最多的任务的执行次数
1 2 3 4 5 6 7 8 9 class Solution : def leastInterval (self, tasks: List [str ], need: int ) -> int : n, c = len (tasks), Counter(tasks) most = c.most_common() first_freq, cnt = most[0 ][1 ], 1 for i in range (1 , len (most)): if most[i][1 ] == first_freq: cnt += 1 res = (first_freq - 1 ) * (need+1 ) + cnt return res if res >= n else n
合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
1 2 3 4 5 6 7 class Solution : def mergeTrees (self, root1: Optional [TreeNode], root2: Optional [TreeNode] ) -> Optional [TreeNode]: if root1 is None : return root2 if root2 is None : return root1 return TreeNode(root1.val + root2.val, self .mergeTrees(root1.left, root2.left), self .mergeTrees(root1.right, root2.right))
知道前序(中左右)和中序(左中右),然后递推
1 2 3 4 5 6 7 8 class Solution : def buildTree (self, preorder: List [int ], inorder: List [int ] ) -> Optional [TreeNode]: if not preorder: return None left_size = inorder.index(preorder[0 ]) left = self .buildTree(preorder[1 : 1 + left_size], inorder[:left_size]) right = self .buildTree(preorder[1 + left_size:], inorder[1 + left_size:]) return TreeNode(preorder[0 ], left, right)
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def maxDepth (self, root: Optional [TreeNode] ) -> int : if not root: return 0 if not root.left and not root.right: return 1 l, r = 1 , 1 if root.left: l = self .maxDepth(root.left) + 1 if root.right: r = self .maxDepth(root.right) + 1 return max (l, r)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def levelOrder (self, root: Optional [TreeNode] ) -> List [List [int ]]: if not root: return [] queue = collections.deque([root]) result = [] while queue: level = [] for _ in range (len (queue)): cur = queue.popleft() level.append(cur.val) if cur.left: queue.append(cur.left) if cur.right: queue.append(cur.right) result.append(level) return result
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def isSymmetric (self, root: Optional [TreeNode] ) -> bool : if not root: return True return self .compare(root.left, root.right) def compare (self, left, right ): if left == None and right != None : return False elif left != None and right == None : return False elif left == None and right == None : return True elif left.val != right.val: return False outside = self .compare(left.left, right.right) inside = self .compare(left.right, right.left) return outside and inside
前序遍历
1 2 3 4 5 6 7 8 class Solution : def isValidBST (self, root: Optional [TreeNode], left=-inf, right=inf ) -> bool : if root is None : return True x = root.val return left < x < right and \ self .isValidBST(root.left, left, x) and \ self .isValidBST(root.right, x, right)
f(3) = f(0)*f(2) + f(1)*f(1) + f(2)*f(0)
f(n) = Σf(i)*f(n-1-i)
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def numTrees (self, n: int ) -> int : store = [1 ,1 ] if n <= 1 : return store[n] for m in range (2 ,n+1 ): s = m-1 count = 0 for i in range (m): count += store[i]*store[s-i] store.append(count) return store[n]
1 2 3 4 5 6 7 8 9 10 11 class Solution : def inorderTraversal (self, root: Optional [TreeNode] ) -> List [int ]: res = [] def traverse (node: Optional [TreeNode] ): if node is None : return traverse(node.left) res.append(node.val) traverse(node.right) traverse(root) return res
前缀和:预处理数组 的技术,用于快速计算数组中任意区间的和。使得后续的区间和查询可以在 O(1) 时间内完成。
单调栈:满足栈内元素单调递增或单调递减 的性质,通常用于解决“下一个更大/更小元素” 问题
这里栈中存储的索引对应的值是单调递增的
注意为什么是求完一层j的前缀和就分析最大面积?以某一层为底,并不是最后一层为底面积就最大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def maximalRectangle (self, matrix: List [List [str ]] ) -> int : if not matrix: return 0 m, n = len (matrix), len (matrix[0 ]) pre = [0 ] * (n+1 ) res = 0 for i in range (m): for j in range (n): pre[j] = pre[j] + 1 if matrix[i][j] == "1" else 0 stack = [-1 ] for k, num in enumerate (pre): while stack and pre[stack[-1 ]] > num: index = stack.pop() res = max (res, pre[index] * (k - stack[-1 ] - 1 )) stack.append(k) return res
就是没有上一步的前缀和了,直接用单调栈(还是补个0防止下标越界)
1 2 3 4 5 6 7 8 9 10 11 class Solution : def largestRectangleArea (self, heights: List [int ] ) -> int : res = 0 stack = [-1 ] heights.append(0 ) for k, num in enumerate (heights): while stack and heights[stack[-1 ]] > num: index = stack.pop() res = max (res, heights[index] * (k - stack[-1 ] - 1 )) stack.append(k) return res
只返回一种答案
1 2 3 4 5 6 7 8 9 class Solution : def twoSum (self, nums: List [int ], target: int ) -> List [int ]: record = dict () for index, value in enumerate (nums): if target - value in record: return [index, record[target - value]] else : record[value] = index return []
1 2 3 4 5 6 7 8 9 10 11 class Solution : def subsets (self, nums: List [int ] ) -> List [List [int ]]: res = [] n = len (nums) def helper (i, tmp ): res.append(tmp) for j in range (i, n): helper(j + 1 ,tmp + [nums[j]] ) helper(0 , []) return res
滑动窗口:把右指针从左到右移动,当涵盖时while移动左指针,找到更短的子串。计数器可以比较,很方便判断是否涵盖(注意不要求连续)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def minWindow (self, s: str , t: str ) -> str : ans_left, ans_right = -1 , len (s) cnt_s = Counter() cnt_t = Counter(t) left = 0 for right, c in enumerate (s): cnt_s[c] += 1 while cnt_s >= cnt_t: if right - left < ans_right - ans_left: ans_left, ans_right = left, right cnt_s[s[left]] -= 1 left += 1 return "" if ans_left < 0 else s[ans_left: ans_right + 1 ]
优化:
用defaultdict比Counter速度更快
less 初始化为 len(cnt),即 t 中不同字符的个数 。例如,t = "AABC" 有 3 个不同字符(A、B、C),所以 less = 3。表示当前窗口中还有多少种字符没有满足 t 中的要求 (即窗口中该字符的出现次数 小于 t 中的出现次数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution : def minWindow (self, s: str , t: str ) -> str : ans_left, ans_right = -1 , len (s) cnt = defaultdict(int ) for c in t: cnt[c] += 1 less = len (cnt) left = 0 for right, c in enumerate (s): cnt[c] -= 1 if cnt[c] == 0 : less -= 1 while less == 0 : if right - left < ans_right - ans_left: ans_left, ans_right = left, right x = s[left] if cnt[x] == 0 : less += 1 cnt[x] += 1 left += 1 return "" if ans_left < 0 else s[ans_left: ans_right + 1 ]
维护三个指针 p0:指向 0 应该放置的位置;p0左边全是0, p0本身并不包括0
p2:指向 2 应该放置的位置;p2右边全是2, p2本身并不包括2
i:当前遍历位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def sortColors (self, nums: List [int ] ) -> None : p0, i , p2 = 0 ,0 ,len (nums) - 1 while i <= p2: if nums[i] == 0 : nums[i], nums[p0] = nums[p0], nums[i] p0 += 1 i += 1 elif nums[i] == 2 : nums[i], nums[p2] = nums[p2], nums[i] p2 -= 1 else : i += 1
dp[i][j] 代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数
当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def minDistance (self, word1: str , word2: str ) -> int : n1 = len (word1) n2 = len (word2) dp = [[0 ] * (n2 + 1 ) for _ in range (n1 + 1 )] for j in range (1 , n2 + 1 ): dp[0 ][j] = dp[0 ][j - 1 ] + 1 for i in range (1 , n1 + 1 ): dp[i][0 ] = dp[i - 1 ][0 ] + 1 for i in range (1 , n1 + 1 ): for j in range (1 , n2 + 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], dp[i][j - 1 ], dp[i - 1 ][j - 1 ]) + 1 return dp[-1 ][-1 ]
#dp[n] = dp[n - 1] + dp[n - 2]
#dp[1] = 1
#dp[2] = 2
1 2 3 4 5 6 class Solution : def climbStairs (self, n: int ) -> int : a, b = 1 , 1 for _ in range (n - 1 ): a, b = b, a + b return b
单调性:右边的数永远大于左边的所有数,左边的数永远小于右边的所有数。
找到的是连续子数组,所以left/right找到存在比左边数小的right和存在比右边数大的left
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def findUnsortedSubarray (self, nums ): left, right, min_num, max_num = 0 , 0 , float ("inf" ), float ("-inf" ) for i, n in enumerate (nums): if n < max_num: right = i max_num = max (max_num, n) for i in range (len (nums) - 1 , -1 , -1 ): if nums[i] > min_num: left = i min_num = min (min_num, nums[i]) return 0 if left == right else right - left + 1
找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
1 2 3 4 5 6 7 8 9 10 class Solution : def minPathSum (self, grid: [[int ]] ) -> int : for i in range (len (grid)): for j in range (len (grid[0 ])): if i == j == 0 : continue elif i == 0 : grid[i][j] = grid[i][j - 1 ] + grid[i][j] elif j == 0 : grid[i][j] = grid[i - 1 ][j] + grid[i][j] else : grid[i][j] = min (grid[i - 1 ][j], grid[i][j - 1 ]) + grid[i][j] return grid[-1 ][-1 ]
dfs(i,j)表示从起点 (0,0) 走到 (i ,j ) 的路径数。
1 2 3 4 5 6 7 8 9 10 11 class Solution : def uniquePaths (self, m: int , n: int ) -> int : @cache def dfs (i: int , j: int ) -> int : if i < 0 or j < 0 : return 0 if i == 0 and j == 0 : return 1 return dfs(i - 1 , j) + dfs(i, j - 1 ) return dfs(m - 1 , n - 1 )
按照左端点排序
1 2 3 4 5 6 7 8 9 10 class Solution : def merge (self, intervals: List [List [int ]] ) -> List [List [int ]]: intervals.sort(key=lambda p: p[0 ]) ans = [] for p in intervals: if ans and p[0 ] <= ans[-1 ][1 ]: ans[-1 ][1 ] = max (ans[-1 ][1 ], p[1 ]) else : ans.append(p) return ans
思路:尽可能到达最远的位置。最远能到达某个位置,就一定能到达它前面的任何位置。
1 2 3 4 5 6 7 8 9 10 class Solution : def canJump (self, nums: List [int ] ) -> bool : max_i = 0 for i, jump in enumerate (nums): if max_i >= i and i + jump > max_i: max_i = i + jump if max_i< i: return False return True
1 2 3 4 5 6 7 8 9 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : ans = -inf dp = 0 for n in nums: dp = max (dp, 0 ) + n ans = max (ans, dp) return ans