Cài đặt từ điển bằng bảng băm đóng
Định nghĩa bảng băm đóng :
Bảng băm đóng lưu giữ các phần tử của từ điển ngay trong mảng chứ không dùng mảng làm các chỉ điểm đầu của các danh sách liên kết. Bucket thứ i chứa phần tử có giá trị băm là i, nhưng vì có thể có nhiều phần tử có cùng giá trị băm nên ta sẽ gặp trường hợp sau: ta muốn đưa vào bucket i một phần tử x nhưng bucket này đã bị chiếm bởi một phần tử y nào đó (đụng độ). Như vậy khi thiết kế một bảng băm đóng ta phải có cách để giải quyết sự đụng độ này.
Giải quyết đụng độ :
Cách giải quyết đụng độ đó gọi làchiến lược băm lại(rehash strategy). Chiến lược băm lại là chọn tuần tự các vị trí h1,..., hk theo một cách nào đó cho tới khi gặp một vị trí trống để đặt x vào. Dãy h1,..., hk gọi là dãy các phép thử. Một chiến lược đơn giản là
băm lại tuyến tính, trong đó dãy các phép thử có dạng :
Ví dụ B=8 và các phần tử của từ điển là a,b,c,d có giá trị băm lần lượt là: h(a)=3, h(b)=0, h(c)=4, h(d)=3. Ta muốn đưa các phần tử này lần lượt vào bảng băm.
Khởi đầu bảng băm là rỗng, có thể coi mỗi bucket chứa một giá trị đặc biệt Empty, Empty không bằng với bất kỳ một phần tử nào mà ta có thể xét trong tập hợp các phần tử muốn đưa vào bảng băm.
Ta đặt a vào bucket 3, b vào bucket 0, c vào bucket 4. Xét phần tử d, d có h(d)=3 nhưng bucket 3 đã bị a chiếm ta tìm vị trí h1(x)= (h (x)+1) mod B = 4, vị trí này cũng đã bị c chiếm, tiếp tục tìm sang vị trí h2 (x)= (h(x)+2) mod B= 5 đây là một bucket rỗng ta đặt d vào (xem hình IV.2)
Hình IV.2: Giải quyết đụng độ trong bảng băm đóng bằng chiến lược băm lại tuyến tính Trong bảng băm đóng, phép kiểm tra một thành viên(thủ tục MEMBER (x,A)) phải xét dãy các bucket h(x),h1(x),h2(x),... cho đến khi tìm thấy x hoặc tìm thấy một vị trí trống. Bởi vì nếu hk(x) là vị trí trống được gặp đầu tiên thì x không thể được tìm gặp ở một vị trí nào xa hơn nữa. Tuy nhiên, nói chung điều đó chỉ đúng với trường hợp ta không hề xoá đi một phần tử nào trong bảng băm. Nếu chúng ta chấp nhận phép xoá thì chúng ta qui ước rằng phần tử bị xóa sẽ được thay bởi một giá trị đặc biệt, gọi làDeleted, giá trị Deleted không bằng với bất kỳ một phần tử nào trong tập hợp đang xét vào nó cũng phải khác giá trịEmpty. Empty cũng là một giá trị đặc biệt cho ta biết ô trống.
Ví dụ
- Tìm phần tử e trong bảng băm trên, giả sử h(e)=4. Chúng ta tìm kiếm e tại các vị trí 4,5,6. Bucket 6 là chứa Empty, vậy không có e trong bảng băm.
- Tìm d, vì h(d)=3 ta khởi đầu tại vị trí này và duyệt qua các bucket 4,5. Phần tử d được tìm thấy tại bucket 5.
Sử dụng bảng băm đóng để cài đặt từ điển
Dưới đây là khai báo và thủ tục cần thiết để cài đặt từ điển bằng bảng băm đóng. Để dễ dàng minh hoạ các giá trị Deleted và Empty, giả sử rằng ta cần cài đặt từ điển gồm các chuỗi 10 kí tự. Ta có thể qui ước:
Empty là chuỗi 10 dấu + và Deleted là chuỗi 10 dấu *. Khai báo
#define B 100
#define Deleted -1000//Gia dinh gia tri cho o da bi xoa #define Empty 1000 //Gia dinh gia tri cho o chua su dung
typedef int ElementType; typedef int Dictionary [B];
Tạo hàm băm int H (ElementType X)] { return X%B; } Tạo tự điển rỗng
// Tao tu dien rong
void MakeNullDic(Dictionary D){ for (int i=0;i<B; i++)
D[i]=Empty; }
Kiểm tra sự tồn tại của phần tử trong tự điển
Hàm trả về giá tri 0 nếu phần tử X không tồn tại trong tự điển; Ngược lại, hàm trả về giá trị 1;
int Member(ElementType X, Dictionary D) {
Position init=H(X), i=0;
while ((i<B) && (D[i]!=Empty) && (D[i]!=X)) i++; return (D[i]==X);
Thêm phần tử vào tự điển
void InsertDic(ElementType X, Dictionary D) { int i=0,init;
if (FullDic(D))
printf("Bang bam day"); else if (Member(X,D)==0) { init=H(X); while((i<B)&&(D[(i+init)%B]!=Empty)&&(D[(i+init)%B]!=Deleted)) i++; D[(i+init)%B]=X;
printf("\n Vi tri de xen phan tu %d la %d\n",X,(i+init)%B); }
else
printf ("\nPhan tu da ton tai trong bang bam"); }
Xóa từ ra khỏi tự điển
void DeleteDic(ElementType X, Dictionary D) {
if (EmptyDic(D))
else { int i=0,init =H(X); while ((i<B)&&(D[(i+init)%B]!=X)&&(D[(i+init)%B]!=Deleted)) i++; if ( D[(i+init)%B]==X) D[(i+init)%B]=Deleted; } } Các phương pháp xác định hàm băm Phương pháp chia
"Lấy phần dư của giá trị khoá khi chia cho số bucket" . Tức là hàm băm có dạng: H(x)= x mod B
Phương pháp này rõ ràng là rất đơn giản nhưng nó có thể không cho kết quả ngẫu nhiên lắm. Chẳng hạn B=1000 thì H(x) chỉ phụ thuộc vào ba số cuối cùng của khoá mà không phụ thuộc vào các số đứng trước. Kết quả có thể ngẫu nhiên hơn nếu B là một số nguyên tố.
Phương pháp nhân
"Lấy khoá nhân với chính nó rồi chọn một số chữ số ở giữa làm kết quả của hàm băm". Ví dụ
Vì các chữ số ở giữa phụ thuộc vào tất cả các chữ số có mặt trong khoá do vậy các khoá có khác nhau đôi chút thì hàm băm cho kết quả khác nhau.
Phương pháp tách
Đối với các khoá dài và kích thước thay đổi người ta thường dùng phương pháp phân đoạn, tức là phân khoá ra thành nhiều đoạn có kích thước bằng nhau từ một đầu ( trừ đoạn tại đầu cuối ), nói chung mỗi đoạn có độ dài bằng độ dài của kết quả hàm băm. Phân đoạn có thể là tách hoặc gấp:
a. Tách: tách khóa ra từng đoạn rồi xếp các đoạn thành hàng được canh thẳng một đầu rồi có thể cộng chúng lại rồi áp dụng phương pháp chia để có kết quả băm.
ví dụ: khoá 17046329 tách thành 329
046 017
cộng lại ta được 392. 392 mod 1000 = 392 là kết quả băm khoá đã cho.
b. Gấp: gấp khoá lại theo một cách nào đó, có thể tương tự như gấp giấy, các chữ số cùng nằm tại một vị trí sau khi gấp dược xếp lại thẳng hàng với nhau rồi có thể cộng lại rồi áp dụng phương pháp chia (mod) để cho kết quả băm
Ví dụ: khoá 17046329 gấp hai biên vào ta có 932
046 710