You are on page 1of 17

Bảng băm

Cấu trúc dữ liệu và giải thuật – INT2210 24

Lê Duy Quang – 21020380


Hồng Quân – 21029999 Đức Tân – 21029999

1/11/2022
I – Khái niệm

Trong nhiều bài toán trong thực tế, thường nảy sinh ra nhu cầu gán mỗi một
giá trị nguồn ﴾khóa﴿ tương ứng với một giá trị đích ﴾giá trị﴿ nào đó. Cách đơn
giản nhất là sử dụng một mảng để lưu trữ các cặp khóa – giá trị, rõ ràng là
không thể dùng cách này cho một số lượng lớn giá trị được do phải truy cập
vào từng phần tử trong mảng. Có nhiều giải pháp để làm tăng hiệu quả tìm
kiếm khóa và theo đó là giá trị tương ứng, một trong số đó là sử dụng cấu trúc
dữ liệu bảng băm.
Bảng băm là một cấu trúc dữ liệu dưới dạng như một từ điển: các phần tử là
các cặp khóa – giá trị và bằng cách truy vấn sử dụng khóa thì có thể lấy được
giá trị tương ứng. Kĩ thuật chủ đạo của cấu trúc dữ liệu này là kĩ thuật băm
﴾hashing﴿: mỗi giá trị khóa sẽ được sử dụng làm tham số đầu vào cho một hàm
tính toán gọi là hàm băm, kết quả của hàm này là một số tự nhiên nằm trong
một khoảng cụ thể gọi là giá trị băm. Giá trị băm sẽ được sử dụng để truy cập
bảng băm và tìm ra giá trị đích tương ứng, thường thì bảng băm sẽ là một
mảng với giá trị băm được sử dụng làm chỉ số truy cập vào trong mảng. Do
đó, một khi đã tính toán được giá trị băm thì việc truy cập giá trị đích trở nên
rất dễ dàng.

II – Cấu tạo

1. Hàm băm

Một hàm băm h(k) nhận vào một giá trị khóa k và cho ra một số tự nhiên
nằm trong khoảng [0; m). Giới hạn m cho phép biểu diễn giá trị băm với một
độ dài cố định và thường thì đây cũng chính là kích cỡ của bảng băm. Một hàm
băm được coi là tốt phải thỏa mãn các điều kiện sau:

1. Tốc độ tính toán cao.


2. Các giá trị băm được phân bố đều trong miền giá trị.
3. Xử lí được các loại giá trị khóa khác nhau.

Điều kiện 1 là cần thiết do hàm băm được thực hiện cho mọi thao tác trên
bảng băm. Điều kiện 2 là cần thiết để có thể giảm thiểu các trường hợp có

1
nhiều giá trị khóa sinh ra chung một giá trị băm, gây ra đụng độ. Điều kiện 3
cho phép dễ dàng sử dụng bảng băm cho nhiều kiểu dữ liệu khác nhau.
Dưới đây là một số ví dụ về các hàm có thể được sử dụng làm hàm băm cho
bảng băm.

a﴿ Hàm băm sử dụng phép chia lấy dư

Đây là một trong những hàm băm đơn giản nhất, có dạng h(k) = k mod m,
với k là một số nguyên dương. Nếu chọn m = bd với b và d là các số nguyên
dương thì h(k) sẽ là d chữ số cuối cùng của k biểu diễn dưới cơ số b, tức là
không phụ thuộc hoàn toàn vào tất cả nội dung của k, do đó cần phải tránh
trường hợp này. Tốt nhất là nên chọn m sao cho h(k) phụ thuộc đầy đủ vào
khóa k, thông thường m sẽ được chọn là một số nguyên tố.

b﴿ Hàm băm sử dụng phương pháp nhân

Hàm băm có dạng h(k) = ⌊m frac(kA)⌋, với√0 < A < 1 và k là một số nguyên
5−1
dương. Một giá trị được coi là tốt cho A là ; các giá trị được coi là tốt
2
cho m là 2p với p là một số nguyên dương.

c﴿ Hàm băm phổ quát

