Tại sao trường hợp xấu nhất cho hàm này O (n ^ 2)?


44

Tôi đang cố gắng tự dạy mình cách tính ký hiệu BigO cho một hàm tùy ý. Tôi tìm thấy chức năng này trong một cuốn sách giáo khoa. Cuốn sách khẳng định rằng hàm là O (n 2 ). Nó đưa ra một lời giải thích về lý do tại sao, nhưng tôi đang cố gắng làm theo. Tôi tự hỏi nếu ai đó có thể chỉ cho tôi toán học đằng sau lý do tại sao điều này là như vậy. Về cơ bản, tôi hiểu rằng đó là một cái gì đó ít hơn O (n 3 ), nhưng tôi không thể độc lập hạ cánh trên O (n 2 )

Giả sử chúng ta được cung cấp ba chuỗi số A, B và C. Chúng ta sẽ giả sử rằng không có chuỗi riêng lẻ nào chứa các giá trị trùng lặp, nhưng có thể có một số số nằm trong hai hoặc ba chuỗi. Vấn đề phân biệt tập hợp ba chiều là xác định xem giao điểm của ba chuỗi có trống không, cụ thể là không có phần tử x sao cho x ∈ A, x ∈ B và x ∈ C.

Ngẫu nhiên, đây không phải là vấn đề bài tập về nhà đối với tôi - con tàu đó đã ra khơi nhiều năm trước :), chỉ là tôi đang cố gắng để thông minh hơn.

def disjoint(A, B, C):
        """Return True if there is no element common to all three lists."""  
        for a in A:
            for b in B:
                if a == b: # only check C if we found match from A and B
                   for c in C:
                       if a == c # (and thus a == b == c)
                           return False # we found a common value
        return True # if we reach this, sets are disjoint

[Chỉnh sửa] Theo sách giáo khoa:

Trong phiên bản cải tiến, không chỉ đơn giản là chúng ta tiết kiệm thời gian nếu gặp may mắn. Chúng tôi tuyên bố rằng thời gian chạy trường hợp xấu nhất cho sự rời rạc là O (n 2 ).

Lời giải thích của cuốn sách, mà tôi đấu tranh để làm theo, là đây:

Để tính tổng thời gian chạy, chúng tôi kiểm tra thời gian thực hiện từng dòng mã. Việc quản lý vòng lặp for qua A đòi hỏi thời gian O (n). Việc quản lý vòng lặp for trên B chiếm tổng thời gian O (n 2 ), vì vòng lặp đó được thực hiện n lần khác nhau. Phép thử a == b được ước tính O (n 2 ) lần. Thời gian còn lại phụ thuộc vào số lượng cặp (a, b) phù hợp tồn tại. Như chúng tôi đã lưu ý, có nhiều nhất n cặp như vậy, và do đó, việc quản lý vòng lặp trên C và các lệnh trong phần thân của vòng lặp đó, sử dụng tối đa thời gian O (n 2 ). Tổng thời gian sử dụng là O (n 2 ).

(Và để cung cấp tín dụng phù hợp ...) Cuốn sách là: Cấu trúc dữ liệu và thuật toán trong Python của Michael T. Goodrich et. tất cả, xuất bản Wiley, pg. 135

[Chỉnh sửa] Một lời biện minh; Dưới đây là mã trước khi tối ưu hóa:

def disjoint1(A, B, C):
    """Return True if there is no element common to all three lists."""
       for a in A:
           for b in B:
               for c in C:
                   if a == b == c:
                        return False # we found a common value
return True # if we reach this, sets are disjoint

Ở trên, bạn có thể thấy rõ đây là O (n 3 ), vì mỗi vòng lặp phải chạy hết mức. Cuốn sách sẽ khẳng định rằng trong ví dụ đơn giản hóa (được đưa ra trước), vòng lặp thứ ba chỉ là độ phức tạp của O (n 2 ), do đó phương trình phức tạp đi theo k + O (n 2 ) + O (n 2 ) mà cuối cùng mang lại O (n 2 ).

