雜湊技術
雜湊技術的基本思想
把記錄(元素)的儲存位置和該記錄的關鍵字的值之間建立一種對映關係。關鍵字的值在這種對映關係下的像,就是相應記錄在表中的儲存位置。雜湊技術在理想情況下,無需任何比較就可以找到待查的關鍵字,其查詢的期望時間為O(1)。

雜湊技術的相關概念
雜湊函式:
設 U 表示所有可能出現的關鍵字集合,K表示實際出現(實際儲存)的關鍵字集合,即K 屬於U,F[B – 1]是一個數組,其中B =O( | K| )。則,從 U 到表F[ B – 1]下標集合上的一個對映 h:U->{ 0, 1, 2, …,B –1}稱為雜湊函式(雜湊函式,雜湊函式)
散列表
陣列 F 稱為散列表(Hash表,雜湊表)。陣列 F 中的每個單元稱為桶(bucket)。
雜湊地址
對於任意關鍵字k∈U,函式值h ( k )稱為 k 的雜湊地址(Hash地址,雜湊值,儲存地址,桶號)

雜湊衝突(碰 撞)
將結點(記錄)按其關鍵字的雜湊地址儲存到散列表中的過程稱為雜湊。 (collision)不同的關鍵字具有相同雜湊地址的現象稱為雜湊衝突(碰 撞)。而發生衝突的兩個關鍵字稱為同義詞(synonym)。

雜湊函式的構造
雜湊函式的構造的原則:
- 計算簡單:雜湊函式不應該有很大的計算量,否則會降低查詢效率。
- 分佈均勻:雜湊函式值即雜湊地址,要儘量均勻分佈在地址空間,這樣才能保證儲存空間的有效利用並減少衝突。
雜湊函式的構造方法
-
直接定址法
雜湊函式是關鍵字值的線性函式,即:h(key) = a * key + b (a,b為常數)
示例:關鍵字的取值集合為{10, 30, 50, 70, 80, 90},選取的雜湊函式為h(key)=key/10,則散列表為:

適用情況:事先知道關鍵字的值,關鍵字取值集合不是很、大且連續性較好。 -
質數除餘法
雜湊函式為:h(key)=key % m
一般情況下,選m為小於或等於表長B的最大質數。示例:關鍵字的取值集合為{14, 21, 28, 35, 42, 49, 56, 63},表長B=12。則選取m=11,雜湊函式為h(key)=key % 11,則散列表為:

適用情況:質數除餘法是一種最簡單、也是最常用的構造雜湊函式的方法,並且不要求事先知道關鍵碼的分佈。 -
平方取中法
取 key^2 的中間的幾位數作為雜湊地址
擴大相近數的差別,然後根據表長取中間幾位作為雜湊值,使地址值與關鍵字的每一位都相關。
雜湊地址的位數要根據B來確定,有時要設一個比例因子,限制地址越界。

適用情況:事先不知道關鍵碼的分佈且關鍵碼的位數不是很大 -
摺疊法
若關鍵字位數較多,可根據B的位數將關鍵字分割成位數相同的若干段(最後一段位數可以不同),然後將各段疊加和(捨去進位)作為雜湊地址。
示例:圖書編號: 0-442-20586-4

適用情況:關鍵碼位數很多,事先不知道關鍵碼的分
布。 -
數字分析法
根據關鍵字值在各個位上的分佈情況,選取分佈比較均勻的若干位組成雜湊地址。
示例:假設散列表長為100,可取中間四 位中的兩位為雜湊地址。

適用情況:若事先知道關鍵字集合,且關鍵字的位數比散列表的地址位數多
小結:構造Hash函式應注意以下幾個問題:
計算簡單
- 計算Hash函式所需時間
分佈均勻:
- 關鍵字的長度
- 散列表的大小
- 關鍵字的分佈情況
- 記錄的查詢頻率
衝突處理的方法
開放定址法
基本思想:
當衝突發生時,使用某些探測技術在散列表中形成一個探測序列,沿此序列逐個單元查詢,直到找到給定的關鍵字或者碰到一個開放地址(即該空的地址單元、空桶)或者既未找到給定的關鍵字也沒碰到一個開放地址為止。
常用的探測技術----如何尋找下一個空的雜湊地址?
- 線性探測法
- 線性補償探測法
- 二次探測法
- 隨機探測法
線性探測法(c=1)
基本思想:當發生衝突時,從衝突位置的下一個位置起,依次尋找空的雜湊地址。
探測序列:設關鍵字值key的雜湊地址為h(key),閉散列表的長度為B,則發生衝突時,尋找下一個雜湊地址的公式為:hi=(h(key)+di) % B (di=1,2,…,m-1)
示例:關鍵字取值集合為 {47, 7, 29, 11, 16, 92, 22, 8, 3},雜湊函式為h(key)=key %11,用線性探測法處理衝突,則散列表為:

