Bảng băm có thể thực sự là O (1) không?


114

Có vẻ như kiến ​​thức thông thường rằng bảng băm có thể đạt được O (1), nhưng điều đó chưa bao giờ có ý nghĩa đối với tôi. Ai đó có thể vui lòng giải thích nó? Dưới đây là hai tình huống xuất hiện trong tâm trí:

A. Giá trị là một int nhỏ hơn kích thước của bảng băm. Do đó, giá trị là giá trị băm của riêng nó, vì vậy không có bảng băm. Nhưng nếu có, nó sẽ là O (1) và vẫn không hiệu quả.

B. Bạn phải tính một băm của giá trị. Trong trường hợp này, thứ tự là O (n) cho kích thước của dữ liệu đang được tra cứu. Tra cứu có thể là O (1) sau khi bạn làm công việc của O (n), nhưng điều đó vẫn hiện ra với O (n) trong mắt tôi.

Và trừ khi bạn có một bảng băm hoàn hảo hoặc một bảng băm lớn, có lẽ sẽ có một số mục trong mỗi nhóm. Vì vậy, nó biến thành một tìm kiếm tuyến tính nhỏ tại một số thời điểm.

Tôi nghĩ rằng bảng băm thật tuyệt vời, nhưng tôi không nhận được ký hiệu O (1) trừ khi nó chỉ được coi là lý thuyết.

Bài viết của Wikipedia về bảng băm luôn đề cập đến thời gian tra cứu liên tục và hoàn toàn bỏ qua chi phí của hàm băm. Đó có thực sự là một biện pháp công bằng?


Chỉnh sửa: Để tóm tắt những gì tôi đã học được:

  • Về mặt kỹ thuật, nó đúng vì hàm băm không bắt buộc phải sử dụng tất cả thông tin trong khóa và do đó có thể là thời gian không đổi, và bởi vì một bảng đủ lớn có thể đưa va chạm xuống gần thời gian không đổi.

  • Nó đúng trong thực tế vì theo thời gian, nó sẽ hoạt động miễn là hàm băm và kích thước bảng được chọn để giảm thiểu va chạm, mặc dù điều đó thường có nghĩa là không sử dụng hàm băm theo thời gian cố định.


31
Nó được khấu hao O (1), không phải O (1).
kennytm

Hãy nhớ O () là giới hạn cho một số lượng lớn các phép toán. Trung bình, bạn sẽ không có nhiều va chạm - không nhất thiết là một hoạt động riêng lẻ không có va chạm.
Martin Beckett

Tùy thuộc vào việc triển khai chuỗi, các chuỗi có thể mang theo giá trị băm của chúng, vì vậy điều này sẽ không đổi. Vấn đề là, nó không liên quan đến độ phức tạp của tra cứu băm.
Rich Remer

@kennytm Chắc chắn, việc tra cứu sau khi bạn băm thông tin đầu vào sẽ được khấu hao O (1). Nhưng chi phí tính toán băm có thực sự không đáng kể? Giả sử chúng ta đang băm một chuỗi - một mảng ký tự. Để tạo hàm băm, mỗi ký tự được lặp lại, vì vậy hàm băm một chuỗi là O (N) trong đó N là độ dài của chuỗi. Đó là cách nó được ghi lại cho C # và đây là cách hashCode()phương thức của Java được triển khai cho a String. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
spaaarky21

1
@ spaaarky21 N trong O (N) mà bạn đang nói đến là độ dài của chuỗi, khác với n là kích thước của bảng băm. Câu trả lời của Mark Byer đã giải quyết vấn đề này.
kennytm

Câu trả lời:


65

Bạn có hai biến ở đây, m và n, trong đó m là độ dài của đầu vào và n là số lượng mục trong hàm băm.

Yêu cầu về hiệu suất tra cứu O (1) đưa ra ít nhất hai giả định:

  • Các đối tượng của bạn có thể bằng nhau so với thời gian O (1).
  • Sẽ có ít va chạm băm.

