Điều gì sẽ khiến một thuật toán có độ phức tạp O (log n)?


106

Kiến thức của tôi về big-O còn hạn chế, và khi các thuật ngữ log hiển thị trong phương trình, nó càng khiến tôi cảm thấy khó chịu hơn.

Ai đó có thể giải thích cho tôi bằng những thuật ngữ đơn giản O(log n)thuật toán là gì không? Logarit đến từ đâu?

Điều này đặc biệt xuất hiện khi tôi đang cố gắng giải câu hỏi thực hành giữa kỳ này:

Cho X (1..n) và Y (1..n) chứa hai danh sách các số nguyên, mỗi số được sắp xếp theo thứ tự không giảm. Đưa ra thuật toán O (log n)-time để tìm trung vị (hoặc số nguyên nhỏ nhất thứ n) của tất cả 2n phần tử kết hợp. Ví dụ: X = (4, 5, 7, 8, 9) và Y = (3, 5, 8, 9, 10), thì 7 là giá trị trung bình của danh sách kết hợp (3, 4, 5, 5, 7 , 8, 8, 9, 9, 10). [Gợi ý: sử dụng các khái niệm về tìm kiếm nhị phân]


29
O(log n)có thể thấy như: Nếu bạn tăng gấp đôi kích thước bài toán n, thuật toán của bạn chỉ cần thêm một số bước không đổi.
phimuemue 05/02/12

3
Trang web này đã giúp tôi udnerst và ký hiệu Big O: recursive-design.com/blog/2010/12/07/…
Brad

1
Tôi đang tự hỏi tại sao 7 lại là trung bình của ví dụ trên, nhưng nó cũng có thể là 8. Không phải là quá tốt của một ví dụ phải không?
stryba

13
Một cách tốt để suy nghĩ về các thuật toán O (log (n)) là trong mỗi bước, chúng giảm kích thước của vấn đề đi một nửa. Lấy ví dụ về tìm kiếm nhị phân - trong mỗi bước, bạn kiểm tra giá trị ở giữa phạm vi tìm kiếm của mình, chia phạm vi làm đôi; sau đó bạn loại bỏ một trong hai nửa khỏi phạm vi tìm kiếm của mình và nửa còn lại trở thành phạm vi tìm kiếm của bạn cho bước tiếp theo. Và do đó trong mỗi bước, phạm vi tìm kiếm của bạn giảm đi một nửa về kích thước, do đó độ phức tạp của thuật toán là O (log (n)). (giảm không phải là chính xác một nửa, nó có thể được một phần ba, 25%, tỷ lệ bất kỳ liên tục; nửa là nhất phổ biến)
Krzysztof Kozielczyk

cảm ơn các bạn, đang giải quyết vấn đề trước đây và sẽ sớm giải quyết vấn đề này, rất mong các bạn giải đáp! sẽ quay lại sau để nghiên cứu này
user1189352

Câu trả lời:


290

Tôi phải đồng ý rằng thật kỳ lạ khi lần đầu tiên bạn nhìn thấy một thuật toán O (log n) ... lôgarit đó đến từ đâu vậy? Tuy nhiên, hóa ra có một số cách khác nhau để bạn có thể có được một thuật ngữ nhật ký để hiển thị trong ký hiệu big-O. Ở đây có một ít:

Chia lặp lại cho một hằng số

Lấy một số n bất kỳ; nói, 16. Bạn có thể chia n cho hai lần trước khi nhận được một số nhỏ hơn hoặc bằng một? Đối với 16, chúng tôi có điều đó

16 / 2 = 8
 8 / 2 = 4
 4 / 2 = 2
 2 / 2 = 1

Lưu ý rằng điều này kết thúc với bốn bước để hoàn thành. Điều thú vị là chúng ta cũng có log 2 16 = 4. Hmmm ... 128 thì sao?

128 / 2 = 64
 64 / 2 = 32
 32 / 2 = 16
 16 / 2 = 8
  8 / 2 = 4
  4 / 2 = 2
  2 / 2 = 1

