TL; DR: Đảm bảo bảng băm O(1)
thời gian dự kiến trong trường hợp xấu nhất nếu bạn chọn ngẫu nhiên hàm băm của mình một cách đồng nhất từ nhóm hàm băm phổ biến. Dự kiến trường hợp xấu nhất không giống trường hợp trung bình.
Tuyên bố từ chối trách nhiệm: Tôi không chính thức chứng minh bảng băm là gì O(1)
, vì vậy hãy xem video này từ khóa học [ 1 ]. Tôi cũng không thảo luận về các khía cạnh khấu hao của bảng băm. Điều đó trực quan với cuộc thảo luận về băm và va chạm.
Tôi thấy có rất nhiều sự nhầm lẫn đáng ngạc nhiên xung quanh chủ đề này trong các câu trả lời và nhận xét khác, và sẽ cố gắng khắc phục một số trong số đó trong câu trả lời dài này.
Suy luận về trường hợp xấu nhất
Có nhiều loại phân tích trường hợp xấu nhất khác nhau. Phân tích mà hầu hết các câu trả lời đã thực hiện ở đây cho đến nay không phải là trường hợp xấu nhất, mà là trường hợp trung bình [ 2 ]. Phân tích trường hợp trung bình có xu hướng thực tế hơn. Có thể thuật toán của bạn có một đầu vào trường hợp xấu nhất, nhưng thực sự hoạt động tốt cho tất cả các đầu vào có thể có khác. Tóm lại là thời gian chạy của bạn phụ thuộc vào tập dữ liệu bạn đang chạy.
Hãy xem xét đoạn mã giả sau của get
phương thức bảng băm. Ở đây tôi giả sử chúng ta xử lý va chạm bằng cách chuỗi, vì vậy mỗi mục nhập của bảng là một danh sách được liên kết của các (key,value)
cặp. Chúng tôi cũng giả sử số lượng nhóm m
là cố định nhưng là O(n)
, n
số phần tử trong đầu vào là ở đâu.
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Như các câu trả lời khác đã chỉ ra, điều này chạy trong O(1)
trường hợp trung bình và xấu nhấtO(n)
. Chúng ta có thể phác thảo một chút về một bằng chứng thử thách ở đây. Thử thách diễn ra như sau:
(1) Bạn đưa thuật toán bảng băm của mình cho một đối thủ.
(2) Kẻ thù có thể nghiên cứu và chuẩn bị bao lâu tùy thích.
(3) Cuối cùng đối thủ cung cấp cho bạn một đầu vào có kích thước n
để bạn chèn vào bảng của mình.
Câu hỏi đặt ra là: bảng băm của bạn trên đầu vào đối thủ nhanh như thế nào?
Từ bước (1) đối thủ biết hàm băm của bạn; trong bước (2), đối thủ có thể tạo ra một danh sách các n
phần tử giống nhau hash modulo m
, bằng cách tính toán ngẫu nhiên hàm băm của một loạt các phần tử; và sau đó trong (3) họ có thể cung cấp cho bạn danh sách đó. Nhưng xin lưu ý, vì tất cả các n
phần tử đều băm vào cùng một nhóm, nên thuật toán của bạn sẽ mất O(n)
thời gian để duyệt qua danh sách được liên kết trong nhóm đó. Bất kể chúng ta thử lại thử thách bao nhiêu lần, đối thủ luôn thắng, và đó là thuật toán của bạn tệ đến mức nào, trong trường hợp xấu nhất O(n)
.
Làm thế nào đến băm là O (1)?
Điều khiến chúng tôi gặp khó khăn trong thử thách trước là đối thủ biết rất rõ hàm băm của chúng tôi và có thể sử dụng kiến thức đó để tạo ra đầu vào tồi tệ nhất có thể. Điều gì sẽ xảy ra nếu thay vì luôn sử dụng một hàm băm cố định, chúng ta thực sự có một tập hợp các hàm băm H
, mà thuật toán có thể chọn ngẫu nhiên trong thời gian chạy? Trong trường hợp bạn tò mò, H
nó được gọi là họ phổ quát của các hàm băm [ 3 ]. Được rồi, hãy thử thêm một số ngẫu nhiên vào điều này.
Trước tiên, giả sử bảng băm của chúng ta cũng bao gồm một hạt giống r
và r
được gán cho một số ngẫu nhiên tại thời điểm xây dựng. Chúng tôi chỉ định nó một lần và sau đó nó được sửa cho phiên bản bảng băm đó. Bây giờ chúng ta hãy truy cập lại mã giả của chúng ta.
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Nếu chúng ta thử thách thức một lần nữa: từ bước (1) đối thủ có thể biết tất cả các hàm băm mà chúng ta có H
, nhưng bây giờ hàm băm cụ thể mà chúng ta sử dụng phụ thuộc vào r
. Giá trị của r
là riêng tư đối với cấu trúc của chúng ta, kẻ thù không thể kiểm tra nó trong thời gian chạy, cũng như dự đoán nó trước thời hạn, vì vậy anh ta không thể tạo ra một danh sách luôn có hại cho chúng ta. Giả sử rằng ở bước (2) kẻ thù chọn một chức năng hash
trong H
một cách ngẫu nhiên, sau đó ông hàng thủ một danh sách các n
va chạm dưới hash modulo m
, và gửi đó cho bước (3), băng qua ngón tay mà khi chạy H[r]
sẽ giống nhau hash
họ đã chọn.
Đây là một cuộc đặt cược nghiêm túc đối với kẻ thù, danh sách mà anh ta tạo ra xung đột với nhau hash
, nhưng sẽ chỉ là một đầu vào ngẫu nhiên dưới bất kỳ hàm băm nào khác trong đó H
. Nếu anh ta thắng cược này thì thời gian chạy của chúng tôi sẽ là trường hợp xấu nhất O(n)
như trước đây, nhưng nếu anh ta thua thì chúng tôi chỉ được cung cấp một đầu vào ngẫu nhiên, mất O(1)
thời gian trung bình . Và thực sự thì hầu hết các lần đối thủ sẽ thua, anh ta chỉ thắng một lần trong mỗi |H|
thử thách, và chúng ta có thể kiếm |H|
được rất lớn.
Đối chiếu kết quả này với thuật toán trước đó mà đối thủ luôn thắng trong thử thách. Ở đây hơi chờ đợi một chút, nhưng vì hầu hết các trường hợp đối thủ sẽ thất bại, và điều này đúng với tất cả các chiến lược khả thi mà đối thủ có thể thử, nên mặc dù trường hợp xấu nhất là O(n)
, trường hợp xấu nhất dự kiến vẫn là thực tế O(1)
.
Một lần nữa, đây không phải là một bằng chứng chính thức. Đảm bảo mà chúng tôi nhận được từ phân tích trường hợp xấu nhất dự kiến này là thời gian chạy của chúng tôi hiện không phụ thuộc vào bất kỳ đầu vào cụ thể nào . Đây là một đảm bảo thực sự ngẫu nhiên, trái ngược với phân tích trường hợp trung bình, nơi chúng tôi cho thấy một kẻ thù có động cơ có thể dễ dàng tạo ra các đầu vào xấu.