数据结构---->哈希表

浪尽此生 提交于 2020-02-23 10:23:25

一、哈希表

    哈希表又称散列表。哈希表存储的基本思想是:以数据表中的每个记录的关键字k为自变量,通过一种函数H(k)计算出函数值。把这个值解释为一块连续存储空间(即数组空间)的单元地址(即下标),将该记录存储到这个单元中。在此称该函数H为哈希函数或散列函数。按这种方法建立的表称为哈希表或散列表。

例如,要将关键字值序列(3,15,22,24),存储到编号为0到4的表长为5的哈希表中。 计算存储地址的哈希函数可取除5的取余数算法H(k)=k% 5。则构造好的哈希表如图所示。

理想情况下,哈希函数在关键字和地址之间建立了一个一一对应关系,从而使得查找只需一次计算即可完成。由于关键字值的某种随机性,使得这种一一对应关系难以发现或构造。因而可能会出现不同的关键字对应一个存储地址。即k1≠k2H(k1)=H(k2),这种现象称为冲突把这种具有不同关键字值而具有相同哈希地址的对象称同义词

在大多数情况下,冲突是不能完全避免的。这是因为所有可能的关键字的集合可能比较大,而对应的地址数则可能比较少。

 对于哈希技术,主要研究两个问题:

(1)如何设计哈希函数以使冲突尽可能少地发生。

(2)发生冲突后如何解决

二、哈希函数

构造好的哈希函数的方法,应能使冲突尽可能地少,因而应具有较好的随机性。这样可使一组关键字的散列地址均匀地分布在整个地址空间。根据关键字的结构和分布的不同,可构造出许多不同的哈希函数。

1.直接定址法

   直接定址法是以关键字k本身或关键字加上某个数值常量c作为哈希地址的方法。该哈希函数H(k)为:

     H(k)=k+c  (c≥0)

  这种哈希函数计算简单,并且不可能有冲突发生。当关键字的分布基本连续时,可使用直接定址法的哈希函数。否则,若关键字分布不连续将造成内存单元的大量浪费。

2.除留余数法

   取关键字k除以哈希表长度m所得余数作为哈希函数地址的方法。即:

     H(k)=k%m   

   这是一种较简单、也是较常见的构造方法。这种方法的关键是选择好哈希表的长度m。使得数据集合中的每一个关键字通过该函数转化后映射到哈希表的任意地址上的概率相等。理论研究表明,在m取值为素数(质数)时,冲突可能性相对较少。  

3.平方取中法

   取关键字平方后的中间几位作为哈希函数地址(若超出范围时,可再取模)。

4.折叠法

   这种方法适合在关键字的位数较多,而地址区间较小的情况

   将关键字分隔成位数相同的几部分。然后将这几部分的叠加和作为哈希地址(若超出范围,可再取模)。

   例如,假设关键字为某人身份证号码430104681015355,则可以用4位为一组进行叠加。即5355+8101+1046+430=14932,舍去高位。则有H(430104681015355)=4932为该身份证关键字的哈希函数地址。

三、冲突处理方法

假设哈希表的地址范围为0m-l当对给定的关键字k,由哈希函数H(k)算出的哈希地址为i(0≤i≤m-1)的位置上已存有记录,这种情况就是冲突现象处理冲突就是为该关键字的记录找到另一个的哈希地址。即通过一个新的哈希函数得到一个新的哈希地址。如果仍然发生冲突,则再求下一个,依次类推。直至新的哈希地址不再发生冲突为止。

常用的处理冲突的方法有开放地址法链地址法两大类

1.开放定址法

  用开放定址法处理冲突就是当冲突发生时,形成一个地址序列。沿着这个序列逐个探测,直到找出一个的开放地址。将发生冲突的关键字值存放到该地址中去。

  如 Hi=(H(k)+d(i)) % m, i=1,2,k  (k<m-1)

  其中H(k)为哈希函数,m为哈希表长,d为增量函数,d(i)=dl,d2dn-l

  增量序列的取法不同,可得到不同的开放地址处理冲突探测方法。

(1)线性探测法

   线性探测法是从发生冲突的地址(设为d)开始,依次探查d+l,d+2,m-1(当达到表尾m-1时,又从0开始探查)等地址,直到找到一个空闲位置来存放冲突处的关键字。

   若整个地址都找遍仍无空地址,则产生溢出。

   线性探查法的数学递推描述公式为:

          d0=H(k)

                      di=(di-1+1)% m  (1≤i≤m-1)

 

