python数据结构之Hash表(哈希表)
数组(顺序表)的特点:
寻址容易,插入和删除困难;
链表的特点:
寻址困难,插入和删除容易。
综合两者,做出一种寻址容易,插入删除也容易的数据结构:Hash表
1.Hash表原理
hash函数就是根据key计算出应该存储地址的位置,而哈希表是基于哈希函数建立的一种查找表
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫做散列函数,
存放记录的数组叫做散列表。
Hash优缺点:
优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即O(1)的时间级。实际上,这只需要几条机器指令。
哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。
如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
Leetcode例题-1.两数之和
老问题!!!不能拿数找索引,要反过来拿索引找数!!!
# 两数之和(twosum)
# 方法二哈希表
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashtable = dict()
for i, num in enumerate(nums):
if target - num in hashtable:
return [hashtable[target - num], i]
hashtable[nums[i]] = i
return []
3. 无重复字符的最长子串-Java
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
//方法:滑动的窗口
//常用的数据结构为哈希集合(即Java 中的 HashSet,Python 中的 set)。
//在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
import java.util.*;
public class LengthOfLongestSubstring {
public int lengthOfLongestSubstring(String s){
//Java语言为内置数据类型char提供了包装类Character类。
//Character类提供了一系列方法来操纵字符,使用Character的构造方法创建一个Character类对象
Set<Character> tal = new HashSet<Character>();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
int n = s.length();
for(int i=0; i<n; i++){
while(rk+1<n && !tal.contains(s.charAt(rk+1))){
// 不断地移动右指针
tal.add(s.charAt(rk+1));
rk++;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans,rk+1-i);
// 左指针向右移动一格,移除一个字符
tal.remove(s.charAt(i));
}
return ans;
}
}
160. 相交链表
编写一个程序,找到两个单链表相交的起始节点。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
# hash表算法
class Solution(object):
def getIntersectionNode(self, headA: ListNode, headB: ListNode):
"""
:type head1, head1: ListNode
:rtype: ListNode
"""
if headA is None or headB is None: # 先判断链表A和链表B是否为空
return None
cur1 = headA
cur2 = headB
hashmap = set() # 创建哈希表
while cur1:
hashmap.add(cur1) # headA中的每个节点存储到哈希表中
cur1 = cur1.next
while cur2:
if cur2 in hashmap: # 判断当前cur2这个节点是否存在于hashmap中,是他即为相交节点
return cur2
cur2 = cur2.next
nums1 = [4,1,8,4,5]
nums2 = [5,0,1,8,4,5]
op = Solution()
shu = op.getIntersectionNode(nums1,nums2)
print(shu)
2.哈希函数构造
常见的散列法:
元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较常用的:
1、除法【求余】散列法
最直观的一种,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
一般哈希表的大小为素数,因为素数不存在因子,所以大大减少了位置冲突的概率
2、MAD法
除余法存在的不足
除余法虽能一定程度保证词条均匀分布,但从关键码空间到散列地址空间依然残留有一定的连续性,如 相邻关键码对应散列地址也相邻。
因此便有mad法,若常数ab选取得当,可以很好地克服除余法的这种连续性。除余法也可以看作Mad法a=1和b=0的特例,只是两个常数并未发挥实质作用。
表达式
hash(key) = (a*key+b) % M, 其中M仍为素数,a>0,b>0,且a % M != 0
3、数字分析法(selecting digits)
注:以下各方法为保证落在合法的散列地址空间上,最后通常还需对表长M取余。
思路
从关键码key特定进制的展开中抽取特定的若干位,构成整型地址。
表达式
例:选取key十进制展开中的奇数位
hash(123456789) = 13579
4、平方取中法(mid-square)
思路
从关键码key的平方的十进制或二进制展开中取居中的若干位,构成一个整型地址。
表达式
例:取平方并用十进制展开中的居中3位作为散列地址
123^2 = 15129,hash(123) = 512
5、折叠法(folding)
思路
将关键码的十进制或二进制展开分割成等宽的若干段,取其总和作为散列地址。
表达式
例:以十进制三个数位为分割单位
hash(123456789) = 123+456+789 = 1368
6、异或法(xor)
思路
将关键码的二进制展开分割成等宽的若干段,经异或运算得到散列地址。
表达式
例:以二进制三个数位为分割单位
hash(411) = hash(110011011b) = 110011011 = 110b = 6
7、平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
index = (value * value) >> 28
(右移,除以2^28。记法:左移变大,是乘。右移变小,是除。)
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。
8、斐波那契(Fibonacci)散列法
平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
3.哈希表冲突
哈希冲突的产生原因
哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值。这时候就产生了哈希冲突。
产生哈希冲突的影响因素
装填因子(装填因子=数据总数 / 哈希表长)、哈希函数、处理冲突的方法
解决哈希冲突的四种方法
1.开放地址方法
(1)线性探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。
(2)再平方探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
(3)伪随机探测
按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
2.链式地址法(HashMap的哈希冲突解决方法)
对于相同的值,使用链表进行连接。使用数组存储每一个链表。
优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。
3.建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据。
4.再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
4.哈希表的python实现
python中的字典就是哈希表,下面代码实现了一个简单的字典
class Dict:
def __init__(self, size=10):
self.size = size
self.key = [None] * self.size
self.data = [None] * self.size
def __setitem__(self, key, value):
assert isinstance(key, int)
index = self.hash(key)
if not self.key[index]:
self.key[index] = key
self.data[index] = value
elif self.key[index] == key:
self.data[index] = value
else:
start = index
while self.key[index] and self.key[index] != key:
index = self.re_hash(index)
if index == start:
raise Exception('dict is full')
if self.key[index]:
self.data[index] = value
else:
self.key[index] = key
self.data[index] = value
def __getitem__(self, item):
assert isinstance(item, int)
index = self.hash(item)
if not self.key[index]:
raise KeyError(item)
else:
if self.key[index] == item:
return self.data[index]
else:
start = index
while self.key[index] and self.key[index] != item:
index = self.re_hash(index)
if start == index:
raise KeyError(item)
if self.key[index] == item:
return self.data[index]
else:
raise KeyError(item)
def __contains__(self, item):
assert isinstance(item, int)
index = self.hash(item)
if not self.key[index]:
return False
else:
if self.key[index] == item:
return True
else:
start = index
while self.key[index] and self.key[index] != item:
index = self.re_hash(index)
if start == index:
break
if self.key[index] == item:
return True
else:
return False
def hash(self, key):
index = key % self.size
return int(index)
def re_hash(self, index):
return index+1
a = Dict()
a[1]='3'
a[2]='4'
print(a[1])