不出意外的话,这应该是我的第一篇博客。
今天下午上课,听的东西完全不进脑子,状态奇差(不过好像很多课我都这样),于是打开几个月没碰的leetcode找找状态,顺便复习一下以前做过的题。没想到才做leetcode 1就发现事情不对劲。闲话少说,先上题。
给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两
个整数,并返回他们的数组下标。你可以假设每个输入只对应一种答案,但是你不能
重复利用这个数组中同样的元素。
实例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回[0, 1]
题目的意思很清晰了,不做解释。而这道题在leetcode上难度是easy。看到这种题,只要当时不是没睡醒,基本上都是能两下子就想到解法的。既然这样,那就要想办法优化算法,尽量降低时间复杂度和空间复杂度,尤其是时间复杂度。而我一拿到这个题的时候,脑子里马上就冒出来暴力搜索。
暴力搜索,外层循环遍历一遍数组,内层循环也遍历一遍数组。这样的时间复杂度是O(n^2)。显然,这是不能让人满意的。
将数组排序,然后一层循环遍历一遍数组,对于数组中的每个元素it,在数组中二分查找target - it。排序的时间复杂度是O(nlgn),二分查找n次的时间复杂度也是O(nlgn),因此整个算法的时间复杂度是O(n*lgn)。
算到这里,其实我心里是有种预感,还有更简单的算法的。以leetcode一贯的套路来看,数组的easy题,往往最优解法是不容易想到的,说不定有O(n)的解法。但是当时我状态的确不好,所以想了一会儿以后就作罢了,去海外官网的讨论区一查,果然有。
我是看完了这个解法,再回来想一下这个解法是怎么想到的,尝试重现一下当时他们想到这个解法的心路历程。举例来说,现在有一个测试用例,target == 5,然后我们想要找到值为2的元素,有没有元素与其配对,也就是说,有没有值为3的元素。在上面两个解法中,暴力搜索的查找的时间复杂度是O(n),而排序后二分查找的时间复杂度是O(lgn)。而无论什么解法,遍历一遍数组是免不了的。所以如果想要把时间复杂度控制在O(n),那么对于每一个元素来说,想要判断它有没有配对的元素,并且定位这个配对元素的位置,花的时间不能超过O(1)。而搜索的时间复杂度为O(n)的数据结构有什么,没错,就是哈希表。
想明白这点,也就不难想到解法了。我们遍历一遍数组,对于每个元素it来说,我们去找哈希表中有没有target - it这个键,有的话,返回it的下标和哈希表中target - it的值,也就是它的下标;否则,把it作为键,it的下标作为值,存入哈希表中。最后在O(n)时间内总能找到两个和为target的数。
到这里,不知道有没有人会怀疑,这个算法的时间复杂度到底是不是O(n),因为在哈希表中,想找到一个key,时间复杂度真的是O(1)吗?没错,这就是这篇博客的重点,可以说,以下才是正文。我在看到这个解法的时候,小小的眼睛里闪烁着大大的疑惑:在哈希表中找key不也是要遍历的吗?为什么时间复杂度不是O(n)而是O(1)?于是我去请教舍友,并在他终于受不了,“你数据结构白学了”的时候去百度(其实翻去年的数据结构教材效果也是一样的),回想起了哈希映射这个概念。
什么是哈希映射?具体的概念不解释了(因为我也懒得翻书),意思就是说,假如现在有一个哈希表,有5个格子,编号分别为0,1,2,3,4,然后我们想把key == 11放进去,那么放到哪个格子呢?有一种方法是,11除以5的余数是1,那么就放到编号为1的格子里去,那么这个时候的哈希映射(映射函数)就是h(x) = x % 5。
有了映射函数,接下来看一下查找某个key的时间复杂度。假设我们要查找key == 11,我们会怎么做呢?遍历一遍哈希表?不不不,这也太low了,完全没有体现出哈希表的优越性。我们根据哈希映射h(x) = x % 5,把x == 11代进去,一下子就知道了在1号格子里,自然时间复杂度也就是O(1)。
至于哈希冲突,举个例子,在把key == 11放到1号格子之后,来了个key == 16,按照哈希映射应该放到1号格子,但是这里已经有key == 11了,怎么办?这个有机会接触到再好好说道说道吧,今天有点累,不说了。
AC代码的话,其实在说了这个解法之后,就没有太大意义了,放一个官网讨论区的吧。
class Solution
{
public:
vector twoSum(vector &numbers, int target)
{
//Key is the number and value is its index in the vector.
unordered_map hash;
vector result;
for (int i = 0; i < numbers.size(); i++)
{
int numberToFind = target - numbers[i];
//if numberToFind is found in map, return them
if (hash.find(numberToFind) != hash.end())
{
//+1 because indices are NOT zero based
result = { hash[numberToFind], i };
return result;
}
//number was not found. Put it in the map.
hash[numbers[i]] = i;
}
return result;
}
};
写到这里,大概就完事了吧。今天是这几个月第一次回去写写题,没想到就发现自己这么大一个知识盲区。之前我总说自己,对某些很底层很基础的东西是很欠缺的,很多人还不信,现在,你们信了吧。
小狗过河
2019.10.18