leetcode hot 100
哈希
双指针
滑动窗口
子串
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
int cur = 0;
vector<int> res(n - k + 1);
deque<int> dq;
// 单调减队列(队头到队尾)
for (int i = 0; i < n; ++i) {
// dq.back() < i && nums[dq.back()] < nums[i], 此时:
// dq.back()要么在(i-k, i]范围内,但是这个范围中最大值肯定不是nums[dq.back()],因为nums[i]>nums[dq.back()]
// dq.back()要么在(i-k, i]不在范围内,更可以删除(如果nums[dq.back()] >= nums[i],就给下个for循环删除)
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) { // 从第k个数才开始记录
while (dq.front() <= i - k) { // 将超出(i-k, i]范围的数字删除
dq.pop_front();
}
res[cur++] = nums[dq.front()];
}
}
return res;
}
};
class Solution {
public:
string minWindow(string s, string t) {
int matchChar = 0; // 目前已匹配的字符数
int res_left = -1; // 目前匹配的最小区间的左区间
int res_len = s.size() + 1; // 目前匹配的最小区间的长度
int left = 0, right = 0; // 滑动窗口
map<char, int> tmap;
map<char, int> smap;
for (char c : t) {
tmap[c]++;
}
while (right < s.size()) {
while (right < s.size() && matchChar < t.size()) { // 一直匹配,直至s[left, right]能完全覆盖,或者right右区间超出范围
int cnt = tmap[s[right]];
if (cnt > 0) { // 只记录t有的字符
if (smap[s[right]] < cnt) { // 对于重复的字符c,当区间中c的数量已超t中c的数量,就不再增加匹配字符数
++matchChar;
}
++smap[s[right]]; // 记录区间中的字符对应的数量
}
++right;
}
while (matchChar == t.size()) { // 缩小,直至区间不能完全覆盖t(通过匹配字符数来判断)
int cnt = tmap[s[left]];
if (cnt > 0) {
if (--smap[s[left]] < cnt) {
--matchChar;
}
}
++left;
}
if (right - left + 1 < res_len) { // 记录最小区间[left - 1, right)
res_len = right - left + 1;
res_left = left - 1;
}
}
if (res_left == -1) {
return "";
} else {
return s.substr(res_left, res_len);
}
}
};
普通数组
矩阵
链表
- 相交链表:找出相交点,可以先计算长度,让较长的链表先走,然后再一起走
- 反转链表:没什么好说的。迭代和递归都要懂
- 环形链表:快慢指针一起跑就好了,能碰到就代表有环
- 环形链表 II:不止要判断是否有环,而且要给出环的入口。先判断有环,有环后再让一个指针从头开始,然后快慢指针一步步走,碰到的就是环的入口(难的是数学证明)
- 合并两个有序链表:没什么好说的
- 两数相加:将和存到list1就好了,记录进位,到最后,如果还有进位还是1,就新增
- 删除链表的倒数第 N 个结点:注意边界情况,则链表长度是否大于等于N。a先走到第n个点,b开始指向链表头,a、b开始一起一步步走,b到链表尾,a就是结果。l = n + m,倒数第n,就是正数第l-n(m),a先走n,b再下场,这样一起走,a后面刚好走l-n到链表尾,b刚好到第l-n个点
- 两两交换链表中的节点:就是有点恶心,本身不难。迭代很恶心,递归简单一点。
- K 个一组翻转链表:其实就是上一题的进阶,用递归简单点,先判断当前节点开头的链表是否有k个,不够直接返回,够的话先记下这k个节点的开头s和结尾e,翻转这个区间,然后对e->next递归调用,得到结果i,然后链接s->next=i,返回e
- 随机链表的复制:随机链表其实就是比普通链表多了一个random节点,然后这个random节点随机指向链表中的一个节点。可以用一个map保存旧节点映射到新节点的,第一次就按普通链表复制,第二次遍历旧链表,查看random链接情况,根据map,按照映射关系链接新节点的random
- 排序链表:n2要么用插入、要么冒泡。nlogn:自底向上的归并。n2:自顶向下的归并(先快慢指针划分链表,然后递归对两个链表排序)
- 合并 K 个升序链表:就是合并两个升序链表的升级,两两合并就好。但是一般是选择当前最短的两个链表合并,这个可以通过堆来完成。
- LRU 缓存:经典题目,值得背诵。首先要记得是map+双链表完成的。越接近链表头部,就是最近用的。map存的是key到链表节点的映射,链表存的是key和value
- put操作:
- key已存在时,通过key获取到节点node,更新value,然后将node移动到链表头
- key未存在时,且未满时,新建节点node,将node插入到链表尾,并且更新map(添加key到node的映射)
- key未存在时,且已满时,同上上面的情况一样(新建节点node,将node插入到链表尾,并且更新map(添加key到node的映射)),只是要将将链表尾节点从链表删除,更新map(删除链表尾节点中的key到链表尾节点的映射)
- get操作:
- 通过key获取到节点node,获取value,然后将node移动到链表头
- 链表设计:强烈建议一个head一个tail作为头尾,并实现以下函数:
- removeNode(node) : 移除节点node
- removeTail(): 移除尾部节点(不是tail,而是tail->prev)
- addToHead(node): 往链表头部插入节点node
- moveToHead(node): 将node从原来位置删除,并插入到链表头
- put操作:
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现
LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。函数
get
和put
必须以O(1)
的平均时间复杂度运行。
二叉树
- 二叉树的中序遍历、前序遍历、后序遍历:
递归不解释,迭代:
主要用栈模拟递归过程,注意就是节点访问(visit)和出栈的时机
前序:visit:入栈前先visit,出栈:当一直往左走,无路可走时,出栈一个节点,取其右孩子(注意,栈中的节点都是已经访问过的)
中序:visit:出栈之后再visit,出栈:当一直往左走,无路可走时,出栈一个节点,取其右孩子(注意,栈中的节点都是没有访问过的,并且他们的左孩子都已入栈)
后序:这个有点特殊,需要用一个prev节点记录上一次visit的节点,这样当root->right == prev,证明root可以出栈,也可以访问了。
- 二叉树的最大深度: 三种做法:bfs统计层数,后序遍历迭代法,dfs递归
- 翻转二叉树:先反转左右子树,然后递归遍历左右子树
- 对称二叉树:将根节点的左右子树作为两棵树进行递归对比
- 二叉树的直径:经过当前节点的最大直径,就是左右子树的深度之和,这样在递归求深度的过程中,记录每个节点的左右子树的深度之和,这样就能得出答案。
- 二叉树的层序遍历:略
- 将有序数组转换为二叉搜索树:划分数组,将中间节点作为当前的根节点,左右两边数组作为左右子树,继续递归
- 验证二叉搜索树:给对每个节点给定一个值的范围,超出这个范围,则代表不是bst,然后递归对左右子树进行这个操作
- 二叉搜索树中第K小的元素:中序遍历,用一个标记变量标记当前是走到第几个节点
- 二叉树的右视图:层次遍历,每层从左到右遍历,每次取最后一个元素
- 二叉树展开为链表:递归前序遍历,用一个指针的指针prev记录前一个访问的节点,对每个节点root,prev->right = root; root->left = nullptr; 左右子树递归
- 从前序与中序遍历序列构造二叉树:略
- 路径总和 III:前缀合求,但是注意什么时候加,什么时候减。
- 二叉树的最近公共祖先:递归,略。
- 二叉树中的最大路径和:递归。辅助函数:求从root开始的最大路径和rootPathSum。先递归求左右子树的,则从当前root开始的最大路径和为prootsum = root->val + max(rightsum, leftsum),记录最大的路径和为 max( prootsum , root->val + rightsum + leftsum).注意空节点返回0,左右和先和0比较
图论
回溯
二分查找
栈
一个重要的结论:
对于每条柱heights[i], 以他为矩形的长,最大面积是 heights[i] = (right_i - left_i + 1)。
right_i是从i开始,往右找,第一个heights[right_i] < heights[i]
left_i是从i开始,往左找,第一个heights[left_i] < heights[i]答案肯定是以某个height[i]为高的矩形,遍历所有的可能即可。
根据这个结论,可以利用单调增栈的方式,找出每个i的left_i和right_i。
为了让第一个能找到left_i和最后一个元素能找到right_i,在数组前后各添加一个0.
单调增栈出入栈。一个元素出栈时就能找到left_i和right_i,此时计算面积即可。
堆
-
数据流的中位数:用两个堆来完成,一个大根堆,一个小根堆。1、大根堆放数据流的较小半部分,小根堆放数据流的较大的半部分。2、大根堆元素个数始终最多比小根堆元素个数多一。插入元素时主要要维持这两个性质。
-
前 K 个高频元素:map统计数字出现的频次,然后维护大小为k的小根堆。
-
数组中的第K个最大元素:维护大小为k的小根堆 或者 基于快排
贪心算法
动态规划
-
打家劫舍:dp[i] = max(dp[i-1], dp[i - 2] + nums[i])
-
完全平方数: 完全背包,物品是各个完全平方数,而背包大小是目标和。用最少的物品装满背包,物品可以重复利用。用一维的话,背包从小到大就是完全背包.从大到小,就是01背包。dp[j] 就是j的答案。
-
零钱兑换:同上,一样是完全背包,一样是求物品数最少
-
单词拆分:dp[j]代表s[0~j]可以划分,每到一个位置对所有单词遍历,看看是否能划分
多维动态规划
- 不同路径: dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]
- 最小路径和: grid[i] [j] += min(grid[i-1] [j], gridi)
- 最长回文子串: 对每个字符和作为中心,从两边展开获取回文串。n^2。(回文串长度既可以是单数也可以是双数)
- 最长公共子序列: dp[i] [j] = (t1[i] == t2[j]) ? dp[i-1] [j-1] + 1 : min(dp[i-1] [j], dp[i] [j - 1])
- 编辑距离: 要知道删除和增加和修改都是等价的. word1[i] == word2[j] , dp[i] [j] = dp[i-1] [j-1], 否则,dp[i] [j] = min(dp[i-1] [j-1], dp[i-1] [j], dp[i] [j]) + 1,注意不要漏了dp[i-1] [j-1]
技巧
class Solution {
public:
void sortColors(vector<int>& nums) {
int p0 = 0, p1 = 0;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] == 0) {
swap(nums[i], nums[p0]);
if (p0 < p1) { // 如果p0 < p1,证明nums[p0]=1
swap(nums[i], nums[p1]);// 将1换回来
}
++p0; // 因为是0,p0和p1都需要更新
++p1;
} else if (nums[i] == 1) {
swap(nums[i], nums[p1]);
++p1;
}
}
}
};
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int len = nums.size();
if (len < 2) return ;
int i = len - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
--i;
}
// 此时nums[i] < nums[i+1],代表nums[i+1, n)中必有数nums[k]大于nums[i]
// 尽量从后面找nums[k], 此时
int k = len - 1;
if (i >= 0) {
while (nums[i] >= nums[k]) {
--k;
}
swap(nums[i], nums[k]);
}
reverse(nums.begin()+i+1, nums.end());
}
};
123456
123465
123546
…
654321
可以看到有这样的关系:123456 < 123465 < 123546 < … < 654321。
算法推导
如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:
我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
我们还希望下一个数增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:在尽可能靠右的低位 进行交换,需要从后向前查找将一个尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
以上就是求 “下一个排列” 的分析过程。
算法过程
标准的 “下一个排列” 算法可以描述为:
从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
将 A[i] 与 A[k] 交换
可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4
该方法支持数据重复,且在 C++ STL 中被采用。
- 寻找重复数: 数组元素与下标组成有环链表,变成找出环的入口。变成快慢指针找有环链表环的入口。
0 1 2 3 4
[3,1,3,4,2]
0->3->4->2->3