Bài 6 · Nâng cao · 22 phút
Bảng băm
Biên soạn bởi Nguyễn Anh Tuấn
Bảng băm (hash table): hàm băm biến khoá thành chỉ số để tra cứu gần như O(1); xử lý va chạm (collision) bằng chaining; vì sao Map và Set nhanh.
Bài Cây nhị phân tìm kiếm cho ta tra cứu O(log n) mà vẫn giữ thứ tự. Nhưng nhiều lúc mèo con chẳng cần thứ tự - chỉ muốn hỏi “khoá này có chưa?” hoặc “lấy giá trị của khoá này” thật nhanh. Có cách nhanh hơn cả O(log n) không?
Có: bảng băm (hash table), trung bình O(1). Hình dung một dãy ngăn tủ đánh số. Thay vì dò từng ngăn, ta có một hàm băm nhận khoá và nói ngay “khoá này thuộc ngăn số mấy” - đi thẳng tới đó, không cần tìm.
- ▸Bảng băm tra cứu theo khoá gần như tức thì - trung bình O(1).
- ▸Bí quyết: hàm băm tính thẳng ra vị trí cần tới, không phải dò.
- ▸Đánh đổi: bảng băm KHÔNG giữ thứ tự (khác cây nhị phân tìm kiếm).
Một hàm băm (hash function) biến khoá (chuỗi, số…) thành một con số. Lấy số đó chia lấy dư cho số xô (bucket) là ra chỉ số xô để đặt khoá. Tra cứu cũng tính y vậy rồi tới thẳng xô đó. Hàm băm đồ chơi của ta: cộng mã ký tự.
- ▸hash(khoá) → số; số mod (số xô) → chỉ số xô.
- ▸Đặt và tra đều dùng cùng công thức, nên luôn tới đúng xô.
- ▸Một hàm băm tốt phải rải khoá ĐỀU ra các xô, và tính nhanh.
Vì nhiều khoá hơn số xô, kiểu gì cũng có hai khoá rơi cùng một xô - gọi là va chạm (collision). Với hàm cộng mã, mọi từ đảo chữ đều va chạm: cat và act cùng tổng 312, cùng xô. Cách hoá giải phổ biến là chaining: mỗi xô giữ một danh sách các khoá (đúng danh sách liên kết ở bài 2), va chạm thì nối thêm vào chuỗi:
bang-bam.ts · va chạm rơi chung một xô, nối thành chuỗi
function hashCode(key: string): number {
let sum = 0;
for (let i = 0; i < key.length; i++) sum += key.charCodeAt(i);
return sum;
}
const xo: string[][] = Array.from({ length: 8 }, () => []); // 8 xo, moi xo mot danh sach
const them = (key: string) => xo[hashCode(key) % 8].push(key);
them('cat'); them('dog'); them('act'); // 'cat' & 'act' dao chu -> cung tong
console.log(hashCode('cat') % 8); // xo cua 'cat'
console.log(hashCode('act') % 8); // xo cua 'act' -> trung! (va cham)
console.log(xo[0]); // hai khoa noi thanh chuoi trong xo 0
console.log(hashCode('fox') % 8); // 'fox' di xo khac Kết quả khi chạy
0 0 [ 'cat', 'act' ] 5
Khi tra một khoá, ta tới đúng xô rồi đi trong chuỗi của xô đó để so. Xô ngắn thì gần như tức thì; xô dài thì chậm dần.
- ▸Va chạm: hai khoá khác nhau cùng chỉ số xô - không tránh hết được.
- ▸Chaining: mỗi xô là một danh sách; va chạm thì nối thêm.
- ▸Tra cứu = tới xô + đi trong chuỗi của xô đó.
Gõ một khoá rồi bấm Thêm để xem hàm băm chọn xô nào; thử thêm cat rồi act để thấy va chạm nối chuỗi. Bấm Tìm để xem phải so mấy lần trong xô:
hash("fox") = 333 → 333 mod 8 = xô 5
Thêm cat rồi act (đảo chữ) để thấy va chạm: cùng giá trị băm nên rơi chung một xô, nối thành chuỗi. Xô càng dài thì tra cứu càng chậm - đó là vì sao cần hàm băm rải đều.
- ▸Cùng giá trị băm → cùng xô → nối vào chuỗi (chaining).
- ▸Tìm trong xô ngắn tốn ít phép so; xô dài tốn nhiều.
- ▸Hệ số tải càng cao thì các xô càng dài.
Nếu hàm băm rải khoá đều và số xô đủ nhiều, mỗi xô chỉ có một hai khoá, nên tra cứu gần như O(1). Để giữ điều đó khi thêm nhiều khoá, bảng băm theo dõi hệ số tải (load factor) = số khoá / số xô; vượt ngưỡng thì tăng số xô và rải lại (rehash).
Trung thực
- ▸Hàm băm tốt + đủ xô → mỗi xô ngắn → O(1) trung bình.
- ▸Hệ số tải cao → xô dài → cần tăng số xô (rehash).
- ▸Hàm băm dở dồn hết vào một xô → O(n), như danh sách liên kết.
Mèo con giờ có hai cách tra cứu nhanh, cho hai nhu cầu khác nhau: bảng băm (O(1), không thứ tự - hợp khi chỉ cần “có/không” hay lấy theo khoá) và cây nhị phân tìm kiếm (O(log n), giữ thứ tự). Đây cũng là lý do Map/Set (JS) và dict (Python) nhanh đến vậy: chúng là bảng băm.
Bước tiếp theo
Câu hỏi thường gặp
Về tra cứu “có/không” và lấy theo khoá thì có: trung bình O(1) so với O(log n) của BST. Nhưng bảng băm KHÔNG giữ thứ tự - bạn không thể hỏi “phần tử nhỏ nhất” hay “liệt kê tăng dần” một cách rẻ. Cần thứ tự thì dùng BST; chỉ cần tra nhanh thì dùng bảng băm.
Không - nó quá dễ va chạm (mọi từ đảo chữ như cat/act đều cùng giá trị). Bài dùng nó cho dễ hiểu. Thực tế Python và V8 (JavaScript) dùng hàm băm mạnh như SipHash, rải khoá đều hơn nhiều và chống được kẻ cố tình tạo va chạm. Ý TƯỞNG thì y hệt: khoá → số → xô.
Không, về nguyên tắc. Nếu có nhiều khoá hơn số xô thì kiểu gì cũng có hai khoá chung xô (nguyên lý chuồng bồ câu). Mục tiêu không phải xoá sạch va chạm mà là giữ cho mỗi xô NGẮN, bằng hàm băm tốt và đủ số xô.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Hàm băm (hash function) trong bảng băm dùng để làm gì?
- 1
Tính xô bằng tay
Hàm băm cộng mã ASCII (
a=97 …z=122). Với bảng 8 xô, tính chỉ số xô của khoábe(b=98,e=101).Hoàn thành khi: hash(
be) = 98 + 101 = 199; 199 mod 8 = 7. Vậybevào xô 7. - 2
Tìm một va chạm
Vẫn hàm băm cộng mã, tìm hai từ tiếng Anh KHÁC nhau nhưng cùng giá trị băm (gợi ý: đảo chữ).
Hoàn thành khi: Ví dụ hợp lý:
catvàact(đều = 312), hoặcdogvàgod. Hai khoá khác nhau, cùng tổng. - 3
Vẽ chaining
Thêm lần lượt
cat, dog, act, fishvào bảng 8 xô, vẽ các xô và khoá trong mỗi xô.Hoàn thành khi: Xô 0:
cat → act; xô 2:dog → fish; các xô khác trống. (Đảo chữ và trùng tổng nên chung xô.) - 4
Đếm bước tra cứu
Trong bảng vừa vẽ, tìm
actvà tìmowltốn bao nhiêu phép so khoá trong xô?Hoàn thành khi: Tìm
act: xô 0 cócat, act- so 2 lần mới thấy. Tìmowl: vào xô của nó (rỗng) - 0 lần, kết luận không có. - 5
Khi nào O(n)
Mô tả một tình huống khiến bảng băm tra cứu chậm như
O(n), và nêu cách khắc phục.Hoàn thành khi: Khi hàm băm dở dồn (gần) hết khoá vào một xô - xô thành danh sách dài. Khắc phục: hàm băm tốt hơn và tăng số xô (giảm hệ số tải).
- 6
Băm hay cây?
Cho 2 yêu cầu, chọn bảng băm hay BST: (1) kiểm tra một username đã tồn tại chưa; (2) liệt kê toàn bộ điểm thi theo thứ tự tăng dần.
Hoàn thành khi: (1) Bảng băm (chỉ cần có/không,
O(1)). (2) BST/cấu trúc có thứ tự (cần duyệt tăng dần).