首页 > 编程语言 >小白学算法-数据结构和算法教程: 使用开放寻址线性探测实现自己的哈希表

小白学算法-数据结构和算法教程: 使用开放寻址线性探测实现自己的哈希表

时间:2023-10-24 11:04:03浏览次数:38  
标签:head 哈希 int 复杂度 hashCode 算法 小白学 key

小白学算法-数据结构和算法教程: 使用开放寻址线性探测实现自己的哈希表_算法

Java 中使用链接实现哈希表

所有数据结构都有其自身的特点,例如,当需要快速搜索元素(在log(n)中)时,会使用BST。当需要在恒定时间内获取最小或最大元素时,使用堆或优先级队列。类似地,哈希表用于在恒定时间内获取、添加和删除元素。在继续实施方面之前,任何人都必须清楚哈希表的工作原理。因此,这里是哈希表工作的简要背景,还应该注意的是,我们将互换使用哈希映射和哈希表术语,尽管在 Java 中哈希表是线程安全的,而 HashMap 不是。

背景:每个哈希表都以(键,值)组合的形式存储其数据。有趣的是,哈希表中的每个键都是唯一的,但值可以重复,这意味着其中存在的不同键的值可以相同。现在,当我们在数组中观察以获取值时,我们提供与该数组中的值相对应的位置/索引。在哈希表中,我们不使用索引,而是使用键来获取与该键对应的值。

每次生成密钥时。密钥被传递给哈希函数。每个哈希函数都有两部分:哈希码和压缩器。 

哈希码是一个整数(随机或非随机)。在Java中,每个对象都有自己的哈希码。我们将在哈希函数中使用 JVM 生成的哈希码,并根据哈希表的大小对哈希码取模 (%) 来压缩哈希码。所以模运算符在我们的实现中是一个压缩器。

现在我们要做的是制作一个与哈希表的特定桶相对应的链表,以容纳映射到同一桶的不同键对应的所有值。 

小白学算法-数据结构和算法教程: 使用开放寻址线性探测实现自己的哈希表_数据结构_02

现在可能存在一种情况,所有键都映射到同一个存储桶,并且我们有一个来自单个存储桶的 n(哈希表的大小)大小的链表,所有其他存储桶都是空的,这是最坏的情况其中哈希表充当链表,搜索的时间复杂度为 O(n)。 

小白学算法-数据结构和算法教程: 使用开放寻址线性探测实现自己的哈希表_程序员_03

哈希冲突和负载因子

那么我们该怎么办?

负载系数:如果 n 是我们最初决定填充的桶总数,假设为 10,现在假设其中 7 个已被填充,那么负载系数为 7/10=0.7。 

在我们的实现中,每当我们向哈希表添加键值对时,我们都会检查负载因子,如果它大于 0.7,我们就会将哈希表的大小加倍。

执行:

哈希节点数据类型

我们将尝试制作一个通用映射,而不对键和值的数据类型施加任何限制。此外,每个哈希节点都需要知道它在链表中指向的下一个节点,因此还需要一个下一个指针。

我们计划保留在哈希图中的函数如下: 

  • get(K key) :如果HTHast Table )中存在该键,则返回该键对应的
  • getSize():返回 HT 的大小
  • add():向 HT 添加一个新的有效键、值对,如果已经存在则更新该值
  • remove():删除键、值对
  • isEmpty():如果大小为零则返回 true
ArrayList<HashNode<K, V>> Bucket = new ArrayList<>();

实现辅助函数来获取键的索引,以避免其他函数(如 get、add 和 remove)中的冗余。该函数使用内置的java函数生成哈希码,我们将哈希码压缩HT的大小,使得索引在HT的大小范围内

get()

get 函数仅将键作为输入,如果该键存在于表中,则返回相应的值,否则返回 null。步骤是:  

  • 检索输入的key,找到HT中的索引
  • 遍历 HT 对应的链表,如果找到该值则返回该值,否则如果完全遍历该链表而不返回,则意味着该值不存在于表中,无法获取,因此返回 null

remove()

  • 使用辅助函数获取输入键对应的索引
  • 链表的遍历和get()类似,但是这里的特殊之处是在查找的同时还需要删除key,会出现两种情况
  • 如果要删除的键位于链表的头部
  • 如果要移除的钥匙不在头部而是在其他地方

add()

现在来看看整个实现中最有趣和最具挑战性的功能。这很有趣,因为当负载因子高于我们指定的值时,我们需要动态增加列表的大小。  

  • 就像删除步骤直到遍历和添加一样,两种情况(在头点或非头点添加)保持不变。
  • 接近尾声时,如果负载系数大于 0.7
  • 我们将数组列表的大小加倍,然后在现有键上递归调用 add 函数,因为在我们的例子中,生成的哈希值使用数组的大小来压缩我们使用的内置 JVM 哈希码,因此我们需要获取新的索引现有的钥匙。理解这一点非常重要,请重新阅读本段,直到您掌握 add 函数中发生的情况为止。

