前言
- 文章作为日记或心得,记录学习过程
- 本文记录本题(源自LeetCode)遇到的所有问题、疑惑
- 如对内容有任何建议或看法,欢迎评论区学习交流
正文
题目
第一次解答:暴力法
目前首先想到的仍然是暴力法
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
vector<int> ans;
for (size_t i = 0; i < nums.size(); i++)
{
for (size_t j = i + 1; j < nums.size(); j++)
{
if (nums[i] + nums[j] == target)
{
ans.push_back(i);
ans.push_back(j);
return ans;
}
}
}
return ans;
}
};
结果
由于时间复杂度为O(n2),所以运行时间很长。空间复杂度为O(1)。
哈希表法可将时间复杂度降为O(n),不过空间复杂度也提升到了O(n)
总结
暴力法简单直观;哈希表之前没有接触过,而官方答案中存在该解法,所以简单地了解了一下。
关于哈希表的学习
基于《大话数据结构》
定义
暴力查找法需要通过对a[i]与key值的比较进行查找,而散列技术在记录的存储位置和它的关键字之间建立了一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。这个f则为哈希函数(散列函数)。采用散列技术将记录储存在一块连续的储存空间中,这块连续的储存空间即哈希表(散列表)。
散列过程
在储存时,通过散列函数计算记录的散列地址,并将记录储存在该地址中;
在查找时,通过散列函数计算记录的散列地址,并按此地址访问该记录。
- 散列技术既是储存方法也是查找方法。
- 散列的记录中间不存在逻辑关系。
- 散列技术最适合求解的问题是查找与给定值相等的记录。
- 对散列函数的要求:简单、均匀、存储利用率高。
- 散列函数会存在冲突(f(key1)=f(key2)),如何处理冲突成为重要课题。
散列函数的构造方法
- 直接定址法:简单、无冲突,不常用
- 数字分析法:抽取关键字一部分来计算散列储存位置。事先知道关键字分布且其若干位分布均匀,常用
- 平方取中法:适用于不知道关键字分布,位数不大的情况
- 折叠法:把数分成两部分叠加求和,取后几位。事先不需要知道关键字分布,适合位数较大的情况。
- 除留余数法:f(key) = key mod p (p<=m)(最常用的方法)
其中 mod是取模(求余数)的意思。
为了避免冲突,若散列表表长为m,通常p为<=m(接近m)的最小质数或不包含小于20质因子的合数。 - 随机数法
处理散列冲突的方法
- 开放地址法:发生冲突时寻找下一个空的散列地址
fi(key)=(f(key) + di) MOD m (di=1,2,3……,m-1) 线性探测法
fi(key)=(f(key) + di) MOD m (di=12,-12,22,-22……,q2,-q2(q<=m/2)) 二次探测法,目的是不让关键字聚集在某一块区域
fi(key)=(f(key) + di) MOD m (di是随机数列) 随机探测法 - 再散列函数法:冲突时换一个散列函数
- 链地址法:冲突化为单链表中的增加节点
- 公共溢出区法:冲突数据放到溢出表
第二次解答:两遍哈希表
在c++中,引入头文件#include < unordered_map >后可使用unordered_map,其内部包含哈希表,因此不用自己再去建立,直接使用即可,适合用于查找
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
vector<int> ans;
//unordered_map<key, value>,第一个为键值,第二个为映射值
unordered_map<int, int> myhash;
//第一次哈希表:初始化myhash,键值为要查找的值,映射值则为数组下标
for (size_t i = 0; i < nums.size(); i++)
{
myhash[nums[i]] = i;
}
//第二次哈希表:查找相应的数字对应的下标
for (size_t i = 0; i < nums.size(); i++)
{
//第一个数为nums[i],下面需要查找nums中是否存在target-nums[i]
//则,键值即为target-nums[i],需要返回其下标(即映射值)
//如果这个下标存在,并且这个下表的值不是又一个i
if (myhash[target-nums[i]] && myhash[target-nums[i]] != i)
{
ans.push_back(i);
ans.push_back(myhash[target - nums[i]]);
return ans;
}
}
return ans;
}
};
结果
可见,这是牺牲空间换取时间的方法。速度得到了大幅度提升
第三次解答:一遍哈希表
看了很长时间不同的哈希表的解法,有些解法对我这种初学者不太友好,里面貌似用到了很多其他我没接触过的东西。只找到一种思路还算比较清楚的我可以理解的方法。
一遍哈希表的过程是一个边找边存的过程,在一次遍历的过程中,此时的值为nums[i],那么在之前存储的哈希表中是否有target-nums[i]呢?如果有,那么两个值就有了,分别是之前的target-nums[i]对应的下标(因为是哈希表中已有的,所以其下角标较小)和目前的i值。如果在之前存储的哈希表中没有target-nums[i],就将此时的nums[i]存到哈希表中。
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map<int, int> myhash;
vector<int> ans;
for (size_t i = 0; i < nums.size(); i++)
{
int last = target - nums[i];
//是否存在于之前存储到哈希表中的关键字,若存在在,则返回,否则在哈希表中存储nums[i]
if (myhash.find(last) != myhash.end())//此处的判断语句很陌生,需要学习。非如此不可吗?
{
ans.push_back(myhash[last]);
ans.push_back(i);
return ans;
}
myhash[nums[i]] = i;
}
return ans;
}
};
结果
可见一遍哈希表的效率是最高的
遗留问题
if (myhash.find(last) != myhash.end())
查找之前存储的结果是否存在,非如此不可吗