線性補償探測法:
當發生衝突時,尋找下一個雜湊地址的公式為:hi=(h(key)+di) % B (di =1c,2c,…)
二次探測法:
當發生衝突時,尋找下一個雜湊地址的公式為:
hi=(h(key)+di) % B (di=12,-12,22,-22,…,q2,-q2且q≤B/2)
隨機探測法
當發生衝突時,下一個雜湊地址的位移量是一個隨機數列,即尋找下一個雜湊地址的公式為:hi=(h(key)+di) % B (其中,d1, d2, …,dB-1是1, 2, …, B-1的隨機序列。)
注意:插入、刪除和查詢時,要使用同一個隨機序列。
閉散列表:用開放定址法處理衝突得到的散列表叫閉散列表.
堆積現象:在處理衝突的過程中出現的非同義詞之間對同一個雜湊地址爭奪的現象。
帶溢位表的內雜湊法
基本思想:擴充散列表中的每個桶,形成帶溢位表的散列表。每個桶包括兩部分:一部分是主表元素;另一部分或者為空或者由一個連結串列組成溢位表,其首結點的指標存入主表的鏈域。主表元素的型別與溢位表的型別相同。
示例:關鍵字取值集合為 {47, 7, 29, 11, 16, 92, 22, 8, 3},雜湊函式為h(key)=key %11,用帶溢位表的內雜湊法處理衝突,則散列表為:

特點:
- 主表及其溢位表元素的雜湊地址相同。
- 空間利用率不高;
拉鍊法(鏈地址法)
基本思想:將所有雜湊地址相同的記錄,即所有同義詞的記錄儲存在一個單鏈表中(稱為同義詞子表),在散列表中儲存的是所有同義詞子表的頭指標(桶)。
示例:關鍵字取值集合為 {47, 7, 29, 11, 16, 92, 22, 8, 3},雜湊函式為h(key)=key%11,用鏈地址法處理衝突,則散列表為:

開散列表: 用拉鍊法處理衝突構造的散列表叫做開散列表。
雜湊查詢的效能分析
由於衝突的存在,產生衝突後的查詢仍然是給定值與關鍵碼進行比較的過程。
在查詢過程中,關鍵碼的比較次數取決於產生衝突的概率。
而影響衝突產生的因素有:
- 雜湊函式是否均勻
- 處理衝突的方法
- 散列表的裝載因子 α=表中填入的記錄數/表的長度
幾種不同處理衝突方法的平均查詢長度

開散列表與閉散列表的比較