Mặc dù tôi không thể chứng minh đây là trường hợp (do đó là câu hỏi), người đọc có thể đồng ý rằng độ phức tạp của thuật toán đơn giản hóa ít nhất là ít hơn so với bản gốc.

[Chỉnh sửa] Và để chứng minh rằng phiên bản đơn giản hóa là bậc hai:

if __name__ == '__main__':
    for c in [100, 200, 300, 400, 500]:
        l1, l2, l3 = get_random(c), get_random(c), get_random(c)
        start = time.time()
        disjoint1(l1, l2, l3)
        print(time.time() - start)
        start = time.time()
        disjoint2(l1, l2, l3)
        print(time.time() - start)

Sản lượng:

0.02684807777404785
0.00019478797912597656
0.19134306907653809
0.0007600784301757812
0.6405444145202637
0.0018095970153808594
1.4873297214508057
0.003167390823364258
2.953308343887329
0.004908084869384766

Vì sự khác biệt thứ hai là bằng nhau, nên hàm đơn giản hóa thực sự là bậc hai:

nhập mô tả hình ảnh ở đây

[Chỉnh sửa] Và thậm chí còn chứng minh thêm:

Nếu tôi giả sử trường hợp xấu nhất (A = B! = C),

if __name__ == '__main__':
    for c in [10, 20, 30, 40, 50]:
        l1, l2, l3 = range(0, c), range(0,c), range(5*c, 6*c)
        its1 = disjoint1(l1, l2, l3)
        its2 = disjoint2(l1, l2, l3)
        print(f"iterations1 = {its1}")
        print(f"iterations2 = {its2}")
        disjoint2(l1, l2, l3)

sản lượng:

iterations1 = 1000
iterations2 = 100
iterations1 = 8000
iterations2 = 400
iterations1 = 27000
iterations2 = 900
iterations1 = 64000
iterations2 = 1600
iterations1 = 125000
iterations2 = 2500

Sử dụng thử nghiệm khác biệt thứ hai, kết quả trường hợp xấu nhất là chính xác bậc hai.

nhập mô tả hình ảnh ở đây


6
Hoặc là cuốn sách sai hoặc phiên âm của bạn là.
candied_orange

6
Không. Sai là sai bất kể trích dẫn tốt như thế nào. Hoặc giải thích lý do tại sao chúng ta không thể đơn giản giả định những điều này nếu đi theo cách tồi tệ nhất có thể khi thực hiện phân tích O lớn hoặc chấp nhận kết quả bạn nhận được.
candied_orange

8
@candied_orange; Tôi đã thêm một số biện minh cho khả năng tốt nhất của mình - không phải là bộ đồ mạnh mẽ của tôi. Tôi sẽ yêu cầu bạn một lần nữa cho phép khả năng bạn thực sự có thể không chính xác. Bạn đã thực hiện quan điểm của bạn, thực hiện đúng.
SteveJ

8
Số ngẫu nhiên không phải là trường hợp xấu nhất của bạn. Điều đó chứng tỏ không có gì.
Telastyn

7
àh Được chứ. "Không có chuỗi nào có giá trị trùng lặp" sẽ thay đổi trường hợp xấu nhất vì C chỉ có thể kích hoạt một lần cho mỗi A. Xin lỗi về sự thất vọng - đó là những gì tôi nhận được khi tham gia stackexchange vào cuối ngày thứ bảy: D
Telastyn

Câu trả lời:


63

Cuốn sách thực sự chính xác, và nó cung cấp một lập luận tốt. Lưu ý rằng thời gian không phải là một chỉ số đáng tin cậy về độ phức tạp thuật toán. Thời gian chỉ có thể xem xét phân phối dữ liệu đặc biệt hoặc các trường hợp thử nghiệm có thể quá nhỏ: độ phức tạp thuật toán chỉ mô tả cách sử dụng tài nguyên hoặc quy mô thời gian chạy vượt quá kích thước đầu vào lớn phù hợp.

Cuốn sách đưa ra lập luận rằng độ phức tạp là O (n²) vì if a == bnhánh được nhập nhiều nhất n lần. Điều này là không rõ ràng vì các vòng lặp vẫn được viết là lồng nhau. Rõ ràng hơn nếu chúng ta trích xuất nó:

def disjoint(A, B, C):
  AB = (a
        for a in A
        for b in B
        if a == b)
  ABC = (a
         for a in AB
         for c in C
         if a == c)
  for a in ABC:
    return False
  return True

Biến thể này sử dụng máy phát điện để thể hiện kết quả trung gian.

  • Trong trình tạo AB, chúng ta sẽ có tối đa n phần tử (vì đảm bảo rằng danh sách đầu vào sẽ không chứa các bản sao) và việc tạo trình tạo có độ phức tạp O (n²).
  • Sản xuất máy phát điện ABCbao gồm một vòng lặp trên máy phát ABcó chiều dài n và trên Cchiều dài n , do đó độ phức tạp thuật toán của nó cũng là O (n²).
  • Các hoạt động này không được lồng nhau mà xảy ra độc lập, do đó tổng độ phức tạp là O (n² + n²) = O (n²).

Bởi vì các cặp danh sách đầu vào có thể được kiểm tra tuần tự, do đó, việc xác định xem có bất kỳ số lượng danh sách nào tách rời có thể được thực hiện trong thời gian O (n²) hay không.

Phân tích này không chính xác vì nó giả định rằng tất cả các danh sách có cùng độ dài. Chúng ta có thể nói chính xác hơn là ABcó tối đa min (| A |, | B |) và tạo ra nó có độ phức tạp O (| A | • | B |). Sản xuất ABCcó độ phức tạp O (min (| A |, | B |) • | C |). Tổng độ phức tạp sau đó phụ thuộc vào cách danh sách đầu vào được sắp xếp. Với | A | ≤ | B | ≤ | C | chúng ta có tổng độ phức tạp trong trường hợp xấu nhất của O (| A | • | C |).

Lưu ý rằng chiến thắng hiệu quả là có thể nếu các thùng chứa đầu vào cho phép kiểm tra tư cách thành viên nhanh hơn là phải lặp lại trên tất cả các yếu tố. Đây có thể là trường hợp khi chúng được sắp xếp sao cho có thể thực hiện tìm kiếm nhị phân hoặc khi chúng là bộ băm. Nếu không có các vòng lặp lồng nhau rõ ràng, nó sẽ trông như sau:

for a in A:
  if a in B:  # might implicitly loop
    if a in C:  # might implicitly loop
      return False
return True

hoặc trong phiên bản dựa trên máy phát điện:

AB = (a for a in A if a in B)
ABC = (a for a in AB if a in C)
for a in ABC:
  return False
return True

4
Điều này sẽ rõ ràng hơn rất nhiều nếu chúng ta vừa xóa bỏ nbiến số ma thuật này , và nói về các biến số thực tế khi chơi.
Alexander

15
@code_dredd Không, không, nó không có kết nối trực tiếp với mã. Đó là một sự trừu tượng mà hình dung rằng len(a) == len(b) == len(c), mặc dù đúng trong bối cảnh phân tích độ phức tạp thời gian, có xu hướng gây nhầm lẫn cho cuộc trò chuyện.
Alexander

10
Có lẽ nói rằng mã của OP có độ phức tạp trường hợp xấu nhất O (| A | • | B | + min (| A |, | B |) • | C |) là đủ để kích hoạt sự hiểu biết?
Pablo H

3
Một điều khác về kiểm tra thời gian: như bạn phát hiện ra, chúng không giúp bạn hiểu được chuyện gì đang xảy ra. Mặt khác, họ dường như đã cho bạn thêm tự tin khi đứng trước nhiều tuyên bố không chính xác nhưng tuyên bố mạnh mẽ rằng cuốn sách rõ ràng là sai, vì vậy đó là một điều tốt, và trong trường hợp này, thử nghiệm của bạn đã đánh bại bằng tay trực quan .. Để hiểu rõ, một cách kiểm tra hiệu quả hơn sẽ là chạy nó trong trình gỡ lỗi với các điểm dừng (hoặc thêm bản in các giá trị của biến) vào mục nhập của mỗi vòng lặp.
sdenham

