说到哈希,总感觉到它很高深,说到底它只不过是实现集合和字典(满足数据增删改查的结构)的一种方式,而不是总把它当成单独的一种数据结构或者算法去讨论。
散列法:
- 定义:将数据中key传给一个哈希函数f(),按照计算出来的哈希值h=f(key)将那个带有key的数据分布到一个哈希表中(大小m自定义)。
- 当m<数据量n的时候,会出现碰撞(即哈希值为同一个,意味着哈希值相同的数据要储存在哈希表的同一个哈希地址里),甚至大于等于的时候也会出现(取决于哈希函数),所以此时需要解决碰撞的机制。
- 解决碰撞1开散列:每个哈希单元格都存储着一个链表,所有对应哈希值的数据都存储在这个链表里,所以碰撞后,直接将相同哈希值的数据依次往链表里push,所以可想而知最坏情况就是所有键的哈希值都是一样,这样的话所有数据都在一个哈希地址的单元格链表里。这样的话增删改查都是遍历对应那个哈希值的链表即可。
- 解决碰撞2闭散列:当碰撞时,对应哈希值的单元格已经存储数据,这时我们就线性探测(往这个单元格后面依次遍历,只要找到一个空的就将元素插在里面),这种方法比前一种更有可能出现最坏情况:会导致合并的哈希单元格越来越多,到最后可能插入一个元素就需要O(n),因为可能需要遍历很多单元格才能找到空的。往哈希表里添加是这样,查找也是如此,依次往后找(但是如果哈希合并了就很麻烦了),删除更加麻烦,因为删除了以后查找某个键,就不会往后找,直接就不存在了,所以我们就需要在删除时添加个占位符,表示曾经有过数据,但是被删除了。而且闭散列有的时候防止最坏情况的发生,需要辅助双散列或者重散列。
个人还是选择开散吧。
散列法对数据的操作为O(1)~O(n)(最坏情况),假如选择适当的哈希函数和哈希表的大小基本上为O(1),实现集合/字典降低了操作的时间复杂度。
与另一种实现字典的数据结构平衡二叉搜索树相比:
- 时间复杂度的不同:哈希平均为O(1),最坏为O(n),而平衡二叉搜索树最好最坏都是O(logn)
- 有序性保留:哈希不保留key有序性,而后者保留,所以散列表不适合按序遍历和按范围查询的应用。
例题:
- LeetCode 1.两数之和
首先暴力枚举:O(n^2),排个序双指针遍历O(nlogn),关键效率不高的原因时,我需要在遍历给定数组的时候,比如遍历到第i个,知道可以和nums[i]一起组成target的那个数x,是否在我已经遍历到那些数字当中(肯定不会在没遍历到的数字当中),所以我们需要一个数据结构去存储刚才遍历到的那些数字,并且可以数据结构是通过键(这些数字)值来增删改查的,因此我们需要一个集合这样的数据结构,而这道题明显采用哈希表会更优。
#define MP make_pair
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> hash_table;
vector<int> ans;
for(int i = 0;i < nums.size();++i){
if(hash_table.count(target - nums[i])){
return {i,hash_table[target - nums[i]]};
}
else
hash_table.insert(MP(nums[i],i));
}
return ans;
}
};
- LeetCode 217.存在重复元素
首先暴力枚举:O(n^2),排个序遍历看是否和前一个元素相同O(nlogn),关键在于遍历到第i个数时,是否知道前面出现过这个数,一旦出现就说明有重复的,所以需要实现一个集合,很明显以哈希O(1)实现的在这里更快。而且此题也可以换个思路,那就是把所有nums的元素都push到那个哈希集合里,因为在cpp中unordered_set默认只能插入不存在的元素,所以到最后比下哈希集合和nums数组的大小就知道是否重复了。
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
if(nums.empty()) return false;
unordered_set<int> hash_table;
for(int num:nums){
hash_table.insert(num);
}
return hash_table.size() < nums.size();
}
};
- LeetCode 594.最长和谐子序列
首先要明确一点子序列是可以不连续的,而子串是要连续的,所以这道题我们可以不用考虑滑动窗口等方法。
这道题的关键点:如何知道某个数,和它相同的数出现了多少次,比它大1的数出现了多少次(假如在所有数字都会遍历到的情况下,我们只需要知道比它大1或者小1的即可),所以我们需要实现了以数值为键,次数为值的字典来实现查询,很明显这里用哈希实现效率会比红黑树高。
class Solution {
public:
int findLHS(vector<int>& nums) {
unordered_map<int,int> hash_table;
for(int num:nums){
hash_table[num]++;
}
int longest = 0;
for(pair<int,int> t:hash_table){
if(hash_table.count(t.first + 1)){
longest = max(longest,t.second + hash_table[t.first + 1]);
}
}
return longest;
}
};