数据结构基础9:哈希表

前言:哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。还有就是适用于Source中文本重复率高的文本压缩LZW。

一、字典

字典是由一些形如(k,v)的数对所组成的集合,其中k是关键字,v是与关键字k对应的值。任意一个数对,其关键字都不等。

  • 确定字典是否为空
  • 确定字典有多少数对
  • 插入一个数对
  • 删除一个指定关键字的数对
  • 寻找一个指定关键字的数对

字典的一种常见表示方法是哈希表,其实还有跳表有兴趣的朋友可以了解一下。

二、哈希表

散列表(Hash table,也叫哈希表):是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。如果数对p的关键字是k,哈希函数为f,那么在理想情况下,p在哈希表中的位置就是f(k)。

哈希冲突:在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。

1、为什么出现哈希表?

已知顺序表(数组),查找容易,插入、删除困难消耗性能。然而链表虽然解决了顺序表插入删除的问题,但是链表的查找却降低了性能。结合两者的优缺点,于是便出现了,查找容易同时插入删除容易的哈希表,时间复杂度都为o(1)。

2、哈希表原理

如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:用哈希函数把键转换为数组索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。散列函数和键的类型有关,对于每种类型的键我们都需要一个与之对应的散列函数。常见散列函数算法有MD5和SHA-1。