Nếu các đối tượng của bạn có kích thước thay đổi và việc kiểm tra tính bình đẳng yêu cầu xem xét tất cả các bit thì hiệu suất sẽ trở thành O (m). Tuy nhiên, hàm băm không nhất thiết phải là O (m) - nó có thể là O (1). Không giống như hàm băm mật mã, hàm băm để sử dụng trong từ điển không phải xem xét từng bit trong đầu vào để tính toán hàm băm. Việc triển khai miễn phí chỉ xem xét một số bit cố định.

Đối với đủ nhiều mục, số mục sẽ trở nên lớn hơn số băm có thể có và khi đó bạn sẽ nhận được xung đột khiến hiệu suất tăng lên trên O (1), ví dụ O (n) cho một danh sách liên kết đơn giản truyền qua (hoặc O (n * m) nếu cả hai giả thiết đều sai).

Trong thực tế, mặc dù khẳng định O (1) là sai về mặt kỹ thuật, nhưng lại gần đúng với nhiều tình huống trong thế giới thực, và đặc biệt là những tình huống mà các giả định trên được áp dụng.


4
Tương tự như trên, nếu bạn đang sử dụng các đối tượng không thay đổi làm khóa của mình, ví dụ Chuỗi Java, sau khi tính toán băm một lần, bạn có thể nhớ nó và không phải tính toán lại. Mặt khác, bạn thường không thể dựa vào hàm băm để biết liệu hai khóa có bằng nhau hay không khi bạn đã tìm đúng nhóm, vì vậy đối với các chuỗi, bạn cần thực hiện một phép kiểm tra O (m) để tìm xem chúng có bằng nhau hay không.
JeremyP

1
@JeremyP: Điểm tốt khi so sánh bình đẳng O (m). Tôi đã bỏ lỡ điều đó - bài đăng được cập nhật. Cảm ơn!
Đánh dấu Byers

2
Các O(1)khiếu nại là đúng nếu bạn đang băm ints hay cái gì khác mà phù hợp trong một từ máy. Đó là điều mà hầu hết các lý thuyết về băm đều giả định.
Thomas Ahle

Tôi thích lời giải thích đó của bạn Mark, tôi đã trích dẫn nó trong bài viết của tôi về bảng băm trên meshfields.de/hash-tables
Steve K

3
Trong "m là độ dài của đầu vào" - đầu vào quá mơ hồ - nó có thể có nghĩa là tất cả các khóa và giá trị được chèn vào, nhưng sau này sẽ rõ ràng hơn (ít nhất đối với những người đã hiểu chủ đề) bạn muốn nói đến khóa . Chỉ gợi ý sử dụng "key" trong câu trả lời cho rõ ràng. BTW - ví dụ cụ thể - Các std::hashkhóa văn bản của Visual C ++ kết hợp 10 ký tự cách đều nhau dọc theo văn bản thành giá trị băm, vì vậy nó là O (1) bất kể độ dài văn bản (nhưng dễ bị va chạm hơn GCC!). Một cách riêng biệt, các khẳng định của O (1) có một giả thiết khác (thường là đúng) rằng m nhỏ hơn n rất nhiều .
Tony Delroy

22

Bạn phải tính toán băm, vì vậy thứ tự là O (n) cho kích thước của dữ liệu đang được tra cứu. Tra cứu có thể là O (1) sau khi bạn làm công việc của O (n), nhưng điều đó vẫn hiện ra với O (n) trong mắt tôi.

Gì? Để băm một phần tử đơn lẻ cần thời gian không đổi. Tại sao nó sẽ là bất cứ điều gì khác? Nếu bạn đang chèn ncác phần tử, thì vâng, bạn phải tính toán các hàm nbăm và điều đó cần thời gian tuyến tính ... để tra cứu một phần tử, bạn tính toán một hàm băm duy nhất của những gì bạn đang tìm kiếm, sau đó tìm nhóm thích hợp với nó . Bạn không tính toán lại hàm băm của mọi thứ đã có trong bảng băm.