Nếu lựa chọn được một tập hợp các hàm băm H sao cho với mọi hàm h(k)
thuộc H và hai khóa phân biệt k1 và k2 xác suất h(k1 ) = h(k2 ) là m-1 , thì H được
gọi là một tập hợp hàm băm phổ quát. Khi sử dụng một hàm băm ngẫu nhiên
từ H cho mỗi khóa thì khả năng xảy ra đụng độ sẽ thấp.

2. Bảng băm

Bảng băm ở bên trong là một mảng có kích cỡ xác định chứa các giá trị đích.
Giá trị khóa được sử dụng làm chỉ số để truy cập vào mảng này đến giá trị đích
tương ứng với khóa.
Các thao tác cơ bản trên bảng băm là:

2
• Thêm phần tử: tìm một chỗ trống trong bảng băm tương ứng với khóa
và đặt giá trị đích vào chỗ đó.
• Tra cứu: tìm vị trí trong bảng ứng với khóa được cho và trả về giá trị đích
tại vị trí đó nếu có.
• Xỏa phần tử: tra cứu tra vị trí trong bảng tương ứng với khóa, nếu tồn
tại, loại bỏ phần tử tại vị trí đó khỏi bảng.

Do không gian các giá trị khóa thường lớn hơn không gian giá trị băm, nên
chắc chắn sẽ có các tập hợp giá trị khóa với giá trị băm giống nhau, đây gọi là
sự đụng độ. Khi đó, cần thiết phải có các cơ chế xử lí để bảng băm vẫn có thể
hoạt động đúng và hiệu quả. Một số phương pháp xử lí là:

• Kết nối trực tiếp: mỗi vị trí của bảng băm chứa một danh sách liên kết,
chứa các khóa có cùng giá trị băm tương ứng với vị trí đó cùng với các
giá trị đích tương ứng.
• Dò tuần tự: mỗi vị trí của bảng băm chỉ chứa một phần tử, khi thêm một
phần tử mới mà vị trí tương ứng đã có một phần tử khác, các vị trí tiếp
theo sẽ được xét đến cho đến khi tìm thấy một vị trí trống để đặt phần tử
vào; khi tìm kiếm, thực hiện xét từng vị trí bắt đầu từ vị trí tương ứng với
giá trị băm cho đến khi tìm thấy vị trí chứa khóa cần tìm hoặc tìm thấy
một ô trống.
• Dò tuyến tính: tương tự như dò tuần tự, với vị trí tiếp theo để xét cách
một khoảng cố định D vị trí so với vị trí trước đó.
• Băm kép: tương tự như dò tuần tự, với vị trí tiếp theo để xét cách d(k) vị
trí so với vị trí trước đó, d(k) là một hàm băm khác.

Các phương pháp dò được gọi chung là phương pháp đánh địa chỉ mở,
ngược lại phương pháp kết nối trực tiếp là một phương pháp đánh địa chỉ
đóng. Đối với các phương pháp đánh địa chỉ mở, khi thực hiện xóa phần tử sẽ
nảy sinh ra một vấn đề: các phần tử có cùng giá trị băm ở phía sau phần tử bị
xóa có thể sẽ không được xét đến khi tìm kiếm, do vị trí của phần tử đã bị xóa
trở thành một ô trống, khiến cho lầm tưởng rằng khóa đang tìm kiếm không
tồn tại. Một phương pháp giải quyết vấn đề này là đánh dấu vị trí đã từng có
phần tử mà đã bị xóa; khi thực hiện tìm kiếm, nếu vị trí đã được đánh dấu thì
không coi như không tìm thấy khóa mà sẽ tiếp tục kiểm tra vị trí tiếp theo; khi
thực hiện thêm phần tử thì vẫn có thể đưa phần tử đó vào một vị trí đã đánh
dấu như bình thường; khi số lượng ô đánh dấu trở nên quá lớn thì có thể thực

3
hiện băm lại các phần tử.

III – Đặc điểm và tính chất