Điều này mất bảy bước và log 2 128 = 7. Đây có phải là sự trùng hợp không? Không! Có một lý do chính đáng cho điều này. Giả sử rằng chúng ta chia một số n cho 2 i lần. Khi đó ta được số n / 2 i . Nếu chúng ta muốn tìm giá trị của i trong đó giá trị này nhiều nhất là 1, chúng ta nhận được

n / 2 i ≤ 1

n ≤ 2 tôi

log 2 n ≤ i

Nói cách khác, nếu chúng ta chọn một số nguyên i sao cho i ≥ log 2 n, thì sau khi chia n thành một nửa i nhân, chúng ta sẽ có giá trị lớn nhất là 1. Giá trị i nhỏ nhất mà điều này được đảm bảo gần đúng là log 2 n, vì vậy nếu chúng ta có một thuật toán chia hết cho 2 cho đến khi số đủ nhỏ, thì chúng ta có thể nói rằng nó kết thúc ở bước O (log n).

Một chi tiết quan trọng là không quan trọng bạn đang chia n cho hằng số nào (miễn là nó lớn hơn một); Nếu bạn chia cho hằng số k, sẽ cần log k n bước để đạt được 1. Vì vậy, bất kỳ thuật toán nào chia nhiều lần kích thước đầu vào cho một số phân số sẽ cần lần lặp O (log n) để kết thúc. Những lần lặp lại đó có thể mất rất nhiều thời gian và do đó thời gian chạy thực không cần phải là O (log n), nhưng số bước sẽ là logarit.

Vì vậy, điều này xuất hiện ở đâu? Một ví dụ cổ điển là tìm kiếm nhị phân , một thuật toán nhanh để tìm kiếm một giá trị trong mảng đã sắp xếp. Thuật toán hoạt động như sau:

  • Nếu mảng trống, hãy trả về rằng phần tử không có trong mảng.
  • Nếu không thì:
    • Nhìn vào phần tử giữa của mảng.
    • Nếu nó bằng với yếu tố chúng ta đang tìm kiếm, hãy trả lại thành công.
    • Nếu nó lớn hơn phần tử chúng tôi đang tìm kiếm:
      • Vứt bỏ nửa sau của mảng.
      • Nói lại
    • Nếu nó nhỏ hơn phần tử chúng tôi đang tìm kiếm:
      • Bỏ nửa đầu của mảng.
      • Nói lại

Ví dụ: để tìm kiếm 5 trong mảng

1   3   5   7   9   11   13

Đầu tiên chúng ta sẽ xem xét yếu tố giữa:

1   3   5   7   9   11   13
            ^

Vì 7> 5 và vì mảng đã được sắp xếp, chúng ta biết thực tế là số 5 không thể nằm ở nửa sau của mảng, vì vậy chúng ta có thể loại bỏ nó. Cái lá này

1   3   5

Vì vậy, bây giờ chúng ta xem xét phần tử giữa ở đây:

1   3   5
    ^

Vì 3 <5, chúng ta biết rằng 5 không thể xuất hiện trong nửa đầu của mảng, vì vậy chúng ta có thể ném nửa đầu mảng để lại

        5

Một lần nữa chúng ta nhìn vào phần giữa của mảng này:

        5
        ^

Vì đây chính xác là số chúng tôi đang tìm kiếm, chúng tôi có thể báo cáo rằng 5 thực sự nằm trong mảng.

Vì vậy, làm thế nào hiệu quả là điều này? Chà, trên mỗi lần lặp, chúng ta đang loại bỏ ít nhất một nửa số phần tử mảng còn lại. Thuật toán dừng ngay khi mảng trống hoặc chúng ta tìm thấy giá trị mà chúng ta muốn. Trong trường hợp xấu nhất, phần tử không có ở đó, vì vậy chúng tôi tiếp tục giảm một nửa kích thước của mảng cho đến khi hết phần tử. Điều này mất bao lâu? Chà, vì chúng ta cứ lặp đi lặp lại việc cắt mảng làm đôi, nên chúng ta sẽ thực hiện tối đa O (log n) lần lặp, vì chúng ta không thể cắt mảng thành một nửa nhiều hơn O (log n) lần trước khi chạy ngoài các phần tử của mảng.