其实,哈希表的本质就是一个数组。在散列表内部,我们使用桶(bucket来保存键值对,我们前面所说的数组索引即为桶号,决定了给定的键存于散列表的哪个桶中。散列表所拥有的桶数被称为散列表的容量(capacity

 现在假设我们的散列表中有M个桶,桶号为0到M-1。我们的散列函数的功能就是把任意给定的key转为[0, M-1]上的整数。我们对散列函数有两个基本要求:一是计算时间要短,二是尽可能把键分布在不同的桶中。对于不同类型的键,我们需要使用不同的散列函数,这样才能保证有比较好的散列效果。

3、哈希表存取过程

①首先使用哈希散列函数将被查找的键转换为数组的索引。

根据 key 计算出它的哈希值 hashcode。假设桶的个数为 n,那么这个键值对应该放在第 (hashcode % n) 个桶中。

②如果出现哈希冲突,则用拉链法或线性探测法处理哈希冲突。

如果该桶中已经有了键值对,即不同的关键字得到同一散列地址,那么就使用拉链法或者开放寻址法(线性探测法)解决冲突。

4、拉链法实现的哈希表

哈希表本质就是一个数组,底层是由数组+链表组成,数组中的每个元素都是一个链表,我们可以理解为“链表的数组”。

通过散列函数,我们可以将键转换为数组的索引(0-M-1),但是对于两个或者多个键具有相同索引值的情况,一种比较直接的解决办法就是,将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对,这就是拉链法。 

数据结构基础9:哈希表_第1张图片

在使用拉链法解决哈希冲突时,每个桶其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。

5、哈希表的重要属性

负载因子(load factor):它用来衡量哈希表的 空/满 程度,一定程度上也可以体现查询的效率,计算公式为:

负载因子 = 总键值对数 / 箱子个数

负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。

哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

哈希表的自动扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。

基于以上总结,细心的读者可能会发现哈希表的两个问题:

  1. 如果哈希表中本来箱子就比较多,扩容时需要重新哈希并移动数据,性能影响较大。
  2. 如果哈希函数设计不合理,哈希表在极端情况下会变成线性表,性能极低。

 Java 8.0里面的HashMap提供了解决方案,就是当链表长度超过8时,链表会自动转为红黑树。故Java8.0后,HashMap底层是用数组+链表+红黑树实现的。

三、Java实现哈希表

1、定义键值对的结点类Entry

/*
 * 泛型对象表示的键值对
 */

public class Entry 
{
   // key、value模拟键值对的数据
   public K key;
   public V value;
   
   public Entry next;//指针域,下一节点的引用
   
   public Entry(K key,V value) 
   {
	  this.key = key;
	  this.value = value;
   }
   
   public Entry(){};
}

2、定义存储键值对结点的桶,即链表

/*
 * 存储键值对(k,v)的链表
 */

public class EntryLinkList 
{
    int chainSize;
    Entry head;
	
    /*
     * 1、添加键值对结点,如果关键字相同,则覆盖掉重复值
     */

	public void add(K k,V v)
	{
		Entry  newEntry = new Entry(k, v);//构建新节点
		Entry  temp = head;
		
		if(head==null)
		{
			head = new Entry();
			head.next = newEntry;
		}
		
		while(temp.next!=null)
		{    
			//关键字相同,就覆盖
		    if(temp.next.key == k)
		    {
		    	temp.next.value = v;
		    	return;
		    }
			
		    temp = temp.next;//继续向下移动
		}
		
		temp.next = newEntry;
		chainSize++;
	}
	
	/*
	 * 2、删除键值对结点,根据关键字k删除相应键值对
	 */
	public boolean delete(K k)
	{
		Entry temp = head;
		while(temp.next!=null)
		{   
			if(temp.next.key == k)
			{
				temp.next = temp.next.next;
				return true;
			}
			
		    temp = temp.next;
		}
		
		chainSize--;
		return false;
	}
	
	/*
	 * 3、根据key获取value
	 */
	public V get(K k)
	{
		Entry temp = head;
		while(temp.next!=null)
		{   
			if(temp.next.key == k)
			{
				return temp.next.value;
			}
			
		    temp = temp.next;
		}
		
		return null;
	}
	
	public int size()
	{
		return chainSize;
	}
	
	/*
	 * 4、遍历链表,列出所有数据
	 */
	public String scanChain()
	{
		String str = null;
		Entry temp = head;
		while(temp.next!=null)
		{   
			str = str+"("+temp.next.key+","+temp.next.value+"),";
			
		    temp = temp.next;
		}
		return str;
	}
	
	@Override
	public String toString() {
		
		return scanChain();
	}
	
}

3、HashTable类,拉链法实现

/*
 *   哈希表,用拉链法解决哈希冲突
 */

public class HashTable12
{
   private EntryLinkList[] arrayChain;//数组链表,每个数组元素都是一个链表
   private int maxSize;//数组大小,即桶的数量
	
   //默认数组容量为16
   public HashTable12()
   {
	   maxSize = 16;
	   arrayChain = new EntryLinkList[maxSize];
   }
   
   //数字大小自定义
   public HashTable12(int capacity)
   {
	   maxSize = capacity;
	   arrayChain = new EntryLinkList[maxSize];
   }
	in
   /*
    * 1、添加键值对
    */
   public void put(K k,V v)
   {
	   int index = K.hashCode()%maxSize;//首先根据key的哈希函数求得hashcode,然后与桶的数量maxSize取模(求余)就可以得到键值对在数组中的索引
	   if(arrayChain[index]==null)
	   {
		   arrayChain[index] = new EntryLinkList<>();
	   }
	   arrayChain[index].add(K, v);
   }
   
   /*
    * 2、根据key删除键值对
    */
   public boolean delete(K k)
   {
	   int index = K.hashCode()%maxSize;
	   if(arrayChain[index]!=null)
	   {
		  return arrayChain[index].delete(K);//进行链表的删除操作即可
	   }
	   
	   return false;//该数组索引处的链表为空,删除操作不存在
	   
   }
   
   /*
    * 3、根据key得到value
    */
   public V get(K k)
   {
	   int index = K.hashCode()%maxSize;
	   if(arrayChain[index]!=null)
	   {
		   return arrayChain[index].get(K);
	   }
	   
	   return null;
   }
   
   /*
    * 4、遍历哈希表,即遍历数组中的所有链表
    */
   public void output()
   {
	   String str = "[";
	   
	   for(int i=0;i

参考链接:

深入理解哈希表

Java实现hash表的基本操作

 

你可能感兴趣的:(数据结构与算法)