4
"Lưu ý rằng thời gian không phải là một chỉ số hữu ích về độ phức tạp thuật toán." Tôi nghĩ rằng điều này sẽ chính xác hơn nếu nó nói "nghiêm ngặt" hoặc "đáng tin cậy" hơn là "hữu ích".
Tích lũy

7

Lưu ý rằng nếu tất cả các phần tử khác nhau trong mỗi danh sách được giả sử, bạn chỉ có thể lặp lại C một lần cho mỗi phần tử trong A (nếu có phần tử trong B bằng nhau). Vậy vòng lặp bên trong là tổng O (n ^ 2)


3

Chúng tôi sẽ cho rằng không có chuỗi riêng lẻ nào chứa bản sao.

là một phần rất quan trọng của thông tin.

Mặt khác, trường hợp xấu nhất của phiên bản tối ưu hóa vẫn sẽ là O (n³), khi A và B bằng nhau và chứa một phần tử được nhân đôi n lần:

i = 0
def disjoint(A, B, C):
    global i
    for a in A:
        for b in B:
            if a == b:
                for c in C:
                    i+=1
                    print(i)
                    if a == c:
                        return False 
    return True 

print(disjoint([1] * 10, [1] * 10, [2] * 10))

đầu ra nào:

...
...
...
993
994
995
996
997
998
999
1000
True

Về cơ bản, các tác giả cho rằng trường hợp xấu nhất O (n³) không nên xảy ra (tại sao?) Và "chứng minh" rằng trường hợp xấu nhất hiện nay là O (n²).

Tối ưu hóa thực sự sẽ là sử dụng các bộ hoặc dicts để kiểm tra sự bao gồm trong O (1). Trong trường hợp đó, disjointsẽ là O (n) cho mọi đầu vào.


Nhận xét cuối cùng của bạn là khá thú vị, đã không nghĩ về điều đó. Bạn có gợi ý rằng đó là do bạn có thể thực hiện ba thao tác O (n) nối tiếp không?
SteveJ

2
Trừ khi bạn có được một hàm băm hoàn hảo với ít nhất một nhóm cho mỗi phần tử đầu vào, bạn không thể kiểm tra sự bao gồm trong O (1). Một tập hợp được sắp xếp thường có tra cứu O (log n). Trừ khi bạn đang nói về chi phí trung bình, nhưng đó không phải là câu hỏi. Tuy nhiên, có một bộ nhị phân cân bằng nhận được O (n log n) cứng là chuyện nhỏ.
Jan Dorniak

@JanDorniak: Nhận xét tuyệt vời, cảm ơn. Bây giờ có một chút khó xử: Tôi đã bỏ qua trường hợp xấu nhất key in dict, giống như các tác giả đã làm. : - / Để bảo vệ tôi, tôi nghĩ việc tìm ra một lệnh với ncác khóa và nva chạm băm khó hơn nhiều so với việc tạo một danh sách với ncác giá trị trùng lặp. Và với một tập hợp hoặc chính tả, thực sự không thể có bất kỳ giá trị trùng lặp nào. Vì vậy, trường hợp xấu nhất tồi tệ nhất thực sự là O (n²). Tôi sẽ cập nhật câu trả lời của tôi.
Eric Duminil

2
@JanDorniak Tôi nghĩ rằng các bộ và dicts là các bảng băm trong python trái ngược với các cây đỏ đen trong C ++. Vì vậy, trường hợp xấu nhất tuyệt đối là tồi tệ hơn, lên tới 0 (n) cho một tìm kiếm, nhưng trường hợp trung bình là O (1). Trái ngược với O (log n) cho C ++ wiki.python.org/moin/TimeComplexity . Cho rằng đó là một câu hỏi trăn, và rằng vấn đề của vấn đề dẫn đến khả năng thực hiện trường hợp trung bình rất cao, tôi không nghĩ rằng yêu cầu O (1) là kém.
Baldrickk