(2)平方探查法

  设发生冲突的地址为d,则平方探查法的探查序列为:d+12,d+22直到找到一个空闲位置为止。

平方探查法的数学描述公式为:

     d0=H(k)

     di=(d0+i*i) % m (1≤i≤m-1)

2.链地址法

用链地址法解决冲突的方法是:把所有关键字为同义词的记录存储在一个线性链表中,这个链表称为同义词链表。并将这些链表的表头指针放在数组中(下标从0到m-1)。这类似于图中的邻接表和树中孩子链表的结构。

3.再哈希法:

方法:构造若干个哈希函数,当发生冲突时,根据另一个哈希函数计算下一个哈希地址,直到冲突不再发  

 

四.  Hash查找过程

 对于给定值 K,计算哈希地址 i = H(K),若 r[i] = NULL  则查找不成功,若 r[i].key = K  则查找成功,否则“求下一地址 Hi”,直至r[Hi] =NULL  (查找不成功)  或r[Hi].key = K  (查找成功) 为止。

查找及性能分析

第一:与装填因子有关

  所谓装填因子是指哈希表中己存入的元素个数n与哈希表的大小m的比值,即=n/m。

 当越小时,发生冲突的可能性越小,越大(最大为1)时,发生冲突的可能性就越大。

第二:与所构造的哈希函数有关

   若哈希函数选择得当,就可使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生。否则,若哈希函数选择不当,就可能使哈希地址集中于某些区域,从而加大冲突的发生。

第三:与解决冲突的哈希冲突函数有关

  哈希冲突函数选择的好坏也将减少或增加发生冲突的可能性。 

五、java模拟实现

class Node {// 节点数据结构
	private Object value;// 节点的值
	private Node next;// 链表中指向下一结点的引用

	/* 提供了常见的操作 */
	public Node(Object value) {
		this.value = value;
	};

	public Object getValue() {
		return value;
	}

	public Node getNext() {
		return next;
	}

	public void setNext(Node next) {
		this.next = next;
	}
}

public class MyHashSet {// Hash数据结构
	private Node[] array;// 存储数据链表的数组
	private int size = 0;// 表示集合中存放的对象的数目

	public MyHashSet(int length) {
		array = new Node[length];// 创建数组
	}

	public int size() {
		return size;
	}

	private static int hash(Object o) { // 根据对象的哈希码得到一个优化的哈希码,
		// 算法参照java.util.HashMap的hash()方法
		int h = o.hashCode();
		h += ~(h << 9);
		h ^= (h >>> 14);
		h += (h << 4);
		h ^= (h >>> 10);
		return h;
	}

	private int indexFor(int hashCode) { // 根据Hash码得到其索引位置
		// 算法参照java.util.HashMap的indexFor()方法
		return hashCode & (array.length - 1);
	}

	public void add(Object value) {// 把对象加入集合,不允许加入重复的元素
		int index = indexFor(hash(value));// 先根据value得到index
		System.out.println("index:" + index + " value:" + value);
		Node newNode = new Node(value);// 由value创建一个新节点newNode
		Node node = array[index];// 由index得到一个节点node
		if (node == null) {// 若这个由index得到的节点是空,则将新节点放入其中
			array[index] = newNode;
			size++;
		} else {// 若不为空则遍历这个点上的链表(下一个节点要等于空或者该节点不等于新节点的值--不允许重复)
			Node nextNode;
			while (!node.getValue().equals(value)&& (nextNode = node.getNext()) != null) {
				node = nextNode;
			}
			if (!node.getValue().equals(value)) {// 若值不相等则加入新节点
				node.setNext(newNode);
				size++;
			}
		}
	}

	public boolean contains(Object value) {
		int index = indexFor(hash(value));
		Node node = array[index];
		while (node != null && !node.getValue().equals(value)) {
			node = node.getNext();
		}// 横向查找
		if (node != null && node.getValue().equals(value)) {
			return true;
		} else {
			return false;
		}
	}

