LeeCode三百题-2
[TOC]
代码工程获取
git clone --depth=1 https://gitee.com/SevDaisy/LeeCode300.git |
#11 盛最多水的容器
- 左右边界问题 —— 很容易想到双指针 —— 具体来说是 左右指针 依次向中间移动
- 用
while来实现边界的连续位移 - 不用每次位移都做答案更新的判断
- 就是用
while (left < right && height[++left] < old);代替left++ - 就是用
while (left < right && height[--right] < old);代替right-- - 直到找到下一个可能是解的边界后,再做判断。
- 从
6ms => 19%优化到了3ms => 94.41%
package Order300; |
#12 整数转罗马数字
- 将规则稍加完善(指 增加
900:CM这样的映射)后, - 遍历规则集,做贪心匹配,即
- 我们希望每次找到不大于目标值的最大映射对。
- 理论上来说可以可以通过二分法更快的找到这样的最大映射对
- 但是罗马数字的规则集有限。
- 这个优化实在是没什么意义。
- 于是对有序数组(规则集应该是降序存储的)逐个遍历即可。
- 我们希望每次找到不大于目标值的最大映射对。
- 当然,硬编码数字也是一种解法。
- 对规则集进行全扩充,
- 实现
1:I2:II3:III4:IV这样的全映射 - 这也不失为一种解法
- 但是硬编码这一解法:
- 没有让代码量变得少
- 没有优化时间/空间复杂度
- 没有让后期维护更轻松
- 不会吧,不会真的有人做硬编码吧。。。【手动狗头】
package Order300; |
#13 罗马数字转整数
- 第一步 宏替换
IV转为IIII - 第二步 翻译 即 OK
- 函数 +
switch并不是很高效,仅仅是写着方便。4ms => 100%失去了优化的兴致- 毕竟这个题目,复杂度实在有限
package Order300; |
#14 最长公共前缀
- 题目不难,但是解法很多
- 横向比较 —— 字符串两两相比
- 纵向扫描 —— 固定索引同步扫描全部字符串,相同则索引前进,不同则返回结果
- 归并 —— 两两归并着“横向比较”
- 二分查找 —— 反正答案最大是
len(minist(str))最小是0,对于每个可能的answer切分每个字符串并比较- 说它炫技吧,也没多炫
- 说它还行吧,复杂度还比别的方法高
- 给二分法一个面子。勉强算是一种解法。
- 各算法代码及解释详见 官方题解
- 我懒得写。直接上官方代码。
package Order300; |
#15 三数之和
a+b+c=0=>a = -b-c=> 两值转一值 —— 双指针 —— 意义就是:O(n2) => O(n)答案中不可以包含重复的三元组 —— 则 排序 + 遍历时跳过相同对象
重点:
ci的值不应该在b-for中被重置- 否则 b、c 双指针将毫无意义。
- 优化后
23~25ms => 80%~57.49%
遍历时跳过相同对象,值得一品
for (int ai = 0; ai < iMax; ai++) {
/* a 跳过重复对象 */
if (ai > 0 && nums[ai - 1] == nums[ai]) {
continue;
}
}
for (int bi = ai + 1; bi < ci; bi++) {
/* b 跳过重复对象 */
if (bi > ai + 1 && nums[bi - 1] == nums[bi]) {
continue;
}
}
while (bi < ci && nums[bi] + nums[ci] > target) {
/* c 跳过重复对象 这毫无实用价值。只会让时间复杂度变高 */
do {
ci--;
} while (bi < ci && nums[ci] == nums[ci + 1]);
}跳过重复对象的意义在于,用简单的条件判断运算,代替了复杂的求解运算
因为,
c本身并没有求解运算,仅有ci--对于是否跳过重复
c的判断,计算量大大超过了单纯的ci--因此,
c无需跳过重复对象。
package Order300; |
#16 最接近的三数之和
这题我都能从
12:48写到14:45这是我实在是没想到的。双指针 左右指针相向而行 的情况,以后还是用
while(left<right)来处理,写起来更方便关于跳过重复对象,学到了一种更明白的写法,同时方便配合
while(left<right)使用。// 右指针向左 跳过重复项
int tmp_ci = ci - 1;
while (bi < tmp_ci && nums[ci] == nums[tmp_ci]) {
tmp_ci--;
}
ci = tmp_ci;
// 左指针向右 跳过重复项
int tmp_bi = bi + 1;
while (tmp_bi < ci && nums[bi] == nums[tmp_bi]) {
tmp_bi++;
}
bi = tmp_bi;
package Order300; |
#17 电话号码的字母组合
- 从前一直分不清
DFS/BFS和回溯 - 这题,一开始也是想,不就是对一颗树做全遍历么,
DFS/BFS五分钟搞定 - 后来才想明白:
- 如果需要全排列,那么首先考虑回溯算法
- 回溯 首先是
DFS/BFS的必要手段,其次是方便保存从起点至今的路径
DFS/BFS是保证访问过每个节点- 而对于
DFS/BFS而言,- 如果想要保存从起点至今的路径
- 那么节点上除了保存
节点的值节点是否被访问 - 还需要额外保存 从起点至此节点的路径
- 节点变复杂了,
Stack/Queue里存放的元素也需要变得复杂 - 写起来就没那么方便了。
- 回溯函数——仍需多练:回溯函数的编写,远不及
DFS/BFS的迭代实现来得熟练,还需多加练习。
package Order300; |
#18 四数之和
all distinct [a b c d] in [nums...] where a+b+c+d=target第一种解法 时间O(n3) 空间O(n)
a-forb-for常规嵌套循环while (c < d)双指针实现
第二种解法 时间O(n2) 空间O(n2)
a-forb-for常规嵌套循环 保存组合值[a+b]c-ford-for常规嵌套循环 保存组合值[c+d][a+b]for[c+d]for常规嵌套循环,检查a+b+c+d ?= target- 省去这个写法的代码实现,思路懂就好了,不想写这个实现。
关键 第一种解法 时间O(n3) 空间O(n) 中的
a-forb-for常规嵌套循环可以优化for (int ai = 0; ai < iMax - 3; ai++) {
if (ai > 0 && nums[ai - 1] == nums[ai]) {
continue;
}
/* 若 ai,最小的解 ai,ai+1,ai+2,ai+3 已经大于 target 则 ai 已经太大了,没救了 break ai-for */
if (
nums[ai] + nums[ai + 1] + nums[ai + 2] + nums[ai + 3] > target
) break;
if (
nums[ai] + nums[iMax - 3] + nums[iMax - 2] + nums[iMax - 1] < target
) continue;
/* 若 ai,最大的解 ai,iMax-3,iMax-2,iMax-1 已经大于 target 则 ai 还太小,还得再大一点 continue ai-for */
for (int bi = ai + 1; bi < iMax - 2; bi++) {
if (bi > ai + 1 && nums[bi - 1] == nums[bi]) {
continue;
}
... ...ai跳过重复ai最大值为iMax-3ai太小则跳往下一个aiai太大则跳出a-forai最大值为iMax-2
package Order300; |
#19 删除链表的倒数第N个节点
- 方法一:两次扫描 —— 第一次获取总长度 —— 第二次获取倒数第N个节点
- 方法二:倒数 —— 联想到 栈 —— 第一次遍历,节点逐个入栈 —— 然后
popN次 - 方法三:双指针 —— 快慢指针(感觉叫抢跑指针更合适哈哈哈)
- 快指针
抢跑N位,然后快慢指针同速前进 - 快指针到终点时,慢指针在倒数第N个
- 快指针
- 方法二 栈,缺点是空间复杂度达到了
O(n)—— 方法三 快慢指针 空间复杂度O(1) - 哨兵节点 以
dummy为名(直译为 假人) - 哨兵节点指向头节点。即使头节点被删,也不影响返回
dummy.next
/* class ListNode { |
#20 有效的括号
- 括号匹配 —— 对称性匹配 —— 栈
- 图方便用函数代替了
HashSet - 还OK的啦~
2ms => 77.11% stack.pop()前要记得确保栈非空- 判断奇偶:
x & 1和x % 2是等价的
package Order300; |