Và trừ khi bạn có một bảng băm hoàn hảo hoặc một bảng băm lớn, có lẽ sẽ có một số mục trên mỗi nhóm nên dù sao thì nó cũng chuyển thành một tìm kiếm tuyến tính nhỏ tại một số điểm.

Không cần thiết. Các nhóm không nhất thiết phải là danh sách hoặc mảng, chúng có thể là bất kỳ loại vùng chứa nào, chẳng hạn như BST cân bằng. Điều đó có nghĩa là O(log n)trường hợp xấu nhất. Nhưng đây là lý do tại sao điều quan trọng là phải chọn một hàm băm tốt để tránh đặt quá nhiều phần tử vào một nhóm. Như KennyTM đã chỉ ra, trung bình, bạn vẫn sẽ có O(1)thời gian, ngay cả khi thỉnh thoảng bạn phải đào bới.

Việc đánh đổi các bảng băm tất nhiên là sự phức tạp về không gian. Bạn đang giao dịch không gian cho thời gian, đây dường như là trường hợp thông thường trong khoa học máy tính.


Bạn đề cập đến việc sử dụng chuỗi làm khóa trong một trong các nhận xét khác của bạn. Bạn lo lắng về lượng thời gian cần thiết để tính toán băm của một chuỗi, vì nó bao gồm một số ký tự? Như một người khác đã chỉ ra một lần nữa, bạn không nhất thiết phải nhìn vào tất cả các ký tự để tính toán băm, mặc dù nó có thể tạo ra một hàm băm tốt hơn nếu bạn làm vậy. Trong trường hợp đó, nếu có các mký tự trung bình trong khóa của bạn và bạn đã sử dụng tất cả chúng để tính toán hàm băm của mình, thì tôi cho rằng bạn đúng, việc tra cứu sẽ mất O(m). Nếu m >> nsau đó bạn có thể có một vấn đề. Trong trường hợp đó, có lẽ bạn sẽ tốt hơn với một BST. Hoặc chọn một hàm băm rẻ hơn.


bảng băm không sử dụng BST. Các BST không yêu cầu giá trị băm. Bản đồ và Bộ có thể được triển khai dưới dạng BST.
Nick Dandoulakis

3
@Nick: Hả? Không ... BST không yêu cầu giá trị băm ... đó là vấn đề. Chúng tôi giả định rằng tại thời điểm này chúng tôi đã có xung đột (cùng một băm ... hoặc ít nhất là cùng một thùng), vì vậy chúng tôi cần xem xét một thứ khác để tìm phần tử phù hợp, tức là giá trị thực tế.
mpen

ồ, tôi hiểu ý của bạn. Nhưng tôi không chắc rằng việc trộn các BST và băm có đáng để bạn gặp rắc rối. Tại sao không chỉ sử dụng các BST?
Nick Dandoulakis

2
Tôi chỉ nói rằng bạn có thể loại bỏ điều đó O(n)khi va chạm. Nếu bạn đang mong đợi nhiều va chạm, thì bạn đã đúng, có lẽ tốt hơn hết bạn nên chọn một BST ngay từ đầu.
mpen

1
@ spaaarky21 Đúng, nhưng Ntrong trường hợp đó là độ dài của chuỗi. Chúng ta chỉ cần băm một chuỗi để xác định 'thùng' nó cần đi vào - nó không phát triển theo độ dài của bản đồ băm.
mpen

5

Hàm băm có kích thước cố định - tra cứu nhóm băm thích hợp là một hoạt động chi phí cố định. Điều này có nghĩa rằng nó là O (1).

Tính toán hàm băm không nhất thiết phải là một hoạt động đặc biệt tốn kém - chúng ta không nói đến các hàm băm mật mã ở đây. Nhưng đó là bằng cách. Bản thân phép tính hàm băm không phụ thuộc vào số n phần tử; trong khi nó có thể phụ thuộc vào kích thước của dữ liệu trong một phần tử, đây không phải là những gì n đề cập đến. Vì vậy, tính toán của băm không phụ thuộc vào n và cũng là O (1).


