Bảng băm(Hash table) | Cài đặt bảng băm và kỹ thuật xử lý va chạm

This entry is part 3 of 12 in the series This entry is part 3 of 12 in the series Cấu trúc tài liệu

Trong khoa học máy tính, bảng băm(Hash Tables) là một cấu trúc dữ liệu sử dụng hàm băm để ánh xạ từ giá trị xác định, được gọi là khóa (ví dụ như tên của một người), đến giá trị tương ứng (ví dụ như số điện thoại của họ). Do đó, bảng băm là một mảng kết hợp. Hàm băm được sử dụng để chuyển đổi từ khóa thành chỉ số (giá trị băm) trong mảng lưu trữ các giá trị tìm kiếm.

1. Lý thuyết về bảng băm

Băm là một kỹ thuật được sử dụng để định danh một đối tượng người tiêu dùng đơn cử trong một nhóm những đối tượng người dùng tương tự như. Một số ví dụ về việc sử dụng bảng băm trên trong thực tiễn :

  • Trong trường đại học, mỗi sinh viên được chỉ định một mã sinh viên không giống nhau và qua mã sinh viên đó có thể truy xuất các thông tin của sinh viên đó.
  • Trong thư viện, mỗi một cuốn sách một mã số riêng và mã số đó có thể được dùng để xác định các thông tin của sách, chẳng hạn như vị trí chính xác của sách trong thư viện hay thể loại của sách đó,…

Trong cả 2 ví dụ trên, các sinh viên và các cuốn sách được “băm” thành các mã số duy nhất(không trùng lặp).

Giả sử rằng bạn có một đối tượng và bạn muốn gán cho nó một cái khóa(key) để giúp tìm kiếm dễ dàng hơn. Để lưu giữ cặp (nó thường được gọi là hơn), bạn có thể sử dụng mảng bình thường để làm việc này; Với chỉ số mảng là khóa và giá trị tại chỉ số đó là giá trị tương ứng của khóa. Tuy nhiên, trong trường hợp phạm vi của khóa lớn và không thể sử dụng chỉ số mảng được, khi đó bạn sẽ cần tới “băm”(hashing).

Trong hashing, các key có giá trị lớn sẽ được đưa về giá trị nhỏ hơn bằng cách sử dụng hàm băm(hash functions). Các giá trị sau đó được lưu trong một cấu trúc dữ liệu gọi là bảng băm(hash tables). Ý tưởng của hashing là đưa các cặp về một mảng thống nhất; Mỗi phần tử sẽ được gán một khóa định danh(khóa có được sau khi dùng hàm băm). Bằng việc sử dụng khóa định danh đó, chúng ta có thể truy cập trực tiếp tới nó với độ phức tạp O(1). Thuật toán băm sẽ sử dụng khóa băm để tính toán ra khóa định danh của các phần tử hoặc thêm vào bảng băm.

Việc hashing được triển khai qua 2 bước :

  1. Một phần tử sẽ được chuyển đổi thành 1 số nguyên bằng việc sử dụng hàm băm. Phần tử này được sử dụng như một chỉ mục để lưu trữ phần tử gốc và nó sẽ được đưa vào bảng băm.
  2. Phần tử sẽ được lưu giữ trong bảng băm, nó có thể được truy xuất nhanh bằng khóa băm:
    • hash = hashfunc(key)
    • index = hash % array_size

Theo cách trên thì việc băm sẽ phụ thuộc vào kích thước mảng array_size. Chỉ số index sau đó được đưa về [0; array_size-1] bằng việc sử dụng toán tử chia lấy dư %.

2. Hàm băm

Hàm băm là bất kể hàm nào hoàn toàn có thể được sử dụng để ánh xạ tập dữ liệu có size tùy ý thành tập dữ liệu có size cố định và thắt chặt và đưa vào bảng băm. Các giá trị được trả về bởi hàm băm được gọi là giá trị băm .
Một hàm băm được nhìn nhận tốt nếu nó đạt được những nhu yếu cơ bản sau :

  1. Dễ tính toán: Nó phải dễ tính toán và bản thân nó không phải là một thuật toán
  2. Phân bố đồng đều: Nó cần phải phân phối đồng đều trên bảng băm, không xảy ra việc tập trung thành các cụm
  3. Ít va chạm: Va chạm xảy ra khi các cặp phần tử được ánh xạ tới cùng một giá trị băm