Các thuật toán tuân theo kỹ thuật chung là chia để trị (cắt vấn đề thành nhiều phần, giải quyết các phần đó, sau đó đặt vấn đề lại với nhau) có xu hướng có các số hạng logarit trong chúng vì lý do tương tự - bạn không thể tiếp tục cắt một số đối tượng trong gấp rưỡi O (log n) lần. Bạn có thể muốn xem xét sắp xếp hợp nhất như một ví dụ tuyệt vời về điều này.

Xử lý các giá trị một chữ số tại một thời điểm

Có bao nhiêu chữ số trong cơ số 10 số n? Chà, nếu có k chữ số trong số, thì chúng ta sẽ có chữ số lớn nhất là bội số của 10 k . Số lớn nhất có k chữ số là 999 ... 9, k lần, và giá trị này bằng 10 k + 1 - 1. Do đó, nếu chúng ta biết rằng n có k chữ số trong đó, thì chúng ta biết rằng giá trị của n là nhiều nhất là 10 k + 1 - 1. Nếu chúng ta muốn tìm k theo n, chúng ta nhận được

n ≤ 10 k + 1 - 1

n + 1 ≤ 10 k + 1

log 10 (n + 1) ≤ k + 1

(log 10 (n + 1)) - 1 ≤ k

Từ đó ta nhận được rằng k xấp xỉ logarit cơ số 10 của n. Nói cách khác, số chữ số trong n là O (log n).

Ví dụ, chúng ta hãy nghĩ về sự phức tạp của việc cộng hai số lớn quá lớn để vừa với một từ máy. Giả sử rằng chúng ta có các số đó được biểu diễn trong cơ số 10, và chúng ta sẽ gọi các số là m và n. Một cách để cộng chúng là thông qua phương pháp cấp trường - viết các số ra từng chữ số một, sau đó làm từ phải sang trái. Ví dụ: để thêm 1337 và 2065, chúng tôi sẽ bắt đầu bằng cách viết các số dưới dạng

    1  3  3  7
+   2  0  6  5
==============

Chúng tôi thêm chữ số cuối cùng và mang số 1:

          1
    1  3  3  7
+   2  0  6  5
==============
             2

Sau đó, chúng tôi thêm chữ số thứ hai đến cuối cùng ("áp chót") và mang theo 1:

       1  1
    1  3  3  7
+   2  0  6  5
==============
          0  2

Tiếp theo, chúng tôi thêm chữ số từ thứ ba đến cuối cùng ("đối đầu"):

       1  1
    1  3  3  7
+   2  0  6  5
==============
       4  0  2

Cuối cùng, chúng tôi thêm chữ số từ thứ tư đến cuối cùng ("tiền áp chót" ... Tôi yêu tiếng Anh):

       1  1
    1  3  3  7
+   2  0  6  5
==============
    3  4  0  2

Bây giờ, chúng ta đã làm được bao nhiêu việc? Chúng tôi thực hiện tổng số O (1) công việc cho mỗi chữ số (nghĩa là một lượng công việc không đổi) và có tổng số O (max {log n, log m}) cần được xử lý. Điều này cho tổng độ phức tạp O (max {log n, log m}), bởi vì chúng ta cần truy cập từng chữ số trong hai số.

Nhiều thuật toán nhận được số hạng O (log n) trong chúng từ việc làm việc một chữ số tại một thời điểm trong một cơ số nào đó. Một ví dụ cổ điển là sắp xếp cơ số , sắp xếp các số nguyên từng chữ số một. Có nhiều cách sắp xếp theo cơ số, nhưng chúng thường chạy trong thời gian O (n log U), trong đó U là số nguyên lớn nhất có thể được sắp xếp. Lý do cho điều này là mỗi lần vượt qua kiểu sắp xếp mất O (n) thời gian và có tổng số lần lặp O (log U) cần thiết để xử lý từng chữ số O (log U) của số lớn nhất được sắp xếp. Nhiều thuật toán nâng cao, chẳng hạn như thuật toán đường đi ngắn nhất của Gabow hoặc phiên bản mở rộng của thuật toán luồng tối đa Ford-Fulkerson , có thuật ngữ nhật ký về độ phức tạp của chúng vì chúng hoạt động một chữ số tại một thời điểm.