3
tra cứu thùng băm là O (1). Nhưng định vị khóa bên phải, là một thủ tục O (n), trong đó n phụ thuộc vào số lần va chạm băm.
Nick Dandoulakis

1
Vậy trong 3 bước tính băm, tìm gầu, tìm gầu, bước giữa là hằng số? Tìm kiếm nhóm thường không đổi. Tính toán băm thường rẻ hơn một số đơn đặt hàng độ lớn so với các phương pháp tìm nhóm khác. Nhưng điều đó có thực sự cộng lại với thời gian không đổi? Trong một tìm kiếm chuỗi con ngây thơ, bạn sẽ nói O (n * m) cho hai độ dài, vậy tại sao độ dài của khóa lại bị bỏ qua ở đây?
rút tiền vào

việc tìm khóa có độ dài cố định chỉ là O (n) chỉ khi danh sách của nó được hỗ trợ, bảng băm được hỗ trợ cây cân bằng sẽ là O (log (n))
jk.

@Jk Đối với các hàm băm tốt, trường hợp xấu nhất luôn xảy ra logn, hãy xem câu trả lời của tôi tại stackoverflow.com/questions/4553624/hashmap-get-put-complexity/…
Thomas Ahle,

Mở trường hợp xấu nhất phức tạp sẽ được o (n) trong trường hợp va chạm
Saurabh Chandra Patel

3

Hashing là O (1) chỉ khi có số lượng khóa không đổi trong bảng và một số giả thiết khác được thực hiện. Nhưng trong những trường hợp như vậy nó có lợi thế.

Nếu khóa của bạn có biểu diễn n-bit, hàm băm của bạn có thể sử dụng 1, 2, ... n trong số các bit này. Suy nghĩ về một hàm băm sử dụng 1 bit. Đánh giá chắc chắn là O (1). Nhưng bạn chỉ đang phân vùng không gian khóa thành 2. Vì vậy, bạn đang ánh xạ tối đa 2 ^ (n-1) khóa vào cùng một thùng. bằng cách sử dụng tìm kiếm BST, việc này cần đến n-1 bước để tìm một khóa cụ thể nếu gần đầy.

Bạn có thể mở rộng điều này để thấy rằng nếu hàm băm của bạn sử dụng K bit thì kích thước thùng của bạn là 2 ^ (nk).

vì vậy hàm băm K-bit ==> không quá 2 ^ K thùng hiệu dụng ==> tối đa 2 ^ (nK) khóa n-bit trên mỗi thùng ==> (nK) bước (BST) để giải quyết xung đột. Trên thực tế, hầu hết các hàm băm ít "hiệu quả" hơn nhiều và cần / sử dụng nhiều hơn K bit để tạo ra 2 ^ k thùng. Vì vậy, ngay cả điều này là lạc quan.

Bạn có thể xem nó theo cách này - bạn sẽ cần ~ n bước để có thể phân biệt duy nhất một cặp khóa gồm n bit trong trường hợp xấu nhất. Thực sự không có cách nào để vượt qua giới hạn lý thuyết thông tin này, bảng băm hay không.

Tuy nhiên, đây KHÔNG phải là cách / khi bạn sử dụng bảng băm!

Phân tích độ phức tạp giả định rằng đối với các khóa n-bit, bạn có thể có các khóa O (2 ^ n) trong bảng (ví dụ: 1/4 của tất cả các khóa có thể có). Nhưng hầu hết nếu không phải tất cả thời gian chúng ta sử dụng bảng băm, chúng ta chỉ có một số lượng không đổi các khóa n-bit trong bảng. Nếu bạn chỉ muốn một số lượng khóa không đổi trong bảng, giả sử C là số tối đa của bạn, thì bạn có thể tạo một bảng băm gồm các thùng O (C), đảm bảo xung đột dự kiến ​​liên tục (với một hàm băm tốt); và một hàm băm sử dụng ~ logC của n bit trong khóa. Khi đó mọi truy vấn là O (logC) = O (1). Đây là cách mọi người tuyên bố "quyền truy cập bảng băm là O (1)" /

