前言
从力扣100题开始的算法总结,决定真正走科班的代码之路后,果然还是得彻底从头开始总结一些算法代码了,感觉会是一个长期的总结过程,而且还要保持长期热情,只能加油吧。之前技能点全点在做游戏上了,但如果真的决定要改变世界,那就不能止步不前了。
这个总结目前来说还比较简单片面,后续可能考虑针对各个算法进行单一深入应用总结。

哈希
这一部分其实主要是利用哈希表来辅助解决问题。
可能困难比较大的是记住哈希表的使用方式(呃呃这就是记性差的坏处了)
记住之后很多问题在考虑到查找、拼接就可以直接从哈希表入手了:
unordered_map<string, vector<string>> groups; for(auto it = groups.begin(); it != groups.end(); it++) { ans.emplace_back(it->second); }
unordered_set<int> st(nums.begin(), nums.end()); st.contains(x - 1);
|
经典题目链接:
49. 字母异位词分组 - 力扣(LeetCode)
128. 最长连续序列 - 力扣(LeetCode)
双指针
双指针题目比较灵活,可以有以下两类(目前):
- 指针同时出发,主要挪动一个指针,满足特定条件再挪动另一个指针
- 指针同时出发,但是位于数列两端,进行比较选择挪动的指针
代表性题目分别有:
283. 移动零 - 力扣(LeetCode)
42. 接雨水 - 力扣(LeetCode)
滑动窗口
先附上比较玄妙的一道题:
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
这个的话比较字面意思,有点像双指针,主要通过动态调节窗口大小,来满足特定条件
主要操作是拓展窗口、缩减窗口、比较、选择。
然后可能会和子串在一起。
子串
这道题非常有意思:
76. 最小覆盖子串 - 力扣(LeetCode)
双指针两个指针负责不同事物
一个负责找全
一个负责缩小
原理上还是滑动窗口
普通数组
矩阵
48. 旋转图像 - 力扣(LeetCode)留一道巧思题,线性代数
链表
奇妙的题目:142. 环形链表 II - 力扣(LeetCode)
23. 合并 K 个升序链表 - 力扣(LeetCode)
这道题的归并思路非常牛逼,建议长期回溯一下:
class Solution { public: ListNode* mergeTwoLists(ListNode *a, ListNode *b) { if(!a || !b) return a ? a : b; ListNode head; ListNode* tail = &head; while(a && b) { if(a->val < b->val) { tail->next = a; a = a->next; } else { tail->next = b; b = b->next; } tail = tail->next; } tail->next = a ? a : b; return head.next; }
ListNode* merge(int l,int r,vector<ListNode*> &lists) { if(l == r) return lists[l]; if(l > r) return nullptr; int mid = l + (r - l)/2; return mergeTwoLists(merge(l,mid,lists),merge(mid+1,r,lists)); }
ListNode* mergeKLists(vector<ListNode*>& lists) { return merge(0,lists.size()-1,lists); } };
|
二叉树
有一道题需要用到前缀和的思路:437. 路径总和 III - 力扣(LeetCode)
什么是前缀和?
前缀和是一种记录累积和的技术,通常用于快速计算某个区间的和。它的核心思想是:
- 对于一个序列或路径,记录从起点到每个位置的累积和(前缀和)。
- 如果需要计算从位置 i 到位置 j 的子区间和,只需用 prefixSum[j] - prefixSum[i-1] 即可。
在数组问题中,前缀和非常常见。例如:
- 数组 [1, 2, 3, 4] 的前缀和是 [1, 3, 6, 10]。
- 要计算索引 1 到 3 的子数组和(2 + 3 + 4),可以用 prefixSum[3] - prefixSum[0] = 10 - 1 = 9。
在二叉树中,前缀和的概念类似,但需要适应树的结构。我们记录从根节点到当前节点的路径和(前缀和),并利用这些前缀和来快速找到路径和等于目标值的子路径。
为什么在二叉树路径和问题中用前缀和?
你的问题(pathSum)要求找到二叉树中所有从任意节点到任意节点(沿路径向下)的连续路径,其和等于 targetSum。负数的存在使得滑动窗口方法失效,因为路径和可能忽大忽小,无法简单地通过弹出元素调整。
前缀和的优势在于:
- 通用性:它不依赖路径和的单调性,适用于正数、负数和零。
- 高效性:通过哈希表记录前缀和的出现次数,可以在 O(1) 时间内检查是否存在符合条件的子路径。
- 树结构适配:通过递归遍历树,动态维护路径上的前缀和。
前缀和在路径和问题中的核心思想
假设你在二叉树中从根节点走到当前节点,路径上的节点值依次为 [a, b, c, d],当前路径和为 a + b + c + d。你想知道是否存在一个子路径(从某个祖先节点到当前节点),其和等于 targetSum。
前缀和定义:从根到当前节点的路径和 currSum = a + b + c + d 是一个前缀和。
子路径和:如果存在一个祖先节点,其到根的前缀和为 prefixSum,那么从该祖先到当前节点的子路径和为:
currSum - prefixSum = targetSum
即:
prefixSum = currSum - targetSum
哈希表记录:用一个哈希表 prefixSums 记录遍历过程中遇到的所有前缀和及其出现次数。如果 currSum - targetSum 出现在哈希表中,说明存在一条子路径的和等于 targetSum。
class Solution { public: int pathSum(TreeNode* root, int targetSum) { unordered_map<long long, int> prefixSums; prefixSums[0] = 1; return dfs(root, 0, targetSum, prefixSums); }
private: int dfs(TreeNode* root, long long currSum, int targetSum, unordered_map<long long, int>& prefixSums) { if (!root) { return 0; }
currSum += root->val;
int count = prefixSums[currSum - targetSum];
prefixSums[currSum]++;
count += dfs(root->left, currSum, targetSum, prefixSums); count += dfs(root->right, currSum, targetSum, prefixSums);
prefixSums[currSum]--;
return count; } };
|
一道非常经典的想着不难,码的时候很难:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
class Solution { private: unordered_map<int, int> index;
public: TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { int n = preorder.size(); for(int i = 0; i < n; i++) { index[inorder[i]] = i; } return build(preorder,inorder,0,n-1,0,n-1); }
TreeNode* build(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) { if(preorder_left > preorder_right) { return nullptr; }
int preorder_root = preorder_left; int inorder_root = index[preorder[preorder_left]];
TreeNode* root = new TreeNode(preorder[preorder_left]); int size_left_subtree = inorder_root - inorder_left;
root->left = build(preorder,inorder,preorder_left + 1, preorder_left + size_left_subtree, inorder_left,inorder_root - 1);
root->right = build(preorder,inorder,preorder_left + size_left_subtree + 1,preorder_right, inorder_root + 1, inorder_right);
return root; } };
|
图论
回溯
主要是有一个标准的板子
在此记录一下(其实感觉不是很难)
题目出处:46. 全排列 - 力扣(LeetCode)
class Solution { private: vector<vector<int>> res; vector<int> path; public: void backtracking(vector<int>& nums, vector<bool>& used) { if (path.size() == nums.size()) { res.push_back(path); return; }
for (int i = 0; i < nums.size(); i++) { if (used[i] == true) continue;
used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } }
vector<vector<int>> permute(vector<int>& nums) { vector<bool> used(nums.size(), false); backtracking(nums, used); return res; } };
|
还有感觉时不时可以回忆一下n皇后:51. N 皇后 - 力扣(LeetCode)
把**行(row)**视为回溯推进的路标!
感觉重要的就是路标的标定和查找。
二分查找
感觉是因为自己有一个很笨笨的方法,导致写的每次逻辑都非常不通畅。
因此在此记录下两者区别:
题目出处:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
官方题解:
class Solution { public: int findMin(vector<int>& nums) { int low = 0; int high = nums.size() - 1; while (low < high) { int pivot = low + (high - low) / 2; if (nums[pivot] < nums[high]) { high = pivot; } else { low = pivot + 1; } } return nums[low]; } };
作者:力扣官方题解 链接:https: 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
|
我的铸币理解:
class Solution { public: int findMin(vector<int>& nums) { if(nums.size() == 1) return nums[0]; return find(0,nums.size()-1,nums); }
int find(int left,int right,vector<int> nums) { int mid = (left + right)/2; if(left == right-1) { return nums[right] > nums[left] ? nums[left] : nums[right]; } else if(nums[mid]<nums[left]) { return find(left,mid,nums); } else if(nums[mid]>nums[right]) { return find(mid,right,nums); } else { return nums[left]; } } };
|
其实要理解就是一个区间问题,可以再贴一个简明扼要的写法:
这个的理解永远是取[left,right],永远都是闭区间,所以即便最后left == right依然是有意义的,如果此时未能找到,会进入最后一个循环返回-1,而且每次循环middle一定会缩减额外的1格。
class Solution { public: int search(vector<int>& nums, int target) { int left = 0; int right = nums.size()-1; return search(left,right,nums,target); } int search(int left,int right, vector<int>& nums,int target) { if(left > right) { return -1; } int middle = left + (right - left)/2; if(nums[middle] < target) { return search(middle+1,right,nums,target); } else if(nums[middle] > target) { return search(left,middle-1,nums,target); } else { return middle; } } };
|
然后有一道变种题,卡在最后一种情况很久很久:
33. 搜索旋转排序数组 - 力扣(LeetCode)
需要考虑以下题解中的<=号:
if(nums[middle] < target) { if(nums[right] < target && nums[middle] <= nums[right]) { return deep(left,middle-1,nums,target); } else { return deep(middle+1,right,nums,target); } } else if(nums[middle] > target) { if(nums[left] > target && nums[left] <= nums[middle]) { return deep(middle+1,right,nums,target); } else { return deep(left,middle-1,nums,target); } }
|
栈
堆
贪心算法
嘶……
大部分贪心都被我用dp过去了……
不过这道倒是dp没想出来:
45. 跳跃游戏 II - 力扣(LeetCode)
动态规划
感觉没有想象中的那么恐怖,其实就是要找到一条可以表示所有情况的通式。
接着找到他的初状态就可以了。
最好是要常备草稿纸!
补充一道很有难度的动态规划,重点是考虑维护两个数组,主要是因为负数的特殊化以及0的特殊性!:
152. 乘积最大子数组 - 力扣(LeetCode)
多维动态规划
好难我去,需要准备好理解各个维度之间的关系,以及你为什么需要这个维度,这个维度在递推式中代表什么?
416. 分割等和子集 - 力扣(LeetCode)
上面这道题,背包问题,虽然是中等,但是对理解具象维度的含义非常有帮助,摘一下官方的题解:
创建二维数组 dp,包含 n 行 target+1 列,其中 dp[i][j] 表示从数组的 [0,i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j。初始时,dp 中的全部元素都是 false。
作者:力扣官方题解 链接:https://leetcode.cn/problems/partition-equal-subset-sum/solutions/442320/fen-ge-deng-he-zi-ji-by-leetcode-solution/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
|
技巧
快慢指针套圈问题
异或求异
多数数字的相消
下一个排列->找到非降序,然后找到第一个比非降序数大的数交换位置->再对非降序数后的所有数字翻转