建模回来了,杂七杂八的事情处理完竟然耗费了大概两周emmm。
2019.4.16更新一遍,重新看了一下以前写的,有些地方还是naive了一点,这次重新过一遍题目,在原先c++的基础上,加上python3和java语言的解法。
0.问题重述
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9。
所以返回 [0, 1]
1.题目分析
乍一看这个题目就是在一堆数中找到唯一的符合条件的两个数(此为题目为我们做好的简化)。一开始想到的办法是进行一次排序,然后暴力搜索出答案。这样算法的复杂度就是O(n^2)。Too Young too simple。这么暴力的算法自然是不能通过长者的法眼。运行下来的结果是Time Limit Exceeded。那么只能想出来一个O(n)复杂度的算法。
原算法是将数组遍历两次,加上排序的遍历就不止是两次。这样做虽然节省了空间,但是时间上增加了消耗。因此,想要复杂度更低的算法自然是需要使用一定的空间。思路是这样的,要得到一个复杂度为O(n)的算法,遍历最多也只能是1次。一次遍历只能找到一个数,那么就要将第二个数和第一个数之间先建立起联系。自然而然的就想到使用key-value的方式进行查询。
2.整体思路
anyway,思路就是:首先遍历一遍数组,建立每一个元素和下标的联系。你可能会问这功能数组也有,为啥要多于遍历一遍?
naive
当这一遍建立起元素与下标的关系后,之后的查找仅发生在map内部,而map内部的查找是常数级的查找。大大缩短了时间。在完成遍历后,第二次遍历数组,这次直接用target-当前值去找对应的值。需要注意的是,有可能是两个相同的数相加,所以要排除寻找的数是当前的数的可能性。
第二遍看到这里的时候心情复杂,当年戏可真多…
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> res;
int i;
map<int, int> m;
for (i = 0; i < nums.size(); i++) {
m[nums[i]] = i;
}
int left = 0;
for (i = 0; i < nums.size(); i++) {
left = target - nums[i];
if (m.count(left) && m[left] != i) {
res.push_back(i);
res.push_back(m[left]);
break;
}
}
return res;
}
};
最终要求返回的是元素的位置,而不是元素的值。所以push_back的时候注意不要搞错(第一遍submit的时候就错了)。
and finally,第一道leetcode成功的AC了。
3.优化代码
实际上,在上一个版本的代码中,我们的循环做了1-2遍,在这之中荡漾,呃,摆动。我们可以将两个循环合并成一个,每遇到一个数,先把它扔进map里,同时立刻进行一次判断,看有没有符合条件的值(因为再本题的描述中,是不存在多个解的)。如果没有,就在进行下一次的循环,如果有直接结束。
如此一来循环可以做的比一次更少。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//也可以不用新的vector,直接返回{}
int i;
map<int, int> m;
for (i = 0; i < nums.size(); i++) {
m[nums[i]] = i;
if (m[target - nums[i]] != i && m.count(target - nums[i])) {
return { i,m[target - nums[i]] };
}
}
return {};
}
};
然鹅我们会发现这样死活都通不过,但是把下面这句话调换一下位置后就AC了。
map<int, int> m;
换到循环后面,如下
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//也可以不用新的vector,直接返回{}
int i;
map<int, int> m;
for (i = 0; i < nums.size(); i++) {
if (m.count(target - nums[i])&&m[target-nums[i]]!=i) {
return { m[target - nums[i]] ,i};
}
m[nums[i]] = i;
}
return {};
}
};
debug之后明白了,因为map一一对应的特性决定了,不能存在两个key值相同的元素,所以再出现[3,3]这样的情况时,函数就布恩那个得到正确的结果。所以要先判断,再扔进map里。
4.python3解法
还是使用key-value的思想,只不过要熟悉一下接下来两种语言的数据结构。
对于python3而言,直接使用dict字典即可。字典类似于c++中的map,我们将在另外一篇文章中讨论c++、java、python三种语言中key-value数据结构的区别。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dict = {}
for i, v in enumerate(nums):
if target - v in nums_dict:
return [dict[target - v], i]
nums_dict[v] = i
return None
另外一定要注意缩进。
5.java解法
java解法写起来不是很顺利,对java的数据结构不熟悉,hashmap报了几个错。但思路和c++十分相似,只是数据结构内部的方法不同。我相关的hashmap的方法集放在几种数据结构的讨论中。
另外值得注意的是,在生成哈希表的时候,要么:
- 遍历两次,先生成一次,再判断一次;
- 遍历一次,但是要先判断,后加入到哈希表里去。否则很容易在重复的数上报错,比如以下:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<Integer, Integer>();
int num2 = 0;
for(int i=0;i<nums.length;i++){
num2 = target - nums[i];
if(map.containsKey(num2) && map.get(num2)!=i){
return new int[]{map.get(num2), i};
}
map.put(nums[i],i);
}
return null;
}
}
最后,关于不同语言key-value的数据结构的区别,我们放到另一篇文章中说明。