Bảng băm (hash table)
Cập nhật lần cuối
Bảng băm (hash table) lưu các phần tử trong một mảng các bucket. Để đặt hoặc tìm một khóa, bảng băm đưa khóa qua hàm băm và lấy kết quả chia lấy dư cho số bucket, cho ra chỉ số bucket trong O(1). Nhấn Phát ở trên để xem từng khóa được băm vào một bucket và lưu lại, rồi một lần tra cứu nhảy thẳng đến đúng bucket.
Khi hai khóa băm vào cùng một bucket, gọi là xung đột (collision), mô phỏng này dùng phương pháp nối chuỗi (separate chaining): mỗi bucket giữ một danh sách nhỏ, và các khóa xung đột được thêm vào đó. Miễn là bảng không quá đầy (hệ số tải thấp) và hàm băm phân tán khóa tốt, các chuỗi sẽ ngắn và việc chèn, tra cứu, xóa đều là O(1) trung bình.
Độ phức tạp thời gian
| Thao tác | Trung bình | Xấu nhất |
|---|---|---|
| Chèn | O(1) | O(n) (mọi khóa đều xung đột) |
| Tra cứu | O(1) | O(n) |
| Xóa | O(1) | O(n) |
| Không gian | O(n) | O(n) |
Các khái niệm chính
| Thuật ngữ | Ý nghĩa |
|---|---|
| Hàm băm | Ánh xạ một khóa thành chỉ số bucket |
| Xung đột | Hai khóa rơi vào cùng một bucket |
| Nối chuỗi | Mỗi bucket giữ một danh sách các mục bị xung đột |
| Địa chỉ mở | Cách thay thế: dò tìm ô trống tiếp theo |
| Hệ số tải | số mục / số bucket, quyết định khi nào thay đổi kích thước |
Ví dụ minh họa
Chèn các khóa 20, 34, 9, 13 vào một bảng có 7 bucket, dùng hash(k) = k % 7:
| Bước | Cấu trúc | Thao tác |
|---|---|---|
Chèn 20 | bucket 6: [20] | 20 % 7 = 6, bucket 6 đang trống, lưu 20 |
Chèn 34 | bucket 6: [20, 34] | 34 % 7 = 6, xung đột với 20, thêm 34 vào chuỗi |
Chèn 9 | bucket 2: [9] | 9 % 7 = 2, bucket 2 đang trống, lưu 9 |
Chèn 13 | bucket 6: [20, 34, 13] | 13 % 7 = 6, xung đột, thêm 13 vào chuỗi của bucket 6 |
Tra cứu 34 | bucket 6: [20, 34, 13] | 34 % 7 = 6, duyệt chuỗi: 20 không khớp, 34 khớp, đã tìm thấy |
Khi nào nên dùng bảng băm
| Nên dùng khi | Tránh dùng khi |
|---|---|
Bạn cần chèn, tra cứu và xóa theo khóa với chi phí trung bình O(1) | Bạn cần giữ khóa theo thứ tự đã sắp xếp: hãy dùng BST cân bằng |
| Khóa không có thứ tự hữu ích và bạn chỉ kiểm tra sự tồn tại theo đúng giá trị | Bạn cần truy vấn theo khoảng hoặc tìm khóa gần nhất |
| Bạn có thể chấp nhận thêm chút bộ nhớ cho các bucket và hệ số tải thấp | Bộ nhớ cực kỳ eo hẹp và từng byte đều quan trọng |
| Có sẵn một hàm băm tốt cho kiểu khóa của bạn | Độ trễ trong trường hợp xấu nhất phải có giới hạn: xung đột khiến nó thành O(n) |
Code Hash Table
Một cách cài đặt Hash Table gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Hash Table bằng Python
1class HashTable:2 def __init__(self, size=8):3 self.size = size4 self.buckets = [[] for _ in range(size)]5
6 def _index(self, key):7 # Hash the key to a bucket; different keys can collide8 return sum(ord(ch) for ch in key) % self.size9
10 def set(self, key, value):11 bucket = self.buckets[self._index(key)]12 for i, (k, _) in enumerate(bucket):13 if k == key:14 bucket[i] = (key, value) # update existing key15 return16 bucket.append((key, value)) # chain on collision17
18 def get(self, key):19 for k, v in self.buckets[self._index(key)]:20 if k == key:21 return v22 raise KeyError(key)23
24 def delete(self, key):25 bucket = self.buckets[self._index(key)]26 for i, (k, _) in enumerate(bucket):27 if k == key:28 del bucket[i]29 return30 raise KeyError(key)31
32
33table = HashTable()34table.set("apple", 3)35table.set("banana", 7)36table.set("cherry", 5)37
38print("apple ->", table.get("apple"))39print("banana ->", table.get("banana"))40table.set("apple", 10)41print("apple ->", table.get("apple"))42table.delete("banana")43print("bucket sizes:", [len(b) for b in table.buckets])Code Hash Table bằng JavaScript
1class HashTable {2 constructor(size = 8) {3 this.size = size;4 this.buckets = Array.from({ length: size }, () => []);5 }6
7 // Fold character codes into a bucket index8 hash(key) {9 let h = 0;10 for (const ch of key) h = (h * 31 + ch.charCodeAt(0)) % this.size;11 return h;12 }13
14 set(key, value) {15 const bucket = this.buckets[this.hash(key)];16 const pair = bucket.find(([k]) => k === key);17 if (pair) pair[1] = value;18 else bucket.push([key, value]); // Separate chaining on collision19 }20
21 get(key) {22 const bucket = this.buckets[this.hash(key)];23 const pair = bucket.find(([k]) => k === key);24 return pair ? pair[1] : undefined;25 }26
27 delete(key) {28 const bucket = this.buckets[this.hash(key)];29 const index = bucket.findIndex(([k]) => k === key);30 if (index === -1) return false;31 bucket.splice(index, 1);32 return true;33 }34}35
36const table = new HashTable();37table.set("apple", 3);38table.set("banana", 7);39table.set("cherry", 5);40table.set("apple", 4);41console.log("apple ->", table.get("apple"));42console.log("banana in bucket", table.hash("banana"));43table.delete("cherry");44console.log("cherry after delete:", table.get("cherry"));Code Hash Table bằng Java
1public class Main {2 static class Entry {3 String key; int value;4 Entry next; // chain for keys sharing a bucket5 Entry(String key, int value) { this.key = key; this.value = value; }6 }7
8 static final int CAPACITY = 8;9 static Entry[] buckets = new Entry[CAPACITY];10
11 static int index(String key) { return Math.abs(key.hashCode()) % CAPACITY; }12
13 static void put(String key, int value) {14 int i = index(key);15 for (Entry e = buckets[i]; e != null; e = e.next) {16 if (e.key.equals(key)) { e.value = value; return; }17 }18 Entry entry = new Entry(key, value);19 entry.next = buckets[i]; // prepend to the chain20 buckets[i] = entry;21 }22
23 static Integer get(String key) {24 for (Entry e = buckets[index(key)]; e != null; e = e.next) {25 if (e.key.equals(key)) return e.value;26 }27 return null;28 }29
30 static void remove(String key) {31 int i = index(key);32 Entry prev = null;33 for (Entry e = buckets[i]; e != null; prev = e, e = e.next) {34 if (e.key.equals(key)) {35 if (prev == null) buckets[i] = e.next;36 else prev.next = e.next;37 return;38 }39 }40 }41
42 public static void main(String[] args) {43 put("apple", 3); put("banana", 7); put("cherry", 5);44 System.out.println("apple -> " + get("apple"));45 put("apple", 10); // update in place46 System.out.println("apple -> " + get("apple"));47 remove("banana");48 System.out.println("banana -> " + get("banana"));49 }50}Code Hash Table bằng C++
1#include <iostream>2#include <string>3#include <vector>4
5struct HashTable {6 static constexpr int CAPACITY = 8;7 // Separate chaining: each bucket holds (key, value) pairs8 std::vector<std::vector<std::pair<std::string, int>>> buckets{CAPACITY};9
10 size_t hash(const std::string& key) const {11 size_t h = 0;12 for (char c : key) h = h * 31 + static_cast<unsigned char>(c);13 return h % CAPACITY;14 }15
16 void set(const std::string& key, int value) {17 auto& bucket = buckets[hash(key)];18 for (auto& entry : bucket) {19 if (entry.first == key) {20 entry.second = value; // update existing key21 return;22 }23 }24 bucket.push_back({key, value});25 }26
27 const int* get(const std::string& key) const {28 for (const auto& entry : buckets[hash(key)]) {29 if (entry.first == key) return &entry.second;30 }31 return nullptr;32 }33
34 void erase(const std::string& key) {35 auto& bucket = buckets[hash(key)];36 for (size_t i = 0; i < bucket.size(); ++i) {37 if (bucket[i].first == key) {38 bucket.erase(bucket.begin() + i);39 return;40 }41 }42 }43};44
45int main() {46 HashTable table;47 table.set("apple", 3);48 table.set("banana", 7);49 table.set("apple", 4); // overwrites the old value50 for (const std::string& key : {"apple", "banana", "grape"}) {51 const int* value = table.get(key);52 if (value != nullptr) std::cout << key << " = " << *value << "\n";53 else std::cout << key << " not found\n";54 }55 table.erase("banana");56 std::cout << "banana after erase: "57 << (table.get("banana") ? "found" : "not found") << "\n";58 return 0;59}Code Hash Table bằng C
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4
5#define CAPACITY 86
7// Separate chaining: each bucket is a linked list of entries8typedef struct Entry {9 char key[16];10 int value;11 struct Entry* next;12} Entry;13
14Entry* buckets[CAPACITY];15
16unsigned int hash(const char* key) {17 unsigned int h = 5381; // djb2, then modulo the bucket count18 for (; *key; key++) h = h * 33 + (unsigned char)*key;19 return h % CAPACITY;20}21
22void set(const char* key, int value) {23 unsigned int i = hash(key);24 for (Entry* e = buckets[i]; e != NULL; e = e->next) {25 if (strcmp(e->key, key) == 0) {26 e->value = value; // update existing key27 return;28 }29 }30 Entry* e = malloc(sizeof(Entry));31 strcpy(e->key, key);32 e->value = value;33 e->next = buckets[i];34 buckets[i] = e;35}36
37int* get(const char* key) {38 for (Entry* e = buckets[hash(key)]; e != NULL; e = e->next) {39 if (strcmp(e->key, key) == 0) return &e->value;40 }41 return NULL;42}43
44void removeKey(const char* key) {45 Entry** link = &buckets[hash(key)];46 while (*link != NULL) {47 if (strcmp((*link)->key, key) == 0) {48 Entry* old = *link;49 *link = old->next;50 free(old);51 return;52 }53 link = &(*link)->next;54 }55}56
57void printBuckets(void) {58 for (int i = 0; i < CAPACITY; i++) {59 printf("bucket %d:", i);60 for (Entry* e = buckets[i]; e != NULL; e = e->next) {61 printf(" (%s=%d)", e->key, e->value);62 }63 printf("\n");64 }65}66
67int main(void) {68 set("apple", 3);69 set("banana", 7);70 set("apple", 4); // overwrites the old value71 set("grape", 9);72 printBuckets();73 int* value = get("apple");74 printf("get(apple): %d\n", value ? *value : -1);75 removeKey("banana");76 printf("banana after remove: %s\n", get("banana") ? "found" : "not found");77 return 0;78}Câu hỏi thường gặp về bảng băm
Xung đột băm là gì và được giải quyết như thế nào?
Xung đột xảy ra khi hai khóa khác nhau băm vào cùng một bucket. Hai cách xử lý phổ biến là nối chuỗi (mỗi bucket giữ một danh sách và các khóa xung đột được thêm vào, như minh họa ở đây) và địa chỉ mở (dò tìm ô trống tiếp theo trong mảng). Cả hai đều giữ việc tra cứu chính xác; chúng khác nhau ở cách bố trí bộ nhớ và hiệu năng khi tải cao.
Độ phức tạp thời gian của bảng băm là bao nhiêu?
O(1) trung bình khi hàm băm phân bố khóa đều và hệ số tải được giữ thấp. Trong trường hợp xấu nhất, khi mọi khóa đều xung đột vào một bucket, các thao tác suy biến thành O(n), vì vậy hàm băm tốt và việc thay đổi kích thước rất quan trọng.Hệ số tải là gì?
Hệ số tải (load factor) là số mục đã lưu chia cho số bucket. Khi nó tăng, các chuỗi dài ra và thao tác chậm đi, nên phần lớn bảng băm sẽ thay đổi kích thước (băm lại vào một mảng lớn hơn) khi vượt ngưỡng như 0,75.
Khi nào nên dùng bảng băm thay cho cây tìm kiếm nhị phân?
O(1) mà không cần thứ tự. Dùng cây tìm kiếm nhị phân cân bằng khi bạn cần khóa theo thứ tự đã sắp xếp, truy vấn theo khoảng hoặc tìm phần tử liền trước/liền sau, những việc bảng băm không làm hiệu quả được. BST bảo đảm các thao tác O(log n), còn bảng băm đánh đổi bảo đảm đó để có hiệu năng trung bình nhanh hơn.Nối chuỗi và địa chỉ mở khác nhau như thế nào?
Nối chuỗi lưu các khóa xung đột trong một danh sách riêng cho mỗi bucket, nên một bucket có thể chứa nhiều mục và bảng không bao giờ thực sự đầy. Địa chỉ mở giữ mọi thứ ngay trong mảng và dò tìm ô trống tiếp theo khi xung đột, cách này thân thiện với bộ nhớ đệm nhưng hiệu năng giảm mạnh khi hệ số tải tiến gần 1 và cần xử lý việc xóa cẩn thận. Nối chuỗi chịu được hệ số tải cao hơn; địa chỉ mở dùng bộ nhớ gọn hơn.
Vì sao không thể trông cậy vào bảng băm để giữ khóa theo thứ tự chèn hoặc thứ tự đã sắp xếp?
dict của Python giữ thứ tự chèn, nhưng đó là bảo đảm của ngôn ngữ, không phải tính chất vốn có của bảng băm). Đừng bao giờ giả định thứ tự duyệt khớp với cách bạn chèn khóa trừ khi ngôn ngữ cam kết rõ ràng điều đó.