3
Tôi nghĩ rằng tôi thấy vấn đề ở đây: khi các tác giả nói "chúng tôi sẽ cho rằng không có chuỗi riêng lẻ nào chứa các giá trị trùng lặp", đó không phải là một bước để trả lời câu hỏi; đúng hơn, đó là một điều kiện tiên quyết mà theo đó câu hỏi sẽ được giải quyết. Đối với các mục đích sư phạm, điều này biến một vấn đề không thú vị thành một vấn đề thách thức trực giác của mọi người về big-O - và dường như đã thành công ở đó, đánh giá bởi số người đã khẳng định mạnh mẽ rằng O (n²) phải sai. .. Ngoài ra, trong khi nó đang ở đây, việc đếm số bước trong một ví dụ không phải là một lời giải thích.
sdenham

3

Để đưa mọi thứ vào các điều khoản mà cuốn sách của bạn sử dụng:

Tôi nghĩ rằng bạn không có vấn đề gì khi hiểu rằng kiểm tra a == blà trường hợp xấu nhất O (n 2 ).

Bây giờ trong trường hợp xấu nhất cho vòng thứ ba, mỗi atrong Acó một trận đấu trong B, do đó vòng lặp thứ ba sẽ được gọi mỗi lần. Trong trường hợp akhông tồn tại C, nó sẽ chạy qua toàn bộ Ctập hợp.

Nói cách khác, đó là 1 lần cho mỗi avà 1 lần cho mỗi c, hoặc n * n. O (n 2 )

Vì vậy, có O (n 2 ) + O (n 2 ) mà cuốn sách của bạn chỉ ra.


0

Thủ thuật của phương pháp tối ưu hóa là cắt góc. Chỉ khi a và b khớp nhau, c sẽ được xem xứng đáng. Bây giờ bạn có thể hình dung rằng trong trường hợp xấu nhất, bạn vẫn sẽ phải đánh giá từng c. Đây không phải là sự thật.

Bạn có thể nghĩ rằng trường hợp xấu nhất là mọi kiểm tra cho a == b đều dẫn đến việc chạy qua C vì mọi kiểm tra cho a == b đều trả về một kết quả khớp. Nhưng điều này là không thể bởi vì các điều kiện cho điều này là mâu thuẫn. Để làm việc này, bạn sẽ cần một A và B chứa các giá trị giống nhau. Chúng có thể được sắp xếp khác nhau nhưng mỗi giá trị trong A sẽ phải có giá trị khớp nhau trong B.

Bây giờ đây là kicker. Không có cách nào để sắp xếp các giá trị này sao cho mỗi a bạn sẽ phải đánh giá tất cả các b trước khi bạn tìm thấy kết quả khớp của mình.

A: 1 2 3 4 5
B: 1 2 3 4 5

Điều này sẽ được thực hiện ngay lập tức bởi vì 1 khớp là phần tử đầu tiên trong cả hai loạt. Thế còn

A: 1 2 3 4 5
B: 5 4 3 2 1

Điều đó sẽ làm việc cho lần chạy đầu tiên trên A: chỉ phần tử cuối cùng trong B sẽ mang lại một cú đánh. Nhưng lần lặp tiếp theo trên A sẽ phải nhanh hơn vì vị trí cuối cùng trong B đã bị chiếm bởi 1. Và thực sự điều này sẽ chỉ mất bốn lần lặp lại lần này. Và điều này trở nên tốt hơn một chút với mỗi lần lặp lại tiếp theo.

Bây giờ tôi không phải là nhà toán học nên tôi không thể chứng minh điều này sẽ kết thúc bằng O (n2) nhưng tôi có thể cảm thấy nó trên đôi guốc của mình.


1
Thứ tự của các yếu tố không đóng vai trò ở đây. Yêu cầu quan trọng là không có sự trùng lặp; lập luận sau đó là các vòng lặp có thể được chuyển thành hai O(n^2)vòng riêng biệt ; cung cấp cho tổng thể O(n^2)(hằng số được bỏ qua).
AnoE

@AnoE Thật vậy, thứ tự của các yếu tố không quan trọng. Đó chính xác là những gì tôi đang chứng minh.
Martin Maat