Bảng băm không giữ lại thứ tự của các phần tử, do hàm băm đã thực hiện
xáo trộn và rải đều các phần tử vào các vị trí của bảng.
Một thông số đáng lưu tâm đối với bảng băm là hệ số tải, đây là tỉ lệ giữa số
phần tử được lưu trữ trong bảng băm tại một thời điểm và số vị trí của bảng
băm. Với một hàm băm đạt chất lượng, thao tác tra cứu một giá trị trong bảng
băm sẽ có thể được thực hiện với chi phí không đổi khi hệ số tải không quá
1; trong trường hợp còn lại, các sự xung đột sẽ làm cho thao tác phải tốn thời
gian để giải quyết, hoặc trong trường hợp sử dụng phương pháp đánh địa chỉ
mở, không còn chỗ trong bảng băm để có thể chứa thêm các phần tử mới nữa.
Khi đó, cần phải tạo ra một bảng băm mới có kích cỡ lớn hơn, kéo theo đó là m
cũng sẽ lớn hơn, nên cần phải băm lại các khóa của các phần tử để đưa chúng
vào bảng băm mới. Theo chiều ngược lại, một hệ số tải quá nhỏ cũng có nghĩa
là phần lớn các vị trí không được dùng đến, gây ra lãng phí; khi đó có thể tạo
lại một bảng băm nhỏ hơn và băm lại các phần tử vào bảng mới.

IV – Ví dụ hoạt động

Phần này đưa ra một ví dụ về cách thức hoạt động của bảng băm. Bảng băm
lưu trữ các số nguyên, sử dụng phương pháp dò tuần tự, với hàm băm là phép
chia lấy dư và kích cỡ bảng băm là 10. Các giá trị 32; 53; 22; 92; 17; 34; 24; 37;
56 lần lượt được thêm vào bảng.

4
Vị trí
0 1 2 3 4 5 6 7 8 9
Bước
1 32
2 32 53
3 32 53 22
4 32 53 22 92
5 32 53 22 92 17
6 32 53 22 92 34 17
7 32 53 22 92 34 17 24
8 32 53 22 92 34 17 24 37
9 56 32 53 22 92 34 17 24 37
Hình 1. Nội dung của bảng băm qua các lần thêm phần tử.

Đầu tiên, 32 và 53 được thêm vào bảng một cách bình thường ﴾bước 1 và 2﴿.
Khi đến phần tử 22 ﴾bước 3﴿, vị trí của nó trùng với 32, nên các vị trí tiếp theo
được kiểm tra đến khi tìm thấy chỗ trống là vị trí số 4, và 22 được đặt vào vị trí
đó. Phần tử 92 cũng xảy ra va chạm tương tự và được đặt vào vị trí số 5 ﴾bước
4﴿. Phần tử 17 được đặt bình thường vào vị trí số 7 do nó đang trống ﴾bước 5﴿.
Phần tử 34 va chạm với phần tử 22 đã được đặt tại vị trí số 4, vị trí gần nhất
trống là vị trí số 6, vậy 34 được đặt vào vị trí số 6 ﴾bước 6﴿. Xử lí tương tự đối
với 24 và 37, thêm lần lượt vào hai vị trí số 8 và số 9 ﴾bước 7 và 8﴿. Cuối cùng
﴾bước 9﴿, phần tử 56 va chạm với 34 tại vị trí số 6, tiếp tục khảo sát các vị trí 7;
8; 9 đều không trống; quay ngược trở lại sang vị trí số 0 thì tìm thấy ô trống,
vậy 56 được đặt vào vị trí số 0.

V – Ví dụ cài đặt và sử dụng

Phần này đưa ra một số đoạn mã nguồn bằng ngôn ngữ C++ để minh họa
cho việc hiện thực hóa một thiết kế bảng băm và thực hiện một số thao tác
trên bảng băm.

1. Cài đặt

Bảng băm được cài đặt trong đoạn mã dưới đây ánh xạ mỗi một giá trị số
nguyên đến một giá trị số nguyên, sử dụng phương pháp kết nối trực tiếp, với
5
hàm băm là phép chia lấy dư và kích cỡ bảng băm là số nguyên tố nhỏ nhất
mà không nhỏ hơn một yêu cầu về kích cỡ tối thiểu được cho khi tạo bảng.

