数据结构(Java语言)——HashTable(分离链接法)简单实现

本文详细阐述了散列表数据结构的核心思想及其关键组件,包括散列函数、装填因子和解决冲突的方法。重点介绍了分离链接法的原理与实现,通过实例展示了如何利用该方法构建和操作散列表,同时分析了其性能特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    散列表的实现通常叫做散列hashing。散列是一种用于以常数平均时间执行插入、删除和查找的技术。但是,那些需要元素间任何排序信息的树操作将不会得到有效的支持。理想的散列表数据结构只不过是一个包含一些项的具有固定大小的数组。通常查找是对项的某个部分(数据域)进行的,这部分叫做关键字。例如,项可以由一个串(作为关键字)和其它一些数据域组成。我们把表的大小记作TableSize,并将其理解为散列数据结构的一部分,而不仅仅是浮动于全局的的某个变量。通常习惯是让表从0到TableSize-1变化。

    每个关键字被映射到从0到TableSize-1这个范围中的某个数,并且被放到适当的单元中。这个映射就叫做散列函数,理想情况下它应该计算起来简单,并且应该保证任何两个不同的关键字映射到不同的单元。不过,这是不可能的,因为单元的数目是有限的,而关键字实际上是用不完的。因此,我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字。

    这就是散列的基本想法,剩下的问题就是要选择一个函数,决定当两个关键字散列到同一个值得时候(即发生冲突)应该做什么以及如何确定散列表的大小。

  • 散列函数
    如果输入的关键字是整数,则一般合理的方法就是直接返回Key mod TableSize,除非Key碰巧具有某些不合乎需要的性质。在这种情况下,散列函数的选择需要仔细地考虑。例如,若表的大小是10而关键字都以0位个位,则此时上述标准的散列函数就不太好。为了避免上面的情况,好的方法通常是保证表的大小是素数。当输入的关键字是随机整数时,散列函数不仅计算起来简单而且关键字的分配也很均匀。
    通常,关键字是字符串,在这种情况下,散列函数需要仔细地选择。剩下的主要编程细节是解决冲突的消除问题。如果当一个元素被插入时与一个已经插入的元素散列到相同的值,那么就产生一个冲突,这个冲突需要消除。解决这种冲突的方法有几种,这里主要介绍分离链接法、开放定址法。
  • 分离链接法
