102. 二叉树的层序遍历 (Binary Tree Level Order Traversal)
标签: 广度优先搜索
解法:
- 直接套用广度优先搜索的样板代码即可.
代码:
|
|
104. 二叉树的最大深度 (Maximum Depth of Binary Tree)
标签: 深度优先搜索, 广度优先搜索
解法:
- 第一种解法是使用深度优先搜索. 因为一棵二叉树的最大深度等于其所有子树的最大深度之间的较大者加一, 因此可以使用递归来进行求解.
- 第二种解法是使用广度优先搜索. 每当进入下一层便将深度加一, 返回最终的深度即可.
代码 (深度优先搜索的解法):
|
|
代码 (广度优先搜索的解法):
(略)
105. 从前序与中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal)
标签: 哈希表, 分治
解法:
- 前序序列的第一个元素必然是当前二叉树的树根的值. 由于序列中不存在重复的元素, 因此我们可以使用一个哈希表来存储从元素的值到元素在中序序列中的下标的映射, 同时使用该映射以及已知的根结点的值来确定左子树和右子树的大小并进一步确定它们在前序序列中的范围.
代码:
|
|
114. 二叉树展开为链表 (Flatten Binary Tree to Linked List)
标签: 前序遍历, 递归, 迭代
解法:
- 第一种方法是使用前序遍历, 即先通过前序遍历获取到前序序列, 然后再一遍构造出链表.
- 第二种方法是使用递归. 要将一棵二叉树展开为链表, 只需要分别将其左右子树展开为链表并将根结点, 左子树链表以及右子树链表这三者串联起来即可. 注意到这种方法需要获取到左子树的最后一个结点以便串联, 同时也需要获取到右子树的最后一个结点用于作为整棵树的最后一个结点并返回, 因此递归函数将需要额外的两个参数.
- 第三种方法是使用迭代模拟前序遍历. 我们使用一个栈来存储 "所有待处理的右子树的根结点". 对于当前遍历到的结点:
- 如果该结点具有左子树, 那么下一个结点就是左子树的根结点.
- 如若不然, 如果该结点具有右子树, 那么下一个结点就是右子树的根结点.
- 如果不然, 如果 "所有待处理的右子树的根结点" 的栈非空, 那么下一个结点就是栈顶结点.
- 如果不然, 说明整棵树已经遍历完毕, 直接返回即可.
代码 (前序遍历的解法):
(略)
代码 (递归的解法):
(略)
代码 (迭代的解法):
|
|
121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock)
标签: 动态规划
解法:
- 遍历数组中的每个元素. 对于当前位置, 考虑 "在当前位置卖出能够获取的最大利润". 显然该值等于 "当前位置的价格" 减去 "在该位置之前的价格的最低值" (若为负数则忽略), 因此可以在遍历过程中维护 "在该位置之前的价格的最低值", 并不断更新最终答案.
代码:
|
|
124. 二叉树中的最大路径和 (Binary Tree Maximum Path Sum)
标签: 递归
解法:
本题中的 "路径" 可以是形如 "从左子树的结点到根结点, 再到右子树的结点" 这样的路径, 因此需要分情况讨论:
- 对于不经过根结点的那些路径, 它们其实就是左子树中的所有路径加上右子树中的所有路径.
- 对于经过根结点的那些路径, 它们相当于左子树中那些以左子树的根结点为端点的路径, 加上根结点, 加上右子树中那些以右子树的根结点为端点的路径这三者的串联.
综上所述, 递归函数应该包含两个返回参数, 分别是 "形状任意的路径的最大路径和" 以及 "以根结点为端点的路径的最大路径和".
代码:
|
|
128. 最长连续序列 (Longest Consecutive Sequence)
标签: 哈希表
解法:
- 想象原始数组中的所有元素在数轴上构成一个个集合, 每个集合由连续的数字构成. 对于任意一个集合, 我们从该集合中的任意一个数字开始, 不断向两边扩展, 一边扩展一边删除所有遍历到的数字, 直至将整个集合都删除, 此时我们便得到了该集合的长度, 并且同时还准备好进入下一个循环. 不断重复这一过程并更新最终答案即可.
代码:
|
|
136. 只出现一次的数字 (Single Number)
标签: 位运算
解法:
- 经典老题, 直接使用异或运算将出现两次的数字两两抵消, 剩下的便是只出现一次的数字.
代码:
|
|
139. 单词拆分 (Word Break)
标签: 动态规划, 哈希表
解法:
- 首先应当转变过来的思想是本题不应该从 "一个单词是否出现在字符串中" 入手, 而应该从 "字符串的当前子串是否出现在字典中" 入手, 因为后者能够使用哈希表进行优化, 同时还能够使得原问题转化为等价的子问题, 并能进一步使用动态规划进行求解.
- 我们首先使用一个哈希表存储字典, 然后从头开始对原始字符串进行遍历. 对于从原始字符串开头到当前位置为止所形成的子串, 我们将其分割为左右两个部分, 其中右半部分的长度从 1 开始逐渐增大. 对于每个不同的右半部分, 我们使用哈希表检查该右半部分是否恰好为字典中的某个单词, 如果是, 那么当前子串能够拆分当且仅当当前字串的左半部分能够拆分, 因此我们只需要对所有不同的右半部分进行查询即可获知当前子串是否能够拆分.
代码:
|
|
141. 环形链表 (Linked List Cycle)
标签: 快慢指针
解法:
- 经典环形链表, 经典快慢指针的解法. 首先确保链表不为空. 一开始设置两个快慢指针指向头结点, 然后开始循环, 在每一轮循环中快指针向前走两步, 慢指针向前走一步, 如果快指针无法向前走两步说明遇到链表尾, 于是进一步说明链表无环, 否则链表有环, 并且快慢指针最终将会相遇, 于是我们只需在快慢指针相遇时返回即可.
代码:
|
|
142. 环形链表 II (Linked List Cycle II)
标签: 脑筋急转弯, 快慢指针
解法:
本题比 "141. 环形链表 (Linked List Cycle)" 要难上一个数量级, 原因在于不仅需要确定是否有环, 还需要求出环的入口.
要求解本题首先需要具备一些前置知识. 规定链表中两个结点之间的距离为由这两个结点所构成的路径的边数. 现在假设链表有环, 并且快慢指针已经在环中相遇. 考虑将时间倒回至慢指针刚刚到达入环口的时刻, 此时慢指针所走过的距离为头结点与入环口结点之间的距离, 记为
a
, 而快指针所走的距离为该距离的两倍, 记环的长度为c
, 那么此时快指针应当位于距离入环口b = a % c
的位置, 并且只需要再走d = c - b
步便能再次到达入环口, 而由于慢指针已经位于入环口, 可以想象当慢指针从入环口开始走d
步之后, 快指针将走2d
步, 其中d
步用于到达入环口, 另外的d
步用于追上慢指针, 两者相遇. 由前述推导我们可以得知两件事情:(a + d) % c = 0
.- 快慢指针相遇时, 两者恰好位于从入环口出发走
d
步的位置, 并且只需要再走b = a % c
步即可再次到达入环口.
那么问题就转化为了如何能够使快指针或慢指针再走
b = a % c
步的问题. 而这恰好可以通过令快指针重新从链表头结点开始与慢指针一同出发 (此时快指针不再一次走两步, 而是和慢指针一样一次走一步), 并判断两者是否相遇来做到, 因为如果快指针走了a
步, 那么慢指针便恰好走b = a % c
步 (注意到慢指针位于环中), 于是可知此时两者均位于入环口.
代码:
|
|
146. LRU 缓存 (LRU Cache)
标签: 模拟, 双向链表, 哈希表
解法:
- 由于需要以 $O(1)$ 的时间进行插入和删除, 并且需要按最近使用的顺序对键值对进行排序, 这就决定了必须使用双向链表来存储键值对. 此外为了支持 $O(1)$ 的查找, 还需要使用一个哈希表来存储从键到链表中对应结点的地址的映射. 剩下的便是如何实现一个双向链表以及如何维护最大缓存容量的问题了.
代码:
|
|
148. 排序链表 (Sort List)
标签: 快慢指针, 分治, 归并排序
解法:
- 要想仅使用 $O(1)$ 的额外空间对链表进行排序, 就只能使用分治 + 归并排序的方法. 对于一条链表, 首先使用快慢指针将其切割为等长的两部分, 然后递归地对这两条子链表进行排序, 最后使用归并排序将这两条子链表合并为完整的一条链表.
代码:
|
|
152. 乘积最大子数组 (Maximum Product Subarray)
标签: 动态规划
解法:
- 因为是求最大乘积, 并且数组中的元素的符号不定 (有正有负), 当前最大的正数乘积乘上当前元素 (假设为负数) 之后就可能变成一个负数了, 因此我们不能只盯着一个乘积的当前值来看, 而应该着眼于乘积的绝对值. 考虑同时维护 "以当前元素结尾的非空连续子数组的乘积的最大值" 和 "以当前元素结尾的非空连续子数组的乘积的最小值", 不论这两个变量的符号如何变化, 新的值总是能够在这两个值当中产生, 也能够很容易地写出状态转移方程, 这里就不详细展开了.
代码:
|
|
155. 最小栈 (Min Stack)
标签: 动态规划
解法:
- 由于栈的先进后出的特殊性质, 对于当前栈中的序列, 其最小值等于将栈顶弹出后的较短序列的最小值与栈顶元素之间的较小值, 反过来向栈中压入一个元素后形成的更长序列的最小值等于当前栈中序列的最小值与所压入元素之间的较小值. 因此我们考虑使用另一个辅助栈来存储 "当前主栈中的序列的最小值", 这样就能方便快捷地达到 $O(1)$ 时间的查询效率.
代码:
|
|
160. 相交链表 (Intersection of Two Linked Lists)
标签: 脑筋急转弯, 双指针
解法:
- 首先假设两个链表存在相交结点. 设从链表 1 到达相交结点的距离为 a, 从链表 2 到达相交结点的距离为 b, 从相交结点到达链表尾的距离为 c, 由加法交换律, $(a + c) + b = (b + c) + a$, 如果我们令指针 1 从链表 1 开始, 指针 2 从链表 2 开始, 并令指针 1 在到达链表尾部时转而从链表 2 开始, 令指针 2 在到达链表尾部时转而从链表 1 开始, 那么最终两者将恰好在相交结点处相遇 (在 $a = b$ 的情况下甚至还会提前相遇). 如果两个链表不存在相交结点, 那么这样做将会令两个指针同时到达链表尾部. 因此我们可以根据两个指针的值的不同情况对链表中是否存在环做出判断.
代码:
|
|
169. 多数元素 (Majority Element)
标签: 脑筋急转弯, Boyer-Moore 投票算法
解法:
- 假设存在多数元素, 现将数组中的互不相同的元素两两打包并抵消, 那么剩下的数就必然是多数元素. 如果不存在多数元素, 那么最后剩下的元素有可能是 "少数元素" 中无法被抵消的 "幸存者", 因此还需要从头对数组进行遍历以检查这些剩下的元素是否确实是多数元素. 但由于本题中已经约定了必然存在多数元素, 因此最后的遍历可以省去.
代码:
|
|
198. 打家劫舍 (House Robber)
标签: 动态规划
解法:
- 本题属于较为简单的一类一维动态规划的题目. 考虑 "从数组开头到当前位置为止的范围内能够偷窃到的最高金额", 如果偷窃当前位置的房屋, 那么由于报警系统的制约, 能够偷窃到的最高金额等于当前房屋的价值加上 "从数组开头到当前位置的前两个位置为止的范围内能够偷窃到的最高金额"; 反之如果不偷窃当前位置的房屋, 那么能够偷窃到的金额就等于 "从数组开头到当前位置的前一个位置为止的范围内能够偷窃到的最高金额". 于是我们可以很容易地写出状态转移方程.
代码:
|
|
200. 岛屿数量 (Number of Islands)
标签: 深度优先搜索, 广度优先搜索, 并查集
解法:
- 第一种解法是使用深度优先搜索. 最外层循环对矩阵进行逐行逐列的遍历, 每当遇到为
1
的元素便使用深度优先搜索将该元素所处的岛屿填充为0
, 这样最外层循环所遍历到的为1
的元素的总数就是岛屿的数量. - 第二种解法是使用广度优先搜索. 其中最外层循环的逻辑和深度优先搜索中的相同, 不同的是对岛屿的填充方式由深度优先搜索更改为广度优先搜索.
- 第三种解法是使用并查集, 即将当前元素与上下左右四个元素所处的岛屿进行合并.
代码 (深度优先搜索的解法):
|
|
代码 (广度优先搜索的解法):
(略)
代码 (并查集的解法):
(略)
206. 反转链表 (Reverse Linked List)
标签: 迭代
解法:
- 第一种解法是使用迭代. 直接一板一眼地使用迭代对链表进行反转即可.
- 第二种解法是使用递归. 递归的解法稍微有些复杂, 对于当前链表, 我们将从 "链表的头结点的下一结点" 开始的子链表送入递归中进行反转并得到子链表的反转后的链表的头结点, 此时原子链表的头结点变为新子链表的尾结点, 于是我们可以将原始链表的头结点插入新子链表的尾结点之后作为新链表的尾结点.
代码 (迭代的解法):
|
|
代码 (递归的解法):
|
|