#include <algorithm>
#include <iostream>
#include <list>
#include <optional>
#include <utility>
#include <vector>

using Element = std pair<int, int>;

class HashTable final {


private:
int size;
std vector<std list<Element table;

int hash(int key);


public:
HashTable(int minimumSize);
void insert(Element element);
void erase(int key);
std optional<int> search(int key);
void display();
};

static bool checkPrime(const int n) {


if (n < 2) return false;
for (int i = 2; i < n/2; ++i)
if (n % i 0) return false;
return true;
}

static int getNearestPrime(int n) {


if (n % 2 0) ++n;
while (!checkPrime(n)) n += 2;
return n;
}

HashTable HashTable(const int minimumSize):


size(getNearestPrime(minimumSize)), table(size)
{}

int HashTable hash(const int key) {


return key % size;
}

6
void HashTable insert(const Element element) {
table[hash(element.first)].push_back(element);
}

void HashTable erase(const int key) {


auto &chain = table[hash(key)];
const auto iterator = std find_if(
chain.begin(), chain.end(),
[key](const Element e){ return e.first key; }
);
if (iterator chain.end()) chain.erase(iterator);
}

std optional<int> HashTable search(const int key) {


auto &chain = table[hash(key)];
const auto iterator = std find_if(
chain.begin(), chain.end(),
[key](const Element e){ return e.first key; }
);
return iterator chain.end()
? std optional<int>()
: std optional<int>(iterator second);
}

void HashTable display() {


for (int i = 0; i size; ++i) {
std cout i;
for (const auto &element : table[i])
std cout " " element.first;
std cout '\n';
}
}

Đoạn mã 1. Ví dụ cài đặt bảng băm.

2. Tìm phần tử xuất hiện nhiều nhất trong một danh sách

Đoạn mã dưới đây cài đặt hai phương pháp khác nhau để tìm ra phần tử có
số lần xuất hiện nhiều nhất trong một danh sách các số nguyên. Phương pháp
thứ nhất là một phương pháp thông thường ﴾hàm mostFrequent_normalMethod﴿,
còn phương pháp thứ hai sử dụng bảng băm đã được cài đặt ở phần trên ﴾hàm
mostFrequent_hashTableMethod﴿.

7
int mostFrequent_normalMethod(const std vector<int> &list) {
const int size = list.size();
int max, maxCount = 0;
for (int i = 0; i size; ++i) {
int count = 0;
for (int j = 0; j size; ++j)
if (list[i] list[j]) ++count;
if (count > maxCount) {
maxCount = count;
max = list[i];
}
}
return max;
}

int mostFrequent_hashTableMethod(const std vector<int> &list) {


HashTable table(list.size());
int max, maxCount = 0;
for (const int element : list) {
const auto result = table.search(element);
int newCount;
if (result) {
newCount = *result + 1;
table.erase(element);
} else {
newCount = 1;
}
table.insert({element, newCount});
if (newCount > maxCount) {
maxCount = newCount;
max = element;
}
}
return max;
}

Đoạn mã 2. Cài đặt hai phương pháp tìm phần tử xuất hiện nhiều nhất.

Trong khi phương pháp thông thường có độ phức tạp O(n2 ) thì phương
pháp sử dụng bảng băm có độ phức tạp là O(n), một sự cải thiện đáng kể.

8
3. Kiểm tra hai danh sách có chứa cùng các phần tử không

Đoạn mã dưới đây sử dụng bảng băm đã được cài đặt ở phần trên để thực
hiện kiểm tra xem hai danh sách số nguyên có chứa các phần tử giống nhau
hay không, tức là, danh sách này là một hoán vị của danh sách kia.

