Log
- 【170405】恢复算法题训练首日,已经比较晚了,先完成了 Python 第一版代码。至于代码优化(例如缩短运行时)、其他语言下的写法(C?CPP?Java?…)则暂时搁置。具体如何制订个规则来保证自己会回头去重新研究,再议。最迟在 170409 结束前应给出一个初版。
- 【170406】重新改了一下报告的格式,以提交次数为三级标题,把「语言」放到报告中作为一个项目;然后把提交 01 的 Python 代码该成了 C++ 的写法(提交 02)
- 【170407】补充了一种优化思路对应的代码和结果。补充了参考答案对应的思路和结果。
题目
Two Sum
【题目类型备注】
O(n^2)优化, O(n), 哈希表, HashMap
提交
01
【语言】
Python
【时间】
170405
【思路】
〖复杂度?〗由于有且仅有 1 个答案,所以不可避免地要遍历数组中的所有二元数对,因此复杂度应该是 $O(n^2)$ 的
〖能否剪枝?〗能。
对于任意给定的二元数对 (a, b) 来说,和是唯一的。因此如果要写 2 个 for 循环嵌套,内层循环不需要再次遍历外层循环此前已经遍历过的数对。
> 例如对于 [1, 2, 3, 4, 5],若 i = 1 时 j = 3 已经遍历过了 (1, 3),那么当 i = 3 时就不必再遍历一次 j = 1
由于不会用到自身,所以每次遍历时,内层循环只要从 (i + 1) 开始即可
不过如果仅仅是这样剪枝,遍历次数的复杂度也仍然是 $O(n^2)$
〖什么时候停?〗因为有且仅有 1 个答案,在内层循环中找到即可停止
【代码】
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
lengthOfArray = len(nums)
answer = []
for i in range(lengthOfArray - 1):
for j in range(i + 1, lengthOfArray):
if (nums[i] + nums[j] == target):
answer = [i, j]
return answer
【结果】
运行时:492 ms
报告链接:https://leetcode.com/submissions/detail/99180059/
不知道其他拥有 LeetCode 帐号的人能不能看到该报告,所以顺便附图如下:
(虽然超过了 45% 的选手,不过看了下其他语言,蛇语就是慢啊……)
02
【语言】
C++
【时间】
170406
【思路】
同提交 01
【代码】
class Solution {
public:
vector twoSum(vector& nums, int target) {
vector indices = {};
int lenOfNums = nums.size();
for (int i=0; i < lenOfNums - 1; i++)
{
for (int j=i+1; j < lenOfNums; j++)
{
if (nums[i] + nums[j] == target)
{
indices.push_back(i);
indices.push_back(j);
return indices;
}
}
}
}
};
【结果】
运行时:146 ms
报告链接:https://leetcode.com/submissions/detail/99279559/
不知道其他拥有 LeetCode 帐号的人能不能看到该报告,所以顺便附图如下:
(虽然时间变成了大约是原来的 1/3,但看了一下我的水平,似乎在 CPP 提交者中成了渣渣…看来这题应该有更优化的算法,需要再思考下)
03
【语言】
C++
【时间】
1704006
【思路】
之前的 2 次提交(01,02)的复杂度都是 $O(n^2)$ 的。之前听说过一句话:
有经验的程序员看到复杂度是 $O(n^2)$ 时,本能地会试图去把程序优化成 $O(n logn)$
那么如何优化成 n logn 呢?考虑到快速排序的思想,因此我考虑了分治策略:把大问题的解,分解成若干小问题的解
如何分解为小问题的解?大问题要求的解是:
一对下标 [i, j],满足:
nums[i] + nums[j] == target
能够分解成若干组下标吗?沿着这个思路我想不通。但能不能分解成 2 个下标呢?就像快排一样:
- 我能不能以某个数为中轴(pivot)、假设中轴就是要找的数之一,向左去找看看是否有配对的数,向右去找找是否有配对的数,找到了就返回该数对应的下标
- 这样就分解成了 2 个小问题:从找 2 个数的大问题,变成了各找 1 个数的小问题
- 然后每次找这个数时,我可以每次都去找中轴,如果找到了就返回该数的坐标,否则左边找、右边找
- 再辅以一些特殊情况的处理(例如:本例中实现分治,需要递归,那么递归的终止条件就是那些特殊情况;以及,找到了数以后如何表示……)
于是就有了下面的这两段差不太多的代码
【代码】
class Solution {
public:
int helper(vector& arr, int target, int begin, int end) {
//if only 1 element
if (end - begin == 0)
{
if (target == arr[begin])
{
return begin;
}
else
{
return -1;
}
}
//if 2 elements
if (end - begin == 1)
{
if (target == arr[begin])
{
return begin;
}
else if (target == arr[end])
{
return end;
}
else
{
return -1;
}
}
//if 3+ elements
if (end - begin >= 2)
{
int mid = begin + (end - begin)/2;
int x = helper(arr, target, begin, mid-1);
int y = helper(arr, target, mid+1, end);
if (-1 != x)
{
return x;
}
else if (-1 != y)
{
return y;
}
else
{
return -1;
}
}
}
vector twoSum(vector& nums, int target) {
int lenOfNums = nums.size();
int x, y;
//if (2 == lenOfNums)
//{
// vector indices = {0, 1};
// return indices;
//}
for (int i=1; i indices = {0, i};
return indices;
}
}
for (int i=1; i indices = {x, i};
return indices;
}
else if (-1 != y)
{
vector indices = {i, y};
return indices;
}
else
{
continue;
}
}
}
};
class Solution {
public:
int helper(vector& arr, int target, int begin, int end) {
//if only 1 element
if (end - begin == 0)
{
if (target == arr[begin])
{
return begin;
}
else
{
return -1;
}
}
//if 2 elements
if (end - begin == 1)
{
if (target == arr[begin])
{
return begin;
}
else if (target == arr[end])
{
return end;
}
else
{
return -1;
}
}
//if 3+ elements
if (end - begin >= 2)
{
int mid = begin + (end - begin)/2;
if (target == arr[mid])
{
return mid;
}
int x = helper(arr, target, begin, mid-1);
int y = helper(arr, target, mid+1, end);
if (-1 != x)
{
return x;
}
else if (-1 != y)
{
return y;
}
else
{
return -1;
}
}
}
vector twoSum(vector& nums, int target) {
int lenOfNums = nums.size();
int x, y;
//if (2 == lenOfNums)
//{
// vector indices = {0, 1};
// return indices;
//}
for (int i=1; i indices = {0, i};
return indices;
}
}
for (int i=1; i indices = {x, i};
return indices;
}
else if (-1 != y)
{
vector indices = {i, y};
return indices;
}
else
{
continue;
}
}
}
};
【结果】
一个用了 382 ms,一个用了 492 ms
越改越慢。。。
04
【语言】
C++
【时间】
170407
【思路】
考虑提交 03 中,似乎在分治时没有在找到答案后就返回,而是在「左分治」完成后就算找到了答案(没有返回 -1)也把「右分治」跑一遍——然而这显然是不必要的:由于有且仅有 1 个答案,而且在进入分治策略时,已经假定了外层循环 i 对应的下标对应的数字是要找的其中一个,那么只要任一分治找到了答案,就没必要继续找另一边了。
于是我考虑优先考虑「左分治」:如果左边先找到了,就返回,不需要再计算「右分治」的结果。
【代码】
class Solution {
public:
int helper(vector& arr, int target, int begin, int end) {
//if only 1 element
if (end - begin == 0)
{
if (target == arr[begin])
{
return begin;
}
else
{
return -1;
}
}
//if 2 elements
if (end - begin == 1)
{
if (target == arr[begin])
{
return begin;
}
else if (target == arr[end])
{
return end;
}
else
{
return -1;
}
}
//if 3+ elements
if (end - begin >= 2)
{
int mid = begin + (end - begin)/2;
int x = helper(arr, target, begin, mid-1);
if (-1 != x)
{
return x;
}
else
{
int y = helper(arr, target, mid+1, end);
if (-1 != y)
{
return y;
}
else
{
return -1;
}
}
}
}
vector twoSum(vector& nums, int target) {
int lenOfNums = nums.size();
int x, y;
//if (2 == lenOfNums)
//{
// vector indices = {0, 1};
// return indices;
//}
for (int i=1; i indices = {0, i};
return indices;
}
}
for (int i=1; i indices = {x, i};
return indices;
}
else
{
y = helper(nums, target-nums[i], i+1, lenOfNums-1);
if (-1 != y)
{
vector indices = {i, y};
return indices;
}
}
}
}
};
【结果】
这次用了 265 ms,比昨天那个提交好点。但还是很头疼……我的算法设计出了什么大问题么,为什么我以为的更低的复杂度导致了更多的计算时间?
00
参考解法:
- Python
- C++
- Java
注意:其中的 C++ 和 Java 解法有个小瑕疵,导致答案和题意不符,不过仔细看一下就明白。瑕不掩瑜
参考答案的复杂度居然直接降到了 $O(n)$ !惊喜超过了懊恼,太棒了。所有解法都巧妙运用了 HashMap 这类的数据结构,太棒了!
自己实现一遍最优解:
- 。。。