Chú ý: Bất kể hàm băm có tốt đên đâu, va chạm vẫn có thể xảy ra. Vì vậy, để duy trì hiệu suất của bảng băm, điều quan trọng là phải quản lý va chạm thông qua các kỹ thuật giải quyết va chạm.

Tại sao phải cần hàm băm đủ tốt?

Hãy đi vào một ví dụ để dễ tưởng tượng nhé. Giả sử rằng bạn cần lưu giữ những string sau đây trong bảng băm : { “ abcdef ”, “ bcdefa ”, “ cdefab ”, “ defabc ” } .
Để thống kê giám sát chỉ mục lưu giữ những string này, tất cả chúng ta dùng một hàm băm như sau : Chỉ mục của mỗi string sẽ được tính bằng tổng giá trị ASCII của những ký tự trong nó sau đó lấy dư với 599 .
Do 5 là số nguyên tố và nó lớn hơn số lượng thành phần, nó có năng lực lập ra những chỉ mục khác nhau ( giảm va chạm ). Số nguyên tố luôn là lựa chọn tốt nếu bạn muốn dùng phép lấy phần dư. Các giá trị ASCII của a, b, c, d, e và f lần lượt là 97, 98, 99, 100, 101 và 102 .
Dễ nhận thấy, những string kia chỉ là những hoán vị của cùng 1 chuỗi, do đó chỉ mục được tạo ra của chúng đều giống nhau và đều bằng 597 % 5 = 2 .
Lúc này, hàm băm sẽ đo lường và thống kê và toàn bộ những string kia đều có cùng 1 chỉ mục. Khi đó, bạn hoàn toàn có thể tạo 1 list tại chỉ số đó để lưu những string trong list đó. Như hình ảnh minh họa này :

Bảng băm trong c

Bạn thấy đó, bạn vẫn sẽ mất O ( n ) để tìm kiếm một string đơn cử. Điều đó có nghĩa hàm băm này chưa thực sự tốt .
Hãy thử với 1 hàm băm khác nhé. Chỉ mục của những string sẽ được tính bằng tổng của những mã ASCII của từng ký tự theo sau là vị trí của ký tự đó trong string. Sau đó đem chia dư cho số nguyên tố 2069 .

String                                Hash function                               Index
abcdef       (971 + 982 + 993 + 1004 + 1015 + 1026)%2069       38
bcdefa       (981 + 992 + 1003 + 1014 + 1025 + 976)%2069       23
cdefab       (991 + 1002 + 1013 + 1024 + 975 + 986)%2069       14
defabc       (1001 + 1012 + 1023 + 974 + 985 + 996)%2069       11

Lúc này, những chỉ mục của mỗi string là không trùng lặp ( không va chạm ) .

Bảng băm và hàm băm

3. Kỹ thuật xử lý va chạm

3.1. Separate chaining (open hashing)

Separate chaining là một kỹ thuật giải quyết và xử lý và chạm phổ cập nhất. Nó thường được thiết lập với list link. Để lưu giữ một thành phần trong bảng băm, bạn phải thêm nó vào một list link ứng với chỉ mục của nó. Nếu có sự va chạm xảy ra, những thành phần đó sẽ nằm cùng trong 1 list link ( xem ảnh để hiểu hơn ) .

Bảng băm danh sách liên kết

Như vậy, kỹ thuật này sẽ đạt được vận tốc tìm kiếm O ( 1 ) trong trường hợp tối ưu và O ( N ) nếu tổng thể những thành phần ở cùng 1 list link duy nhất. Đó là do có điều kiện kèm theo 3 trong tiêu chuẩn hàm băm tốt .

Cài đặt bảng băm sử dụng Separate chaining

Giả định : Hàm băm sẽ trả về số int trong [ 0, 19 ] .

0123