bool containSameElements(
const std vector<int> &list1, const std vector<int> &list2
) {
if (list1.size() list2.size()) return false;
const int size = list1.size();
HashTable table(size);
int uniqueCount = 0;
for (const int element : list1) {
const auto result = table.search(element);
int newCount;
if (result) {
newCount = *result + 1;
table.erase(element);
} else {
newCount = 1;
++uniqueCount;
}
table.insert({element, newCount});
}
for (const int element : list2) {
const auto result = table.search(element);
if (result) {
const int newCount = *result - 1;
table.erase(element);
if (newCount 0) --uniqueCount;
else table.insert({element, newCount});
} else {
return false;
}
}
return uniqueCount 0;
}

Đoạn mã 3. Cài đặt kiểm tra hai danh sách chứa cùng các phần tử.

9
VI – Sử dụng bảng băm trong một số ngôn ngữ lập
trình

Bảng băm là một cấu trúc dữ liệu cơ bản và thường xuyên được sử dụng,
nên nhiều ngôn ngữ lập trình cung cấp một phiên bản bảng băm đã được cài
đặt sẵn.

1. C++

Thư viện chuẩn của C++ cung cấp mẫu lớp std unordered_map. Bảng băm sử
dụng phương pháp kết nối trực tiếp; người sử dụng có thể cung cấp một hàm
làm hàm băm, hoặc nếu không thì mặc định sẽ là std hash.
std unordered_map có nhiều hàm thành viên có thể được sử dụng để tra cứu
một phần tử. at trả về giá trị tương ứng với khóa được cho nếu khóa đó tồn
tại trong bảng, ngược lại sẽ ném ngoại lệ std out_of_range. find tìm phần tử
tương ứng với khóa và trả về dưới dạng một con trỏ duyệt, con trỏ duyệt đó
sẽ là đuôi nếu khóa không tồn tại. operator[] trả về giá trị tương ứng với khóa,
tuy nhiên nếu khóa không tồn tại, nó sẽ ngay lập tức được chèn vào bảng và
một giá trị đích mặc định sẽ được khởi tạo rồi trả về. contains thực hiện kiểm
tra xem khóa có tồn tại bên trong bảng hay không.
Người dùng cũng có hai lựa chọn cơ bản về hành vi khi thêm dữ liệu vào
std unordered_map. Lựa chọn thứ nhất là chỉ chèn khi khóa chưa tồn tại trong
bảng băm, đó là các hàm insert, emplace và try_emplace. Lựa chọn thứ hai là ghi
đè dữ liệu mới vào bảng băm khi dữ liệu tương ứng với khóa đã tồn tại, đó là
hàm insert_or_assign.
Để xóa phần tử tương ứng với một khóa trong bảng băm, sử dụng hàm erase.

2. Java

Thư viện chuẩn của Java cung cấp lớp tổng quát java.util.HashMap. Bảng băm
sử dụng phương pháp kết nối trực tiếp; hàm băm là hàm hashCode của kiểu đối
tượng được sử dụng làm khóa.
Thao tác tra cứu được thực hiện sử dụng phương thức get, với giá trị tương
ứng với khóa được trả về nếu tồn tại hoặc null nếu không. containsKey có thể

10
được sử dụng để kiểm tra xem khóa có tồn tại hay không, phương thức này là
cần thiết nếu các giá trị đích được đưa vào bảng băm có thể là null. Phương
thức put thực hiện đưa một cặp khóa – giá trị vào trong bảng, thay thế giá trị
cũ nếu khóa đã tồn tại và trả về giá trị cũ đó. remove xóa phần tử tương ứng với
khóa được cho khỏi bảng băm.

3. Python

Ngôn ngữ Python cung cấp kiểu dữ liệu dict. Hàm băm là một hàm đặc biệt
__hash__ của các đối tượng khóa.