Có một số điểm nổi bật ở đây - trước tiên, nói rằng bạn không cần tất cả các bit có thể chỉ là một thủ thuật thanh toán. Đầu tiên, bạn không thể thực sự chuyển giá trị khóa cho hàm băm, bởi vì điều đó sẽ di chuyển n bit trong bộ nhớ là O (n). Vì vậy, bạn cần phải làm ví dụ như chuyển một tham chiếu. Nhưng bạn vẫn cần lưu trữ nó ở đâu đó đã là một phép toán O (n); bạn chỉ cần không lập hóa đơn cho phép băm; bạn tổng thể nhiệm vụ tính toán không thể tránh được điều này. Thứ hai, bạn thực hiện băm, tìm thùng rác và tìm thấy nhiều hơn 1 khóa; chi phí của bạn phụ thuộc vào phương pháp giải quyết của bạn - nếu bạn thực hiện so sánh dựa trên (BST hoặc Danh sách), bạn sẽ có thao tác O (n) (khóa gọi lại là n-bit); Nếu bạn thực hiện hàm băm thứ 2, thì bạn cũng gặp phải vấn đề tương tự nếu hàm băm thứ 2 có xung đột.

Hãy xem xét phương án thay thế, ví dụ BST, trong trường hợp này. có các khóa C, do đó, một BST cân bằng sẽ có chiều sâu là O (logC), do đó tìm kiếm thực hiện các bước O (logC). Tuy nhiên, so sánh trong trường hợp này sẽ là một phép toán O (n) ... vì vậy có vẻ như hàm băm là một lựa chọn tốt hơn trong trường hợp này.


1

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 getphươ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 mlà cố định nhưng là O(n), nsố 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 nphầ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 nphầ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ò, Hnó đượ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 rrđượ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 rlà 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 hashtrong Hmột cách ngẫu nhiên, sau đó ông hàng thủ một danh sách các nva 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 hashhọ đã 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.


0

Có hai cài đặt mà bạn có thể nhận được O (1) lần trong trường hợp xấu nhất.

  1. Nếu thiết lập của bạn là tĩnh, thì băm FKS sẽ giúp bạn đảm bảo O (1) trong trường hợp xấu nhất . Nhưng như bạn đã chỉ ra, cài đặt của bạn không tĩnh.
  2. Nếu bạn sử dụng hàm băm Cuckoo, thì các truy vấn và xóa là O (1) trường hợp xấu nhất, nhưng việc chèn chỉ là O (1) được mong đợi. Hàm băm Cuckoo hoạt động khá tốt nếu bạn có giới hạn trên về tổng số lần chèn và đặt kích thước bảng lớn hơn khoảng 25%.

Được sao chép từ đây


0