Đối với câu hỏi thứ hai của bạn về cách bạn giải quyết vấn đề đó, bạn có thể muốn xem câu hỏi liên quan này khám phá một ứng dụng nâng cao hơn. Với cấu trúc chung của các vấn đề được mô tả ở đây, bây giờ bạn có thể hiểu rõ hơn về cách suy nghĩ về các vấn đề khi bạn biết có một thuật ngữ nhật ký trong kết quả, vì vậy tôi khuyên bạn không nên xem câu trả lời cho đến khi bạn đưa ra. Một vài suy nghĩ.

Hi vọng điêu nay co ich!


8

Khi chúng ta nói về mô tả big-Oh, chúng ta thường nói về thời gian cần thiết để giải quyết các vấn đề có kích thước nhất định . Và thông thường, đối với các bài toán đơn giản, kích thước đó chỉ được đặc trưng bởi số lượng phần tử đầu vào, và nó thường được gọi là n, hoặc N. (Rõ ràng điều đó không phải lúc nào cũng đúng - các vấn đề với đồ thị thường được đặc trưng bằng số đỉnh, V và số cạnh, E; nhưng bây giờ, chúng ta sẽ nói về danh sách các đối tượng, với N đối tượng trong danh sách.)

Chúng tôi nói rằng một vấn đề "là lớn-Oh của (một số chức năng của N)" nếu và chỉ khi :

Đối với tất cả N> một số N_0 tùy ý, có một số hằng số c, sao cho thời gian chạy của thuật toán nhỏ hơn hằng số đó c lần (một số hàm của N.)

Nói cách khác, đừng nghĩ về những vấn đề nhỏ mà "chi phí liên tục" trong việc thiết lập vấn đề, hãy nghĩ về những vấn đề lớn. Và khi nghĩ về các vấn đề lớn, big-Oh của (một số hàm của N) có nghĩa là thời gian chạy vẫn luôn nhỏ hơn một số thời gian không đổi của hàm đó. Luôn luôn.

Tóm lại, hàm đó là một giới hạn trên, lên đến một hệ số không đổi.

Vì vậy, "big-Oh của log (n)" có nghĩa giống như tôi đã nói ở trên, ngoại trừ "một số hàm của N" được thay thế bằng "log (n)."

Vì vậy, vấn đề của bạn yêu cầu bạn nghĩ về tìm kiếm nhị phân, vì vậy chúng ta hãy nghĩ về điều đó. Giả sử bạn có một danh sách gồm N phần tử được sắp xếp theo thứ tự tăng dần. Bạn muốn tìm hiểu xem một số đã cho có tồn tại trong danh sách đó hay không. Một cách để làm điều đó không phải là tìm kiếm nhị phân là chỉ cần quét từng phần tử của danh sách và xem đó có phải là số mục tiêu của bạn hay không. Bạn có thể gặp may mắn và tìm thấy nó trong lần thử đầu tiên. Nhưng trong trường hợp xấu nhất, bạn sẽ kiểm tra N lần khác nhau. Đây không phải là tìm kiếm nhị phân và cũng không phải là tìm kiếm lớn của nhật ký (N) vì không có cách nào để buộc nó vào các tiêu chí mà chúng tôi đã phác thảo ở trên.

Bạn có thể chọn hằng số tùy ý đó là c = 10 và nếu danh sách của bạn có N = 32 phần tử, bạn ổn: 10 * log (32) = 50, lớn hơn thời gian chạy là 32. Nhưng nếu N = 64 , 10 * log (64) = 60, nhỏ hơn thời gian chạy là 64. Bạn có thể chọn c = 100 hoặc 1000 hoặc gazillion và bạn vẫn có thể tìm thấy một số N vi phạm yêu cầu đó. Nói cách khác, không có N_0.

Tuy nhiên, nếu chúng tôi thực hiện tìm kiếm nhị phân, chúng tôi chọn phần tử ở giữa và thực hiện so sánh. Sau đó, chúng tôi ném ra một nửa số, và làm lại, lặp đi lặp lại, v.v. Nếu N = 32 của bạn, bạn chỉ có thể làm điều đó khoảng 5 lần, đó là log (32). Nếu N = 64, bạn chỉ có thể thực hiện điều này khoảng 6 lần, v.v. Bây giờ bạn có thể chọn hằng số c tùy ý, theo cách sao cho yêu cầu luôn được đáp ứng đối với các giá trị lớn của N.