Bảng băm sử dụng phương pháp đánh địa chỉ mở. Kích cỡ bảng là 2i với i
nguyên dương, khi xảy ra đụng độ, vị trí tiếp theo được xác định bằng công
thức j1 = (5j0 + 1 + p) mod 2i , với p ban đầu là giá trị băm của khóa và được
thay đổi theo công thức p1 = ⌊p0 : 32⌋ trước khi được sử dụng trong công thức
trên; quá trình này được cho là đảm bảo một sự xáo trộn hiệu quả, tránh các
xung đột liên tiếp khi các giá trị băm liên tiếp nhau [1].
Thao tác tra cứu được thực hiện bằng cách sử dụng toán tử [] với khóa, giá
trị đích tương ứng sẽ được trả về nếu tồn tại, ngược lại ngoại lệ KeyError sẽ
được ném ra. Một khóa có thể được kiểm tra xem có tồn tại trong bảng băm
hay không sử dụng toán tử in. Việc đưa dữ liệu vào bảng cũng sử dụng toán tử
[] theo sau đó là phép gán, giá trị đích sẽ được chèn vào nếu khóa chưa tồn tại
và thay thế giá trị đích cũ nếu khóa đã tồn tại. Việc xóa một phần tử với khóa
được cho được thực hiện sử dụng phương thức pop.

4. JavaScript

Thư viện chuẩn của JavaScript cung cấp lớp Map. Bảng băm sử dụng phương
pháp kết nối trực tiếp; tuy nhiên hàm băm lại không thực sự băm giá trị của
các khóa, thay vào đó, một số nguyên ngẫu nhiên sẽ được tạo và gán cho mỗi
đối tượng được sử dụng làm khóa.
Thao tác trên Map được thực hiện thông qua các phương thức. get tra cứu giá
trị ứng với khóa hoặc trả về undefined nếu khóa đó không tồn tại; has kiểm tra
khóa có tồn tại trong bảng hay không; set gán giá trị đích tương ứng với khóa;
và delete xóa phần tử tương ứng với khóa ra khỏi bảng.

11
VII – So sánh với một số cấu trúc dữ liệu khác

1. Mảng danh sách

Với việc phải duyệt qua toàn bộ các phần tử của mảng để tìm một phần tử
mong muốn thì lợi thế về mặt chi phí của bảng băm so với mảng là quá rõ
ràng. Tuy nhiên trong một số trường hợp thì có một yêu cầu giữ lại trình tự
chèn vào của các phần tử, một giải pháp có thể là sử dụng kết hợp mảng với
bảng băm; khi đó phép chèn vẫn có độ phức tạp là O(1) nhưng phép xóa sẽ là
O(n).
Về mặt bản chất, bảng băm cũng thực chất chỉ là một mảng, có điều các
phần tử được sắp xếp theo quy luật sao cho có thể tìm kiếm nhanh vị trí của
chúng bên trong mảng.

2. Cây tìm kiếm nhị phân cân bằng

Việc tìm kiếm một phần tử trong cây nhị phân cần O(log(n)) bước, ứng với
mỗi bước là một phép so sánh phần tử cần tìm kiếm với phần tử đang được trỏ
tới trong cây, tức là một lần truy cập tới dữ liệu tại nút cây đó. Đối với những
kiểu dữ liệu có kích thước lớn thì việc so sánh sẽ trở thành chi phí chủ yếu cho
việc tìm kiếm. Trong khi đó, một bảng băm được thiết kế hợp lí trong phần lớn
các trường hợp chỉ cần một lần tính toán giá trị băm của giá trị cần tìm, một
lần truy cập vào ô tương ứng với giá trị băm đó và một hay một vài lần so sánh
với các phần tử tại ô đó. Vì thế về mặt chi phí, bảng băm thường tốn ít chi phí
hơn so với cây tìm kiếm nhị phân.
Về độ thân thiện với bộ đệm, các phần tử của cây tìm kiếm nhị phân khi
được duyệt thường nằm rải rác trong bộ nhớ, mà mỗi lần tìm kiếm lại có một
con số tương đối các phần tử cần phải truy cập dữ liệu nên nhìn chung cây tìm
kiếm nhị phân không thân thiện với bộ nhớ đệm của CPU, điều này càng làm
tăng thêm chi phí về mặt thời gian để tìm kiếm phần tử. Bảng băm thưởng chỉ
truy cập một số rất ít phần tử nên ảnh hưởng của việc trượt bộ đệm nếu có là
không đáng kể.
Khi thêm phần tử mới, cây nhị phân cần phải thực hiện các bước duyệt tương
tự như khi tìm kiếm, rồi sau đó phải thực hiện di chuyển các nút để thiết lập lại
trạng thái cân bằng, thường thì không phải tất cả các nút đều bị di chuyển mà

