小橙書閱讀指南(十一)——散列表
阿新 • • 發佈:2018-09-23
sym code pro target delet h+ 素數 ash arr
算法描述:散列表是一種在時間和空間上做出權衡的查找算法。使用查找算法分為兩步。第一步是通過散列函數將被查找的鍵轉化未數組的一個索引。理想情況下,不同的鍵都能轉為不同的索引值。當然,這只是理想情況,所以我們需要面對兩個或多個鍵都被散列到相同索引值的情況。因此,散列查找的第二部就是處理碰撞沖突的過程。
一個比較令人滿意的散列函數能夠均勻並獨立地將所有鍵散布於0到M-1之間。
一、基於拉鏈法的散列表
算法圖示:
拉鏈散列表算法的本質是將哈希值相同的鍵保存在一個普通鏈表中,當我們需要調整數組長度的時候,需要將所有鍵在新的數組中重新散列。
代碼示例:
import java.util.ArrayList;import java.util.List; public class SeparateChainingHashSymbolTable<Key, Value> { private int initCapacity; // 初始散列數組的長度 private int size; // 鍵值總數 private int len; // 散列數組的長度 private SequentialSearchSymbolTable<Key, Value>[] st; // 散列數組 public SeparateChainingHashSymbolTable(intlen) { this.len = len; this.initCapacity = len; st = (SequentialSearchSymbolTable<Key, Value>[]) new SequentialSearchSymbolTable[len]; for (int i = 0; i < len; i++) { st[i] = new SequentialSearchSymbolTable<>(); } } public Value get(Key key) {int h = hash(key); return st[h].get(key); } public boolean contains(Key key) { return get(key) != null; } public void put(Key key, Value val) { // 當包含元素的數量大於散列數組長度10倍時,擴展容量 if (size > 10 * len) { resize(2 * len); } int h = hash(key); if (!contains(key)) { size++; } st[h].put(key, val); } public void delete(Key key) { int h = hash(key); if (contains(key)) { st[h].delete(key); size--; } if (size > initCapacity && size <= 2 * len) { resize(len / 2); } } public Iterable<Key> keys() { List<Key> keys = new ArrayList<>(); for (int i = 0; i < len; i++) { for (Key key : st[i].keys()) { keys.add(key); } } return keys; } private void resize(int capacity) { SeparateChainingHashSymbolTable<Key, Value> nst = new SeparateChainingHashSymbolTable<>(capacity); // 遍歷原先散列表中保存的元素,並重新散列進新的散列表 for (int i = 0; i < len; i++) { for (Key key : st[i].keys()) { nst.put(key, st[i].get(key)); } } this.size = nst.size; this.len = nst.len; this.st = nst.st; } /** * 散列算法 * * @param key * @return */ private int hash(Key key) { return (key.hashCode() & 0x7fffffff) % len; } }
二、基於探測法的散列表
這種算法在處理碰撞的時候並非將所有相同哈希鍵的對象保存在一條鏈表中而是沿數組向後查找並插入到一個空槽中。
算法圖示:
探測法哈希算法的一個比較重要的特征是:當我們需要刪除一個鍵的時候,不能僅僅將數組中對應的位置設置未null,因為這會使得在此位置之後的元素無法被查找。因此,我們需要將簇中被刪除的鍵的右側的所有鍵重新散列計算並插入散列表。這個過程會比較復雜。
代碼示例:
import java.util.ArrayList; import java.util.List; public class LinearProbingHashSymbolTable<Key, Value> { private int size; private int len; private Key[] keys; private Value[] vals; public LinearProbingHashSymbolTable(int capacity) { len = capacity; size = 0; keys = (Key[]) new Object[capacity]; vals = (Value[]) new Object[capacity]; } public void put(Key key, Value val) { // 始終保證元素數量只占數組長度的50% if (size > len / 2) { resize(2 * len); } // 線性碰撞檢測 int h; for (h = hash(key); keys[h] != null; h = (h + 1) % len) { if (key.equals(keys[h])) { vals[h] = val; return; } } keys[h] = key; vals[h] = val; size++; } public Value get(Key key) { for (int h = hash(key); keys[h] != null; h = (h + 1) % len) { if (key.equals(keys[h])) { return vals[h]; } } return null; } public boolean contains(Key key) { return get(key) != null; } public void delete(Key key) { if (!contains(key)) { return; } int h = hash(key); while (!keys[h++].equals(key)) { h = h % len; } keys[h] = null; vals[h] = null; // 由於在刪除了一個鍵之後可能造成查詢的不連續,因此需要對一些鍵重新散列 h = (h + 1) % len; while (keys[h] != null) { // 在被刪除的鍵後至空鍵前的所有鍵重新散列保存 Key nkey = keys[h]; Value nval = vals[h]; keys[h] = null; vals[h] = null; put(nkey, nval); h = (h + 1) % len; // 每次循環size--的目的時抵消put中的size++ size--; } size--; // 當包含的元素數量小於數組長度的12.5%時,按照1/2的比例收縮數組 if (size > 0 && size < len / 8) { resize(len / 2); } } public Iterable<Key> keys() { List<Key> keyList = new ArrayList<>(); for (int i = 0; i < len; i++) { if (keys[i] != null) { keyList.add(keys[i]); } } return keyList; } private int hash(Key key) { return (key.hashCode() & 0x7fffffff) % len; } private void resize(int capacity) { LinearProbingHashSymbolTable<Key, Value> nst = new LinearProbingHashSymbolTable<>(capacity); for (int i = 0; i < size; i++) { if (keys[i] != null) { nst.put(keys[i], vals[i]); } } this.size = nst.size; this.keys = nst.keys; this.vals = nst.vals; } }
相關鏈接:
Algorithms for Java
Algorithms for Qt
小橙書閱讀指南(十一)——散列表