Với tất cả nền tảng đó, điều mà O (log (N)) thường có nghĩa là bạn có một số cách để thực hiện một điều đơn giản, giúp giảm một nửa kích thước vấn đề của bạn. Cũng giống như tìm kiếm nhị phân đang làm ở trên. Sau khi bạn cắt vấn đề làm đôi, bạn có thể cắt nó làm đôi một lần nữa, lặp đi lặp lại. Tuy nhiên, quan trọng nhất, những gì bạn không thể làm là một số bước tiền xử lý sẽ mất nhiều thời gian hơn thời gian O (log (N)) đó. Vì vậy, ví dụ, bạn không thể xáo trộn hai danh sách của mình thành một danh sách lớn, trừ khi bạn cũng có thể tìm cách thực hiện điều đó trong thời gian O (log (N)).

(LƯU Ý: Gần như luôn luôn, Log (N) có nghĩa là log-base-hai, đó là những gì tôi giả định ở trên.)


4

Trong giải pháp sau, tất cả các dòng có lệnh gọi đệ quy được thực hiện trên một nửa kích thước đã cho của mảng con của X và Y. Các dòng khác được thực hiện trong một thời gian không đổi. Hàm đệ quy là T (2n) = T (2n / 2) + c = T (n) + c = O (lg (2n)) = O (lgn).

Bạn bắt đầu với MEDIAN (X, 1, n, Y, 1, n).

MEDIAN(X, p, r, Y, i, k) 
if X[r]<Y[i]
    return X[r]
if Y[k]<X[p]
    return Y[k]
q=floor((p+r)/2)
j=floor((i+k)/2)
if r-p+1 is even
    if X[q+1]>Y[j] and Y[j+1]>X[q]
        if X[q]>Y[j]
            return X[q]
        else
            return Y[j]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q+1, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j+1, k)
else
    if X[q]>Y[j] and Y[j+1]>X[q-1]
        return Y[j]
    if Y[j]>X[q] and X[q+1]>Y[j-1]
        return X[q]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j, k)

3

Thuật ngữ Nhật ký xuất hiện rất thường xuyên trong phân tích độ phức tạp của thuật toán. Dưới đây là một số giải thích:

1. Làm thế nào để bạn đại diện cho một số?

Hãy lấy số X = 245436. Ký hiệu “245436” này có thông tin ngầm trong đó. Làm cho thông tin đó rõ ràng:

X = 2 * 10 ^ 5 + 4 * 10 ^ 4 + 5 * 10 ^ 3 + 4 * 10 ^ 2 + 3 * 10 ^ 1 + 6 * 10 ^ 0

Đó là khai triển thập phân của số. Vì vậy, lượng thông tin tối thiểu chúng ta cần để biểu diễn số này là 6 chữ số. Đây không phải là ngẫu nhiên, vì bất kỳ số nào nhỏ hơn 10 ^ d đều có thể được biểu diễn bằng d chữ số.

Vậy để biểu diễn X cần có bao nhiêu chữ số? Đó là số mũ lớn nhất của 10 trong X cộng với 1.

==> 10 ^ d> X
==> log (10 ^ d)> log (X)
==> d * log (10)> log (X)
==> d> log (X) // Và nhật ký xuất hiện một lần nữa ...
==> d = tầng (log (x)) + 1

Cũng lưu ý rằng đây là cách ngắn gọn nhất để biểu thị số trong phạm vi này. Bất kỳ sự giảm nào cũng sẽ dẫn đến mất thông tin, vì một chữ số bị thiếu có thể được liên kết với 10 số khác. Ví dụ: 12 * có thể được ánh xạ thành 120, 121, 122,…, 129.

2. Làm thế nào để bạn tìm kiếm một số trong (0, N - 1)?

Lấy N = 10 ^ d, chúng tôi sử dụng quan sát quan trọng nhất của mình:

Lượng thông tin tối thiểu để xác định duy nhất một giá trị trong phạm vi từ 0 đến N - 1 = log (N) chữ số.

