玩轉資料結構(21)-- 雜湊表
雜湊表
一、雜湊表基礎
從習題入手【題目連結】
思路:可以不使用樹結構來實現對映,可以直接設定包含 26個 元素的陣列,對陣列中每一位表示某一個字元對應的頻率即可;索引為 0 的位置表示 a ,索引為 1 的位置表示 b ,以此類推。
雜湊表定義:把所關心的鍵通過雜湊函式轉換為索引,然後直接把內容存在陣列中即可
程式碼:
class Solution { public int firstUniqChar(String s) { int[] freq = new int[26]; for(int i = 0 ; i < s.length() ; i ++) freq[s.charAt(i) - 'a'] ++; for(int i = 0 ; i < s.length() ; i ++) if(freq[s.charAt(i) - 'a'] == 1) return i; return -1; } }
輸出:
int[26] freq 就是一個雜湊表,每一個字元都和一個索引相對應
雜湊表要解決的關鍵問題:1.雜湊函式的設計 2. 如何解決雜湊衝突 【核心思想:空間換時間;雜湊表是時間和空間之間的平衡】
雜湊表就是一種以 鍵-值(key-indexed) 儲存資料的結構,我們只要輸入待查詢的值即key,即可查詢到其對應的值。【詳解】
雜湊的思路很簡單,如果所有的鍵都是整數,那麼就可以使用一個簡單的無序陣列來實現:將鍵作為索引,值即為其對應的值,這樣就可以快速訪問任意鍵的值。這是對於簡單的鍵的情況,我們將其擴充套件到可以處理更加複雜的型別的鍵。
使用雜湊查詢有兩個步驟:
- 使用雜湊函式將被查詢的鍵轉換為陣列的索引。在理想的情況下,不同的鍵會被轉換為不同的索引值,但是在有些情況下我們需要處理多個鍵被雜湊到同一個索引值的情況。所以雜湊查詢的第二個步驟就是處理衝突
- 處理雜湊碰撞衝突。有很多處理雜湊碰撞衝突的方法,如拉鍊法和線性探測法。
雜湊表是一個在時間和空間上做出權衡的經典例子。如果沒有記憶體限制,那麼可以直接將鍵作為陣列的索引。那麼所有的查詢時間複雜度為O(1);如果沒有時間限制,那麼我們可以使用無序陣列並進行順序查詢,這樣只需要很少的記憶體。雜湊表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整雜湊函式演算法即可在時間和空間上做出取捨。
二、雜湊函式的設計
“鍵” 通過雜湊函式得到的 “索引” 分佈越均勻越好
1.整型
小範圍正整數直接使用
小範圍負整數進行偏移
大整數:
2.浮點型
在計算機中都是 32位 或 64位 的二進位制表示,只不過計算機解析成了浮點數
轉成整型處理,再用前面的 取模 方法來處理
3.字串
方法:轉成 整型 處理
4.複合型別
方法:轉成 整型 處理
雜湊函式設計原則:、
1.一致性:如果 a == b , 則 hash(a) == hash(b)
2.高效性:計算高效簡便
3.均勻性:雜湊值均勻分佈
三、Java中的 hashCode 方法
檢視 Java 中不同型別的雜湊值計算方法
Main.java
import java.util.HashSet;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
int a = 42;
System.out.println(((Integer)a).hashCode());
int b = -42;
System.out.println(((Integer)b).hashCode());
double c = 3.1415926;
System.out.println(((Double)c).hashCode());
String d = "imooc";
System.out.println(d.hashCode());
System.out.println(Integer.MAX_VALUE + 1);
System.out.println();
Student student = new Student(3, 2, "Bobo", "Liu");
System.out.println(student.hashCode());
HashSet<Student> set = new HashSet<>();
set.add(student);
HashMap<Student, Integer> scores = new HashMap<>();
scores.put(student, 100);
Student student2 = new Student(3, 2, "Bobo", "Liu");
System.out.println(student2.hashCode());
}
}
自定義類:Student.java
public class Student { //複合的資料型別
int grade;
int cls;
String firstName;
String lastName;
Student(int grade, int cls, String firstName, String lastName){
this.grade = grade;
this.cls = cls;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public int hashCode(){
int B = 31;
int hash = 0;
hash = hash * B + ((Integer)grade).hashCode();
hash = hash * B + ((Integer)cls).hashCode();
hash = hash * B + firstName.toLowerCase().hashCode();
hash = hash * B + lastName.toLowerCase().hashCode();
return hash;
}
@Override
public boolean equals(Object o){
if(this == o)
return true;
if(o == null)
return false;
if(getClass() != o.getClass())
return false;
Student another = (Student)o;
return this.grade == another.grade &&
this.cls == another.cls &&
this.firstName.toLowerCase().equals(another.firstName.toLowerCase()) &&
this.lastName.toLowerCase().equals(another.lastName.toLowerCase());
}
}
輸出:
四、雜湊衝突的處理--鏈地址法 (Seperate Chaining)
1.將 元素k1 轉換為 雜湊表中對應的索引值
2.假如計算後 k1 的索引對應是 4 的話,在雜湊表索引為 4 的位置儲存 k1
3.再加入元素 k2 ,則用相同的方法計算出索引值 為 1,插入到雜湊表中
4.再加入元素 k3 ,則用相同的方法計算出索引值 為 1,產生雜湊衝突,但仍然將其插入到雜湊表中,將其作為 連結串列 即可
鏈地址法:對整個雜湊表,開闢 m 個空間,對於每一個空間,由於有雜湊衝突的存在,其本質上都是儲存 一個連結串列(查詢表),查詢表的底層實現不一定是連結串列,也可以使用樹結構,如圖
(HashMap 本質就是一個 TreeMap 陣列 / HashSet 本質就是一個 TreeSet 陣列)
【Java8 之前,每一個位置對應一個連結串列,Java8 開始,當雜湊衝突達到一定程度,每一個位置從連結串列轉成紅黑樹】
五、實現屬於我們自己的雜湊表
程式碼實現:
HashTable.java
import java.util.TreeMap;
public class HashTable<K, V> {
private TreeMap<K, V>[] hashtable;
private int size;
private int M;
public HashTable(int M){
this.M = M;
size = 0;
hashtable = new TreeMap[M];
for(int i = 0 ; i < M ; i ++)
hashtable[i] = new TreeMap<>();
}
public HashTable(){
this(97);
}
private int hash(K key){
return (key.hashCode() & 0x7fffffff) % M;
}
public int getSize(){
return size;
}
public void add(K key, V value){ //增
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key))
map.put(key, value);
else{
map.put(key, value);
size ++;
}
}
public V remove(K key){ //刪
V ret = null;
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key)){
ret = map.remove(key);
size --;
}
return ret;
}
public void set(K key, V value){ //改
TreeMap<K, V> map = hashtable[hash(key)];
if(!map.containsKey(key))
throw new IllegalArgumentException(key + " doesn't exist!");
map.put(key, value);
}
public boolean contains(K key){ //查
return hashtable[hash(key)].containsKey(key);
}
public V get(K key){
return hashtable[hash(key)].get(key);
}
}
測試時間複雜度:Main.java
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
// Collections.sort(words);
// Test BST
long startTime = System.nanoTime();
BST<String, Integer> bst = new BST<>();
for (String word : words) {
if (bst.contains(word))
bst.set(word, bst.get(word) + 1);
else
bst.add(word, 1);
}
for(String word: words)
bst.contains(word);
long endTime = System.nanoTime();
double time = (endTime - startTime) / 1000000000.0;
System.out.println("BST: " + time + " s");
// Test AVL
startTime = System.nanoTime();
AVLTree<String, Integer> avl = new AVLTree<>();
for (String word : words) {
if (avl.contains(word))
avl.set(word, avl.get(word) + 1);
else
avl.add(word, 1);
}
for(String word: words)
avl.contains(word);
endTime = System.nanoTime();
time = (endTime - startTime) / 1000000000.0;
System.out.println("AVL: " + time + " s");
// Test RBTree
startTime = System.nanoTime();
RBTree<String, Integer> rbt = new RBTree<>();
for (String word : words) {
if (rbt.contains(word))
rbt.set(word, rbt.get(word) + 1);
else
rbt.add(word, 1);
}
for(String word: words)
rbt.contains(word);
endTime = System.nanoTime();
time = (endTime - startTime) / 1000000000.0;
System.out.println("RBTree: " + time + " s");
// Test HashTable
startTime = System.nanoTime();
// HashTable<String, Integer> ht = new HashTable<>();
HashTable<String, Integer> ht = new HashTable<>(131071);
for (String word : words) {
if (ht.contains(word))
ht.set(word, ht.get(word) + 1);
else
ht.add(word, 1);
}
for(String word: words)
ht.contains(word);
endTime = System.nanoTime();
time = (endTime - startTime) / 1000000000.0;
System.out.println("HashTable: " + time + " s");
}
System.out.println();
}
}
輸出:
六、雜湊表的動態空間處理與複雜度分析
總共有 M 個地址,如果放入雜湊表的元素為 N,時間複雜度:
1.如果每個地址是連結串列 :O(N/M);最壞情況:O(n)【全部發生雜湊衝突】
2.如果每個地址是平衡樹:O(log(N/M));最壞情況:O(log n)【全部發生雜湊衝突】
如何讓時間複雜度變為 O(1) :1.陣列應該是動態記憶體分配,空間隨著 n 的改變可以進行自適應(使用 resize 方法)
resize 方法:
平均每個地址承載的元素多過一定的程度,即擴容【N/M >= upperTol】
平均每個地址承載的元素少過一定的程度,即擴容【N/M < lowerTol】
HashTable.java
import java.util.Map;
import java.util.TreeMap;
public class HashTable<K, V> {
private static final int upperTol = 10; //上限
private static final int lowerTol = 2; //下限
private static final int initCapacity = 7; //初始雜湊表容量
private TreeMap<K, V>[] hashtable;
private int size;
private int M;
public HashTable(int M){
this.M = M;
size = 0;
hashtable = new TreeMap[M];
for(int i = 0 ; i < M ; i ++)
hashtable[i] = new TreeMap<>();
}
public HashTable(){
this(initCapacity);
}
private int hash(K key){
return (key.hashCode() & 0x7fffffff) % M;
}
public int getSize(){
return size;
}
public void add(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key))
map.put(key, value);
else{
map.put(key, value);
size ++;
if(size >= upperTol * M) //超過上限,進行擴容
resize(2 * M);
}
}
public V remove(K key){
V ret = null;
TreeMap<K, V> map = hashtable[hash(key)];
if(map.containsKey(key)){
ret = map.remove(key);
size --;
if(size < lowerTol * M && M / 2 >= initCapacity) //小於下限,進行縮容
resize(M / 2);
}
return ret;
}
public void set(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if(!map.containsKey(key))
throw new IllegalArgumentException(key + " doesn't exist!");
map.put(key, value);
}
public boolean contains(K key){
return hashtable[hash(key)].containsKey(key);
}
public V get(K key){
return hashtable[hash(key)].get(key);
}
private void resize(int newM){
TreeMap<K, V>[] newHashTable = new TreeMap[newM];
for(int i = 0 ; i < newM ; i ++)
newHashTable[i] = new TreeMap<>();
int oldM = M;
this.M = newM; //注意更新M
for(int i = 0 ; i < oldM ; i ++){
TreeMap<K, V> map = hashtable[i];
for(K key: map.keySet())
newHashTable[hash(key)].put(key, map.get(key));
}
this.hashtable = newHashTable;
}
}
雜湊表的複雜度分析:
知雜湊表均攤時間複雜度為:O(1)
程式碼中的Bug: Comparable(可比較性矛盾)
七、雜湊表更復雜的動態空間處理方法
擴容 M--2*M ,但 2*M 可能不是 素數 了,但希望 雜湊表 的空間最好是 素數;
解決方法:不要簡單的 *2,而是根據下表,超過 53 就擴容為 97 ,超過 97 就擴容為 193,以此類推。
八、更多雜湊衝突的處理方法
1.開放地址法
在鏈地址法中,每一個鍵計算出雜湊值之後,雜湊值索引所在的地址只屬於所屬的雜湊值等於這個索引相應的元素【封閉地址】
開放地址法和上述方法相反,對於雜湊表中的每一個地址,所有雜湊值的元素都有機會進入
每一個地址就是直接存元素,11 和 31 產生雜湊衝突,就從索引為 1 的地方向下找,空的地方是索引為 2 的地方,故將 31 放在 2 中;
再插入元素 81,發生雜湊衝突,原理同上,插入到索引 3 的位置【此方法:線性探測,遇到雜湊衝突 +1】
平方探測:遇到雜湊衝突 +1 +4 + 9 ...【提高效能】
二次雜湊:遇到雜湊衝突 +hash2(key)
更多方法: