Skip to content

现在真是不知道还应不应该做 LeetCode 了

数组/双指针

两数之和

给定一个数组字和一个目标值,问其中哪两个数字加起来正好等于目标值?只要找出一对就行(通常是返回它们的下标)

关键洞察:

1、对于 x,我们马上知道它应该匹配 y=target-x

2、我们只需要知道 target-x 是否曾经出现过,以及如果出现了,在哪里

所以每次往前走,只需要放在 hashmap 里

三数之和

给定一串数,找出所有不重复的三元组,使得三个数加起来等于 0(或某个目标值)。

字符串

最长无重复子串

在一个序列里寻找一段最长的连续区间,区间内没有任何重复元素

双指针,中间是“ABC”,前面的指针,遇到重复的了,比如“B”,就让后一个指针,直接跳转到“C”,形成“CB”即可

动态规划

这东西看完就会,会完了马上忘

将问题拆解成相互重叠的子问题,通过定义“状态”和“状态转移方程”来有序地重用中间结果,从而避免重复计算

重叠子问题和无后效性的状态

感觉基本上,除了爬楼梯,都有“抉择”,即 max 在两个状态里二选一

哦哦,我大概抓住重点了。重点是,你假装,有人已经告诉你了,n-1 时,状态的答案

爬楼梯

你正在爬楼梯,一次可以爬 1 阶或 2 阶,到达第 n 阶有多少种不同的方法

dp[i] = dp[i-1] + dp[i-2]

背包问题

0-1 背包:每件物品只有 1 个,选或不选。

完全背包:每件物品有无限个,可以选任意次数。

多重背包:每件物品有有限个(给定数量)。

二维费用背包:除了重量,还有体积等其他限制。

分组背包:物品分组,每组最多选一件。

做题时,一般就是经典背包问题,即 0-1 背包

定义:给定 N 件物品和一个容量为 V 的背包,每个物品有重量和价值。不超重,拿最大价值。

关键:

我们对物品一件一件的看过去,当看到第 i 件时:

我们不管之前你拿了多少物品,只关心当前背包剩余的容量

你此时只有两个选择:(1)拿起 i(2)不拿 i

定义 dp[i][j] 表示面对第 i 个物品时,背包剩余容量为 j,获取的最大总价值

如果能拿起,则:dp[i][j] = max(dp[i-1][j] , dp[i-1][j - w[i]] + v[i])

如果拿不起,那就跳:dp[i][j] = dp[i-1][j]

拿起时那个公式并不贪心,因为 j - w[i] 会导致列序号缩小

完全平方数

我们要求 13 的最小完全平方数组成个数

我们假装已知 12、11...(实际上是现场求)

13 减去任何一个 4,得到 9,相当于 9 的完全平方数+1。对着每一个完全平方数试过去,找最小的

零钱兑换

零钱兑换和完全平方数类似

也是,如果你要知道 11 元怎么找,你假装知道 10、9... 所有的找法

你用 11,减去现有所有面额,你再和已知的进行比较,这里一个个枚举即可

单词拆分

问题就是,能否用一些字符串,拼凑成一个新的字符串

还是那个道理,如果你想知道你这个字符串能不能拼成,那你就用你的字符串,减去所有的候选字符串,看剩下的是否能拼成,这就是状态转移

最长增长子序列 LIS

定义 dp[i] 为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取

问题关键还是状态的定义:状态就是某个条件下,子问题的答案

和前面的题有微小区别的是,这个题不是返回 dp[n] 而是返回 max_element(dp.begin(), dp.end())

毕竟那个最长的不一定是用最后一个元素结尾

排序

冒泡排序

O(n2)

从前到后的扫描数组,每次遇到逆序,就交换,直到这次没有交换

选择排序

O(n2)

将数组划分为两个部分

每一轮从未排序部分中选出最小(或最大)的元素,将其与未排序部分的第一个元素交换位置

插入排序

O(n2)

将数组划分为两个部分

每次从未排序部分取出第一个元素,在已排序部分从后向前扫描,找到它应该插入的位置

归并排序

O(nlogn)

先将整个序列递归地二分,直到每个子序列只剩一个元素(一个树)

将相邻的两个有序子序列合并成一个更大的有序序列(合并时,两个节点中都是一堆有序序列,你看谁开头更小,就先取谁)

二叉树

前中后

前:根左右

中:左根右

后:左右根

即左永远在右前面,前中后序描述的是根所在的位置。

呃,从树的图形来看,遍历是,一定是从根节点,向左侧拉一根线,逐步的包围整改树

前中后序,实际上就是左下右序

前序就是,按照每个节点的左侧,画一个圆点,这根线碰到圆点的先后顺序

中序后序同理,无非圆点在下面和右边

算法上,递归的很好写,无非按照前中后,调整自己 getName 函数的位置

非递归,你只需要记住,它是一个栈来模拟。

递归时,f(LChild),就是栈里左孩子压栈,右侧同理

getName,就是弹出并访问

所以前序其实就是入栈,弹,左,右

中序就是,左(递归的看,直到没有),弹,右

后序比较独特。

定义:

完全二叉树 + 堆性质

完全二叉树 = 了最后一层,其他层都是满的,且最后一层的节点都靠左排列

利用数组可以紧凑地表达完全二叉树的结构,无需指针

堆性质:任意节点的值 ≥ 其子节点的值。根节点是整个堆中的最大值

特点:兄弟节点之间没有必然顺序

对于数组中索引为 i 的节点(假设从 0 开始):

父节点索引:(i - 1) // 2

左子节点索引:2 * i + 1

右子节点索引:2 * i + 2

C++ 里实际上有一套堆相关的算法(我还真没用过)

std::priority_queue 优先队列,内部是一个堆,只定义了操作:插入元素、取出最大 or 最小元素(只能是一种)

没有 std::heap,优先队列封装了堆

<algorithm>里面有一些和堆相关的函数