程式碼實現帶溢位表的內雜湊法
import java.util.Scanner;
/**
* @anthor longzx
* @create 2021 01 30 16:19
* @Description
**/
public class HashTabDemo {
public static void main(String[] args) {
//建立雜湊表
HashTab hashTab = new HashTab(7);
//寫一個簡單的選單
String key = "";
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("add: 新增僱員");
System.out.println("del: 刪除僱員");
System.out.println("list: 顯示僱員");
System.out.println("find: 查詢僱員");
System.out.println("exit: 退出系統");
key = scanner.next();
switch (key) {
case "add":
System.out.println("輸入id");
int id = scanner.nextInt();
System.out.println("輸入名字");
String name = scanner.next();
//建立 僱員
Emp emp = new Emp(id, name);
hashTab.add(emp);
break;
case "del":
System.out.println("輸入id");
int id2 = scanner.nextInt();
hashTab.del(id2);
break;
case "list":
hashTab.list();
break;
case "find":
System.out.println("請輸入要查詢的id");
id = scanner.nextInt();
hashTab.findEmpById(id);
break;
case "exit":
scanner.close();
System.exit(0);
default:
break;
}
}
}
}
//建立HashTab 管理多條連結串列
class HashTab {
private EmpLinkedList[] empLinkedListArray;
private int size; //表示有多少條連結串列
//構造器
public HashTab(int size) {
this.size = size;
//初始化empLinkedListArray
empLinkedListArray = new EmpLinkedList[size];
//?留一個坑, 這時不要分別初始化每個連結串列
for(int i = 0; i < size; i++) {
empLinkedListArray[i] = new EmpLinkedList();
}
}
//新增僱員
public void add(Emp emp) {
//根據員工的id ,得到該員工應當新增到哪條連結串列
int empLinkedListNO = hashFun(emp.id);
//將emp 新增到對應的連結串列中
empLinkedListArray[empLinkedListNO].add(emp);
}
//刪除僱員
public void del(int id) {
//根據員工的id ,得到該員工應當新增到哪條連結串列
int empLinkedListNO = hashFun(id);
//將emp 新增到對應的連結串列中
empLinkedListArray[empLinkedListNO].delete(id);
}
//遍歷所有的連結串列,遍歷hashtab
public void list() {
for(int i = 0; i < size; i++) {
empLinkedListArray[i].list(i);
}
}
//根據輸入的id,查詢僱員
public void findEmpById(int id) {
//使用雜湊函式確定到哪條連結串列查詢
int empLinkedListNO = hashFun(id);
Emp emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
if(emp != null) {//找到
System.out.printf("在第%d條連結串列中找到 僱員 id = %d\n", (empLinkedListNO + 1), id);
}else{
System.out.println("在雜湊表中,沒有找到該僱員~");
}
}
//編寫雜湊函式, 使用一個簡單取模法
public int hashFun(int id) {
return id % size;
}
}
//建立EmpLinkedList ,表示連結串列
class EmpLinkedList {
//頭指標,執行第一個Emp,因此我們這個連結串列的head 是直接指向第一個Emp
private Emp head; //預設null
//新增僱員到連結串列
//說明
//1. 假定,當新增僱員時,id 是自增長,即id的分配總是從小到大
// 因此我們將該僱員直接加入到本連結串列的最後即可
public void add(Emp emp) {
//如果是新增第一個僱員
if(head == null) {
head = emp;
return;
}
//如果不是第一個僱員,則使用一個輔助的指標,幫助定位到最後
Emp curEmp = head;
while(true) {
if(curEmp.next == null) {//說明到連結串列最後
break;
}
curEmp = curEmp.next; //後移
}
//退出時直接將emp 加入連結串列
curEmp.next = emp;
}
//刪除
//根據僱員ID刪除僱員資訊
public void delete(int id){
if(head == null) { //說明連結串列為空
System.out.println("連結串列為空");
return;
}
if(head.id == id){
head =head.next;
System.out.println("刪除僱員成功");
return;
}
//輔助指標
Emp curEmp = head;
while(true) {
if(curEmp.next.id == id) {//找到
curEmp.next = curEmp.next.next;
System.out.println("刪除僱員成功");
break;//這時curEmp就指向要查詢的僱員
}
//退出
if(curEmp.next == null) {//說明遍歷當前連結串列沒有找到該僱員
System.out.println("未找到待刪除的資訊");
break;
}
curEmp = curEmp.next;//以後
}
}
//遍歷連結串列的僱員資訊
public void list(int no) {
if(head == null) { //說明連結串列為空
System.out.println("第 "+(no+1)+" 連結串列為空");
return;
}
System.out.print("第 "+(no+1)+" 連結串列的資訊為");
Emp curEmp = head; //輔助指標
while(true) {
System.out.printf(" => id=%d name=%s\t", curEmp.id, curEmp.name);
if(curEmp.next == null) {//說明curEmp已經是最後結點
break;
}
curEmp = curEmp.next; //後移,遍歷
}
System.out.println();
}
//根據id查詢僱員
//如果查詢到,就返回Emp, 如果沒有找到,就返回null
public Emp findEmpById(int id) {
//判斷連結串列是否為空
if(head == null) {
System.out.println("連結串列為空");
return null;
}
//輔助指標
Emp curEmp = head;
while(true) {
if(curEmp.id == id) {//找到
break;//這時curEmp就指向要查詢的僱員
}
//退出
if(curEmp.next == null) {//說明遍歷當前連結串列沒有找到該僱員
curEmp = null;
break;
}
curEmp = curEmp.next;//以後
}
return curEmp;
}
}
//表示一個僱員
class Emp {
public int id;
public String name;
public Emp next; //next 預設為 null
public Emp(int id, String name) {
super();
this.id = id;
this.name = name;
}
}