	public boolean remove(Object value) {
		int index = indexFor(hash(value));
		Node node = array[index];
		if (node != null && node.getValue().equals(value)) {// 若是第一个节点就是要remove的
			array[index] = node.getNext();
			size--;
			return true;
		}
		Node lastNode = null;
		while (node != null && !node.getValue().equals(value)) {// 若不是第一个节点就横向查找
			lastNode = node;// 记录上一个节点
			node = node.getNext();
		}
		if (node != null && node.getValue().equals(value)) {
			lastNode.setNext(node.getNext());
			size--;
			return true;
		} else {
			return false;
		}
	}

	public Object[] getAll() {
		Object[] values = new Object[size];
		int index = 0;
		for (int i = 0; i < array.length; i++) {
			Node node = array[i];
			while (node != null) {
				values[index++] = node.getValue();
				node = node.getNext();
			}
		}
		return values;
	}

	public static void main(String[] args) {
		MyHashSet set = new MyHashSet(6);
		Object[] values = { "Tom", "Mike", "Mike", "Jack", "Mary", "Linda",
				"Rose", "Jone" };
		for (int i = 0; i < values.length; i++) {
			set.add(values[i]);
		}
		set.remove("Mary");
		System.out.println("size=" + set.size());
		values = set.getAll();
		for (int i = 0; i < values.length; i++) {
			System.out.println(values[i]);
		}
		System.out.println(set.contains("Jack"));
		System.out.println(set.contains("Linda"));
		System.out.println(set.contains("Jane"));
	}
}

六、java对哈希表支持 Java.util.Hashtable

Java.util.Hashtable提供了种方法让用户使用哈希表,而不需要考虑其哈希表真正如何工作。
  哈希表类中提供了三种构造方法,分别是:
  public Hashtable()
  public Hashtable(int initialcapacity)
  public Hashtable(int initialCapacity,float loadFactor)
  参数initialCapacity是Hashtable的初始容量,它的值应大于0。loadFactor又称装载因子,是一个0.0到0.1之间的float型的浮点数。它是一个百分比,表明了哈希表何时需要扩充,例如,有一哈希表,容量为100,而装载因子为0.9,那么当哈希表90%的容量已被使用时,此哈希表会自动扩充成一个更大的哈希表。如果用户不赋这些参数,系统会自动进行处理,而不需要用户操心。
  Hashtable提供了基本的插入、检索等方法。
  插入
  public synchronized void put(Object key,Object value)
给对象value设定一关键字key,并将其加到Hashtable中。若此关键字已经存在,则将此关键字对应的旧对象更新为新的对象Value。这表明在哈希表中相同的关键字不可能对应不同的对象(从哈希表的基本思想来看,这也是显而易见的)。
  检索
  public synchronized Object get(Object key)
  根据给定关键字key获取相对应的对象。
  public synchronized boolean containsKey(Object key)
  判断哈希表中是否包含关键字key。
  public synchronized boolean contains(Object value)
  判断value是否是哈希表中的一个元素。
  删除
  public synchronized object remove(object key)
  从哈希表中删除关键字key所对应的对象。
  public synchronized void clear()
  清除哈希表
  另外,Hashtalbe还提供方法获取相对应的枚举集合:
  public synchronized Enumeration keys()
  返回关键字对应的枚举对象。
  public synchronized Enumeration elements()
  返回元素对应的枚举对象。

import java.util.Hashtable;
import java.util.Enumeration;

public class HashApp {
	public static void main(String args[]) {
		Hashtable hash = new Hashtable(2, (float) 0.8);
		// 创建了一个哈希表的对象hash,初始容量为2,装载因子为0.8

		hash.put("Jiangsu", "Nanjing");
		// 将字符串对象“Jiangsu”给定一关键字“Nanjing”,并将它加入hash
		hash.put("Beijing", "Beijing");
		hash.put("Zhejiang", "Hangzhou");

		System.out.println("The hashtable hash1 is: " + hash);
		System.out.println("The size of this hash table is " + hash.size());
		// 打印hash的内容和大小

		Enumeration enum1 = hash.elements();
		System.out.print("The element of hash is: ");
		while (enum1.hasMoreElements())
			System.out.print(enum1.nextElement() + " ");
		System.out.println();
		// 依次打印hash中的内容
		if (hash.containsKey("Jiangsu"))
			System.out.println("The capatial of Jiangsu is "
					+ hash.get("Jiangsu"));
		hash.remove("Beijing");
		// 删除关键字Beijing对应对象
		System.out.println("The hashtable hash2 is: " + hash);
		System.out.println("The size of this hash table is " + hash.size());
	}
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!