12
chỉ là những nút nằm lân cận với nhánh cây chứa phần tử vừa được thêm vào
cây, tuy nhiên nếu cây được hiện thực hóa sử dụng một mảng thì khi mảng hết
chỗ, tất cả các phần tử phải được di chuyển sang mảng mới có kích thước lớn
hơn. Bảng băm chỉ cần chèn phần tử mới vào vị trí tương ứng với giá trị băm
tìm được, tuy nhiên khi hết dung lượng lưu trữ, các phần tử không chỉ phải
được di chuyển sang vị trí lưu trữ mới, mà còn phải được tính toán lại giá trị
băm để sắp xếp vào số ô lưu trữ mới với số lượng lớn hơn, trừ khi các giá trị
băm được lưu trữ cùng với các phần tử. Nhìn chung, bảng băm cũng tốn ít chi
phí hơn khi thêm phần tử so với cây tìm kiếm nhị phân.
Khi xóa phần tử, cả hai cấu trúc dữ liệu trước hết đều phải thực hiện thao tác
tìm kiếm vị trí phần tử để loại bỏ; sau đó, cây tìm kiếm nhị phân phải thực hiện
loại bỏ nút tìm được ra khỏi cây, một thao tác có thể ảnh hưởng đến các nút
khác và làm cho chúng phải di chuyển, sau đó lại còn phải tiếp tục thực hiện
thao tác cân bằng lại cây; còn bảng băm chỉ cần đơn giản xóa dữ liệu phần tử
trong ô lưu trữ đi. Như vậy, trong nhiệm vụ xóa phần tử thì bảng băm thậm chí
còn tốn ít chi phí hơn nữa so với cây tìm kiếm nhị phân.
Tuy vậy, một đặc điểm chỉ cây tìm kiếm nhị phân có mà bảng băm không có
chính là thứ tự của các phần tử có thể được khôi phục một cách dễ dàng. Khi
có nhu cầu như vậy thì về mặt thực tế độ chênh lệch chi phí khi tìm kiếm cũng
không phải là lớn, nên cũng không thực sự cần thiết phải kết hợp với bảng
băm.

3. Danh sách nhảy cóc ﴾skip list﴿

3 44

3 14 44 72

3 8 14 21 44 57 72 90

Hình 2. Ví dụ một danh sách nhảy cóc.

13
Dựa trên nguyên lí “chia để trị”, danh sách nhảy cóc gồm nhiều tầng, mỗi
tầng là một danh sách liên kết. Tầng thấp nhất chứa mọi phần tử sắp xếp theo
thứ tự tăng dần, tầng phía trên chứa một tỉ lệ nhất định các phần tử của tầng
bên dưới, phân bố sao cho vị trí các phần tử cách đều nhau, cứ như vậy cho
đến tầng cuối cùng chứa một số tuyệt đối rất ít phần tử. Khi tìm kiếm, tầng
trên cùng được duyệt qua trước, từ đó quyết định được phần nào trong danh
sách chứa phần tử cần tìm kiếm và phạm vi tìm kiếm do đó được thu hẹp, rồi
tầng tiếp theo bên dưới được duyệt bắt đầu từ vị trí tương ứng tìm được ở
tầng bên trên, cứ như vậy cho đến khi tìm thấy giá trị yêu cầu hoặc đã hết tầng
cuối cùng mà không tìm thấy.
Do phạm vi tìm kiếm được chia nhỏ theo tỉ lệ qua mỗi tầng, độ phức tạp tìm
kiếm trung bình là O(log(n)). Độ phức tạp khi thêm và xóa phần tử trung bình
cũng là O(log(n)), như vậy danh sách nhảy cóc vẫn có các chi phí hoạt động
cao hơn bảng băm. Và do được cấu tạo từ các danh sách liên kết nên cấu trúc
dữ liệu này cũng chịu các bất lợi liên quan đến bộ đệm của CPU. Tuy nhiên
danh sách nhảy cóc cũng bảo toàn thứ tự sắp xếp của các phần tử và các tính
chất của nó mang lại các thuận lợi khi thiết kế các ứng dụng đa luồng [2].