Có vẻ như dựa trên thảo luận ở đây, rằng nếu X là giá trị trần của (# phần tử trong bảng / # thùng), thì câu trả lời tốt hơn là O (log (X)) giả sử thực hiện tra cứu bin hiệu quả.


0

A. Giá trị là một int nhỏ hơn kích thước của bảng băm. Do đó, giá trị là giá trị băm của riêng nó, vì vậy không có bảng băm. Nhưng nếu có, nó sẽ là O (1) và vẫn không hiệu quả.

Đây là một trường hợp mà bạn có thể ánh xạ các khóa tới các nhóm riêng biệt, vì vậy một mảng có vẻ là lựa chọn cấu trúc dữ liệu tốt hơn so với bảng băm. Tuy nhiên, sự kém hiệu quả không tăng theo kích thước bảng.

(Bạn vẫn có thể sử dụng bảng băm vì bạn không tin tưởng các int vẫn nhỏ hơn kích thước bảng khi chương trình phát triển, bạn muốn làm cho mã có khả năng sử dụng lại khi mối quan hệ đó không giữ hoặc bạn không muốn mọi người đọc / duy trì mã phải lãng phí nỗ lực tinh thần để hiểu và duy trì mối quan hệ).

B. Bạn phải tính một băm của giá trị. Trong trường hợp này, thứ tự là O (n) cho kích thước của dữ liệu đang được tra cứu. Tra cứu có thể là O (1) sau khi bạn làm công việc của O (n), nhưng điều đó vẫn hiện ra với O (n) trong mắt tôi.

Chúng ta cần phân biệt giữa kích thước của khóa (ví dụ tính bằng byte) và kích thước của số lượng khóa được lưu trữ trong bảng băm. Tuyên bố rằng bảng băm cung cấp các hoạt động O (1) có nghĩa là các hoạt động (chèn / xóa / tìm) không có xu hướng chậm lại hơn nữa khi số lượng khóa tăng từ hàng trăm đến hàng nghìn đến hàng triệu đến hàng tỷ (ít nhất là không nếu tất cả dữ liệu được truy cập / cập nhật trong bộ nhớ nhanh như nhau, có thể là RAM hoặc đĩa - hiệu ứng bộ nhớ cache có thể phát huy tác dụng nhưng ngay cả chi phí bỏ lỡ bộ nhớ cache trong trường hợp xấu nhất có xu hướng là một số liên tục của lần truy cập trường hợp tốt nhất).

Hãy xem xét một danh bạ điện thoại: bạn có thể có những cái tên trong đó khá dài, nhưng cho dù cuốn sách đó có 100 tên hay 10 triệu tên thì độ dài tên trung bình sẽ khá nhất quán và trường hợp xấu nhất trong lịch sử ...

Kỷ lục thế giới Guinness cho cái tên dài nhất được mọi người sử dụng được lập bởi Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Wolfeschlegelsteinhausenbergerdorff, Senior

... wccho tôi biết đó là 215 ký tự - đó không phải là giới hạn trên cứng đối với độ dài khóa, nhưng chúng ta không cần phải lo lắng về việc có nhiều hơn nữa.

Điều đó phù hợp với hầu hết các bảng băm trong thế giới thực: độ dài khóa trung bình không có xu hướng tăng theo số lượng khóa được sử dụng. Có những trường hợp ngoại lệ, ví dụ như quy trình tạo khóa có thể trả về các chuỗi nhúng các số nguyên tăng dần, nhưng ngay cả sau đó mỗi khi bạn tăng số lượng khóa theo thứ tự độ lớn, bạn chỉ tăng độ dài khóa thêm 1 ký tự: điều đó không đáng kể.

Cũng có thể tạo hàm băm từ một lượng dữ liệu khóa có kích thước cố định. Ví dụ: Visual C ++ của Microsoft có triển khai Thư viện tiêu chuẩn std::hash<std::string>tạo ra một hàm băm kết hợp chỉ mười byte cách đều nhau dọc theo chuỗi, vì vậy nếu các chuỗi chỉ khác nhau ở các chỉ số khác, bạn sẽ có xung đột (và do đó trong thực tế không phải là hành vi O (1) về phía tìm kiếm sau va chạm), nhưng thời gian tạo hàm băm có giới hạn trên cứng.

Và trừ khi bạn có một bảng băm hoàn hảo hoặc một bảng băm lớn, có lẽ sẽ có một số mục trong mỗi nhóm. Vì vậy, nó biến thành một tìm kiếm tuyến tính nhỏ tại một số thời điểm.

Nói chung là đúng, nhưng điều tuyệt vời về bảng băm là số lượng khóa được truy cập trong quá trình "tìm kiếm tuyến tính nhỏ" đó là - đối với cách tiếp cận chuỗi riêng biệt đối với va chạm - một hàm của hệ số tải bảng băm (tỷ lệ khóa trên nhóm).

Ví dụ: với hệ số tải là 1,0, độ dài trung bình của các tìm kiếm tuyến tính đó là ~ 1,58, bất kể số lượng khóa (xem câu trả lời của tôi tại đây ). Đối với băm đóng thì phức tạp hơn một chút, nhưng không tệ hơn nhiều khi hệ số tải không quá cao.

Về mặt kỹ thuật, nó đúng vì hàm băm không bắt buộc phải sử dụng tất cả thông tin trong khóa và do đó có thể là thời gian không đổi, và bởi vì một bảng đủ lớn có thể đưa va chạm xuống gần thời gian không đổi.

Loại này bỏ sót điểm. Bất kỳ loại cấu trúc dữ liệu kết hợp nào đôi khi cũng phải thực hiện các phép toán trên mọi phần của khóa (sự bất bình đẳng đôi khi có thể được xác định chỉ từ một phần của khóa, nhưng sự bình đẳng thường yêu cầu mọi bit được xem xét). Ở mức tối thiểu, nó có thể băm khóa một lần và lưu trữ giá trị băm, và nếu nó sử dụng một hàm băm đủ mạnh - ví dụ: MD5 64-bit - thì thực tế nó có thể bỏ qua khả năng hai khóa băm thành cùng một giá trị (một công ty Tôi đã làm việc để thực hiện chính xác điều đó cho cơ sở dữ liệu phân tán: thời gian tạo băm vẫn không đáng kể so với truyền mạng toàn WAN). Vì vậy, không có quá nhiều điều ám ảnh về chi phí để xử lý khóa: đó là vốn có trong việc lưu trữ khóa bất kể cấu trúc dữ liệu và như đã nói ở trên - doesn '

Đối với các bảng băm đủ lớn làm giảm va chạm, điều đó cũng thiếu điểm. Đối với chuỗi riêng biệt, bạn vẫn có chiều dài chuỗi va chạm trung bình không đổi ở bất kỳ hệ số tải nhất định nào - nó chỉ cao hơn khi hệ số tải cao hơn và mối quan hệ đó là phi tuyến tính. Người dùng SO Hans bình luận về câu trả lời của tôi cũng được liên kết ở trên rằng:

chiều dài gầu trung bình được điều chỉnh trên các gầu khác là thước đo hiệu quả tốt hơn. Nó là a / (1-e ^ {- a}) [trong đó a là hệ số tải, e là 2,71828 ...]

Vì vậy, chỉ riêng hệ số tải sẽ xác định số lượng phím va chạm trung bình mà bạn phải tìm kiếm trong các thao tác chèn / xóa / tìm. Đối với chuỗi riêng biệt, nó không chỉ là không đổi khi hệ số tải thấp - nó luôn không đổi. Đối với địa chỉ mở, mặc dù xác nhận quyền sở hữu của bạn có một số giá trị: một số phần tử va chạm được chuyển hướng đến các nhóm thay thế và sau đó có thể gây trở ngại cho các hoạt động trên các khóa khác, do đó, ở các hệ số tải cao hơn (đặc biệt là> .8 hoặc .9) độ dài chuỗi va chạm trở nên tồi tệ hơn đáng kể.

Nó đúng trong thực tế vì theo thời gian, nó sẽ hoạt động miễn là hàm băm và kích thước bảng được chọn để giảm thiểu va chạm, mặc dù điều đó thường có nghĩa là không sử dụng hàm băm theo thời gian cố định.

Chà, kích thước bảng sẽ dẫn đến hệ số tải lành mạnh với sự lựa chọn băm gần hoặc chuỗi riêng biệt, nhưng cũng có thể nếu hàm băm hơi yếu và các phím không ngẫu nhiên, việc có một số nhóm nguyên tố thường giúp giảm xung đột cũng vậy ( hash-value % table-sizesau đó bao bọc xung quanh như vậy mà chỉ thay đổi thành một hoặc hai bit bậc cao trong giá trị băm vẫn giải quyết các nhóm rải giả ngẫu nhiên trên các phần khác nhau của bảng băm).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.