热题100学习笔记
热题100学习笔记
哈希
字母异位词分组
- 字母异位词分组:49.字母异位词分组
- 思路:这道题目通过长度为
26
的string
记录字母数量一样的字符串(由于unordered_map
要求Key
是可哈希的,因此Key
不能够存vector
类型),通过unordered_map
来找到属于相同字母异位词的字符串,最后将其合并到数组中。
- 代码:
1 |
|
最长连续序列
- 最长连续序列:128.最长连续序列
- 思路:用
unordered_set
存所有数,再遍历数组,以其中数为起点向左右扩展判断是否连续,把已经访问过的数删掉避免重复。这样每个数只会被访问一次,整体 。 - 代码:
1 |
|
双指针
移动零
- 移动零:283.移动零
- 思路:这道题目采用双指针的解法,首先定义
left
和right
同时指向0
位置,移动right
指针,当right
所指的元素不等于0
时,交换left
和right
所指的元素,并且left
右移一位。- 需要注意的是不需要保证
left
初始指向的是0
,left
如果初始指向的是非0
,那么left
和right
就会指向同一个位置,同一个位置互换等于没有操作,之后left
和right
指针一起往右边移动一位。只有当left
第一次值为0
的时候,right
就会和left
指针分离,直到找到一个非0
的数然后跟left
去进行交换。
- 需要注意的是不需要保证
- 代码:
1 |
|
盛最多水的容器
- 盛最多水的容器:11.盛最多水的容器
- 思路:
- 采用双指针法:
- 用两个指针
i
和j
分别指向数组的开头和末尾,代表选取的两条竖线。- 计算当前容器的水量:用一个变量
ans
记录最大面积。
- 计算当前容器的水量:用一个变量
- 指针移动策略:
- 如果
height[i] < height[j]
,说明高度受限于左边的竖线,只有右移i
才有可能找到更高的竖线,从而可能获得更大的面积。 - 反之,如果
height[j] <= height[i]
,则左边已经不再是瓶颈,右边才是短板,因此左移j
。 - 如此不断缩小区间,直到两个指针相遇。
- 如果
- 用两个指针
- 采用双指针法:
- 代码:
1 |
|
滑动窗口
无重复字符的最长子串
- 无重复字符的最长子串:3.无重复字符的最长子串
- 思路:用两个指针
left
和right
表示当前考察的窗口区间[left, right)
。unordered_set<char>
用来存储窗口内的字符,保证窗口内不出现重复。(滑动窗口+哈希集合)的问题。- 当
s[right]
不在集合里时,说明窗口可以安全扩张,将s[right]
插入集合,right++
向右移动,更新最大长度result = max(result, right - left)
。 - 当
s[right]
已经在集合中时,说明出现重复字符,删除s[left]
,left++
收缩窗口,直到窗口内没有重复字符。
- 当
- 代码:
1 |
|
找到字符串中所有字母异位词
- 找到字符串中所有字母异位词:438.找到字符串中所有字母异位词
- 思路:
- 滑动窗口:
- 由于我们需要检查
s
中所有长度为p.size()
的子字符串是否是p
的字母异位词,我们可以使用 滑动窗口 方法。 - 滑动窗口的长度固定为
p.size()
,并且每次移动一位,检查当前窗口内的子字符串是否是p
的字母异位词。
- 由于我们需要检查
- 主要步骤:
- 统计
p
中字符的频率:首先我们通过哈希表统计字符串p
中每个字符出现的频率。 - 滑动窗口遍历
s
:- 使用两个哈希表:
umapP
用于存储p
中字符的频率,umapS
用于存储当前窗口内字符的频率。 - 使用一个计数器
have
来记录当前窗口内与p
相同频率的字符数。当umapS
中的字符频率与umapP
相等时,have
增加。
- 使用两个哈希表:
- 统计
- 滑动窗口操作:
- 初始化时,将窗口的前
p.size()
个字符添加到umapS
中,并更新have
计数器。 - 然后,每次将窗口右移一位:删除左侧字符,添加右侧新字符,更新
umapS
和have
。
- 初始化时,将窗口的前
- 判断条件:如果
have
等于umapP
中不同字符的数目(即Pcount
),说明当前窗口内的字符串是p
的字母异位词,将该窗口的左边界索引添加到结果列表中。
- 滑动窗口:
- 代码:
1 |
|
子串
和为 K 的子数组
- 和为 K 的子数组:560.和为 K 的子数组
- 思路:这道题是典型的前缀和+哈希表解法,用来统计和为
k
的连续子数组个数。- 在开始时放入
mp[0] = 1
,表示「前缀和为 0 出现过 1 次」,这样可以处理从数组开头开始的子数组。 - 用一个哈希表
mp
存储某个前缀和出现的次数。在遍历过程中,此时前缀和为sum
时,只要sum - k
在哈希表中存在,说明之前有某个前缀和能让子数组和刚好为k
。于是把mp[sum - k]
累加到答案ans
中。
- 在开始时放入
- 代码:
1 |
|
最小覆盖子串
- 最小覆盖子串:3.无重复字符的最长子串
- 思路:
- 哈希表统计需求
- 用一个哈希表
umpT
统计字符串t
中每个字符的需求频次。 - 用另一个哈希表
umpS
记录当前滑动窗口中各字符出现的次数。
- 用一个哈希表
- 滑动窗口
- 用两个指针
left
和right
表示窗口的左右边界。 right
每次右移,扩展窗口,把新字符加入统计。- 当窗口满足条件(即窗口中所有字符频次均达到要求时),尝试收缩
left
,尽量缩小窗口长度,同时更新最优解。
- 用两个指针
- 满足条件的判断
tCount
:t
中不同字符的个数。have
:窗口中已经满足要求的字符种类数。- 当
have == tCount
时,说明当前窗口已覆盖了t
的所有字符。
- 更新答案
- 每次满足条件时,如果窗口长度比之前记录的更小,就更新最短子串的起点和长度。
- 正确性核心
- 右扩:保证窗口最终能覆盖所有需要的字符。
- 左缩:保证在满足条件的情况下尽可能缩短窗口,找到最优解。
- 利用哈希表准确维护每个字符的需求量,保证不会遗漏或冗余。
- 哈希表统计需求
- 代码:
1 |
|
普通数组
轮转数组
- 轮转数组:189.轮转数组
- 思路:先翻转整个数组,再翻转前
k
个元素和后k
个元素就能得到答案。 - 代码:
1 |
|
除自身以外数组的乘积
- 除自身以外数组的乘积:238.除自身以外数组的乘积
- 思路:
- 一个数的结果 = 左边所有元素的乘积 × 右边所有元素的乘积。
- 构造前缀积和后缀积:
prefix[i]
= 从nums[0]
到nums[i-1]
的乘积(不包含nums[i]
)。postfix[i]
= 从nums[i+1]
到nums[n-1]
的乘积(不包含nums[i]
)。
- 合并结果
- 遍历数组,用
ans[i] = prefix[i] * postfix[i]
得到答案。
- 遍历数组,用
- 代码:
1 |
|
矩阵
矩阵置零
- 矩阵置零:73.矩阵置零
- 思路:首先找到找到
0
元素所在位置,再分别按照0
元素所在位置的行和列都置为0
。 - 代码:
1 |
|
螺旋矩阵
- 螺旋矩阵:54.螺旋矩阵
- 思路:
-
初始化边界:
- 为了逐步收缩矩阵,我们需要维护四个边界:
top
,bottom
,left
,right
。这些边界表示当前有效矩阵的四条边,它们会逐渐向内收缩,直到所有的元素都被遍历。
top
:上边界,初始值为 0。bottom
:下边界,初始值为矩阵的行数m
。left
:左边界,初始值为 0。right
:右边界,初始值为矩阵的列数n
。
- 为了逐步收缩矩阵,我们需要维护四个边界:
-
按螺旋顺序遍历:
- 我们的遍历过程按照螺旋顺序进行,依次走四个方向:
- 向右:遍历当前
top
行,从left
到right-1
。 - 向下:遍历当前
right-1
列,从top
到bottom-1
。 - 向左:遍历当前
bottom-1
行,从right-1
到left
。 - 向上:遍历当前
left
列,从bottom-1
到top
。
- 向右:遍历当前
- 每遍历完一个方向后,更新对应的边界:
- 向右遍历后,
top
增加。 - 向下遍历后,
right
减少。 - 向左遍历后,
bottom
减少。 - 向上遍历后,
left
增加。
- 向右遍历后,
- 我们的遍历过程按照螺旋顺序进行,依次走四个方向:
-
边界条件:
- 每次遍历后,我们检查新的边界条件,确保当前的有效区域仍然有效。如果
top >= bottom
或left >= right
,则表示矩阵已经遍历完,退出循环。
- 每次遍历后,我们检查新的边界条件,确保当前的有效区域仍然有效。如果
-
结束条件:
- 当所有元素被遍历完时,
ans
向量包含了所有按螺旋顺序排列的元素。
- 当所有元素被遍历完时,
- 代码实现流程:
- 逐步处理矩阵的四个方向,并更新边界,直到遍历完所有元素。
- 每次遍历一个方向时,检查是否还有有效的元素,避免越界。
-
- 代码:
1 |
|
旋转图像
- 旋转图像:48.旋转图像
- 思路:
- 定义四个指针:
top
、bottom
、left
、right
,分别表示当前处理层的四个边界。- 在
left < right
的前提下,每次循环处理一层(从外到内)。
- 在
- 对于每层:
- 逐个元素进行四边交换(
top → right → bottom → left → top
),利用一个tmp
做中转。 - 完成一圈后,收缩边界:
top++
,bottom--
,left++
,right--
,进入内层。
- 逐个元素进行四边交换(
- 最终所有层完成后,矩阵即被原地旋转
90°
。
- 定义四个指针:
- 代码:
1 |
|
搜索二维矩阵 II
- 搜索二维矩阵 II:240.搜索二维矩阵 II
- 思路:
- 利用矩阵的有序性,从右上角 (
matrix[0][n-1]
) 作为起点开始搜索。- 如果
target < 当前值
:说明目标数一定在左边,列指针lie--
; - 如果
target > 当前值
:说明目标数一定在下边,行指针hang++
; - 如果相等,则直接返回
true
- 如果
- 不断缩小搜索范围,直到行或列越界结束。
- 利用矩阵的有序性,从右上角 (
- 代码:
1 |
|
链表
回文链表
- 回文链表:234.回文链表
- 思路:该题的思路分为三部分:
- 通过快慢指针找到链表中间,初始的时候(
ListNode* slow = head;
、ListNode* fast = head->next;
) - 翻转后半部分链表 (从
slow->next
开始,ListNode* node = reverse(slow->next);
) - 判断链表的前后两部分是否相等。
- 通过快慢指针找到链表中间,初始的时候(
- 代码:
1 |
|
两数相加
- 两数相加:2.两数相加
- 思路:
- 使用指针遍历两个链表:
- 定义指针
cur1
和cur2
,分别从l1
和l2
的头开始。 - 定义变量
jinwei
存储进位。
- 定义指针
- 同时遍历相加:
- 当
cur1
和cur2
都非空时:- 计算当前位:
num = cur1->val + cur2->val + jinwei
- 新节点的值 =
num % 10
,新的进位jinwei = num / 10
- 把新节点接到结果链表后面
- 计算当前位:
- 当
- 处理剩余链表:
- 如果
l1
还没走完,就继续把cur1->val + jinwei
加进结果。 - 如果
l2
还没走完,就继续把cur2->val + jinwei
加进结果。
- 如果
- 处理最后的进位:
- 如果遍历结束后
jinwei != 0
,说明最高位还有进位,需要再创建一个节点。
- 如果遍历结束后
- 返回结果:
- 结果链表从
head->next
开始,因为head
是虚拟头结点。
- 结果链表从
- 使用指针遍历两个链表:
- 代码:
1 |
|
随机链表的复制
- 随机链表的复制:138.随机链表的复制
- 思路:
- 本解法使用 哈希表(unordered_map) 来建立 旧节点 → 新节点 的映射关系,分两步完成:
- 第一步:复制所有节点(仅
val
)- 遍历原链表,把每个节点
cur
的副本节点tmp
创建出来。 - 用
map[cur] = tmp
保存映射关系。 - 此时,副本节点只存了值,
next
和random
都还没处理。
- 遍历原链表,把每个节点
- 第二步:建立指针关系
- 再次遍历原链表。
- 对于每个旧节点
cur
,通过映射关系补全副本节点的指针:mp[cur]->next = mp[cur->next]
mp[cur]->random = mp[cur->random]
- 这样,新节点的
next
和random
就正确指向新链表中的节点。
- 代码:
1 |
|
K 个一组翻转链表
- K 个一组翻转链表:25.K 个一组翻转链表
- 思路:
- 虚拟节点辅助:创建
dummy
指向头结点,方便处理头部被翻转的场景。用groupPre
表示当前要翻转这一组的前驱节点(初始为dummy
)。
- 定位一组范围
[groupPre->next, cur]
- 从
groupPre
出发向前数k
个节点,到达该组的末尾cur
。 - 若不足
k
个(cur == nullptr
),说明剩余节点不足一组,直接结束(保持不变)。
- 从
- 记录下一段起点:
groupNxt = cur->next
,翻转只在[groupPre->next, cur]
这段闭区间内进行。 - 原地翻转这一组
pre = groupPre->next
(组内的第一个结点,翻转后会变成该组的最后一个)。cur = pre->next
,循环把cur
指向pre
,逐步逆转直到cur == groupNxt
为止。- 这一段标准链表原地反转模板,不额外开空间。
- 接回链表
- 翻转完成后,
pre
指向该组新的头;groupPre->next
应该连到pre
。 - 原组头(翻转前的
groupPre->next
,此时用临时变量tmp
保存)变成组尾,需要连到groupNxt
。 - 更新
groupPre = tmp
,准备处理下一组。
- 翻转完成后,
- 循环直到无法再成组,返回
dummy->next
。
- 虚拟节点辅助:创建
- 代码:
1 |
|
排序链表
- 排序链表:148.排序链表
- 思路一:使用
multimap<int, ListNode*> mp;
数据结构中key
有序的特性遍历存储链表的节点,最后再按key
的顺序从map
中取出连接链表得到结果。 - 代码:
1 |
|
- 思路二:
- 1、分割链表
- 使用 快慢指针 找到中点:
slow
每次走一步,fast
每次走两步。- 主要快慢指针的起始点:
ListNode* slow = head;
、ListNode* fast = head->next;
- 当
fast
到尾部时,slow
正好在中点。
- 把链表切成两半:
ListNode* second = slow->next;
和slow->next = nullptr;
- 使用 快慢指针 找到中点:
- 2、递归排序
- 对左右两个子链表分别递归调用
sortList
。 - 递归直到子链表为空或只有一个元素(天然有序)。
- 对左右两个子链表分别递归调用
- 3、合并有序链表
- 用双指针遍历两个有序链表,像归并排序数组一样合并。
- 时间复杂度 。
- 1、分割链表
- 代码:
1 |
|
合并 K 个升序链表
- 合并 K 个升序链表:23.合并 K 个升序链表
- 思路一:
- 定义一个辅助函数
mergeList
,合并两个有序链表(和归并排序合并步骤一样)。
- 从
lists[0]
开始,依次和下一个链表合并:- 先合并
lists[0]
和lists[1]
- 再把结果和
lists[2]
合并 - 直到处理完所有链表
- 先合并
- 返回最终链表。
- 定义一个辅助函数
- 代码:
1 |
|
- 思路二:
- 定义一个辅助函数
mergeList
,合并两个有序链表(和归并排序合并步骤一样)。
- 初始
lists
里有 k 个链表。 - 每次遍历,按下标
0&1, 2&3, ...
两两合并,放入tmp_lists
。 - 然后用
tmp_lists
替换原lists
,数量减半。 - 直到
lists.size() == 1
。 - 返回最终链表。
- 定义一个辅助函数
- 代码:
1 |
|
LRU 缓存
- LRU 缓存:146.LRU 缓存
- 思路:
- 1、关键点
- 哈希表:实现
key -> 节点指针
的 查找 - 双向链表:维护节点的使用顺序,保证插入/删除
- 两者结合,实现了 的
get
和put
操作。
- 哈希表:实现
- 2、数据结构设计
- 使用
unordered_map<int, node*> mp
,存储key
到双向链表节点的映射; - 双向链表(带头尾哨兵节点
head
和tail
),新访问/更新的节点放到链表头(表示最近使用过),淘汰时删除尾部前一个节点(最久未使用)。
- 使用
- 3、操作逻辑
get(key)
,查哈希表:- 不存在返回
-1
- 存在:把该节点移到链表头(表示最近使用过),返回该节点的值
- 不存在返回
put(key, value)
的操作:- 如果
key
已存在:- 更新节点值
- 移动到链表头
- 如果
key
不存在:- 创建新节点,插入到链表头,并放入哈希表
- 如果容量超限:
- 删除链表尾部节点
- 同时从哈希表删除对应的 key
- 如果
- 链表操作:
LRUremove(node* tmp)
:把某个节点从链表中摘除insertHead(node* tmp)
:把某个节点插入链表头
- 1、关键点
- 代码:
1 |
|
二叉树
二叉树的直径
- 二叉树的直径:543.二叉树的直径
- 思路:计算每个节点左子树的高度和右子树的高度相加即为经过该节点的最长直径,主要二叉树的最长直径不一定经过根节点,因此在递归计算每个节点的最长直径后取最大值保存,最终即为答案。
- 代码:
1 |
|
二叉搜索树中第 K 小的元素
- 二叉搜索树中第 K 小的元素:230.二叉搜索树中第K小的元素
- 思路:
- 递归中序遍历:
- 先递归访问左子树;
- 访问当前节点,计数
index++
; - 如果
index == k
,说明当前节点就是答案; - 最后递归右子树。
- 通过一个全局变量
index
记录访问了多少个节点。 - 用
ans
保存结果,一旦找到就停止继续递归(用if(ans >= 0) return;
剪枝)。
- 递归中序遍历:
- 代码:
1 |
|
二叉树展开为链表
- 二叉树展开为链表:114.二叉树展开为链表
- 思路:
- 前序遍历:
- 首先,我们需要对二叉树进行 前序遍历(根节点 -> 左子树 -> 右子树),并将遍历得到的节点值存储在一个列表中。前序遍历确保我们能够按根-左-右的顺序访问每个节点。
- 将树变成链表:
- 接下来,我们需要重新构建二叉树,使得每个节点的左子树为
nullptr
,并且右子树指向下一个节点。这个链表实际上就是前序遍历得到的节点值的顺序。
- 接下来,我们需要重新构建二叉树,使得每个节点的左子树为
- 实现步骤:
- 先定义一个辅助函数
travelsal
,进行前序遍历并将节点值保存到list
中。 - 在
flatten
函数中,我们遍历保存的list
,重新构建树:- 根节点的值设为
list[0]
。 - 之后的每个节点都作为当前节点的右子节点,左子节点设为
nullptr
。
- 根节点的值设为
- 先定义一个辅助函数
- 通过这种方式,我们将树变成了链表。
- 前序遍历:
- 代码:
1 |
|
路径总和 III
- 路径总和 III:437.路径总和 III
- 思路:
- 这道题用的是前缀和思想,类似数组中“和为 K 的子数组”的解法,扩展到树结构。
- 定义:
cur
表示从根到当前节点的路径和。 - 若存在某个前缀和
pre = cur - targetSum
,则说明从pre
节点到当前节点之间的路径和等于targetSum
。 - 用哈希表
prefix
存储「前缀和 → 出现次数」。
- 定义:
- 核心逻辑:
- 到达当前节点时:更新当前路径和
cur += root->val
。 - 统计路径数:查找
prefix[cur - targetSum]
,说明之前出现过多少次这样的前缀和,它们对应的路径加上当前路径刚好等于目标。 - 递归左右子树:继续向下搜索。
- 回溯:递归返回时,减少当前前缀和的次数,保证不影响其他路径。
- 到达当前节点时:更新当前路径和
- 这道题用的是前缀和思想,类似数组中“和为 K 的子数组”的解法,扩展到树结构。
- 代码:
1 |
|
二叉树中的最大路径和
- 二叉树中的最大路径和:124.二叉树中的最大路径和
- 思路:
- 递归函数
travelsal(root)
的含义:- 返回值:以当前节点为起点,向上延伸时能提供的最大路径和(只能选择“左+root” 或 “右+root” 或 “root 自身”)。
- 过程:
- 递归计算左子树和右子树能提供的最大贡献值。如果结果为负,直接取
0
,因为负数会拖累路径和。 - 用当前节点作为“最高点”,计算
left + right + root->val
,更新全局最大值ans
。 - 返回“向父节点提供的最大贡献” =
max(root->val, root->val+left, root->val+right)
。
- 递归计算左子树和右子树能提供的最大贡献值。如果结果为负,直接取
- 递归函数
- 代码:
1 |
|
回溯
括号生成
- 括号生成:22.括号生成
- 思路:定义
left
和right
分别表示左右括号的数量,终止条件为当左右括号的数量都为n
的时候。递归过程中有两种情况:- 一是左括号数量小于
n
(left < n
),此时能够往path
里面加入左括号; - 二是
right<n && left>right
这个条件下可以往path
里面加右括号。
- 一是左括号数量小于
- 代码:
1 |
|
单词搜索
- 单词搜索:79.单词搜索
- 思路:
- 这是一道典型的回溯搜索(DFS)问题。核心思想是从每个格子出发,尝试走一条路径,如果走不通就回溯。
- 起点枚举:因为单词可能从任意格子开始,所以在
exist
里用两层循环,把每个格子作为起点尝试。 - 递归函数设计:
- 参数带上当前坐标
(i, j)
,以及全局的path
。 - 判断是否匹配当前字符,不匹配就直接剪枝返回。
- 如果已经匹配到
word.size()
长度,说明找到了整条路径,返回成功。
- 参数带上当前坐标
- 四方向扩展:递归尝试上下左右四个方向。
- 访问标记与回溯:用
visit
数组标记已访问的格子,走完这条路要恢复标记和路径(回溯),保证其他路径能用。 - 剪枝:
- 越界时立即返回;
- 字符不匹配立即返回;
- 找到答案时立即停止继续搜索。
- 当前格子字符和
word[path.size()]
相等→ 继续搜索; - 否则 → 回溯。直到
path.size() == word.size()
,返回true
。
- 代码:
1 |
|
二分查找
搜索插入位置
- 搜索插入位置:35.搜索插入位置
- 思路:经典二分查找问题,可以通过模拟查找的过程找出问题所在。
- 代码:
1 |
|
搜索二维矩阵
- 搜索二维矩阵:74.搜索二维矩阵
- 思路:
- 定义搜索范围
[left, right] = [0, m*n-1]
,其中m
是行数,n
是列数。
- 在二分查找过程中:
- 计算中点
mid = (left + right) / 2
; - 将一维下标
mid
转换为二维下标(关键):- 行号
hang = mid / n
- 列号
lie = mid % n
- 行号
- 比较
matrix[hang][lie]
与target
:- 相等 → 返回
true
; - 大于 → 缩小右边界;
- 小于 → 缩小左边界。
- 相等 → 返回
- 计算中点
- 查找结束仍未找到 → 返回
false
。
- 定义搜索范围
- 代码:
1 |
|
在排序数组中查找元素的第一个和最后一个位置
- 在排序数组中查找元素的第一个和最后一个位置:34.在排序数组中查找元素的第一个和最后一个位置
- 思路:
- 定义一个辅助函数
binSearch(nums, target, isLeft)
:
- 当
isLeft = true
时:找到target
最左边的位置。- 如果当前
nums[mid] == target
,继续往左缩小范围(right = mid - 1
)。
- 如果当前
- 当
isLeft = false
时:找到target
最右边的位置。- 如果当前
nums[mid] == target
,继续往右缩小范围(left = mid + 1
)。
- 如果当前
- 其余情况正常二分:
target > nums[mid]
就往右,否则往左。
- 这样分别调用两次,就能得到左边界和右边界。
- 定义一个辅助函数
- 代码:
1 |
|
搜索旋转排序数组
- 搜索旋转排序数组:33.搜索旋转排序数组
- 思路:
- 普通二分查找只适用于完全有序数组。但是旋转后的数组,可以分为两段依然有序的区间:
- 左半段有序;
- 右半段有序;
- 如果
nums[left] <= nums[mid]
,说明左半段有序:- 如果
target
落在[nums[left], nums[mid])
之间,就去左边查找; - 否则去右边查找。
- 如果
- 否则说明右半段有序:
- 如果
target
落在(nums[mid], nums[right]]
之间,就去右边查找; - 否则去左边查找。
- 如果
- 通过这种方式,每次都能排除一半的区间,保持对数复杂度。
- 普通二分查找只适用于完全有序数组。但是旋转后的数组,可以分为两段依然有序的区间:
- 代码:
1 |
|
寻找旋转排序数组中的最小值
- 寻找旋转排序数组中的最小值:153.寻找旋转排序数组中的最小值
- 思路:
- 在旋转后的有序数组中,总有一半区间是单调递增的。通过比较
nums[mid]
与nums[left]
、nums[right]
,可以判断当前mid
落在哪一半,并决定如何缩小搜索范围。 - 收缩到单调区间的情况:
- 如果
nums[left] <= nums[right]
,说明当前[left, right]
区间已经是单调递增的,此时最小值就是nums[left]
。
- 如果
- 判断 mid 所在区间
- 如果
nums[mid] >= nums[left]
,说明mid
在左半段有序区间内,最小值一定在mid
右边,于是移动left = mid + 1
。 - 否则(即
nums[mid] <= nums[right]
),说明mid
落在右半段区间,最小值在[left, mid]
,于是移动right = mid
。
- 如果
- 不断收缩左右边界,直到区间收缩为单调递增区间,直接返回
nums[left]
。
- 在旋转后的有序数组中,总有一半区间是单调递增的。通过比较
- 代码:
1 |
|
栈
最小栈
- 最小栈:155.最小栈
- 思路:这道题首先定义两个栈,一个栈正常存储元素,另一个栈为最小值栈,当入栈的元素小于最小栈元素就将该最小值加入到最小栈中,否则去最小栈的栈顶元素重新加入到最小栈。
- 代码:
1 |
|
字符串编码
- 字符串编码:394.字符串编码
- 思路:定义一个
string
类型的辅助栈,- 1、遍历字符串并压栈直到遇到
"]"
; - 2、定义辅助
string
类型的tmp
并弹出栈直到遇到"["
,将字符串记录至tmp
,注意从左边加到右边,最后弹出"["
; - 3、定义辅助
string
类型的num
并弹出栈直到字符不再是数字,将重复的次数加到num
中,注意从左边加到右边; - 4、通过
stoi(num)
将string
类型转换为int
类型,并采用for
循环构建重复的字符串,并重新压入栈中; - 5、最后从左边到右边拼接栈中字符串即可得到结果。
- 1、遍历字符串并压栈直到遇到
- 代码:
1 |
|
堆
数据流的中位数
- 数据流的中位数:295.数据流的中位数
- 思路:直接排序复杂度高,所以引入 两个堆:
- 大根堆(
left_que
):存储较小的一半数,堆顶是这部分的最大值。 - 小根堆(
right_que
):存储较大的一半数,堆顶是这部分的最小值。 - 为了能直接取出中位数,必须维持两个不变量:
- 有序性不变量:所有左边的元素 ≤ 所有右边的元素,即:
left_que.top() <= right_que.top()
,这样两个堆的交界就是中间。 - 规模平衡不变量:两个堆的元素个数差不超过
1
(注意这里不要用left_que.size() - right_que.size() > 1
来判断,因为size_t
是无符号数,负数会被解释成一个非常大的正整数),保证总数为奇数时,中位数就是元素更多的那边的堆顶,总数为偶数时,中位数就是两个堆顶的平均。
- 有序性不变量:所有左边的元素 ≤ 所有右边的元素,即:
- 大根堆(
- 代码:
1 |
|
动态规划
杨辉三角
- 杨辉三角:118.杨辉三角
- 思路:先特殊处理前两行,然后从第三行开始,每一行的首尾固定为
1
,中间元素由上一行相邻两个数相加,逐行生成直至numRows
。 - 代码:
1 |
|
乘积最大子数组
- 乘积最大子数组:152.乘积最大子数组
- 思路:
- 按照动态规划五部曲进行分析:
- 确定
dp
数组及下标的含义:dp_max[i]
和dp_min[i]
分别表示以i
结尾的最大子数组乘积最小值和最大值。 - 确定递推公式:
- 由于
nums[i]
可能存在负数的情况,所以要把最小值也记录下来,最后从递推公式中分别取出最大值和最小值。 - 最大值:
dp_max[i] = max(dp_max[i-1]*nums[i], max(dp_min[i-1]*nums[i], nums[i]));
- 最小值:
dp_min[i] = min(dp_max[i-1]*nums[i], min(dp_min[i-1]*nums[i], nums[i]));
- 由于
dp
数组如何初始化:除了dp_max[0]
和dp_min[i]
初始化成nums[0]
,其他全部初始化成0
;- 确定遍历顺序:在
i
上正序遍历; - 举例推导
dp
数组符合要求。
- 确定
- 按照动态规划五部曲进行分析:
- 代码:
1 |
|
最长有效括号
- 最长有效括号:32.最长有效括号
- 思路:定义
dp[i]
:表示以索引i
结尾的最长有效括号子串长度。遍历字符串,从i=1
开始(因为dp[0]
无法形成括号):- 情况一:
s[i] == ')' 且 s[i-1] == '('
- 形如
"...()"
- 那么
dp[i] = dp[i-2] + 2
(如果i-2 >= 0
,否则就是2
)。
- 形如
- 情况二:
s[i] == ')' 且 s[i-1] == ')'
- 形如
"...))"
- 那么需要去找与当前
')'
匹配的'('
: - 找位置
m = i - dp[i-1] - 1
(即前一个有效括号子串之前的那个位置)。- 如果
m >= 0 且 s[m] == '('
,说明匹配成功:dp[i] = dp[i-1] + 2 + dp[m-1]
(如果m-1 >= 0
,否则dp[i] = dp[i-1] + 2
)。
- 如果
- 形如
- 遍历过程中,维护
ans = max(ans, dp[i])
。
- 情况一:
- 代码:
1 |
|
多维动态规划
最小路径和
- 最小路径和:64.最小路径和
- 思路:
- 按照动态规划五部曲进行分析:
- 确定
dp
数组及下标的含义:dp[i][j]
表示从grid[0][0]
走到grid[i-1][j-1]
的最小路径和。 - 确定递推公式:
dp[i][j]
只由它的左边和上边元素递推得到,取两者的最小值即可,即dp[i][j] = min(dp[i-1][j], dp[i][j-1])+grid[i-1][j-1];
dp
数组如何初始化:除了dp[0][1]
和dp[1][0]
初始化成0
,其他全部初始化成INT_MAX
;- 确定遍历顺序:在
i
上正序遍历,在j
上正序遍历; - 举例推导
dp
数组符合要求。
- 确定
- 按照动态规划五部曲进行分析:
- 代码:
1 |
|
技巧
只出现一次的数字
- 只出现一次的数字:136.只出现一次的数字
- 思路:根据任意数与
0
异或:a ^ 0 = a
,任意数与自身异或:a ^ a = 0
,数组里所有数异或在一起,成对出现的数会两两抵消为0
,最后只剩下那个只出现一次的数。 - 代码:
1 |
|
多数元素
- 多数元素:169.多数元素
- 思路:设置一个候选值
res
和计数器count
。- 遍历数组:
- 如果
count == 0
,把当前数设为候选人res
,并把count
置为 1。 - 如果当前数等于
res
,count++
。 - 如果当前数不等于
res
,count--
。
- 如果
- 遍历完成后,
res
就是多数元素。
- 遍历数组:
- 代码(摩尔投票法):
1 |
|
- 代码(普通哈希表法):
1 |
|
寻找重复数
- 寻找重复数:287.寻找重复数
- 思路:
- 问题本质:
- 这道题的核心问题是要找到一个在数组中重复的数字,并且给定了数组的特殊性质:
- 数组的长度为
n + 1
,所以必定有一个重复元素。 - 数字的值范围是
1
到n
,这意味着每个数字都可以看作是数组的一个“指针”指向另一个位置。
- 使用快慢指针(Floyd’s Tortoise and Hare):
- 这是一个经典的 快慢指针 方法,用于在循环链表中寻找环的入口。
- 数组中的元素可以看作是指向另一个位置的指针。因此,如果数组中存在重复元素,那么这个重复元素将导致数组形成一个 环。
- 具体步骤:
- 初始化:使用两个指针
slow
和fast
。slow
每次向前走一步,fast
每次向前走两步。 - 第一步:找到相遇点:
slow = nums[slow]
:slow
每次向前走一步。fast = nums[nums[fast]]
:fast
每次向前走两步。- 当
slow
和fast
相遇时,说明数组中存在环。
- 第二步:找到环的入口(重复的数字):
- 重新将
fast
指针指向数组的起始位置,保持slow
指针在相遇点。 - 然后,两者同时每次向前走一步:
slow = nums[slow]
和fast = nums[fast]
。 - 当
slow
和fast
再次相遇时,相遇的位置就是环的入口,也就是重复的数字。
- 重新将
- 初始化:使用两个指针
- 问题本质:
- 代码:
1 |
|
下一个排列
- 下一个排列:31.下一个排列
- 思路:
- 给定序列
nums
,我们寻找“下一个更大”的字典序排列。核心是把序列划分为两部分:- 前缀部分:
[0 .. i]
- 候选集合:
(i .. end]
,即从i+1
到结尾这一段
- 前缀部分:
- 这里的 候选集合 是当前后缀中“尽可能小但又足以让整体变大的那一段”,也是一个严格非增序列(从右往左看单调不增),这保证了它已经是该后缀的最大排列。
- 具体步骤
- 定位“转折点” i
- 从右往左找到第一个满足
nums[i] < nums[i+1]
的位置i
。
- 若找不到,说明整个数组是非增的,已经是全局最大排列,直接对整个数组升序排序即可(最小排列)。
- 从右往左找到第一个满足
- 在候选集合中选交换对象
- 候选集合是区间
(i .. end]
。在这段里找到第一个大于nums[i]
且尽可能小的元素与之交换。
- 由于候选集合从右往左是非增的,从左到右扫描到第一个不大于的位置即可停下;你代码用
k
线性前进,最终交换k-1
,等价于“在候选集合中找刚好比nums[i]
大的最右元素”。
- 候选集合是区间
- 重置候选集合为最小排列
- 交换完成后,候选集合内部仍是“最大排列”形态。为了让整个序列成为“刚好比原序列大、但增幅最小”的排列,需要把候选集合 升序排序(也可写成反转,因为原本是非增的)。
- 给定序列
- 代码:
1 |
|
缺失的第一个正数
- 缺失的第一个正数:41.缺失的第一个正数
- 思路:
- 核心思路:
- 目标:把数组中值在
[1, n]
的元素,放到正确的位置 ——nums[i] = i+1
。- 比如
1
应该在下标0
;2
应该在下标1
。
- 比如
- 放置完成后,只要扫描一遍,找到第一个位置不满足
nums[i] == i+1
的下标,答案就是i+1
。 - 如果都满足,说明数组包含
[1,2,...,n]
,缺失的数是n+1
。
- 目标:把数组中值在
- 实现步骤:
- 遍历数组,对每个元素
nums[i]
:- 如果它在
[1, n]
范围内,并且它没有放到正确的位置,就和目标位置的元素交换。 - 用
while
而不是if
,因为交换过来的数也可能需要再调整。
- 如果它在
- 遍历调整后的数组:
- 如果发现
nums[i] != i+1
,返回i+1
。 - 如果全部正确,返回
n+1
。
- 如果发现
- 遍历数组,对每个元素
- 核心思路:
- 代码:
1 |
|
热题100学习笔记
http://example.com/2025/09/07/hot100/