337. 打家劫舍 III (House Robber III)
标签: 递归
解法:
- 对于当前遍历到的树, 其最大金额等于 "偷窃根结点所能得到的最大金额" 与 "不偷窃根结点所能得到的最大金额" 之间的较大值, 因此本题使用标准的递归即可解答.
代码:
|
|
338. 比特位计数 (Counting Bits)
标签: 位运算, 动态规划
解法:
- 第一种解法是使用 Brian Kernighan 算法, 该算法常用于一种名叫 "树状数组" 的数据结构中. 该算法的关键在于其注意到表达式
x & (x - 1)
会将x
的 "最低非零比特位" 置零. 于是我们便可通过观察将整个x
置零所需的次数来确定x
的比特位总数. - 第二种解法是使用关于最高有效位的动态规划方法. 观察到将一个数的最高有效位置零后所得到的数必然小于该数, 并且所得到的数的比特位总数总是等于该数的比特位总数减一, 因此我们可以通过在从小到大遍历的过程中维护当前数的最高有效位来快速得到当前数的比特位总数.
- 第三种解法是使用关于最低有效位的动态规划方法. 观察到将一个数向右进行逻辑移位一位所得到的数必然小于该数, 并且所得到的数的比特位总数总是等于该数的比特位总数减去最低有效位, 因此我们可以直接通过该数自身来快速得到当前数的比特位总数.
代码 (使用 Brian Kernighan 算法的解法):
(略)
代码 (使用关于最高有效位的动态规划的解法):
(略)
代码 (使用关于最低有效位的动态规划的解法):
|
|
347. 前 K 个高频元素 (Top K Frequent Elements)
标签: 堆排序, 快速排序
解法:
- 第一种解法是使用堆排序. 首先使用一个哈希表获取每个元素的出现频数, 然后针对哈希表中的频数数组进行堆排序. 由于哈希表无法随机读写, 此处选择维护一个大小为
k
的堆并动态添加或删除元素 (正常做法是建堆然后弹出k
个堆顶元素). - 第二种解法是使用快速排序中的扫描算法. 这类似于 "215. 数组中的第K个最大元素 (Kth Largest Element in an Array)" 中的做法, 这里就不做过多介绍了.
代码 (堆排序的解法):
|
|
代码 (快速排序的解法):
(略)
394. 字符串解码 (Decode String)
标签: 递归
解法:
考虑对原始字符串从左到右进行遍历. 对于当前遍历到的字符:
- 如果该字符是一般的小写英文字母, 那么直接将其加入当前子串中.
- 如果该字符是数字, 说明遇到了整数, 那么我们将该整数解码, 定位到紧接着该整数的下一个左括号, 然后使用递归对由该左括号确定的模式进行解析. 返回时当前位置将位于对应的右括号上, 我们将当前位置加一跳过该右括号即可.
最终所得的子串即为当前模式所对应的原始子串.
代码:
|
|
399. 除法求值 (Evaluate Division)
标签: 并查集
解法:
- 由于除法具有传递性, 因而具有类似前缀和的性质. 我们可以使用并查集维护各个结点所位于的集合, 并存储结点与其根结点之间的商, 这样我们就能够快速判别出查询是否合法 (等价于所查询的两个元素是否位于同一个集合), 以及在合法的情况下两个元素的商.
代码:
|
|
406. 根据身高重建队列 (Queue Reconstruction by Height)
标签: 模拟
- 第一种解法是从矮到高进行模拟. 对于当前遍历到的人, 假设身高小于这个人的所有人都已经被安排好位置, 那么我们只需要将当前这个人安排在一个位置使其前面恰好具有足够多的空位能够容纳接下来身高大于等于他的人即可.
- 第二种解法是从高到矮进行模拟. 对于当前遍历到的人, 假设身高大于等于这个人并且排位也比这个人靠前的所有人都已经被安排好位置, 那么我们只需要将其插入至一个位置使其前面恰好有足够多身高大于等于他的人即可. 注意到与从矮到高进行模拟不同, 在从高到矮进行模拟的过程中我们无法维护空位, 因此对于排位靠后的人来说我们无法预先为其预留空位来容纳那些身高等于他但排位比他靠前的人, 因此在遍历时我们还需同时按照排位从前到后进行排序.
代码 (从矮到高进行模拟的解法):
|
|
代码 (从高到矮进行模拟的解法):
(略)
416. 分割等和子集 (Partition Equal Subset Sum)
标签: 动态规划
解法:
- 如果要分割集合为两个和相等的子集, 那么和已知, 集合已知, 我们便能够套用 0-1 背包问题的解法来求解. 这是一道典型的动态规划的题目.
代码:
|
|
437. 路径总和 III (Path Sum III)
标签: 深度优先搜索
解法:
- 由于本题中的路径仅位于同一个树中, 深度优先搜索经过的路径实际上形成了前缀和, 因此我们可以通过哈希表来快速计算出 "以当前根结点为结尾的长度等于目标和的路径" 的数量.
代码:
|
|
438. 找到字符串中所有字母异位词 (Find All Anagrams in a String)
标签: 哈希表, 滑动窗口
解法:
- 直接使用哈希表维护当前滑动窗口中的所有不同字符的个数, 并在遍历过程中维护一个用于表示当前滑动窗口是否合法的状态信息. 每当添加一个字符或删除一个字符的时候便更新该状态, 并在状态变为合法时将当前窗口的起始位置添加进答案集合中, 重复上述过程即可.
代码:
|
|
448. 找到所有数组中消失的数字 (Find All Numbers Disappeared in an Array)
标签: 哈希表
解法:
- 由于本题除原始数组以外不允许使用额外空间, 因此我们可以考虑使用原地哈希的做法. 我们从左到右遍历数组, 对于当前遍历到的元素, 该元素原本应当位于的位置等于该元素的值, 因此我们可以观察该元素原本应当位于的位置上的元素, 若该元素原本应当位于的位置上的元素并不等于该元素, 说明该元素应当被复位, 于是我们可以将这两个元素进行交换; 否则该元素已经复位, 并且当前元素是多余的, 于是我们直接忽略该元素. 在上述过程结束之后, 整个数组中的元素要么已经复位, 要么由于重复而位于下标不等于自身的位置上, 因此我们可以通过再次遍历数组来获取所有未能复位的元素, 此即最终答案.
代码:
|
|
461. 汉明距离 (Hamming Distance)
标签: 位运算
解法:
- 根据汉明距离的定义, 两个数之间的汉明距离实际上就是两者的异或所得的值中非零比特位的个数. 直接使用各类编程语言内置的获取整数非零比特位个数的函数即可.
代码:
|
|
494. 目标和 (Target Sum)
标签: 动态规划
解法:
- 由于涉及到减法, 我们可以将减法看成是 "不加", 而将加法看成是 "加两倍", 那么原问题就转化为了等价的 0-1 背包问题.
- 由于操作变为了 "加两倍", 这里实际上额外提出了一个剪枝的角度, 即与原问题等价的 0-1 背包问题的目标值必须是个偶数.
代码:
|
|
538. 把二叉搜索树转换为累加树 (Convert BST to Greater Tree)
标签: 中序遍历, Morris 遍历
解法:
- 第一种解法是使用中序遍历. 由于二叉搜索树的性质, 值大于当前结点的值的结点均位于当前结点的右子树中, 因此我们完全可以通过 (反向) 中序遍历来维护 "当前结点的右子树中所有结点的和".
- 第二种解法与第一种解法类似, 都是使用中序遍历, 只不过第二种解法采用称为 "Morris 遍历" 的算法以时间换空间来进行二叉树的中序遍历, 下面以正向中序遍历为例说明其思想:
- 如果当前树的左子树为非空, 那么首先找到根结点位于左子树中的前驱结点:
- 如果根结点的前驱结点的右子树为空, 那么将其右子树暂时设为当前树 (即将根结点作为该前驱结点的右孩子), 并递归地进入左子树 (注意此处是中序遍历, 因此首先应当进入左子树).
- 如果根结点的前驱结点的右子树已经是根结点, 说明我们已经从左子树中递归返回, 于是可以将前驱结点的右孩子重新置空, 然后正常遍历根结点并递归地进入右子树.
- 如果当前树的左子树为空, 那么正常遍历根结点并直接进入右子树. 由于
- 如果当前树的左子树为非空, 那么首先找到根结点位于左子树中的前驱结点:
代码 (中序遍历的解法):
|
|
代码 (Morris 遍历的解法):
|
|
543. 二叉树的直径 (Diameter of Binary Tree)
标签: 递归
解法:
- 根据定义, 二叉树的直径等于其左子树的直径, 其右子树的直径, 以及经过根结点的最长路径的长度这三者之间的最大值, 于是我们自然而然可以使用递归来求解这三者并得到当前树的直径.
代码:
|
|
560. 和为 K 的子数组 (Subarray Sum Equals K)
标签: 前缀和, 哈希表
解法:
- 由于题目求的是子数组的和 (类似于字符串中的子串, 为连续的区间而非离散的序列), 因此我们自然而然可以想到使用前缀和来进行求解. 首先我们从左到右对数组进行遍历, 遍历过程中我们使用一个哈希表来存储从数组的最左端开始到各个位置为止的子串的元素之和 (即前缀和). 对于当前位置, 如果存在和为
k
(即目标值) 的子串以当前元素结尾, 那么我们可以断定哈希表中存在有前缀和等于当前子串前缀和减去k
的子串, 因此我们只需对哈希表进行查询即可.
代码:
|
|
581. 最短无序连续子数组 (Shortest Unsorted Continuous Subarray)
标签: 贪心
解法:
- 根据定义, 如果我们对数组进行排序, 那么最短无序连续子数组的左右两个端点必然会被移动 (因为如果它们不移动, 说明它们已经有序, 那么删去它们将能够形成更短的最短无序连续子数组, 形成矛盾), 这说明 "在最短无序连续子数组中必然存在小于左端点的数以及大于右端点的数", 而对于那些位于最短无序连续子数组左边的元素而言, 任何位于它们右边的元素均大于等于它们, 同时对于那些位于最短无序连续子数组右边的元素而言, 任何位于它们左边的元素均小于等于它们. 因此我们可以通过从右向左遍历找到尽可能位于左端的不满足 "任何位于它们右边的元素均大于等于它们" 的元素作为最短无序连续子数组的左端点, 然后同理找到右端点, 最终确定最短无序连续子数组的范围.
代码:
|
|
617. 合并二叉树 (Merge Two Binary Trees)
标签: 递归, 迭代
解法:
- 第一种解法是使用递归. 对于当前遍历到的两个根结点:
- 如果两个根结点均为空, 则直接返回空指针.
- 如若不然, 说明两个根结点中存在非空结点. 如果其中一个根结点为空, 那么返回另一个根结点作为已经合并的树.
- 如若不然, 说明两个结点均为非空结点. 于是我们递归地对两棵树的左子树和右子树进行合并.
- 第二种解法是使用迭代来模拟递归.
代码 (递归的解法):
|
|
代码 (迭代的解法):
|
|
621. 任务调度器 (Task Scheduler)
标签: 模拟
解法:
- 由于两个相同的任务之间必须间隔
n
个距离, 考虑向一个列数为n + 1
的矩阵中填入所有任务. 我们从出现次数最多的任务A
开始, 首先将A
填入矩阵的第一列, 此时矩阵的最大行数由任务A
的出现次数决定. 由于矩阵的列数为n + 1
, 第一列中上下任意两个相邻的任务之间必然间隔n
, 因此由第一列构成的任务序列是合法的. 紧接着我们考虑将出现次数第二多的任务B
以 "从左到右, 从下到上" 的顺序填入矩阵, 如果任务B
的出现次数等于任务A
的出现次数, 那么两者将分别填充第一列和第二列; 否则我们从第二列的倒数第二行开始向上填充, 并且有可能会由于任务B
无法填充第二列而在第二列的头几个位置留下几个空槽. 接着将出现次数第三多的任务C
填入矩阵, 任务C
将首先从第二列中任务B
未能填充的部分开始, 并在填充好第二列之后开始自下而上填充第三列, 由于任务C
的出现次数仍然小于等于任务B
的出现次数, 因此和任务B
一样, 任务C
要么单独占据一列从而满足合法序列, 要么由于无法占据满一列而同样满足合法序列, 以此类推. 如果将所有任务填入矩阵后仍然无法填满矩阵, 那么由于所有长度等于任务A
的任务 (包括任务A
在内) 存在, 整个序列无法提前退出, 而必须等待所有这些任务完成才能退出, 因此最短合法序列的长度等于将矩阵填满后得到的序列的长度; 反之如果将所有任务填入矩阵后能够填满矩阵或是发生溢出, 那么最短合法序列的长度就是所有任务的总数.
代码:
|
|
647. 回文子串 (Palindromic Substrings)
标签: 动态规划
解法:
- 本题解法与 "5. 最长回文子串 (Longest Palindromic Substring)" 大同小异, 后者无非是在更新最大回文子串大小, 我们只需要对其稍加改动, 在确认当前子串为回文子串时将回文子串总数加一 (而非更新最长回文子串长度) 即可.
代码:
|
|
739. 每日温度 (Daily Temperatures)
标签: 单调栈
解法:
- 根据题意, 从当前位置开始到下一个更高温度的位置之间的这段范围内出现的温度均小于等于当前位置的温度, 因此所有这些温度都无法更新当前位置的温度, 而当下一个更高温度出现之后, 当前位置所对应的信息就确定不变了. 我们可以使用单调栈来模拟这一现象. 对于当前遍历到的温度, 我们不断弹出单调栈中温度小于当前温度的日期, 将当前温度作为所弹出的温度的下一个更高温度, 而使用 "弹出" 这一操作来表示该位置所对应的信息已经确定不变了.
代码:
|
|