// 反转以 head 为起点的 n 个节点,返回新的头结点 ListNode reverseN(ListNode head, int n){ if (n == 1) { // 记录第 n + 1 个节点 successor = head.next; return head; } // 以 head.next 为起点,需要反转前 n - 1 个节点 ListNode last = reverseN(head.next, n - 1);
//反转一部分 ListNode reverseBetween(ListNode head, int m, int n){ // base case if (m == 1) { return reverseN(head, n); } // 前进到反转的起点触发 base case head.next = reverseBetween(head.next, m - 1, n - 1); return head; }
//k个一组反转 ListNode reverseKGroup(ListNode head, int k){ if (head == null) returnnull; // 区间 [a, b) 包含 k 个待反转元素 ListNode a, b; a = b = head; for (int i = 0; i < k; i++) { // 不足 k 个,不需要反转,base case if (b == null) return head; b = b.next; } // 反转前 k 个元素 ListNode newHead = reverse(a, b); // 递归反转后续链表并连接起来 a.next = reverseKGroup(b, k); return newHead; }
/** 反转区间 [a, b) 的元素,注意是左闭右开 */ ListNode reverse(ListNode a, ListNode b){ ListNode pre, cur, nxt; pre = null; cur = a; nxt = a; // while 终止的条件改一下就行了 while (cur != b) { nxt = cur.next; cur.next = pre; pre = cur; cur = nxt; } // 返回反转后的头结点 return pre; }
回文链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// 回文单链表 // 左侧指针 ListNode left;
booleanisPalindrome(ListNode head){ left = head; return traverse(head); }
booleantraverse(ListNode right){ if (right == null) returntrue; boolean res = traverse(right.next); // 后序遍历代码 res = res && (right.val == left.val); left = left.next; return res; }
2. 二叉树
翻转二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 将整棵树的节点翻转 TreeNode invertTree(TreeNode root){ // base case if (root == null) { returnnull; }
/* 将 nums[lo..hi] 构造成符合条件的树,返回根节点 */ TreeNode build(int[] nums, int lo, int hi){ // base case if (lo > hi) { returnnull; }
// 找到数组中的最大值和对应的索引 int index = -1, maxVal = Integer.MIN_VALUE; for (int i = lo; i <= hi; i++) { if (maxVal < nums[i]) { index = i; maxVal = nums[i]; } }
TreeNode root = new TreeNode(maxVal); // 递归调用构造左右子树 root.left = build(nums, lo, index - 1); root.right = build(nums, index + 1, hi);
TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){
if (preStart > preEnd) { returnnull; }
// root 节点对应的值就是前序遍历数组的第一个元素 int rootVal = preorder[preStart]; // rootVal 在中序遍历数组中的索引 int index = 0; for (int i = inStart; i <= inEnd; i++) { if (inorder[i] == rootVal) { index = i; break; } }
intcount(int lo, int hi){ if (lo > hi) return1; // 查备忘录 if (memo[lo][hi] != 0) { return memo[lo][hi]; }
int res = 0; for (int mid = lo; mid <= hi; mid++) { int left = count(lo, mid - 1); int right = count(mid + 1, hi); res += left * right; } // 将结果存入备忘录 memo[lo][hi] = res;
return res; }
// plus 构造出所有的二叉搜索树 /* 主函数 */ public List<TreeNode> generateTrees(int n){ if (n == 0) returnnew LinkedList<>(); // 构造闭区间 [1, n] 组成的 BST return build(1, n); }
/* 构造闭区间 [lo, hi] 组成的 BST */ List<TreeNode> build(int lo, int hi){ List<TreeNode> res = new LinkedList<>(); // base case if (lo > hi) { res.add(null); return res; }
// 1、穷举 root 节点的所有可能。 for (int i = lo; i <= hi; i++) { // 2、递归构造出左右子树的所有合法 BST。 List<TreeNode> leftTree = build(lo, i - 1); List<TreeNode> rightTree = build(i + 1, hi); // 3、给 root 节点穷举所有左右子树的组合。 for (TreeNode left : leftTree) { for (TreeNode right : rightTree) { // i 作为根节点 root 的值 TreeNode root = new TreeNode(i); root.left = left; root.right = right; res.add(root); } } }
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){ // base case if (root == null) returnnull; if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); // 情况 1 if (left != null && right != null) { return root; } // 情况 2 if (left == null && right == null) { returnnull; } // 情况 3 return left == null ? right : left; }
完全二叉树节点数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
publicintcountNodes(TreeNode root){ TreeNode l = root, r = root; // 记录左、右子树的高度 int hl = 0, hr = 0; while (l != null) { l = l.left; hl++; } while (r != null) { r = r.right; hr++; } // 如果左右子树的高度相同,则是一棵满二叉树 if (hl == hr) { return (int)Math.pow(2, hl) - 1; } // 如果左右高度不同,则按照普通二叉树的逻辑计算 return1 + countNodes(root.left) + countNodes(root.right); }
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) { // 图中共有 numCourses 个节点 List<Integer>[] graph = new LinkedList[numCourses]; for (int i = 0; i < numCourses; i++) { graph[i] = new LinkedList<>(); } for (int[] edge : prerequisites) { int from = edge[1]; int to = edge[0]; // 修完课程 from 才能修课程 to // 在图中添加一条从 from 指向 to 的有向边 graph[from].add(to); } return graph; }
intsplitArray(int[] nums, int m){ // 一般搜索区间是左开右闭的,所以 hi 要额外加一 int lo = getMax(nums), hi = getSum(nums) + 1; while (lo < hi) { int mid = lo + (hi - lo) / 2; // 根据分割子数组的个数收缩搜索区间 int n = split(nums, mid); if (n == m) { // 收缩右边界,达到搜索左边界的目的 hi = mid; } elseif (n < m) { // 最大子数组和上限高了,减小一些 hi = mid; } elseif (n > m) { // 最大子数组和上限低了,增加一些 lo = mid + 1; } } return lo; }
/* 辅助函数,若限制最大子数组和为 max, 计算 nums 至少可以被分割成几个子数组 */ intsplit(int[] nums, int max){ // 至少可以分割的子数组数量 int count = 1; // 记录每个子数组的元素和 int sum = 0; for (int i = 0; i < nums.length; i++) { if (sum + nums[i] > max) { // 如果当前子数组和大于 max 限制 // 则这个子数组不能再添加元素了 count++; sum = nums[i]; } else { // 当前子数组和还没达到 max 限制 // 还可以添加元素 sum += nums[i]; } } return count; }
// 计算数组中的最大值 intgetMax(int[] nums){ int res = 0; for (int n : nums) res = Math.max(n, res); return res; }
// 计算数组元素和 intgetSum(int[] nums){ int res = 0; for (int n : nums) res += n; return res; }
string minWindow(string s, string t){ unordered_map<char, int> need, window; for (char c : t) need[c]++;
int left = 0, right = 0; int valid = 0; // 记录最小覆盖子串的起始索引及长度 int start = 0, len = INT_MAX; while (right < s.size()) { // c 是将移入窗口的字符 char c = s[right]; // 右移窗口 right++; // 进行窗口内数据的一系列更新 if (need.count(c)) { window[c]++; if (window[c] == need[c]) valid++; }
// 判断左侧窗口是否要收缩 while (valid == need.size()) { // 在这里更新最小覆盖子串 if (right - left < len) { start = left; len = right - left; } // d 是将移出窗口的字符 char d = s[left]; // 左移窗口 left++; // 进行窗口内数据的一系列更新 if (need.count(d)) { if (window[d] == need[d]) valid--; window[d]--; } } } // 返回最小覆盖子串 return len == INT_MAX ? "" : s.substr(start, len); }
// 判断 s 中是否存在 t 的排列 bool checkInclusion(string t, string s){ unordered_map<char, int> need, window; for (char c : t) need[c]++;
int left = 0, right = 0; int valid = 0; while (right < s.size()) { char c = s[right]; right++; // 进行窗口内数据的一系列更新 if (need.count(c)) { window[c]++; if (window[c] == need[c]) valid++; }
// 判断左侧窗口是否要收缩 while (right - left >= t.size()) { // 在这里判断是否找到了合法的子串 if (valid == need.size()) returntrue; char d = s[left]; left++; // 进行窗口内数据的一系列更新 if (need.count(d)) { if (window[d] == need[d]) valid--; window[d]--; } } } // 未找到符合条件的子串 returnfalse; }
int left = 0, right = 0; int res = 0; // 记录结果 while (right < s.size()) { char c = s[right]; right++; // 进行窗口内数据的一系列更新 window[c]++; // 判断左侧窗口是否要收缩 while (window[c] > 1) { char d = s[left]; left++; // 进行窗口内数据的一系列更新 window[d]--; } // 在这里更新答案 res = max(res, right - left); } return res; }
bool insert(int val){ // 若 val 已存在,不用再插入 if (valToIndex.count(val)) { returnfalse; } // 若 val 不存在,插入到 nums 尾部, // 并记录 val 对应的索引值 valToIndex[val] = nums.size(); nums.push_back(val); returntrue; }
bool remove(int val){ // 若 val 不存在,不用再删除 if (!valToIndex.count(val)) { returnfalse; } // 先拿到 val 的索引 int index = valToIndex[val]; // 将最后一个元素对应的索引修改为 index valToIndex[nums.back()] = index; // 交换 val 和最后一个元素 swap(nums[index], nums.back()); // 在数组中删除元素 val nums.pop_back(); // 删除元素 val 对应的索引 valToIndex.erase(val); returntrue; }
classSolution{ public: int sz; unordered_map<int, int> mapping;
Solution(int N, vector<int>& blacklist) { sz = N - blacklist.size(); for (int b : blacklist) { mapping[b] = 666; }
int last = N - 1; for (int b : blacklist) { // 如果 b 已经在区间 [sz, N) // 可以直接忽略 if (b >= sz) { continue; } while (mapping.count(last)) { last--; } mapping[b] = last; last--; } }
intpick(){ // 随机选取一个索引 int index = rand() % sz; // 这个索引命中了黑名单, // 需要被映射到其他位置 if (mapping.count(index)) { return mapping[index]; } // 若没命中黑名单,则直接返回 return index; } };
//移除元素 intremoveElement(int[] nums, int val){ int fast = 0, slow = 0; while (fast < nums.length) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } fast++; } return slow; }
//移动零 voidmoveZeroes(int[] nums){ // 去除 nums 中的所有 0 // 返回去除 0 之后的数组长度 int p = removeElement(nums, 0); // 将 p 之后的所有元素赋值为 0 for (; p < nums.length; p++) { nums[p] = 0; } }
// 见上文代码实现 intremoveElement(int[] nums, int val);
TwoSum无序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
int[] twoSum(int[] nums, int target) { int n = nums.length; HashMap<Integer, Integer> index = new HashMap<>(); // 构造一个哈希表:元素映射到相应的索引 for (int i = 0; i < n; i++) index.put(nums[i], i);
for (int i = 0; i < n; i++) { int other = target - nums[i]; // 如果 other 存在且不是 nums[i] 本身 if (index.containsKey(other) && index.get(other) != i) returnnewint[] {i, index.get(other)}; }
returnnewint[] {-1, -1}; }
二、动态规划
1. 基本技巧
凑零钱问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# dp[i] = x表示,当目标金额为i时,至少需要x枚硬币 int coinChange(vector<int>& coins, int amount) { // 数组大小为 amount + 1,初始值也为 amount + 1 vector<int> dp(amount + 1, amount + 1); // base case dp[0] = 0; for (int i = 0; i < dp.size(); i++) { // 内层 for 在求所有子问题 + 1 的最小值 for (int coin : coins) { // 子问题无解,跳过 if (i - coin < 0) continue; dp[i] = min(dp[i], 1 + dp[i - coin]); } } return (dp[amount] == amount + 1) ? -1 : dp[amount]; }
intlongestPalindromeSubseq(string s){ int n = s.size(); // dp 数组全部初始化为 0 vector<vector<int>> dp(n, vector<int>(n, 0)); // base case for (int i = 0; i < n; i++) dp[i][i] = 1; // 反着遍历保证正确的状态转移 for (int i = n - 2; i >= 0; i--) { for (int j = i + 1; j < n; j++) { // 状态转移方程 if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2; else dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); } } // 整个 s 的最长回文子串长度 return dp[0][n - 1]; }
intlongestPalindromeSubseq(string s){ int n = s.size(); // base case:一维 dp 数组全部初始化为 1 vector<int> dp(n, 1);
for (int i = n - 2; i >= 0; i--) { int pre = 0; for (int j = i + 1; j < n; j++) { int temp = dp[j]; // 状态转移方程 if (s[i] == s[j]) dp[j] = pre + 2; else dp[j] = max(dp[j], dp[j - 1]); pre = temp; } } return dp[n - 1]; }
// envelopes = [[w, h], [w, h]...] publicintmaxEnvelopes(int[][] envelopes){ int n = envelopes.length; // 按宽度升序排列,如果宽度一样,则按高度降序排列 Arrays.sort(envelopes, new Comparator<int[]>() { publicintcompare(int[] a, int[] b){ return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]; } }); // 对高度数组寻找 LIS int[] height = newint[n]; for (int i = 0; i < n; i++) height[i] = envelopes[i][1];
return lengthOfLIS(height); }
/* 返回 nums 中 LIS 的长度 */ publicintlengthOfLIS(int[] nums){ int piles = 0, n = nums.length; int[] top = newint[n]; for (int i = 0; i < n; i++) { // 要处理的扑克牌 int poker = nums[i]; int left = 0, right = piles; // 二分查找插入位置 while (left < right) { int mid = (left + right) / 2; if (top[mid] >= poker) right = mid; else left = mid + 1; } if (left == piles) piles++; // 把这张牌放到牌堆顶 top[left] = poker; } // 牌堆数就是 LIS 长度 return piles; }
最大子序和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
intmaxSubArray(int[] nums){ int n = nums.length; if (n == 0) return0; int[] dp = newint[n]; // base case // 第一个元素前面没有子数组 dp[0] = nums[0]; // 状态转移方程 for (int i = 1; i < n; i++) { dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]); } // 得到 nums 的最大子数组 int res = Integer.MIN_VALUE; for (int i = 0; i < n; i++) { res = Math.max(res, dp[i]); } return res; }
// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度 intdp(String s1, int i, String s2, int j){ // base case if (i == s1.length() || j == s2.length()) { return0; } // 如果之前计算过,则直接返回备忘录中的答案 if (memo[i][j] != -1) { return memo[i][j]; } // 根据 s1[i] 和 s2[j] 的情况做选择 if (s1.charAt(i) == s2.charAt(j)) { // s1[i] 和 s2[j] 必然在 lcs 中 memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1); } else { // s1[i] 和 s2[j] 至少有一个不在 lcs 中 memo[i][j] = Math.max( dp(s1, i + 1, s2, j), dp(s1, i, s2, j + 1) ); } return memo[i][j]; }
字符串的删除操作
1 2 3 4 5 6
intminDistance(String s1, String s2){ int m = s1.length(), n = s2.length(); // 复用前文计算 lcs 长度的函数 int lcs = longestCommonSubsequence(s1, s2); return m - lcs + n - lcs; }
// 备忘录 int memo[][]; /* 主函数 */ intminimumDeleteSum(String s1, String s2){ int m = s1.length(), n = s2.length(); // 备忘录值为 -1 代表未曾计算 memo = newint[m][n]; for (int[] row : memo) Arrays.fill(row, -1);
return dp(s1, 0, s2, 0); }
// 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串, // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。 intdp(String s1, int i, String s2, int j){ int res = 0; // base case if (i == s1.length()) { // 如果 s1 到头了,那么 s2 剩下的都得删除 for (; j < s2.length(); j++) res += s2.charAt(j); return res; } if (j == s2.length()) { // 如果 s2 到头了,那么 s1 剩下的都得删除 for (; i < s1.length(); i++) res += s1.charAt(i); return res; }