传送门:https://leetcode.com/problems/two-sum/#/description
一、题目描述
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
二、示例
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
三、问题描述
给定一个数字和一个目标数字,求出数组中两个数字之和等于给定的目标数字。
注意:假定每个数组中有且只有一组解,不能使用同一个数组元素两次
例如:数组[2,7,11,15], 目标数字9,因为nums[0] + nums[1] = 9,所以返回[0,1]
四、分析
1、暴力解法
遍历数组法,直接遍历整个数组,找到两个数组元素和等于target即可。
2、哈希思想
通过建立 <数值,下标>
的哈希表,每遍历到一个元素 i
,就查找所对应的元素 target-i
是否在哈希表中,并且 数值 i
和 数值 target - i
不能是同一个下标。
五、实现
1、暴力解法
==》时间复杂度:O(n2)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> res;
for(int i = 0; i < nums.size(); i++)
{
for(int j = i + 1; j < nums.size(); j++)
{
if((nums[i] + nums[j]) == target)
{
res.push_back(i);
res.push_back(j);
break;
}
}
if(!res.empty())
break;
}
return res;
}
};
提交结果
2、哈希法
==》时间复杂度:O(n)
==》空间复杂度:O(n)——哈希表
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i,sum;
vector<int> results;
map<int, int> hmap;
for(i = 0; i < nums.size(); i++){
hmap.insert(pair<int, int>(nums[i], i));
}
for(i = 0; i < nums.size(); i++){
if(hmap.count(target - nums[i]) && hmap[target - nums[i]] != i)
{
int n = hmap[target - nums[i]];
results.push_back(i);
results.push_back(n);
break;
}
}
return results;
}
};
注:使用count,返回的是被查找元素的个数。如果有,返回1;否则,返回0。注意,map中不存在相同元素,所以返回值只能是1或0。
提交结果
六、哈希算法
1、基本概念
哈希算法(也称散列算法)
将任意长度的输入(又叫做预映射, pre-image)映射为固定长度的输出的映射规则。
==》散列值的空间通常远小于输入的空间
哈希值(也称散列值)
通过对原始数据映射后得到的输出。
哈希表
以 键-值(key-value)
存储数据的结构,只需输入待查找的 key,就可以查找其对应的值。
负载因子
例如要存储80个元素,但可能为这80个元素申请了100个元素的空间。80/100=0.8,这个数字称为负载因子。我们之所以这样做,也是为了“高速存取”的目的。
冲突
两个元素通过 哈希函数 得到同样的结果。
2、哈希算法的设计要求
- 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法);
- 对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同;
- 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;
- 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。
==》无论哈希文本长度,都可得到长度相同的哈希值;
==》两个相似的文本,得到的哈希值完全不同。
3、散列函数设计原则
- 散列函数的设计不能太复杂。否则,消耗很多计算时间,间接影响散列表性能。
- 散列函数生成的值尽可能随机且均匀分布==》避免或最小化散列冲突,即便冲突,也比较平均。
散列函数设计方法:数据分析法(从原始数据中截取部分作为散列函数)、直接寻址法、平方取中法、折叠法、随机数法等
4、常见散列函数的构建方法
- 直接寻址法:取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)
- 数字分析法:分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
- 平方取中法:取keyword平方后的中间几位作为散列地址
- 折叠法:将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
- 随机数法:选择一随机函数,取keyword的随机值作为散列地址,通经常使用于keyword长度不同的场合。
- 除留余数法:取keyword被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅能够对keyword直接取模,也可在折叠、平方取中等运算之后取模。对p的选择非常重要,一般取素数或m,若p选的不好,easy产生同义词。
5、散列冲突
两类方法:开放寻址法(open addressing)和链表法(chaining)
(1)开放寻址法
基本思想: 若存在散列冲突,则探测一个空闲位置,将其插入。
A、探测方法
① 线性探测(Linear Probing)
从当前位置开始,依次往后(若不够则从头继续)查找,看是否有空闲位置,直到找到为止。
② 二次探测(Quadratic Probing)
区别: 步长变成了原来的“二次方“,即:探测的下标序列
hash(key)+0,hash(key)+12,hash(key)+22,hash(key)+32,……
③ 双重散列(Double hashing)
使用一组散列函数 hash1(key),hash2(key),hash3(key)……先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找找到空闲的存储位置。
B、存在问题
当数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,最坏情况下的时间复杂度为 O(n)。同理,在删除和查找时,,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据。
C、装载因子(load factor)
为了尽可能保证散列表的操作效率,利用装载因子(load factor)来表示空闲槽位的多少。
散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
(2)链表法——更常用
所有散列值相同的元素我们都放到相同槽位对应的链表中。
插入:通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可
==》时间复杂度:O(1)
查找、删除:需要遍历,时间复杂度跟链表的长度 k 成正比,也就是 O(k)。
对于散列比较均匀的散列函数来说,理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。
6、应用
最常见的几种应用:安全加密、唯一标识、数据校验、散列函数、负载均衡、数据分片、分布式存储。