【数据结构】Hash表(哈希表)原理与代码实现

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])
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值