如果对应于特定存储桶的链表往往变得太长,Java 在其自己的哈希表实现中会使用二叉搜索树。 

Java 代码实现:

// Java程序演示了使用链式法解决碰撞检测的自定义哈希表实现
import java.util.ArrayList;
import java.util.Objects;

//  链的节点
class HashNode<K, V> {
	K key;
	V value;
	final int hashCode;

// 下一个节点的引用
	HashNode<K, V> next;

// 构造函数
	public HashNode(K key, V value, int hashCode)
	{
		this.key = key;
		this.value = value;
		this.hashCode = hashCode;
	}
}

// 表示整个哈希表的类
class Map<K, V> {
// bucketArray 用于存储链数组
	private ArrayList<HashNode<K, V> > bucketArray;

// 当前数组列表的容量
	private int numBuckets;

// 当前数组列表的大小
	private int size;

// 构造函数(初始化容量、大小和空链。
	public Map()
	{
		bucketArray = new ArrayList<>();
		numBuckets = 10;
		size = 0;

		// 创建空链
		for (int i = 0; i < numBuckets; i++)
			bucketArray.add(null);
	}

	public int size() { return size; }
	public boolean isEmpty() { return size() == 0; }
	
	private final int hashCode (K key) {
		return Objects.hashCode(key);
	}

// 这个实现了一个哈希函数,用于找到一个键的索引
	private int getBucketIndex(K key)
	{
		int hashCode = hashCode(key);
		int index = hashCode % numBuckets;
		index = index < 0 ? index * -1 : index;
		return index;
	}

// Method to remove a given key
	public V remove(K key)
	{
		// 将哈希函数应用于给定的键以找到索引
		int bucketIndex = getBucketIndex(key);
		int hashCode = hashCode(key);
		// 获取链的头部
		HashNode<K, V> head = bucketArray.get(bucketIndex);

		// 在其链中搜索键
		HashNode<K, V> prev = null;
		while (head != null) {
			// 如果找到密钥
			if (head.key.equals(key) && hashCode == head.hashCode)
				break;

			// 否则继续在链中移动
			prev = head;
			head = head.next;
		}

		// 如果键不存在
		if (head == null)
			return null;

		// 缩小
		size--;

		// 删除键
		if (prev != null)
			prev.next = head.next;
		else
			bucketArray.set(bucketIndex, head.next);

		return head.value;
	}

		// 返回键值
	public V get(K key)
	{
		// 找到给定键的链表头部
		int bucketIndex = getBucketIndex(key);
		int hashCode = hashCode(key);
	
		HashNode<K, V> head = bucketArray.get(bucketIndex);

		// 在链中搜索钥匙
		while (head != null) {
			if (head.key.equals(key) && head.hashCode == hashCode)
				return head.value;
			head = head.next;
		}

		// 如果找不到键
		return null;
	}

	// 向哈希表添加键值对
	public void add(K key, V value)
	{
		// 找到给定键的链表头部
		int bucketIndex = getBucketIndex(key);
		int hashCode = hashCode(key);
		HashNode<K, V> head = bucketArray.get(bucketIndex);

		// 检查键是否已经存在
		while (head != null) {
			if (head.key.equals(key) && head.hashCode == hashCode) {
				head.value = value;
				return;
			}
			head = head.next;
		}

		// 将钥匙插入链中
		size++;
		head = bucketArray.get(bucketIndex);
		HashNode<K, V> newNode
			= new HashNode<K, V>(key, value, hashCode);
		newNode.next = head;
		bucketArray.set(bucketIndex, newNode);

		// 如果负载系数超过阈值,则将哈希表大小加倍
		if ((1.0 * size) / numBuckets >= 0.7) {
			ArrayList<HashNode<K, V> > temp = bucketArray;
			bucketArray = new ArrayList<>();
			numBuckets = 2 * numBuckets;
			size = 0;
			for (int i = 0; i < numBuckets; i++)
				bucketArray.add(null);

			for (HashNode<K, V> headNode : temp) {
				while (headNode != null) {
					add(headNode.key, headNode.value);
					headNode = headNode.next;
				}
			}
		}
	}

	// 测试Map类的驱动方法
	public static void main(String[] args)
	{
		Map<String, Integer> map = new Map<>();
		map.add("this", 1);
		map.add("coder", 2);
		map.add("this", 4);
		map.add("hi", 5);
		System.out.println(map.size());
		System.out.println(map.remove("this"));
		System.out.println(map.remove("this"));
		System.out.println(map.size());
		System.out.println(map.isEmpty());
	}
}

添加复杂度

该方法将一个键值对添加到哈希表中。该方法的时间复杂度为O(1),因为它是常数时间。空间复杂度为 O(n),因为它会随着哈希表中存储的项目数量而增加。