分离链状法是将散列到同一个值的所有元素保留到一个表中。我们可以使用标准库表的实现方法。如果空间很紧,则更可取的方法是避免使用它们(因为这些表是双向链接的并且浪费空间)。为执行一次查找,我们使用散列函数来确定究竟遍历哪个链表,然后我们再在相应的链表中查看该元素是否已在适当的位置(如果允许插入重复元,那么通常要留出一个额外的域,这个域当出现匹配事件时增1)。如果这个元素是个新元素,那么它将被插入到链表的前端,这不仅因为方便,还因为常常发生这样的事:新插入的元素最有可能不久又被访问。
    我们定义散列表的装填因子 λ为散列表中的元素个数对该表大小的比。执行一次查找所需要的工作是计算散列函数值所需要的常数时间加上遍历链表所用的时间。再一次不成功的查找中,要考察的节点数平均为λ。一次成功的查找则平均需要遍历大约1+(λ/2)个链。分析指出,散列表的大小实际上并不需要,而装填因子才是重要的。分离链状法的一般法则是是的表的大小与预料的元素个数大致相等,即λ≈1。
    以下就是一个用分离链接法简单实现的散列表:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class SeparateChainingHashTable<AnyType> {
	private static final int DEFAULT_TABLE_SIZE = 10;//默认容量
	private List<AnyType>[] theLists;//散列表的数组
	private int currentSize;//当前数据个数

	public SeparateChainingHashTable() {
		this(DEFAULT_TABLE_SIZE);
	}

	public SeparateChainingHashTable(int size) {
		theLists = new LinkedList[nextPrime(size)];
		for (int i = 0; i < theLists.length; i++) {
			theLists[i] = new LinkedList<AnyType>();
		}
	}

	/**
	 * 使哈希表变空
	 */
	public void makeEmpty() {
		for (List<AnyType> list : theLists) {
			list.clear();
		}
		currentSize = 0;
	}

	/**
	 * 哈希表是否包含某元素
	 * @param x 查询元素
	 * @return 查询结果
	 */
	public boolean contains(AnyType x) {
		List<AnyType> whichList = theLists[myhash(x)];
		return whichList.contains(x);
	}

	/**
	 * 向哈希表中插入某元素,若存在则不操作
	 * @param x 插入元素
	 */
	public void insert(AnyType x) {
		List<AnyType> whichList = theLists[myhash(x)];
		if (!whichList.contains(x)) {
			whichList.add(x);
			if (++currentSize > theLists.length) {
				rehash();
			}
		} else {
		}
	}

	/**
	 * 向哈希表中删除某元素,若不存在则不操作
	 * @param x 删除元素
	 */
	public void remove(AnyType x) {
		List<AnyType> whichList = theLists[myhash(x)];
		if (whichList.contains(x)) {
			whichList.remove(x);
			currentSize--;
		} else {
		}
	}

	/**
	 * 哈希算法,有多种实现方法
	 * @param x 元素
	 * @return 哈希值
	 */
	private int myhash(AnyType x) {
		int hashVal = x.hashCode();
		hashVal %= theLists.length;
		if (hashVal < 0) {
			hashVal += theLists.length;
		}
		return hashVal;
	}

	/**
	 * 再散列函数,插入空间不够时执行
	 */
	private void rehash() {
		List<AnyType>[] oldLists = theLists;
		// 分配一个两倍大小的空表
		theLists = new List[nextPrime(2 * theLists.length)];
		for(int j=0;j<theLists.length;j++){
			theLists[j]=new LinkedList<AnyType>();
		}
		currentSize = 0;
		for (int i = 0; i < oldLists.length; i++) {
			for (AnyType item : oldLists[i]) {
				insert(item);
			}
		}
	}

	/**
	 * 检查某整数是否为素数
	 * @param num 检查整数
	 * @return 检查结果
	 */
	private static boolean isPrime(int num) {
		if (num == 2 || num == 3) {
			return true;
		}
		if (num == 1 || num % 2 == 0) {
			return false;
		}
		for (int i = 3; i * i <= num; i += 2) {
			if (num % i == 0) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 返回不小于某个整数的素数
	 * @param num 整数
	 * @return 下一个素数(可以相等)
	 */
	private static int nextPrime(int num) {
		if (num == 0 || num == 1 || num == 2) {
			return 2;
		}
		if (num % 2 == 0) {
			num++;
		}
		while (!isPrime(num)) {
			num += 2;
		}
		return num;
	}

	/**
	 * 输出散列表
	 */
	public void printTable() {
		for(int i=0;i<theLists.length;i++){
			System.out.println("-----");
			Iterator iterator=theLists[i].iterator();
			while(iterator.hasNext()){
				System.out.print(iterator.next()+" ");
			}
			System.out.println();
		}
	}

	public static void main(String[] args) {
		Random random = new Random();
		SeparateChainingHashTable<Integer> hashTable = new SeparateChainingHashTable<Integer>();
		for (int i = 0; i < 30; i++) {
			hashTable.insert(random.nextInt(30));
		}
		hashTable.printTable();
	}
}

执行结果:
-----

-----
1 24 
-----
25 2 
-----

-----
4 27 
-----

-----
6 29 
-----

-----

-----


-----
10 
-----


-----


-----
13 
-----


-----
15 
-----


-----
17 
-----
18 
-----
19 
-----
20 
-----
21 
-----


### Java 中 ConcurrentHashMap 实现线程安全的底层原理 #### 什么是 ConcurrentHashMap? `ConcurrentHashMap` 是一种支持高并发场景下的线程安全 Map 结构,它允许多个线程同时对其进行读写操作而不会发生数据不一致的问题[^2]。相比传统的 `Hashtable` 和带有 `synchronized` 锁的 `HashMap`,`ConcurrentHashMap` 提供了更高的吞吐量和更低的竞争开销。 --- #### ConcurrentHashMap 的核心设计思想 为了提高性能,`ConcurrentHashMap` 并没有像 `Hashtable` 或者 `Collections.synchronizedMap()` 那样对整个表加锁,而是采用了更细粒度的锁分离策略——**分段锁(Segment Lock)**。具体来说: - 在 JDK 7 中,`ConcurrentHashMap` 将内部的数据分为若干个 Segment 对象,每个 Segment 类似于一个小的 HashTable,并且有自己的锁。这样可以使得不同的线程可以在不同的 Segment 上进行操作而不互相干扰。 - 在 JDK 8 中,`ConcurrentHashMap` 改进了其底层结构,去掉了 Segment 的概念,转而使用了一种更加高效的实现方式:**数组 + 链表 + 红黑树**[^1]。 --- #### JDK 8 中 ConcurrentHashMap 的底层实现细节 ##### 数据结构概述 在 JDK 8 中,`ConcurrentHashMap` 的底层由以下几个部分组成: 1. **Node 数组**:类似于普通的 HashMap,`ConcurrentHashMap` 使用了一个 Node 数组作为主要存储容器。 2. **链表**:当哈希冲突时,节点会被链接成一条单向链表。 3. **红黑树**:如果链表长度超过一定阈值(默认为 8),则会将链表转换为红黑树以减少查找的时间复杂度[^1]。 ##### 关键特性 1. **CAS 操作**:`ConcurrentHashMap` 大量依赖于无锁算中的 CAS(Compare And Swap)技术来进行高效的状态更新。例如,在插入新元素时,先尝试通过 CAS 更新指定位置上的引用;只有当 CAS 失败时才会考虑升级到更高层次的同步手段。 2. **分段锁机制**:虽然 JDK 8 移除了显式的 Segments,但实际上仍然保留了类似的思路。对于每一次修改操作(如 put、remove),只会针对涉及的具体桶位施加重入锁(ReentrantLock),而不是全局锁定整个表[^2]。 3. **帮助式传播(Helpful Propagation)**:为了避免长时间持有锁带来的性能瓶颈,`ConcurrentHashMap` 设计了一些辅助函数让其他线程也能参与到一些耗时任务当中,比如扩容过程中的迁移工作就可以被任何发现正在进行 resize 的线程主动接手完成一部分[^1]。 --- #### 如何保证线程安全性? 1. **读操作无需加锁** - 由于引入了 JMM (Java Memory Model)的支持,`ConcurrentHashMap` 能够确保即使是在多线程环境下,简单的 get 请求也可以安全地返回最新版本的结果,而不需要额外付出同步成本[^3]。 2. **写操作局部化保护** - 插入或删除等变更型 API 主要集中在特定索引处执行,因此只需对该区域实施短暂封锁即可满足需求。例如,在调用 `putVal(K,V)` 方期间,仅需占有对应槽位所属的 ReentrantLock 实例便可顺利推进后续逻辑[^2]。 3. **动态调整容量大小** - 当现有空间不足以容纳新增项时,`ConcurrentHashMap` 会自动触发扩展流程。值得注意的是,这一阶段同样遵循最小影响原则,即尽量不影响正常运转的服务请求。此外,得益于前面提到的帮助式传播理念,即便原作者暂时离开现场,后来者依旧有机会接力承担起剩余职责直至彻底结束整个重构周期。 --- #### 示例代码分析 以下是一个简化版的 `putIfAbsent` 方演示如何利用 CAS 来保障原子性和一致性: ```java final V putVal(int hash, K key, V value) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) != null && (n = tab.length) > 0) { // 计算定位索引 if ((p = tab[i = (n - 1) & hash]) == null) casTabAt(tab, i, null, new Node<K,V>(hash, key, value)); // 使用 CAS 创建首节点 else { lockForPut(p); // 加锁后再继续深入判断 try { Object k; if (valueMatch(p.value)) // 已存在相同key,则跳过赋值 return p.value; else // 新增情况走常规路径 addBinTreeOrList(hash, key, value); } finally { unlock(); // 不管怎样都要记得解锁哦~ } } } return null; } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值