4. Trie

a k s

ab an ke so

and key
Hình 3. Ví dụ một trie. Các nút màu xám biểu thị phần tử tồn tại.

Đối với các giá trị có kiểu dữ liệu chuỗi, trie trở thành một lựa chọn cho việc
tìm kiếm. Trie có thể được biểu diễn bằng một cây với mỗi nút có giá trị là một

14
chuỗi và một cờ cho biết có phần tử ứng với chuỗi đó hay không. Nút gốc là
chuỗi rỗng, mỗi nút có các nút con có chuỗi dài hơn một mắt xích và có các
mắt xích còn lại trùng với nút cha. Thông thường các giá trị có thể của một mắt
xích là một con số không lớn nên các nút con có thể được lưu vào một mảng
có kích cỡ bằng số giá trị thông qua chỉ số bằng giá trị mắt xích cuối cùng.
Để tìm kiếm, thực hiện thăm bắt đầu từ gốc của cây, tìm kiếm trong các con
của nút hiện tại có nút nào có giá trị trùng với phần mở đầu độ dài tương ứng
của giá trị cần tìm hay không, thực chất thì chỉ cần so sánh giá trị mắt xích cuối
cùng của các con. Nếu có thì thực hiện thăm nút con tương ứng và lặp lại đến
khi không tìm thấy con nữa hoặc độ dài con bằng với độ dài giá trị cần tìm, khi
đó kiểm tra giá trị cờ của nút con để xác định xem giá trị cần tìm có tồn tại hay
không.
Số lần thăm trong trường hợp xấu nhất bằng với số mắt xích của giá trị cần
tìm, trong khi đó bảng băm luôn luôn phải duyệt toàn bộ các mắt xích để tính
toán ra giá trị băm, do đó về mặt này thì trie tỏ ra có lợi thế hơn, thể hiện rõ
khi giá trị của các phần tử có xu hướng rời rạc làm cho phần lớn các lần tìm
kiếm gặp ngõ cụt trước khi duyệt hết các mắt xích. Hơn nữa việc cài đặt một
trie không yêu cầu dùng đến một hàm băm, vừa giảm độ phức tạp khi cài đặt,
vừa giảm chi phí thực hiện hàm băm đó. Tuy nhiên do là một cây nên trie cũng
gặp các bất lợi về truy cập bộ nhớ do phải truy cập nhiều lần hơn và vị trí của
dữ liệu cũng rời rạc. Một mặt khác khi các giá trị có xu hướng rời rạc là một số
lượng không nhỏ các ô trong bảng tra cứu nút con của mỗi nút là các ô trống,
gây ra lãng phí bộ nhớ.
Và trie vẫn có đặc điểm là thứ tự sắp xếp của các phần tử có thể được khôi
phục thông qua việc duyệt cây theo chiều sâu, hơn thế nữa còn cho phép liệt
kê chỉ các phần tử có giá trị bắt đầu bằng một tiền tố nào đó.

5. Kết luận

Bảng băm có ưu thế về chi phí hoạt động so với phần lớn các cấu trúc dữ
liệu khác, tuy nhiên nó không có những tính năng đặc biệt mà các cấu trúc dữ
liệu khác mang lại, chủ yếu là tính năng về đảm bảo trật tự của các phần tử,
và hàm băm tốn một chi phí nhất định để thực hiện. Do vậy vẫn phải tùy vào
các yêu cầu cụ thể để chọn các cấu trúc dữ liệu phù hợp, với bảng băm thì có
thể được chọn khi dữ liệu có số lượng không quá nhỏ và các phần tử không
có yêu cầu được sắp xếp theo một trình tự nào đó.

15
Tham khảo

[1] Mã nguồn CPython, Objects/dictobject.c,


https://github.com/python/cpython/blob/3.11/Objects/dictobject.c#L175.
[2] Fomitchev Mikhail, Lock‐free linked lists and skip lists, York University, 2003.

16

You might also like