哈希的基本思想
哈希表,也被称为散列表。每一个数据被映射到哈希表中 固定的位置,通过此能够缩短查找所需要的时间。把关键码值映射到表中唯一位置的映射函数被称为哈希函数或者说散列函数。
哈希函数同时具有压缩的功能。比如说现在给定一个关系表,属性是电话号码和姓名,将其进行存储。如果说直接按照电话号码作为存储地址进行存储的话,能够将插入,查询的时间复杂度编程o(1),但是会存在大量的空间浪费。所以引入散列表,散列表也是一个数组(也可以使用map用键值对来实现),但是 数组的下表就不再是电话号码了,而是经过哈希函数映射之后的输出值。例如说,就将手机号后四位的和作为散列函数的输出值,对应的手机号信息就存放在数组的这个位置。
哈希碰撞
既然散列函数的思想是从一个大空间映射到一个小空间,根据抽屉原理,可能会存在不同的值被映射到同一位置的情况。就比如说按照上面说的按照手机号后四位的和作为散列函数输出值的例子来看。手机尾号是1234和4321的都会映射到数组下标为10的那个空间之中。哈希碰撞是不可能避免的,不过可以通过一些方法来解决这个碰撞问题。具体的解决方法可以参照下面列出的blog。
使用哈希的例子
这里用leetcode的第一题‘两数之和’来进行举例。题目如下:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 的那 两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
首先这道题目的暴力求解思想就是通过两层的循环,首先假定一个数就是目标进行外层循环,内层循环再对于数组进行遍历查找是否有匹配的。暴力搜索的C代码如下:
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
int *r = NULL;
for(int i=0;i<numsSize;i++){
for(int j=i+1;j<numsSize;j++){
if(nums[i]+nums[j]==target){
r = (int *)malloc(sizeof(int)*2);
r[0] = i;
r[1] = j;
*returnSize = 2;
return r;
}
}
}
*returnSize = 0;
return r;
}
显然,采用这个思想的话时间复杂度是 o(n2)。
下面来看用Hash来解这道题。维护一个MAP作为哈希表,其key值是数组的某个值,其对应的value值则是该值在数组中的对应下标。
对数组进行遍历,i为当前遍历到的元素下标。首先判断target-array[i]作为key值的那个位置上有没有被用到过,如果有,则表明找到了要找的两个值。如果没有则将(array[i],i)存到MAP中并继续遍历。通过这个方法,最坏情况只需要遍历一遍数组,时间复杂度为 o(n)。并且,由于题目给出答案只有唯一的一个,因此不需要考虑哈希碰撞的问题。对应的代码如下:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i< nums.length; i++) {
if(map.containsKey(target - nums[i])) {
return new int[] {map.get(target-nums[i]),i};
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}