目录
哈希表基础知识
定义
哈希表又叫散列表(hash table),是根据键(key)而直接访问在内存中存储的数据的一种数据结构。它通过构建一个关于键值的函数,将数据映射到表中的一个位置来访问数据,这个映射函数叫散列函数,这个表格叫做散列表。
特点
- 数据处理速度快;
- 能够快速的进行数据的删除、修改、查找元素;
- 哈希表中的元素没有顺序;
- 哈希表中的元素不会重复;
哈希表的实现有两种
- 数组+链表;
- 数组+二叉树;
哈希表实现的底层用到了数组,在处理哈希冲突的时候用到了链表或者二叉树,所以说哈希表本质上是数组。哈希表可以暂时认为就是散列表,哈希函数就是散列函数,键值对就是entry。
为什么出现哈希表
数组在用下标值获取元素时的速度时非常快的,但是在不知道下标值而进行遍历的时候,当数据量非常大的时候,索引的速度就会非常慢,所以在数组的基础之上设计了哈希表。
哈希冲突
哈希冲突是存储数据时根据哈希函数规则发现位置上已经有了数据的情况,解决哈希冲突的方法主要为开放寻址法、拉链法、再哈希法和建立公共溢出区。
开放寻址法是在位置被占了之后继续向后寻找新的空余的位置;
拉链法是使用链表的结构在原先的位置存储新的数;
拉链法
把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0…m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值为空指针。
结构如图:
特点:
散列表T中存放的是存储数据的链表头节点,是一个链表数组;
存储和访问一个数据时,通过散列函数计算出头节点的位置,根据位置存储到该链表的的末尾或者查找数据;
不能无限制的在一个链表中加入数据,超过一定量时会影响存取的速度,引入散列表扩充,引入最大容量max_size;超过这个值之后对哈希表进行扩容。
开放寻址法
开放寻址法的核心思想是当散列出现冲突的时候,重新寻找一个新的空的位置,将数据插入。寻址的方法有线性探查,二次探查,双重散列。
线性探查:当散列出现冲突时,就顺序的向后查找,一直找到空的位置或者找不到空位置为止。
h(k,i) = (h'(k,i) + i) mod m, i = 0,1,..m-1;
线性探査方法比较容易实现,但它存在着一个问题,称为一次群集。随着连续被占用的槽不断增加,平均査找时间也随之不断增加。群集现象很容易出现,这是因为当个空槽前有i个满的槽时,该空槽为下一个将被占用的概率是(i+1)/m 。连续被占用的槽就会变得越来越长,因而平均查找时间也会越来越大。
二次探查:使用辅助散列函数,后续探查时相比于前面的探查加一个偏移量,偏移量为探查号i的二次方。
h(k,i) = (h'(k,i) + c1*i + c2* i^2) mod m, i = 0,1,..m-1;
双重散列
使用两个辅助散列函数h1和h2;初始的散列位置为h1(k),之后的散列位置在此基础上增加h2(k)*i mod m。是开放寻址最好的办法之一。
h(k,i) = (h1(k) + i*h2(k)) mod m, i = 0,1,..m-1;
开放寻址法的散列表中删除操作元素比较困难。当我们从槽i 中删除关键字时,不能仅将N o n e置于其中来标识它为空。如果这样做,就会有问题:在插入关键字k 时,发现槽i被占用了,则就被插人到后面的位置上;此时将槽i 中的关键字删除后,就无法检索到关键字k 了。有一个解决办法,就是在槽i 中置一个特定的值DELETED替代None来标记该槽。这样就将这样的一个槽当做空槽,使得在此仍然可以插入新的关键字。
再哈希法:
再哈希法也叫再散列法,是指当发生冲突时,把关键字用不同的哈希函数再做一遍哈希,用这个结果作为步长,对指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。经验说明,第二个哈希函数必须具备以下特点:
- 和第一个哈希函数不同
- 不能输出0.
哈希表的扩容
当哈希冲突的概率达到一定的数值之后,对原有的哈希表进行扩容,哈希冲突出现的概率叫做:增长因子或负载因子,定义为被占用的位置与总的位置的一个百分比;一般情况下,默认的负载因子值不能太大,因为其虽然减少了空间开销,但是增加了查询的时间成本;也不能太小,因为这样还会增加rehash的次数,性能较低。
具体操作为将原先的数组扩大为原先的两倍,将原先的数据通过新的哈希函数计算出来的位置进行存放。
哈希表在python中的应用为字典和集合;
哈希表在c++中的应用为unordered_map 和 unordered_set
unordered_map 和 map 辨析
映射是指两个集合之间的元素的相互对应关系。通俗地说,就是一个元素对应另外一个元素。比如一个姓名的集合 {“Tom”, “Jone”, “Marry”},班级集合{1, 2}。姓名与班级之间可以有如下的映射关系: class(“Tom”) = 1 , class(“Jone”) = 2 , class(“Marry”) = 1
我们称其中的姓名集合为 关键字集合(key) , 班级集合为值集合(value) 。 在 C++ 中我们常用的映射是 map。
map内部实现了一个红黑树,红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。
map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map内部实现了一个哈希表(通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。
map优缺点:
- 优点:有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作,内部实现一个红黑书使得map的很多操作在lg(n)的时间复杂度下就可以实现,因此效率非常的高;
- 缺点:空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间;
- 适用处:对于那些有顺序要求的问题,用map会更高效一些。
unordered_map优缺点:
- 优点:因为内部实现了哈希表,其查找速度非常快;
- 缺点:哈希表的建立比较耗费时间;
- 适用处:对于查找问题,unordered_map会更加高效一些;
面试高频leetcode题目
242 有效的字母异位词
242. 有效的字母异位词 - 力扣(LeetCode) (leetcode-cn.com)https://leetcode-cn.com/problems/valid-anagram/
方法1:排序
对字符串 s 和 t 分别排序,看排序后的字符串是否相等。此外,如果 s 和 t 的长度不同,t必然不是 s的异位词。
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size()){
return false;
}
sort(s.begin(),s.end());
sort(t.begin(),t.end());
return s==t;
}
};
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if (len(s) != len(t)):return False
s,t = list(s),list(t)
s.sort()
t.sort()
return s == t
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if (len(s) != len(t)):return False
s = sorted(s)
t = sorted(t)
return s == t
注:sort()函数和sorted()函数辨析
sort()函数时list的内置函数,使用之前确保obj是list格式,sorted()是python内置函数;
sort()函数是原地排序,sorted()函数是生成新的排序列表;
方法2:哈希表
使用字典,将字母作为key,字母出现的次数作为value,遍历s,t,最后计算dict是否相等。
# python
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if (len(s) != len(t)):return False
dict1,dict2 = {}, {}
for item in s:
dict1[item] = dict1.get(item,0) + 1
for item in t:
dict2[item] = dict2.get(item,0) + 1
return dict1 == dict2
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
vector<int> table(26, 0);
for (auto& ch: s) {
table[ch - 'a']++;
}
for (auto& ch: t) {
table[ch - 'a']--;
if (table[ch - 'a'] < 0) {
return false;
}
}
return true;
}
};
1 两数之和
1. 两数之和 - 力扣(LeetCode) (leetcode-cn.com)https://leetcode-cn.com/problems/two-sum/
哈希表算法。
遍历nums,若target-item 在哈希表中,输出索引,如果不在,将元素及索引保存在哈希表中。
# python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hastable = dict()
for index,item in enumerate(nums):
if target - item in hastable:
return [hastable[target-item],index]
hastable[item] = index
return []
//cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map;
for (int i = 0;i < nums.size();i++){
// find()函数返回一个iteator迭代器 it->first is key,it->ssecond is value
auto it = map.find(target-nums[i]);
if (it != map.end()){
return {it->second,i};
}
map.insert({nums[i],i});
// mp[nums[i]]=i;
}
return {};
}
};
注:unordered_map的基本用法(官方文档):
15 三数之和
方法:排序+双指针
首先排序,默认从大到小进行排序,对固定第一个数,遍历,将三数之和转化为两数之和的问题,固定完第一个数之后,定义左left、右right两个指针,左指针在固定数nums[i]的下一位置,右指针在最后一位,问题转化为nums[left] + nums[right] == -nums[i]的问题,若nums[left] + nums[right] > -nums[i];说明两数之和过大,右指针左移一位,反之,左指针右移一位;最后是去除重复的元素;预判,size < 3 或者 排序完之后 nums[0] > 0 都是不符合要求的。
//cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
int size = nums.size();
if (size < 3 || nums[0] > 0) return {};
vector<vector<int>> ans;
//固定第一个数,转化为两数之和
for (int i = 0;i < size;i++){
if (nums[i] > 0){return ans;}
//去重,如果此数已经选过,跳过;
if (i > 0 && nums[i] == nums[i-1]){
continue;
}
//定义左右指针;
int left = i + 1;
int right = size - 1;
while (left < right){
// 两数之和小于第三个数,左指针右移一位
if (nums[left] + nums[right] < -nums[i]){
left++;
}
//两数之和大于第三个数,右指针左移一位;
else if (nums[left] + nums[right] > -nums[i]){
right--;
}
else {
ans.push_back(vector<int>{nums[i],nums[left],nums[right]});
left++;
right--;
//去重,第二个数和第三个数也不重复选取
while (left < right && nums[left] == nums[left-1]) left++;
while (left < right && nums[right] == nums[right+1]) right--;
}
}
}
return ans;
}
};
# python
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
size = len(nums)
if (not nums or size < 3):
return []
nums.sort()
ans = []
for i in range(size):
if nums[i] > 0: return ans
if (i > 0 and nums[i] == nums[i-1]):
continue
left = i + 1
right = size - 1
while (left < right):
if (nums[left] + nums[right] > -nums[i]):
right -= 1
elif (nums[left] + nums[right] < -nums[i]):
left += 1
else:
ans.append([nums[i],nums[left],nums[right]])
while (left < right and nums[left] == nums[left+1]):
left += 1
while (left < right and nums[right] == nums[right-1]):
right -= 1
left += 1
right -= 1
return ans
致谢
(2条消息) 来吧!一文彻底搞定哈希表!_庆哥Java的CSDN技术博客-CSDN博客