Điều này ngụ ý rằng, khi được yêu cầu tìm kiếm một số trên dòng số nguyên, nằm trong khoảng từ 0 đến N - 1, chúng ta cần ít nhất log (N) cố gắng tìm nó. Tại sao? Bất kỳ thuật toán tìm kiếm nào cũng cần phải chọn hết chữ số này đến chữ số khác trong quá trình tìm kiếm số.

Số chữ số tối thiểu nó cần chọn là log (N). Do đó số phép toán tối thiểu được thực hiện để tìm kiếm một số trong không gian có kích thước N là log (N).

Bạn có thể đoán độ phức tạp của thứ tự tìm kiếm nhị phân, tìm kiếm bậc ba hoặc tìm kiếm deca không?
O (log (N))!

3. Làm thế nào để bạn sắp xếp một tập hợp các số?

Khi được yêu cầu sắp xếp một tập hợp các số A thành một mảng B, nó sẽ trông như thế nào ->

Phần tử hoán đổi

Mọi phần tử trong mảng ban đầu phải được ánh xạ tới chỉ mục tương ứng của nó trong mảng đã sắp xếp. Vì vậy, đối với phần tử đầu tiên, chúng ta có n vị trí. Để tìm đúng chỉ số tương ứng trong phạm vi từ 0 đến n - 1 này, chúng ta cần các phép toán… log (n).

Phần tử tiếp theo cần các phép toán log (n-1), log tiếp theo (n-2), v.v. Tổng cộng là:

==> log (n) + log (n - 1) + log (n - 2) +… + log (1)

Sử dụng log (a) + log (b) = log (a * b),

==> log (n!)

Điều này có thể gần đúng với nlog (n) - n.
Đó là O (n * log (n))!

Do đó, chúng tôi kết luận rằng không có thuật toán sắp xếp nào có thể làm tốt hơn O (n * log (n)). Và một số thuật toán có độ phức tạp này là Merge Sort và Heap Sort phổ biến!

Đây là một số lý do tại sao chúng ta thấy log (n) bật lên thường xuyên trong phân tích độ phức tạp của các thuật toán. Điều tương tự có thể được mở rộng cho số nhị phân. Tôi đã làm một video về điều đó ở đây.
Tại sao log (n) lại xuất hiện thường xuyên trong quá trình phân tích độ phức tạp của thuật toán?

Chúc mừng!


2

Chúng tôi gọi độ phức tạp thời gian là O (log n), khi giải pháp dựa trên số lần lặp trên n, trong đó công việc được thực hiện trong mỗi lần lặp là một phần nhỏ của lần lặp trước đó, vì thuật toán hướng tới giải pháp.


1

Không thể bình luận được ... nó được! Câu trả lời của Avi Cohen là không chính xác, hãy thử:

X = 1 3 4 5 8
Y = 2 5 6 7 9

Không có điều kiện nào là đúng, vì vậy MEDIAN (X, p, q, Y, j, k) sẽ cắt cả hai fives. Đây là các chuỗi không giảm, không phải tất cả các giá trị đều khác biệt.

Cũng hãy thử ví dụ về độ dài chẵn này với các giá trị riêng biệt:

X = 1 3 4 7
Y = 2 5 6 8

Bây giờ MEDIAN (X, p, q, Y, j + 1, k) sẽ cắt bốn.

Thay vào đó, tôi đưa ra thuật toán này, hãy gọi nó bằng MEDIAN (1, n, 1, n):

MEDIAN(startx, endx, starty, endy){
  if (startx == endx)
    return min(X[startx], y[starty])
  odd = (startx + endx) % 2     //0 if even, 1 if odd
  m = (startx+endx - odd)/2
  n = (starty+endy - odd)/2
  x = X[m]
  y = Y[n]
  if x == y
    //then there are n-2{+1} total elements smaller than or equal to both x and y
    //so this value is the nth smallest
    //we have found the median.
    return x
  if (x < y)
    //if we remove some numbers smaller then the median,
    //and remove the same amount of numbers bigger than the median,
    //the median will not change
    //we know the elements before x are smaller than the median,
    //and the elements after y are bigger than the median,
    //so we discard these and continue the search:
    return MEDIAN(m, endx, starty, n + 1 - odd)
  else  (x > y)
    return MEDIAN(startx, m + 1 - odd, n, endy)
}
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.