删除复杂度

时间复杂度:O(1)
空间复杂度:O(1)

此方法从哈希表中删除给定的键。该方法的时间复杂度为O(1),因为它是常数时间。空间复杂度为 O(1),因为它不依赖于哈希表中存储的项目数量。

获取 复杂度

时间复杂度:O(1)
空间复杂度:O(1)

此方法返回哈希表中给定键的值。该方法的时间复杂度为O(1),因为它是常数时间。空间复杂度为 O(1),因为它不依赖于哈希表中存储的项目数量。

size 复杂度

时间复杂度:O(1)
空间复杂度:O(1)

标签:head,哈希,int,复杂度,hashCode,算法,小白学,key
From: https://blog.51cto.com/demo007x/8001616

相关文章

  • 【基础算法】- 贪心
    贪心定义贪心算法适用于最优子结构问题。意思是问题在分解成子问题来解决时,子问题的最优解能递推到最终问题的最优解。常见的符合这种性质的问题如:「我们将XXX按照某某顺序排序,然后按某种顺序(例如从小到大)选择。」「我们每次都取XXX中最大/小的东西,并更新XXX。」但比......
  • 子序列相关算法
    1、最长公共子序列最长公共子序列(LongestCommonSubsequence,LCS)是动态规划中的经典问题,顾名思义,即求两个序列最长的公共子序列(可以不连续)。 1#include<iostream>2#include<string>3usingnamespacestd;//使用动态规划算法;分为两种情况4chara[102],b[102];......
  • md5算法实现
    前言md5算法是我们经常会用到的一个hash函数,虽然已经被证明是不安全的了,但其应用依然十分广泛.哈希函数具有如下特点:将任意长度的字符串映射为固定长度源数据微小的改动会导致结果差异巨大不可逆暴力破解困难你有没有好奇过,哈希函数是如何做到这些的呢?本文就拿m......
  • diff算法
    什么是Diff算法?Diff算法是Vue.js的一个核心特性,它是一种用于比较虚拟DOM树的差异,并最小化DOM操作的数量。当Vue.js检测到数据更改时,它会生成一个新的虚拟DOM树,并将其与旧虚拟DOM树进行比较。Diff算法会查找差异,并仅对需要更改的部分进行DOM操作。这种算法可以帮助我们在前端开发中......
  • 算法笔记(3)模拟退火
    原发表于个人博客=模拟退火的引入假如我们有一个函数,要求它的极大值,怎么求呢?如果这个函数满足单调性,可以用二分的方法。如果这是一个单谷(或单峰)函数,可以用三分法。那要是多峰函数怎么半呢?这时就可以用随机化算法。一种朴素的方法是:每次在当前找到的最优方案\(x\)附近寻找一......
  • 算法笔记(4)莫队算法入门
    原发表于我的博客前言本来想学完回滚莫队、树上莫队、二离莫队之后一起写一个博客,但是一直学不会/kk,只好把已会的普通莫队和带修莫队写了(以后会补上的)普通莫队莫队——优雅的暴力莫队算法的引入例题:给定一个数列和若干询问,每次询问询问一段区间内不同种类数字的个数。暴力......
  • 算法笔记(5)贪心算法
    原发表于我的博客贪心算法贪心与其说是一种算法,不如说一种思想。贪心思想,顾名思义,就是总是做出当前最好的选择,这种方式可能在全局上不是最好的结果,但是在一些题目中就可以直接用。最简单的例子就是“货比三家”,在生活中,我们买东西时都会挑性价比最优的,这就是一种贪心。贪心算......
  • 算法笔记(6)数列分块
    原发表于我的博客前言分块不能说是一种数据结构,它是一种思想,无论是数列分块,块状链表,还是数论分块,莫队算法,都应用了分块的思想。本文主要介绍狭义上的分块,即数列分块。数列分块的引入数列分块可以说是暴力,一种优美的暴力,它的基本思路是,把数列分成若干块(一般取\(\sqrtn\)),分块......
  • 算法笔记(2)FHQtreap
    原发布于我的个人博客前言FHQtreap绝对是平衡树里最好写,最实用的,他几乎能做所有splay或其它平衡树能做的事,还能可持久化!这篇文章将会介绍FHQtreap的基本操作和维护区间的操作,并附上例题。基本操作FHQtreap的基本操作只有两个,分裂和合并。有些读者可能会问,分裂和合并和平衡树......
  • K-medoids聚类算法
    发展:们每次选簇的平均值作为新的中心,迭代直到簇中对象分布不再变化。因此一个具有很大极端值的对象会扭曲数据分布,造成算法对极端值敏感在聚类分析中,异常值通常会引起问题,因为它们可能会被分配到一个独立的聚类,从而干扰正常的聚类结果。这可能导致聚类算法产生不合理或不稳定的......