Tôi thấy những gì bạn đang cố gắng làm và những gì bạn đang viết không sai, nhưng từ quan điểm của OP, câu trả lời của bạn chủ yếu chỉ ra lý do tại sao một chuỗi suy nghĩ cụ thể là không liên quan; nó không giải thích làm thế nào để đi đến giải pháp thực tế. OP dường như không đưa ra một dấu hiệu nào cho thấy anh ta thực sự nghĩ rằng điều này có liên quan đến đơn hàng. Vì vậy, không rõ ràng với tôi câu trả lời này sẽ giúp OP như thế nào.
AnoE

-1

Lúc đầu bị bối rối, nhưng câu trả lời của Amon thực sự hữu ích. Tôi muốn xem liệu tôi có thể làm một phiên bản thực sự súc tích không:

Đối với một giá trị nhất định atrong A, chức năng so sánh avới tất cả các khả năng btrong B, và nó có phải nó chỉ một lần. Vì vậy, cho một anó được thực hiện a == bchính xác nlần.

Bkhông chứa bất kỳ bản sao nào (không có danh sách nào được thực hiện), do đó, trong một danh sách nhất định asẽ có nhiều nhất một kết quả khớp. (Đó là chìa khóa). Trường hợp có một trận đấu, asẽ được so sánh với mọi khả năng c, có nghĩa a == clà được thực hiện chính xác n lần. Trường hợp không có trận đấu, a == choàn toàn không xảy ra.

Vì vậy, đối với một nhất định a, có hoặc nso sánh, hoặc 2nso sánh. Điều này xảy ra với mọi người a, vì vậy trường hợp tốt nhất có thể là (n²) và trường hợp xấu nhất là (2n²).

TLDR: mỗi giá trị ađược so sánh với tất cả các giá trị của bvà chống lại mọi giá trị của c, nhưng không phải chống lại mọi sự kết hợp của bc. Hai vấn đề cộng lại, nhưng chúng không nhân lên.


-3

Nghĩ về nó theo cách này, một số số có thể nằm trong hai hoặc ba chuỗi nhưng trường hợp trung bình của điều này là với mỗi phần tử trong tập A, một tìm kiếm toàn diện được thực hiện trong b. Nó được đảm bảo rằng mọi phần tử trong tập A sẽ được lặp đi lặp lại nhưng ngụ ý rằng ít hơn một nửa các phần tử trong tập b sẽ được lặp lại.

Khi các phần tử trong tập b được lặp lại, việc lặp lại sẽ xảy ra nếu có sự trùng khớp. điều này có nghĩa là trường hợp trung bình cho hàm phân tách này là O (n2) nhưng trường hợp xấu nhất tuyệt đối với nó có thể là O (n3). Nếu cuốn sách không đi sâu vào chi tiết, nó có thể sẽ cung cấp cho bạn trường hợp trung bình như một câu trả lời.


4
Cuốn sách khá rõ ràng rằng O (n2) là trường hợp xấu nhất, không phải là trường hợp trung bình.
SteveJ

Một mô tả về hàm theo ký hiệu O lớn thường chỉ cung cấp giới hạn trên cho tốc độ tăng trưởng của hàm. Liên kết với ký hiệu O lớn là một số ký hiệu liên quan, sử dụng các ký hiệu o,, và, để mô tả các loại giới hạn khác về tốc độ tăng trưởng tiệm cận. Wikipedia - Big O
candied_orange

5
"Nếu cuốn sách không đi sâu vào chi tiết, nó có thể sẽ cung cấp cho bạn trường hợp trung bình như một câu trả lời." - Uhm, không. Không có bất kỳ trình độ rõ ràng nào, chúng ta thường nói về độ phức tạp của trường hợp xấu nhất trong mô hình RAM. Khi nói về các hoạt động trên các cấu trúc dữ liệu và rõ ràng từ ngữ cảnh, thì chúng ta thể nói về độ phức tạp của trường hợp xấu nhất được khấu hao trong mô hình RAM. Nếu không có trình độ rõ ràng , chúng ta thường sẽ không nói về trường hợp tốt nhất, trường hợp trung bình, trường hợp dự kiến, độ phức tạp thời gian hoặc bất kỳ mô hình nào khác ngoại trừ RAM.
Jörg W Mittag
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.