0.题目分析
这道题目要求给定一个整数数组 nums
和一个目标值 target
,在该数组中找出和为目标值的 两个 整数。
可以假设每种输入只会对应一个答案。但是,不能重复利用这个数组中同样的元素。
比如:
看到题目,首先想到的是暴力搜索法。
1.暴力解法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
vector<int>twoInt(2);
for (int i = 0; i < len; i++)
for (int j = i+1; j < len; j++)
{
if (nums[i] + nums[j] == target)
{
twoInt[0]=i;
twoInt[1]=j;
}
}
return twoInt;
}
};
此种解法有两层循环,循环里的操作有一个判断和一个赋值,都是常数复杂度,所以整个解法复杂度是O(n^2),比较复杂。查看结果也可以看到速度慢:
2.哈希表
2.1.哈希表的原理
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
总结起来就是:记录的存储位置=f(关键字)
哈希表就是把key通过哈希函数转换成一个整型的数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下
标,将value存储在以该数字为下标的数组空间里,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同输出。
什么意思呢,简单来说,举个例子,假如数组的下标是经过取余过的,比如一个数组的长度为10,哈希表把key1通过哈希函数转换为一个整型数字15,15对10取余为5,5当作数组的下标,而假如哈希表把key2通过哈希函数转换为一个整型数字25,25对10取余也为5,很明显这里key1≠key2,但f(key1)=f(key2),这就是哈希冲突。假如把上面说的数组长度设为1,可以避免哈希冲突,但是空间会增大。所以哈希冲突实际无法避免,但可以减少哈希冲突。
使用哈希表查询的时候,就是再次使用哈希函数将Key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
2.2哈希表的实现
哈希表有不同的实现方法,下面讲一下拉链法,可以理解为“链表的数组”。
左边是一个数组,数组的标号就是由上面我们提到过的f(key)算出来的,也就是记录的存储位置。数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也根据这些特征,找到正确的链表,再从链表中找出这个元素。
2.3哈希表的应用
有了以上的基础知识后,很显然我们可以用哈希表进行查找,我上一种解法是所谓的“暴力解法”:遍历集合中的所有元素,看是否是需要的,需要的话就拿出来,不需要就继续查找。而哈希表是完全另外一种思路:当我知道Key值后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次次地查找。
所以对于LeetCode利用哈希表的思路重写两数之和的代码,主要思路就是找到一种数据内容和数据存放地址之间的映射关系:
如图所示,我们把nums[]数组里的值,当成哈希表的keys值,比如nums=[2,7,11,15] ,那么hash[2]=0,hash[11]=2。
另外需要说明的是:
(1)C++里map映射类型可以用count函数的意思是keys值出现的次数,可以来辅助判断是否里面有此keys。比如map<int, int> hash; hash.count(2)=1,hash.count(7)=1,但hash.count(8)=0。
(2)hash函数映射的时候,若映射没有出现过的keys值,那么映射的结果为0。比如hash[3]=0。
基于上面两个性质,我们此题可以分别写两种判断语句:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
vector<int> twoInt;
map<int, int> hash;
for (int i = 0; i < len; i++)
hash[nums[i]] = i;
for (int i = 0; i < len; i++)
{
int a = target - nums[i];
if (hash[a]&&hash[a]!=i) //或者(if(hash.count(a)&&hash[a]!=i)
{
twoInt.push_back(i);
twoInt.push_back(hash[a]);
break;
}
}
return twoInt;
}
};
此种解法有两个循环,每个循环时间复杂度都为O(n),整体时间复杂度为O(n)。此种解法的思想在于,还是举上面的例子,即有数组nums可以通过0,1,2,3找到对应的数字2,7,11,15,又有Hash可以通过2,7,11,15可以找到0,1,2,3,所以在遍历的时候可以得到 a=target-nums[i]的值,假如在2,7,11,15中,即可通过hash表把a对应的数找出来。
需要说明的是这种解法存哈希表的时候是从前往后,后面的覆盖前面的,判断的时候也是从前往后,由前面的数找到对应后面的数为key值的数 所以不会哈希冲突。
对应用时:
2.4一遍哈希表
上面的解法我们用了两遍遍历,一遍是给哈希表赋值,另一遍是寻找哪两数之和是target,那么我们能否把这两个过程统一在一个遍历里呢?答案是可以的,但此时的判断语句不能写hash[a],因为两遍hash表的时候,寻找两数的时候从i的0开始找后面的数,不会找第0个数,但是这种办法可能找到第0个数,比如nums={3,3},此时第一次hash映射的时候hash[3]=0,若用此种判断语句,则冲突,代码如下:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
vector<int> twoInt;
map<int, int> hash;
for (int i = 0; i < len; i++)
{
int a = target - nums[i];
if (hash.count(a) && hash[a] != i){
twoInt.push_back(i);
twoInt.push_back(hash[a]);
}
hash[nums[i]] = i;
}
return twoInt;
}
};
时间复杂度也为O(n),速度差别不太大。