Đăng ký Đăng nhập

Tài liệu Ti29

.DOC
40
221
135

Mô tả:

CHUYÊN ĐÊỀ HỘI THẢO KHOA HỌC CÁC TRƯỜNG THPT CHUYÊN KHU VỰC DUYÊN HẢI & ĐỒỀNG BẰỀNG BẰẮC BỘ NẰM 2016 CHUYÊN ĐỀ . MẢNG BĂM A, MỞ ĐẦU 1. Lý do chọn đề tài Các thuật toán tìm kiếm trên các cấu trúc dữ liệu như: danh sách, cây nhị phân, … phần lớn được thực hiện bằng cách so sánh các phần tử của cấu trúc, do vậy thời gian truy xuất không nhanh ( O(n), O(logn), …) và phụ thuộc vào kích thước của cấu trúc. Vậy có phương pháp lưu trữ nào cho phép thực hiện tìm kiếm với hiệu suất cao hơn không (độ phức tạp hằng số)? Các phép toán trên mảng băm sẽ giúp hạn chế số lần so sánh, giảm thiếu thời gian truy xuất. Độ phức tạp các phép toán trên mảng băm thường là O(1) và không phụ thuộc vào kích thước của mảng băm. Ví dụ, nếu chúng ta có một mảng nhiều nhất 100 phần tử. Nếu chúng ta biết vị trí mà một phần tử cụ thể được lưu trữ trong mảng, sau đó chúng ta có thể nhanh chóng truy cập vào nó. Chẳng hạn, chúng ta biết rằng các phần tử chúng ta muốn tìm là ở vị trí 3. Ta có thể áp dụng: myItem = myarray [3]. Với điều này, chúng ta không cần phải tìm kiếm thông qua từng phần tử trong mảng, chúng ta chỉ cần truy cập vị trí 3. Câu hỏi đặt ra là, làm thế nào chúng ta biết vị trí 3 chứa dữ liệu mà chúng ta quan tâm? Chúng ta có thể áp dụng một hàm băm để tìm một chỉ số hoặc vị trí mà chúng ta muốn truy cập. Mảng băm thường được sử dụng khi xử lý các bài toán có kích thước dữ liệu lớn. Tuy nhiên trong quá trình giảng dạy tôi thấy học sinh vẫn còn khó khăn trong việc hiểu và cài đặt mảng băm để giải quyết bài toán. Vì vậy trong chuyên đề này tôi mong muốn giúp học sinh có cái nhìn tổng quan hơn về mảng băm và ứng dụng của mảng băm trong giải một số bài toán cụ thể. 2. Mục đích của đề tài Về nội dung kiến thức mảng băm còn chưa được trình bày nhiều trong các tài liệu, trong chuyên đề này tôi sẽ tổng hợp lại các nội dung kiến thức về mảng băm và áp dụng để giải một số bài toán cụ thế, để làm tài liệu tham khảo cho học sinh và giáo viên trong quá trình học tập và giảng dạy các đội tuyển học sinh giỏi. B. NỘI DUNG I. Lý thuyết 1. Hàm băm (tiếng Anh: Hash function) Hàm được dùng để ánh xạ một khoá (key) vào một dãy các số nguyên và dùng các giá trị nguyên này để truy xuất dữ liệu được gọi là hàm băm. Khóa có thể là dạng số hay dạng chuỗi. Có nhiều hàm băm khác nhau. Một số hàm băm biến đổi một khóa số nguyên thành một chỉ số của mảng. Một trong những phương pháp băm phổ biến nhất là phương pháp chia (Division). 2. Mảng băm Mảng băm 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, đến giá trị tương ứng. 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. Hàm băm thường được dùng trong mảng băm nhằm giảm chi phí tính toán khi tìm một khối dữ liệu trong một tập hợp (nhờ việc so sánh các giá trị băm nhanh hơn việc so sánh những khối dữ liệu có kích thước lớn). a. Mô tả dữ liệu Giả sử • K: tập các khoá (set of keys) • A: tập các địa chỉ (set of addresses) • H(k): hàm băm dùng để ánh xạ một khoá k từ tập các khoá K thành một địa chỉ tương ứng trong tập A. b. Một số phép toán trên mảng băm • Khởi tạo (Initialize) val new : int -> hashtable (* New(N) tạo ra một bảng băm với n phần tử *) • Lấy kích thước của mảng băm (Size) val sizetable : hashtab • Tìm kiếm (Search) val Search : hashtable * key -> value option (* Search(ht,key) trả về tương ứng một số giá trị nếu được tìm thấy trong hashtable, nếu không trả về NONE *) • Thêm mới phần tử (Insert) val insert : hashtable * key * value -> unit • Loại bỏ (remove) val remove : hashtable * key -> unit (* remove(ht,key) loại bỏ khóa và giá trị của nó nếu có, Nếu không thì không có tác dụng *) c. Phân loại mảng băm • Mảng băm đóng : mỗi khóa ứng với một địa chỉ, thời gian truy xuất là hằng số • Mảng băm mở : một số khóa có cùng địa chỉ, lúc này mỗi mục địa chỉ sẽ là một danh sách liên kết các phần tử có cùng địa chỉ, thời gian truy xuất có thể bị suy giảm đôi chút. 3. Phương pháp chia (một phương pháp băm cho các số nguyên) Giả sử ta có các khóa sau cần ánh xạ vào một mảng có 10 phần tử 123456 123467 123450 Để áp dụng các phương pháp chia, có thể chia cho 10 (hoặc số tối đa các phần tử trong mảng) và sử dụng phần dư (modulo) là một chỉ số. Sau đây là kết quả: 123456 % 10 = 6 (phần dư là 6) 123.467 % 10 = 7 (phần dư là 7) 123.450 % 10 = 0 (phần dư là 0) Những con số này sẽ được chèn vào mảng tại vị trí 6, 7, và 0 tương ứng.  Điều quan trọng với phương pháp chia là các khóa là các số nguyên. Thường chọn kích thước mảng M là số nguyên tố. Hàm này rất dễ để tính, và có hiệu quả trong phân tán các khóa đều giữa 0 và M-1. Đối với các số không là số nguyên. Ta phải áp dụng một hàm băm để biến chúng thành các số nguyên. Vậy có hai hàm băm: 1. Hàm để có được một số nguyên 2. Hàm để áp dụng một hàm băm từ trên để có được một chỉ số trong một mảng   Số dấu chấm động. Nếu các khóa số thực giữa 0 và 1, chúng ta có thể nhân với M và làm tròn đến số nguyên gần nhất để có được một chỉ số giữa 0 và M-1. Mặc dù trực quan, nhưng phương pháp này là chưa hiệu quả. Một cách khác để giải quyết tình trạng này là sử dụng băm modular trên biểu diễn nhị phân của khóa Strings Chúng ta chỉ đơn giản tính thành số nguyên lớn. Hàm băm biến đổi khoá dạng chuỗi gồm n kí tự thành 1 địa chỉ (số nguyên) có thể được viết như sau: 1. int hashfunc( char *s, int n ) 2. { 3. int sum = 0; 4. while( n-- ) sum = sum + *s++; 5. return sum % 256; 6. } Địa chỉ của khoá "AB" : hashfunc("AB",2) là 131 Địa chỉ của khoá "BA" : hashfunc("BA",2) là 131 Khi hàm băm hai khoá vào cùng một địa chỉ thì gọi là đụng độ (Collision) Hàm băm tốt thỏa mãn các điều kiện sau: • Tính toán nhanh. • Các khoá được phân bố đều trong bảng. • Ít xảy ra đụng độ Ví dụ Khóa là tên của một số người sau: Sarah Jones Tony Balognie Tom Katz Các hàm băm phải làm hai việc:  Chuyển đổi tên thành các số nguyên Ví dụ, chúng ta có một hàm mà biến một chuỗi thành một số nguyên. Kết quả sẽ như sau: Sarah Jones -> 1038 Tony Balognie -> 1259 Tom Katz -> 746  Áp dụng một hàm băm để có được một chỉ số Bây giờ chúng ta có thể áp dụng phương pháp chia để có được một chỉ số cho một mảng có 10 phần tử Sarah Jones -> 1038% 10 -> 8 Tony Balognie -> 1259% 10 -> 9 Tom Katz -> 746% 10 -> 6 Mảng được biết đến như một mảng băm. Nó lưu trữ khóa (được sử dụng để tìm index) cùng với các giá trị liên quan. Một vấn đề xảy ra khi hai khóa mang chỉ số giống nhau. Ví dụ: John Smith -> 948% 10 -> 8 Chúng ta có một sự đụng độ (collision) vì Sarah Jones đã được lưu trữ tại chỉ số 8. Chúng ta cần một phương pháp để giải quyết điều này. Các cách tiếp cận chính là 1. Phương pháp nối kết (chaining) tạo ra một cấu trúc dữ liệu lưu giữ tất cả các dữ liệu có cùng vị trí i và “gắn” cấu trúc dữ liệu này vào vị trí đó trong mảng 2. Phương pháp định địa chỉ mở (open addressing/probing): mỗi khi xảy ra va chạm, tiến hành thăm dò để tìm một vị trí còn trống trong mảng và đặt dữ liệu mới vào đó. Giả sử vị trí ứng với khoá k là i, i=h(k). Từ vị trí này chúng ta lần lượt xem xét các vị trí i0 , i1 , i2 ,…, im ,… Trong đó i0 = i, im (m=0,1,2, …) là vị trí thăm dò ở lần thứ m. Dãy các vị trí này sẽ được gọi là dãy thăm dò. Phương pháp này có các phương pháp cụ thể sau:  Phương pháp dò tuyến tính (Linear Probing)  Phương pháp dò bậc hai (Quadratic Probing Method)  Phương pháp băm đôi (Double hashing)  Phương pháp nối kết Chaining là một cách để giải quyết đụng độ. Mỗi ô của mảng có chứa một liên kết đến một danh sách liên kết có chứa cặp khóa-giá trị. Cặp khóa-giá trị mới được thêm vào cuối danh sách. Thuật toán tìm kiếm thông qua danh sách để tìm khóa phù hợp. Ban đầu mỗi ô của mảng được khởi tạo null. Danh sách được tạo ra khi giá trị băm nào đó được thêm vào lần đầu tiên. Xem sơ đồ của ví dụ trên (hình bên). Có một sự đụng độ giữa Sarah Jones và John Smith. Chú ý rằng John Smith là "liên kết" sau Sarah Jones. Ưu điểm : Số dữ liệu được lưu không phụ thuộc vào cỡ của mảng Nhược điểm: Chèn chậm Cài đặt mảng băm dùng phương pháp nối kết:  Khai báo cấu trúc mảng băm: define M 100 struct nodes { int key; struct nodes *next }; //khai bao kieu con tro chi nut typedef struct nodes *NODEPTR /* khai bao mang bucket chua M con tro dau cua M bucket */ NODEPTR bucket[M];  Các phép toán Giả sử ta chọn hàm băm: h(key) = key % M int hashfunc (int key) { return (key % M); } Phép toán khởi tạo void initbuckets( ) { int b; for (b=0;bkey !=k) { q=p; p=p->next; } if (p == NULL) printf("\n khong co nut co khoa %d" ,k); else if (p == bucket [b]) pop(b); //Tac vu pop cua danh sach lien ket else delafter(q); /*tac vu delafter cua danh sach lien ket*/ } Phép toán traversebucket: Duyệt các phần tử trong bucket b. void traversebucket (int b) { NODEPTR p; p= bucket[b]; while (p !=NULL) { printf("%3d", p->key); p= p->next; } } Phép toán traverse: Duyệt toàn bộ bảng băm. void traverse( ) { int b; for (b = 0;n p->key && p !=NULL) p=p->next; if (p == NULL | | k !=p->key)// khong tim thay return(NULL); else//tim thay // else //tim thay return(p); }  Phương pháp định địa chỉ mở  Linear Probing là cấu trúc dữ liệu chứa một tập các cặp khóa-giá trị . Mỗi ô của mảng băm lưu trữ một cặp khóa-giá trị duy nhất. Khi hàm băm gây ra một vụ đụng độ bằng cách ánh xạ một khóa mới cho một ô của mảng băm đã được chiếm giữ bởi một khóa, Linear Probing tìm kiếm mảng cho vị trí trống tiếp theo gần nhất và chèn khóa mới tại đó. Thuật toán tìm kiếm được thực hiện tương tự, bằng cách tìm kiếm mảng tuần tự bắt đầu tại vị trí nhất định bởi hàm băm, cho đến khi tìm ra một ô với một khóa khớp hoặc một ô trống. F(i) = i, F(i) là hàm tuyến tính của i hoặc ta có thể viết: hi(k) = (h(k) + i) mod size. Cụ thể: h(k) mod size h(k)+1 mod size h(k)+2 mod size …. Đoạn chương trình tìm ô trống tiếp theo gần nhất Bool findEntry(const Key &k, Entry *& entry) { int probePoint = hash1(k); do { entry = &table[probePoint]; probePoint = (probePoint+1) % size; } while (!entry->isEmpty() && entry->key !=k); return !entry->isEmpty(); } Ví dụ: Vẽ quá trình hiển thị các giá trị: 76, 93, 40, 47, 10, 55 được đưa vào một mảng băm với 7 vị trí. Sử dụng phương pháp chia của băm và phương pháp Linear Probing giải quyết va chạm.  Ưu điểm: đơn giản, cho phép xét tất cả các vị trí trong mảng, phép insert luôn được thực hiện trừ khi mảng đầy.  Nhược điểm: phần tử tụm lại không phân đều tập trung thành các đoạn, sử dụng Θ (m) hoán vị của chuỗi chỉ số địa chỉ, tìm kiếm tuần tự trong từng đoạn  Bảng băm với phương pháp dò bậc hai (Quadratic Probing Method) Phương pháp dò bậc hai hoạt động bằng cách lấy chỉ số băm gốc và truy xuất các chỉ số kế tiếp bằng một đa thức bậc hai cho đến khi tìm thấy một chỉ số của ô trống. Nếu dò đến cuối mảng thì trở lại dò từ đầu mảng. Giả sử h(k) là một hàm băm mà ánh xạ một phần tử k vào một tập số nguyên [0,m-1], m là kích thước của mảng. Dò vị trí thứ i cho giá trị k bởi hàm: h i(k) = (h(k)+i2) mod m. m nên chọn là số nguyên tố Đối với một giá trị băm cho trước, các chỉ số được tạo ra bởi dò tuyến tính như sau: h, h+1, h+2, h+3, …, h+k. Việc tìm kiếm những giá trị băm trở nên kém hiệu quả Các chỉ số được tạo ra bởi phương pháp dò bậc hai: h, h+1 2, h+22, h+32, …, h+k2. Ví dụ chuỗi dò: +0, +1, +4, +9, +16, … Đoạn mã tìm ô trống tiếp theo bằng cách sử dụng phương pháp Quadratic probing: bool findEntry(const Key & k, Entry *& entry) { int probePoint = hash1(k), numProbes = 0; do { entry = &table[probePoint]; numProbes++; probePoint = (probePoint+2*numProbes - 1) % size; } while (!entry->isEmpty() && entry->key !=k); return !entry->isEmpty(); } Ví dụ 1: Vẽ quá trình các giá trị: 76, 40, 48, 5, 55 được đưa vào một mảng băm với 7 vị trí. Sử dụng phương pháp chia của băm và phương pháp Quadratic Probing giải quyết va chạm. Ví dụ 2: Vẽ quá trình các giá trị: 76, 93, 40, 35, 47 được đưa vào một mảng băm với 7 vị trí. Sử dụng phương pháp chia của băm và phương pháp Quadratic Probing giải quyết va chạm.  Ưu điểm: tránh được nhược điểm của dò tuyến tính.  Nhược điểm: không cho phép tìm đến tất cả các vị trí trong mảng, phép insert có thể không được thực hiện, nếu cỡ của mảng là số nguyên tố thì dò bình phương tìm đến một nửa số vị trí trong mảng.  Phương pháp băm đôi (Double hasing method) Phương pháp băm đôi được sử dụng trong mảng băm để giải quyết va chạm băm. Băm đôi được thực hiện ở nhiều thư viện. Giống như dò tuyến tính, nó sử dụng một giá trị băm là điểm khởi đầu và sau đó liên tục truy xuất các chỉ số kế tiếp cho đến khi giá trị mong muốn nằm tại vị trí trống được đạt tới, hoặc toàn mảng đã được tìm kiếm, nhưng chỉ số kế tiếp này được tính bởi một hàm băm độc lập thứ hai (do đó tên là băm đôi). Chọn hai hàm băm h1 và h2 độc lập, vị trí thứ i trong chuỗi ô chèn giá trị k trong một mảng băm kích thước m là: h(i,k) = (h1(k) + i.h2(k)) mod m. Cụ thể: h1(k) mod m (h1(k)+1.h2(x)) mod m (h1(k)+2.h2(x)) mod m … m nên chọn là số nguyên tố. Hàm băm thứ hai nên chọn là hàm dễ dàng tính toán, khác so với hàm thứ nhất và không được bằng 0. Một cách chọn tốt là chọn số nguyên tố R < m và h2(x) = R – (x mod R). Đoạn mã tìm ô trống tiếp theo bằng Double hasing: bool findEntry(const Key & k, Entry *& entry) { int probePoint = hash1(k), hashIncr = hash2(k); do { entry = &table[probePoint]; probePoint = (probePoint + hashIncr) % size; } while (!entry->isEmpty() && entry->key != k); return !entry->isEmpty(); } Ví dụ: Vẽ quá trình các giá trị: 76, 93, 40, 47, 10, 55 được đưa vào một mảng băm với 7 vị trí. Sử dụng phương pháp chia của băm và phương pháp Double hasing giải quyết va chạm. Với h1(key) = key mod m, h2(key) = 5- (key mod 5), h(k,i) = (h1(k) + ih2(k)) mod m.  Ưu điểm: phân bố đều hơn, nếu cỡ của mảng và bước thăm dò h2(k) nguyên tố cùng nhau thì phương pháp băm kép cho phép tìm đến tất cả các vị trí trong mảng Chọn kích thước mảng băm Các kích thước tốt nhất để lựa chọn cho mảng băm sẽ phụ thuộc vào số lượng dự kiến của các giá trị đó sẽ được lưu trữ và quan trọng như thế nào sử dụng hết không gian. Ta có thể sử dụng một mảng mà lớn hơn một chút so với số lượng dự kiến của các phần tử (1,25 lần so với số lượng dự kiến). 4. Các ứng dụng của một mảng băm Mảng băm thường được sử dụng trong trường hợp có một lượng lớn dữ liệu mà từ đó ta muốn nhanh chóng tìm kiếm và lấy thông tin. Một vài ứng dụng mảng băm điển hình sau đây: Đối với hồ sơ cấp giấy phép lái xe. Với một mảng băm, bạn có thể nhanh chóng có được thông tin về lái xe (tên, địa chỉ, tuổi.) cho các số giấy phép.  Đối với các bảng biểu tượng trình biên dịch. Trình biên dịch sử dụng một bảng ký hiệu để theo dõi những biểu tượng người dùng định nghĩa trong một chương trình C ++. Điều này cho phép trình biên dịch nhanh chóng tìm kiếm các thuộc tính liên quan với các biểu tượng (ví dụ, tên biến)  Đối với cơ sở dữ liệu danh bạ điện thoại. Bạn có thể sử dụng một mảng băm để nhanh chóng tìm số điện thoại của John Smith.  Đối với danh mục thư viện điện tử. Triển khai mảng băm cho phép tìm kiếm nhanh trong số hàng triệu tài liệu được lưu trữ trong thư viện.  Đối với việc thực hiện mật khẩu cho hệ thống với nhiều người sử dụng. Mảng băm cho phép tìm kiếm nhanh chóng các mật khẩu tương ứng với một tên người dùng nhất định.  Mảng băm dùng để giải các bài toán so khớp xâu.  Giả sử chúng ta có một ứng dụng mà các khóa là số an sinh xã hội Mỹ. Một số an sinh xã hội như 123-45-6789 là một số gồm 9 chữ số chia thành ba trường. Trường đầu tiên xác định các khu vực địa lý mà số được cấp (đối với số trường đầu tiên là 035 đến từ đảo Rhode và số trường đầu tiên là 214 đến từ Maryland) và hai trường khác xác định thông tin cá nhân. Có một tỉ số an sinh xã hội khác nhau, nhưng giả sử rằng ứng dụng của chúng ta cần phải xử lý chỉ là một vài trăm khóa, vì vậy mà chúng ta có thể sử dụng một mảng băm có kích thước M = 1000. Sử dụng tất cả chín chữ số để làm cho một giá trị int, sau đó xem xét các hàm băm cho các số nguyên. 5. Nhận xét thời gian tìm kiếm, chèn và xóa Để tìm một khóa k trong một mảng băm, ta tính toán giá trị hàm băm (v = hash (k)), sau đó xem nếu k là trong mảng. Thời gian tìm kiếm sẽ được tỷ lệ thuận với thời gian cho các hàm băm, cộng thêm thời gian để tìm kiếm k trong cấu trúc dữ liệu mảng. Trong trường hợp tốt nhất, khi nhiều nhất một giá trị băm để mỗi vị trí trong mảng, tra cứu trong mảng [v] sẽ là O(1). Trong trường hợp xấu nhất, tất cả khóa sẽ băm đến cùng một nơi. Trong trường hợp đó, nếu danh sách  liên kết được sử dụng, thời gian tồi tệ nhất để tra cứu sẽ là O (N), trong đó N là số các giá trị được lưu trữ trong mảng băm. Chèn một khóa k trong một mảng băm là tương tự như tìm kiếm: đầu tiên, v = hash (k) được tính toán, sau đó k được thêm vào mảng [v]. Nếu danh sách liên kết được sử dụng, k nên được thêm vào ở phía trước của danh sách (vì có thể được thực hiện trong thời gian hằng). Thời gian chèn là tương tự như thời gian tìm kiếm: tổng các thời gian cho các hàm băm và thời gian để chèn k vào mảng. Nếu danh sách liên kết được sử dụng, thời gian để chèn k vào mảng sẽ luôn là O(1) và O(N) trong trường hợp xấu nhất. Để xóa một khóa k từ một mảng băm, v = hash (k) được tính toán, sau đó k được xóa khỏi danh sách liên kết. Trường hợp xấu nhất là giống như tìm kiếm, vì giá trị được tìm thấy trước khi nó có thể được xóa. Tóm lại: - Mảng băm mở: Tìm kiếm không thành công: O(1+1/(1-α)) Tìm kiếm thành công: O(1+ (1/α)ln( 1/(1-α))) - Chaining: Tìm kiếm không thành công: O(1 + α) Tìm kiếm thành công: O(1 + α/2) = O(1 + α)   n/m , mảng băm với m ô để lưu trữ n phần tử (khóa) II. Bài tập Bài 1. Cho các giá trị sau: 67 46 88 91 123 141 152 155 178 288 390 399 465 572 621 734  Vẽ một sơ đồ để hiển thị các giá trị được đưa vào một mảng băm với 20 vị trí. Sử dụng phương pháp chia của băm và phương pháp Linear Probing giải quyết va chạm.  Vẽ một sơ đồ để hiển thị các giá trị được chèn vào một mảng băm mà sử dụng hàm băm h(key) = key % 10 sử dụng phương pháp chaining để lưu các giá trị. Hướng dẫn  Bước 1: Áp dụng phương pháp Division lấy Index 67% 20 = 7 46% 20 = 6 88% 20 = 8 91% 20 = 11 ... 734% 20 = 14 Bước 2: Tạo mảng sử dụng Phương pháp linear probing (hình bên)  Bước 1: Áp dụng phương pháp Division Lấy Index 67% 10 = 7 46% 10 = 6 88% 10 = 8 91% 10 = 1 ... 734% 10 = 4 Bước 2: Tạo mảng sử dụng phương pháp Chaining (hình dưới) Bài 2 Cho một mảng băm với kích thước m = 11và hai hàm băm h1 và h2: h1(key) = key mod m h2(key) = {key mod (m-1)} + 1 Chèn các khóa {22, 1, 13, 11, 24, 33, 18, 42, 31} theo thứ tự nhất định (từ trái sang phải) đến mảng băm sử dụng mỗi phương pháp băm sau đây: a. Chaining với h1  h(k) = h1(k) b. Linear-Probing với h1  h(k,i) = (h1(k)+i) mod m
- Xem thêm -

Tài liệu liên quan