哈希表
基础知识
1.简介
哈希表:也叫散列表。是根据关键字和值(Key-Value)直接进行访问的数据结构。也就是说,它通过关键字key和一个映射函数Hash(key)计算出对应的值value,然后把键值对映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数(散列函数),用于存放记录的数组叫做哈希表(散列表)。
哈希表的关键思想是使用哈希函数,将键key和值value映射到对应表的某个区块中。可以将算法思想分为两个部分:
- 向哈希表中插入一个关键字:哈希函数决定该关键字的对应值应该存放到表中的哪个区块,并将对应值存放到该区块中。
- 向哈希表中搜索一个关键字:使用相同的哈希函数从哈希表中查找对应的区块,并在特定的区块搜索该关键字对应的值。
2.哈希函数
哈希函数:将哈希表中元素的关键键值映射为元素存储位置的函数。
哈希函数是哈希表中最重要的部分。一般来说,哈希函数会满足以下几个条件:
- 哈希函数应该易于计算,并且尽量使计算出来的索引值均匀分布
- 哈希函数计算得到的哈希值是一个固定长度的输出值
- 如果Hash(key1)不等于Hash(key2),那么key1、key2一定不相等。
- 如果Hash(key1)等于Hash(key2),那么key1、key2可能相等,也可能不相等。
在哈希表的应用中,key可能有很多种类型,可能是数字型、字符串类型、浮点数类型、设置是一些自定义类型(如面向对象中的类对象),一般会将各种类型的关键字先转换为整数类型,再通过哈希函数,映射到哈希表中。
而关于整数类型的关键字,通常用到的哈希方法有:直接定址法、除留余数法、平分取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。
3.具体哈希函数介绍
- 直接定址法
取关键字或关键字的某个线性函数值为哈希地址,即Hash(key) = key或Hash(key) = a * key + b,其中a和b是常数。
该方法计算简单,且不会产生冲突。适合于关键字分布基本连续的情况,如果关键字分布不连续,空位较多,则会造成存储空间的浪费。
- 除留余数法
假设哈希表的表长为m,取一个不大于m但接近或等于m的质数p,利用取模运算,将关键字转换为哈希地址。即Hash(key) = key % p,其中p为不大于m的质数。
关键点在于p的选择,根据经验而言,p取素数或者m,可以尽可能减少冲突。
- 平分取中法
先通过关键字平方值的方式扩大相近数之间的差别,然后根据表长度取关键字平方值的中间几位数为哈希地址。如Hash(key) = (key * key) // 100 % 1000,先计算平方,然后去除末尾两位数,取中间3位数作为哈希地址。
- 基数转换法
将关键字看成另一种进制的数再转换成原来进制的数,然后选其中几位作为哈希地址。
4.哈希冲突
哈希冲突:不同的关键字通过同一个哈希函数可能得到同一哈希地址,即key1 != key2,而Hash(key1) = Hash(key2),这种现象称为哈希冲突。
理想状态下,我们的哈希函数是完美的一对一映射,即一个关键字(key)对应一个值(value),不需要处理冲突。但是一般情况下,不同的关键字key可能对应了同一个值value,这就发生了哈希冲突。
常用的解决哈希冲突方法主要是两类:开放地址法和链地址法。
5.开放地址法
当哈希表中的空地址向处理冲突开放。即当哈希表未满时,处理冲突时需要尝试另外的地址,直到找到空的地址为止。
开放地址法通过该方法获取后继哈希地址:H(i) - (Hash(key) + F(i)) % m, i = 1, 2, 3, …, n
F(i)是冲突解决方法,可以有如下取法:
- 线性探测法:F(i) = 1, 2, 3, …, m - 1
- 二次探测法:F(i) = 1^2, -1^2, 2^2, -2^2, …, n^2 (n <= m / 2)
- 伪随机数序列:F(i) = 伪随机数序列
6.链地址法
将具有相同哈希地址的元素存储在同一个线性链表中。相比于开放地址法,链地址法更加常用且更加简单。
题目解析
存在重复元素
1.题目描述
2.解析思路及代码
- 排序之后,判断相邻两元素是否相等
- 使用哈希表,判断当前数组是否存在当前元素
public boolean containsDuplicate(int[] nums) {
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i ++ ) {
if (nums[i] == nums[i + 1]) return true;
}
return false;
}
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
dict = set()
for num in nums:
if num in dict:
return True
dict.add(num)
return False
有效的数独
1.题目描述
2.解题思路及代码
使用哈希表记录九宫格中每行、每列、每个小九宫格的数字出现次数,如果大于1返回false。
public boolean isValidSudoku(char[][] board) {
int[][] rows = new int[9][9];
int[][] columns = new int[9][9];
int[][][] subboxes = new int[3][3][9];
for (int i = 0; i < 9; i ++ ) {
for (int j = 0; j < 9; j ++ ) {
char c = board[i][j];
if (c != '.') {
int index = c - '0' - 1;
rows[i][index] ++ ;
columns[j][index] ++ ;
subboxes[i / 3][j / 3][index] ++ ;
if (rows[i][index] > 1 || columns[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1)
return false;
}
}
}
return true;
}
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
rows = [dict() for _ in range(9)]
columns = [dict() for _ in range(9)]
boxes = [dict() for _ in range(9)]
for i in range(9):
for j in range(9):
if board[i][j] != '.':
num = int(board[i][j])
box_index = (i // 3) * 3 + j // 3
row_num = rows[i].get(num, 0)
col_num = columns[j].get(num, 0)
box_num = boxes[box_index].get(num, 0)
if row_num > 0 or col_num > 0 or box_num > 0:
return False
rows[i][num] = 1
columns[j][num] = 1
boxes[box_index][num] = 1
return True
存在重复元素 II
1.题目描述
2.解题思路及代码
- 哈希表存储每个数的位置,如果当前在哈希表找到当前数的位置和当前位置的绝对值只差小于等于k返回true
- 滑动窗口
public boolean containsNearbyDuplicate(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i ++ ) {
if (map.containsKey(nums[i]) && i - map.get(nums[i]) <= k)
return true;
map.put(nums[i], i);
}
return false;
}
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
pos = {}
for i, num in enumerate(nums):
if num in pos and i - pos[num] <= k:
return True
pos[num] = i
return False
宝石与石头
1.题目描述
2.解题思路及代码
- 哈希集合
public int numJewelsInStones(String jewels, String stones) {
Set<Character> set = new HashSet<>();
int res = 0;
for (int i = 0; i < jewels.length(); i ++ ) {
set.add(jewels.charAt(i));
}
for (int i = 0; i < stones.length(); i ++ ) {
if (set.contains(stones.charAt(i))) res ++ ;
}
return res;
}
class Solution:
def numJewelsInStones(self, jewels: str, stones: str) -> int:
jewelSet = set(jewels)
return sum(s in jewelSet for s in stones)
子域名访问计数
1.题目描述
2.解题思路及代码
思路:通过前序遍历可以确定第一个节点是根节点,然后是左子树,最后是右子树,而通过根节点和中序遍历可以把左子树和右子树确定出来,然后继续递归根据前序遍历的结果确定左子树的根节点和右子树的根节点。
public List<String> subdomainVisits(String[] cpdomains) {
Map<String, Integer> map = new HashMap<>();
for (String domain : cpdomains) {
String[] cpinfo = domain.split("\\s+");
String[] frags = cpinfo[1].split("\\.");
int count = Integer.valueOf(cpinfo[0]);
String cur = "";
for (int i = frags.length - 1; i >= 0; i -- ) {
cur = frags[i] + (i < frags.length - 1 ? "." : "") + cur;
map.put(cur, map.getOrDefault(cur, 0) + count);
}
}
List<String> ans = new ArrayList<>();
for (String dom : map.keySet()) {
ans.add("" + map.get(dom) + " " + dom);
}
return ans;
}
class Solution:
def subdomainVisits(self, cpdomains: List[str]) -> List[str]:
ans = collections.Counter()
for domain in cpdomains:
count, domain = domain.split()
count = int(count)
frags = domain.split('.')
for i in range(len(frags) - 1, -1, -1):
ans[".".join(frags[i:])] += count
return ["{} {}".format(ct, dom) for dom, ct in ans.items()]