vectorhashTable[20];

inthashTableSize=20;

Thêm vào bảng băm

012345678

voidinsert(strings)

{

/ / Compute the index using Hash Function

intindex=hashFunc(s);

/ / Insert the element in the linked list at the particular index

hashTable[index].push_back(s);

}

Tìm kiếm

012345678910111213141516

voidsearch(strings)

{

/ / Compute the index by using the hash function

intindex=hashFunc(s);

/ / Search the linked list at that specific index

for(inti=0;i

{

if(hashTable[index][i]==s)

{

cout<

return;

}

}

cout<

}

3.2. Linear probing (open addressing or closed hashing)

Trong kỹ thuật giải quyết và xử lý va chạm này, tất cả chúng ta sẽ không dùng linklist để tàng trữ mà chỉ có bản thân array đó thôi .
Khi thêm vào bảng băm, nếu chỉ mục đó đã có thành phần rồi ; Giá trị chỉ mục sẽ được đo lường và thống kê lại theo chính sách tuần tự. Giả sử rằng chỉ mục là chỉ số của mảng, khi đó, việc đo lường và thống kê chỉ mục cho thành phần được tính theo cách sau :

index = index % hashTableSize
index = (index + 1) % hashTableSize
index = (index + 2) % hashTableSize
index = (index + 3) % hashTableSize

Và cứ thế theo cách như vậy chừng nào index thu được chưa có thành phần được sử dụng. Tất nhiên, khoảng trống chỉ mục phải được bảo vệ để luôn có chỗ cho thành phần mới .

Như trong ví dụ dưới đây, chuỗi Hashing bị trùng chỉ mục(2) ở lần đầu tính chỉ mục. Do đó, nó được đẩy lên vị trí trống ở phía sau(3).Xử lý va chạm trong bảng băm

Cài đặt bảng băm dùng Linear probing

Giả sử rằng :

  • Không có nhiều hơn 20 phần tử trong tập dữ liệu
  • Hàm băm sẽ trả về một số nguyên từ 0 đến 19
  • Tập dữ liệu phải là các phần tử duy nhất
0123

stringhashTable[21];

inthashTableSize=21;

Thêm vào bảng băm

012345678910

voidinsert(strings)

{

/ / Compute the index using the hash function

intindex=hashFunc(s);

/ / Search for an unused slot and if the index will exceed the hashTableSize then roll back

while(hashTable[index]!=” “)

index=(index+1)%hashTableSize;

hashTable[index]=s;

}

Tìm kiếm

01234567891011121314

voidsearch(strings)

{

/ / Compute the index using the hash function

intindex=hashFunc(s);

/ / Search for an unused slot and if the index will exceed the hashTableSize then roll back

while(hashTable[index]!=sandhashTable[index]!=” “)

index=(index+1)%hashTableSize;

/ / Check if the element is present in the hash table

if(hashTable[index]==s)

cout<

else

cout<

}

3.3. Quadratic Probing

Ý tưởng cũng khá giống Linear Probing, nhưng cách tính chỉ mục có khác đôi chút :

index = index % hashTableSize
index = (index + 12) % hashTableSize
index = (index + 22) % hashTableSize
index = (index + 32) % hashTableSize

Và cứ thế cho tới khi tìm được chỉ mục trống .

Cài đặt bảng băm dùng Quadratic Probing

Giả sử rằng :

  • Không có nhiều hơn 20 phần tử trong tập dữ liệu
  • Hàm băm sẽ trả về một số nguyên từ 0 đến 19
  • Tập dữ liệu phải là các phần tử duy nhất
0123

stringhashTable[21];

inthashTableSize=21;

Thêm phần tử

012345678

9

1011121314

voidinsert(strings)

{

/ / Compute the index using the hash function

intindex=hashFunc(s);

/ / Search for an unused slot and if the index will exceed the hashTableSize roll back

inth=1;

while(hashTable[index]!=” “)

{

index=(index+h *h)%hashTableSize;

h++;

}

hashTable[index]=s;

}

Tìm kiếm phần tử

0123456789101112131415161718

voidsearch(strings)

{

/ / Compute the index using the Hash Function

intindex=hashFunc(s);

/ / Search for an unused slot and if the index will exceed the hashTableSize roll back

inth=1;

while(hashTable[index]!=sandhashTable[index]!=” “)

{

index=(index+h *h)%hashTableSize;

h++;

}

/ / Is the element present in the hash table

if(hashTable[index]==s)

cout<

else

cout<

}

3.4. Double hashing

Vẫn giống 2 kỹ thuật ngay phía trên, chỉ khác ở công thức tính khi xảy ra va chạm như sau :

index = (index + 1 * indexH) % hashTableSize;
index = (index + 2 * indexH) % hashTableSize;

Và cứ liên tục cho tới khi tìm được chỉ mục chưa được sử dụng .

Cài đặt bảng băm dùng Double hashing

Giả sử rằng :

  • Không có nhiều hơn 20 phần tử trong tập dữ liệu
  • Hàm băm sẽ trả về một số nguyên từ 0 đến 19
  • Tập dữ liệu phải là các phần tử duy nhất
0123

stringhashTable[21];

inthashTableSize=21;

Thêm vào bảng băm

01234567891011

voidinsert(strings)

{

/ / Compute the index using the hash function1

intindex=hashFunc1(s);

intindexH=hashFunc2(s);

/ / Search for an unused slot and if the index exceeds the hashTableSize roll back

while(hashTable[index]!=” “)

index=(index+indexH)%hashTableSize;

hashTable[index]=s;

}

Tìm kiếm trên bảng băm

0123456789101112131415

voidsearch(strings)

{

/ / Compute the index using the hash function

intindex=hashFunc1(s);

intindexH=hashFunc2(s);

/ / Search for an unused slot and if the index exceeds the hashTableSize roll back

while(hashTable[index]!=sandhashTable[index]!=” “)

index=(index+indexH)%hashTableSize;

/ / Is the element present in the hash table

if(hashTable[index]==s)

cout<

else

cout<

}

4. Ứng dụng của bảng băm

Trong các bài toán thông thường, các bạn thường chỉ cần sử dụng cấu trúc dữ liệu được cài đặt sẵn ở các ngôn ngữ lập trình: map, set trong C/C++, Java; Dictionary trong C#, Python. Đó chính là các bảng băm cực kỳ hữu dụng mà chúng ta vẫn hay sử dụng.

Nếu các bạn để ý thì multimapmultiset trong C++ cho phép lưu trữ key trùng nhau với giá trị khác nhau đó. Đó là 2 thằng đã được thiết lập cơ chế xử lý va chạm.

Với những bài toán đặc trưng, những bạn sẽ phải tự viết cho mình hàm băm và thiết kế xây dựng cấu trúc tài liệu bảng băm cho tương thích. Dưới đây là một số ít ứng dụng của bảng băm :

  • Associative arrays: Bảng băm thường được sử dụng để cài đặt nhiều loại in-memory tables. Chúng được sử dụng để thực hiện các mảng kết hợp (các mảng có chỉ số là các chuỗi tùy ý hoặc các đối tượng phức tạp khác).
  • Lập chỉ mục CSDL: Các bảng băm cũng có thể được sử dụng làm cấu trúc dữ liệu để lập chỉ mục dữ liệu trong các CSDL.
  • Caches: Bảng băm dùng để thiết lập cơ chế caches, thường được dùng để tăng tốc độ truy cập dữ liệu.
  • Biểu diễn các đối tượng: Một số ngôn ngữ động, chẳng hạn như Perl, Python, JavaScript và Ruby sử dụng bảng băm để triển khai các đối tượng.
  • Hàm băm được sử dụng trong các thuật toán khác nhau để tăng tốc độ tính toán.

5. Bài tập thực hành

Một số link chứa bài tập thực hành thực tế dành cho bạn :

  1. http://ntucoder.net/Problem/List?ThemeID=29
  2. https://www.hackerearth.com/practice/data-structures/hash-tables/basics-of-hash-tables/practice-problems/
  3. https://www.spoj.com/KSTN/problems/HASH2509/