# 雜湊表(Hash Table) **Google上機題** 有一個公司,當有新的員工來報導時,要求將該員工的資訊加入(id,性別,年齡,住址..),當輸入該員工的id時,要求查找到該員工的所有資訊。 要求:不使用資料庫,儘量節省記憶體,速度越快越好 = 雜湊表 後面我們會來實現這個情境。 ## 基本介紹 雜湊表是根據 **鍵(key)** 直接查詢在記憶體儲存位置的資料結構。 * 鍵 如:剛剛舉例的 id 它通過把鍵值映射到表中一個位置來訪問記錄,以加快搜尋的速度。 這個映射函數叫做雜湊函數,存放記錄的陣列叫做雜湊表。 一個通俗的例子是,為了查找電話簿中某人的號碼,可以創建一個按照人名首字母順序排列的表(即建立人名 `x` 到首字母 `F(x)` 的一個函數關係),在首字母為W的表中查找「王」姓的電話號碼,顯然比直接查找就要快得多。這裡使用**人名**作為關鍵字,「取首字母」是這個例子中雜湊函數的函數法則`F()`,存放首字母的表對應雜湊表。 關鍵字和函數法則理論上可以任意確定。 ## 應用場景 我們常常會需要去資料庫抓取資料然後返回,但有些數據沒有必要每次都去資料庫抓取,所以一般來說會加一個cache,將常用的數據存在cache,直接在cache抓取數據。 通常來說我們可以用常見的Redis、Memcache來實現,但我們也可以自己寫出,比如利用雜湊表,可以把數據先放入到雜湊表中,取數據先在雜湊表中取,這樣取數據的時候便無須到資料庫取,節省了部分時間。 ![](https://i.imgur.com/wNJxret.png) 雜湊表常見的結構: 1. 陣列 + 鏈結串列 2. 陣列 + 二元樹 ## 舉例說明 因為目前還沒提到二元樹的部分,所以我們雜湊表的實現是以陣列加鏈結串列為主。 陣列放的是鏈結串列 => 鏈結串列陣列 用雜湊函數得到這個數據在哪個鏈結串列。 比如這張圖,有 16 個鏈結串列,我要找到 `111` 這個值,只需要 `111 % 16 = 15` 就能知道 111 在第十五個鏈結串列中。 ![](https://i.imgur.com/jQNajgJ.png) ## 思路分析 我們用這張圖來理解使用雜湊表來管理員工資訊至少需要三個class 員工節點 Emp , 鏈結串列 EmpLinkedList , 雜湊表 HashTab ![](https://i.imgur.com/CcgcYxy.png) ## 程式碼實現 節點類 ```java= class Emp { public int id; public String name; public Emp next; // 默認為null public Emp(int id, String name) { super(); this.id = id; this.name = name; } } ``` 鏈結串列類 ```java= class EmpLinkedList { // 頭節點 指向第一個 Emp, 這個鏈結串列的 head 是直接指向第一個員工 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; } // 遍歷鏈結串列的員工資訊 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; } // 遍歷結束沒找到 退出 if(curEmp.next == null) { curEmp = null; break; } curEmp = curEmp.next; } return curEmp; } } ``` 雜湊表 ```java= //Hash Table 管理多條鏈結串列 class HashTable { private EmpLinkedList[] empLinkedListArray; private int size; /** * * @param size 多少條鏈結串列 */ public HashTable(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) { int empLinkedListNum = hashFun(emp.id); // 將 emp 添加到對應鏈結串列中 empLinkedListArray[empLinkedListNum].add(emp); } // 遍歷所有鏈結串列, 遍歷hash table public void list() { for(int i = 0; i < size; i++) { empLinkedListArray[i].list(i); } } // 根據id搜尋員工 public void findEmpById(int id) { // 使用雜湊函數確定到哪條鏈結串列搜尋 int empLinkedListNum = hashFun(id); Emp emp = empLinkedListArray[empLinkedListNum].findEmpById(id); if(emp != null) { System.out.printf("在第 %d 條鏈結串列中找到員工id = %d , name = %s a\n",(empLinkedListNum + 1), id, emp.name); } else { System.out.println("在雜湊表中沒有找到該員工資訊"); } } // 編寫雜湊函數, 使用簡單的取模法 public int hashFun(int id) { return id % size; } } ``` 主程式創建雜湊表和菜單 ```java= public class HashTabDemo { public static void main(String[] args) { // 創建雜湊表 HashTable hashTab = new HashTable(7); String key = ""; Scanner sc = new Scanner(System.in); while(true) { System.out.println("a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統"); key = sc.next(); switch (key) { case "a": System.out.println("輸入員工id: "); int id = sc.nextInt(); System.out.println("輸入員工名字: "); String name = sc.next(); Emp emp = new Emp(id, name); hashTab.add(emp); break; case "l": hashTab.list(); break; case "f": System.out.println("請輸入要搜尋的id: "); id = sc.nextInt(); hashTab.findEmpById(id); break; case "e": sc.close(); System.out.println("退出系統"); System.exit(0); default: break; } } } } ``` output ```java= a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 a 輸入員工id: 1 輸入員工名字: sandy a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 l 第 1 鏈結串列為空 第 2 鏈結串列資訊為 => id = 1 , name = sandy 第 3 鏈結串列為空 第 4 鏈結串列為空 第 5 鏈結串列為空 第 6 鏈結串列為空 第 7 鏈結串列為空 a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 a 輸入員工id: 8 輸入員工名字: tom a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 l 第 1 鏈結串列為空 第 2 鏈結串列資訊為 => id = 1 , name = sandy => id = 8 , name = tom 第 3 鏈結串列為空 第 4 鏈結串列為空 第 5 鏈結串列為空 第 6 鏈結串列為空 第 7 鏈結串列為空 a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 f 請輸入要搜尋的id: 8 在第 2 條鏈結串列中找到員工id = 8 , name = tom a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 e 退出系統 ``` ### 刪除節點 老師留下了刪除員工資訊的作業。 `LinkedList` 類寫一個刪除方法,主要需要使用兩個索引 temp, prev 來遍歷搜尋和刪除,temp 用於指向當前節點,prev指向當前的前一個節點,當 temp 遍歷找到要刪除的節點時,prev.next指向temp.next即能刪除節點。(這部分和雙向鏈結串列有關) ```java= // 刪除元素 public Emp delById(int id) { if (head == null) { return null; } // 非空的鏈結串列才需要循環 Emp temp = head; Emp prev = head; while (true) { if (temp.id == id) { // 找到要刪除的元素 break; } if (temp.next == null) { // 當前節點的為最末尾 temp = null; break; } prev = temp; // 將 prev 指向當前節點 temp = temp.next; // 將當前節點後移 繼續遍歷 } // 沒有找到要刪除的元素 if (temp == null) { return null; } // head 為要刪除的節點 if (head == temp) { head = temp.next; return temp; } // 找到要刪除的元素 temp, 將 prev.next 指向 temp.next prev.next = temp.next; return temp; } ``` 在`HashTab` 類中也要加上刪除方法,用於接收要刪除的id,然後判斷該id屬於哪個鏈結串列之後去調用`LinkedList`類的delById方法。 ```java= // 刪除員工資訊 public Emp delById(int id) { // 先找到 id 所屬的鏈結串列 int no = hashFun(id); // 判斷 no 值是否大於或小於範圍 if (no > size || no < 0) { System.out.printf("id = %d 範圍錯誤\n", id); return null; } Emp emp = empLinkedListArray[no].delById(id); if (emp == null) { System.out.printf("在第 %d 條鏈結串列中沒找到 id = %d 的員工,删除失敗!! \n", no, id); } else { System.out.printf("在第 %d 條鏈結串列中找到 id = %d 的員工, name = %s , 删除成功!! \n", no, id, emp.name); } return emp; } ``` 來試著跑跑看 ```java= public static void main(String[] args) { // 創建雜湊表 HashTable hashTab = new HashTable(7); String key = ""; Scanner sc = new Scanner(System.in); while(true) { System.out.println("a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統"); key = sc.next(); switch (key) { case "a": System.out.println("輸入員工id: "); int id = sc.nextInt(); System.out.println("輸入員工名字: "); String name = sc.next(); Emp emp = new Emp(id, name); hashTab.add(emp); break; case "l": hashTab.list(); break; case "f": System.out.println("請輸入要搜尋的id: "); id = sc.nextInt(); hashTab.findEmpById(id); break; case "d": System.out.println("請輸入要刪除的id: "); id = sc.nextInt(); hashTab.delById(id); break; case "e": sc.close(); System.out.println("退出系統"); System.exit(0); default: break; } } } ``` output ```java= a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 a 輸入員工id: 1 輸入員工名字: tom a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 a 輸入員工id: 2 輸入員工名字: qq a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 a 輸入員工id: 8 輸入員工名字: sandy a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 f 請輸入要搜尋的id: 2 在第 3 條鏈結串列中找到員工id = 2 , name = qq a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 l 第 1 鏈結串列為空 第 2 鏈結串列資訊為 => id = 1 , name = tom => id = 8 , name = sandy 第 3 鏈結串列資訊為 => id = 2 , name = qq 第 4 鏈結串列為空 第 5 鏈結串列為空 第 6 鏈結串列為空 第 7 鏈結串列為空 a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 d 請輸入要刪除的id: 8 在第 1 條鏈結串列中找到 id = 8 的員工, name = sandy , 删除成功!! a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 l 第 1 鏈結串列為空 第 2 鏈結串列資訊為 => id = 1 , name = tom 第 3 鏈結串列資訊為 => id = 2 , name = qq 第 4 鏈結串列為空 第 5 鏈結串列為空 第 6 鏈結串列為空 第 7 鏈結串列為空 a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 d 請輸入要刪除的id: 2 在第 2 條鏈結串列中找到 id = 2 的員工, name = qq , 删除成功!! a(add): 添加員工資訊 | f(find): 搜尋員工資訊 | d(delete): 刪除員工資訊 | l(list): 顯示員工資訊 | e(exit): 退出系統 l 第 1 鏈結串列為空 第 2 鏈結串列資訊為 => id = 1 , name = tom 第 3 鏈結串列為空 第 4 鏈結串列為空 第 5 鏈結串列為空 第 6 鏈結串列為空 第 7 鏈結串列為空 ```