O lớn, làm thế nào để bạn tính toán / gần đúng nó?


881

Hầu hết những người có bằng cấp về CS chắc chắn sẽ biết Big O là viết tắt của từ gì . Nó giúp chúng ta đo lường mức độ quy mô thuật toán.

Nhưng tôi tò mò, làm thế nào để bạn tính toán hoặc tính gần đúng độ phức tạp của các thuật toán của bạn?


4
Có thể bạn thực sự không cần phải cải thiện độ phức tạp của thuật toán, nhưng ít nhất bạn có thể tính toán nó để quyết định ...
Xavier Nodet

5
Tôi thấy đây là một lời giải thích rất rõ ràng về Big O, Big Omega và Big Theta: xoax.net/comp/sci/alerskyms/Lesson6.php
Sam Dutton

33
-1: Thở dài, một sự lạm dụng khác của BigOh. BigOh chỉ là một giới hạn trên không có triệu chứng và có thể được sử dụng cho bất cứ điều gì và không chỉ liên quan đến CS. Nói về BigOh như thể có một duy nhất là vô nghĩa (Thuật toán thời gian tuyến tính cũng là O (n ^ 2), O (n ^ 3), v.v.). Nói nó giúp chúng tôi đo lường hiệu quả là sai lầm quá. Ngoài ra, những gì với liên kết đến các lớp phức tạp? Nếu tất cả những gì bạn quan tâm, là các kỹ thuật để tính toán thời gian chạy của các thuật toán, điều đó có liên quan như thế nào?

34
Big-O không đo lường hiệu quả; nó đo lường mức độ của một thuật toán với kích thước (nó có thể áp dụng cho những thứ khác ngoài kích thước nhưng đó là điều chúng ta có thể quan tâm ở đây) - và đó chỉ là không có triệu chứng, vì vậy nếu bạn không may mắn thì thuật toán có "lớn hơn" O có thể chậm hơn (nếu Big-O áp dụng cho các chu kỳ) so với một chu kỳ khác cho đến khi bạn đạt được số lượng cực lớn.
ILoveFortran

4
Chọn một thuật toán trên cơ sở độ phức tạp Big-O của nó thường là một phần thiết yếu của thiết kế chương trình. Đây chắc chắn không phải là một trường hợp 'tối ưu hóa sớm', mà trong mọi trường hợp là một trích dẫn chọn lọc bị lạm dụng nhiều.
Hầu tước Lorne

Câu trả lời:


1481

Tôi sẽ làm hết sức mình để giải thích nó ở đây bằng những thuật ngữ đơn giản, nhưng được cảnh báo rằng chủ đề này khiến học sinh của tôi mất vài tháng để cuối cùng nắm bắt. Bạn có thể tìm thêm thông tin về Chương 2 của Cấu trúc dữ liệu và thuật toán trong sách Java .


Không có quy trình cơ học nào có thể được sử dụng để có được BigOh.

Là một "sách dạy nấu ăn", để có được BigOh từ một đoạn mã trước tiên bạn cần nhận ra rằng bạn đang tạo một công thức toán học để đếm xem có bao nhiêu bước tính toán được thực hiện với một đầu vào có kích thước.

Mục đích rất đơn giản: để so sánh các thuật toán theo quan điểm lý thuyết, mà không cần phải thực thi mã. Số bước càng ít, thuật toán càng nhanh.

Ví dụ: giả sử bạn có đoạn mã này:

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

Hàm này trả về tổng của tất cả các phần tử của mảng và chúng tôi muốn tạo một công thức để tính độ phức tạp tính toán của hàm đó:

Number_Of_Steps = f(N)

Vì vậy, chúng ta có f(N), một hàm để đếm số bước tính toán. Đầu vào của hàm là kích thước của cấu trúc cần xử lý. Nó có nghĩa là chức năng này được gọi là:

Number_Of_Steps = f(data.length)

Tham số Nlấy data.lengthgiá trị. Bây giờ chúng ta cần định nghĩa thực tế của hàm f(). Điều này được thực hiện từ mã nguồn, trong đó mỗi dòng thú vị được đánh số từ 1 đến 4.

Có nhiều cách để tính toán BigOh. Từ thời điểm này trở đi, chúng ta sẽ giả sử rằng mỗi câu không phụ thuộc vào kích thước của dữ liệu đầu vào sẽ có một Csố bước tính toán không đổi .

Chúng ta sẽ thêm số bước riêng lẻ của hàm và không phải khai báo biến cục bộ cũng như câu lệnh return phụ thuộc vào kích thước của datamảng.

Điều đó có nghĩa là các dòng 1 và 4 có số lượng C mỗi bước và hàm có phần giống như sau:

f(N) = C + ??? + C

Phần tiếp theo là xác định giá trị của forcâu lệnh. Hãy nhớ rằng chúng ta đang đếm số bước tính toán, nghĩa là phần thân của forcâu lệnh được thực thi Nlần. Điều đó giống như thêm C, Nlần:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

Không có quy tắc cơ học nào để đếm số lần cơ thể forđược thực thi, bạn cần đếm nó bằng cách xem mã làm gì. Để đơn giản hóa các tính toán, chúng tôi bỏ qua các phần khởi tạo, điều kiện và phần tăng của biến của forcâu lệnh.

Để có được BigOh thực tế, chúng ta cần phân tích tiệm cận của hàm. Điều này gần như được thực hiện như thế này:

  1. Lấy đi tất cả các hằng số C.
  2. Từ f()nhận được polynomium trong nó standard form.
  3. Chia các điều khoản của polynomium và sắp xếp chúng theo tốc độ tăng trưởng.
  4. Giữ một trong những phát triển lớn hơn khi Ntiếp cận infinity.

Chúng tôi f()có hai điều khoản:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

Lấy đi tất cả các Chằng số và các phần dư thừa:

f(N) = 1 + N ^ 1

Vì thuật ngữ cuối cùng là thuật ngữ phát triển lớn hơn khi f()tiếp cận vô hạn (nghĩ về giới hạn ), đây là đối số BigOh và sum()hàm có BigOh là:

O(N)

Có một vài thủ thuật để giải quyết một số mẹo khó: sử dụng tổng kết bất cứ khi nào bạn có thể.

Ví dụ, mã này có thể được giải quyết dễ dàng bằng cách sử dụng tổng:

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

Điều đầu tiên bạn cần được yêu cầu là thứ tự thực hiện foo(). Trong khi thông thường là như vậy O(1), bạn cần hỏi các giáo sư của bạn về nó. O(1)có nghĩa là (hầu hết, hầu hết) không đổi C, không phụ thuộc vào kích thước N.

Các fortuyên bố về câu số một là khó khăn. Trong khi chỉ số kết thúc tại 2 * N, việc tăng được thực hiện bởi hai. Điều đó có nghĩa là lần đầu tiên forđược thực hiện chỉ Ncác bước và chúng ta cần chia số đếm cho hai.

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

Câu số hai thậm chí còn phức tạp hơn vì nó phụ thuộc vào giá trị của i. Hãy xem: chỉ số i lấy các giá trị: 0, 2, 4, 6, 8, ..., 2 * N và lần thứ hai forđược thực hiện: N lần thứ nhất, N - 2 lần thứ hai, N - 4 thứ ba ... cho đến giai đoạn N / 2, trong đó giai đoạn thứ hai forkhông bao giờ được thực thi.

Về công thức, điều đó có nghĩa là:

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

Một lần nữa, chúng tôi đang đếm số bước . Và theo định nghĩa, mọi tổng kết phải luôn bắt đầu tại một và kết thúc ở một số lớn hơn hoặc bằng một.

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(Chúng tôi đang giả định rằng đó foo()O(1)và thực Chiện các bước.)

Chúng ta có một vấn đề ở đây: khi iđưa giá trị N / 2 + 1lên cao, Tóm tắt bên trong kết thúc ở một số âm! Điều đó là không thể và sai. Chúng ta cần chia tổng cộng làm hai, là điểm then chốt mà thời điểm này icần có N / 2 + 1.

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

Kể từ thời điểm then chốt i > N / 2, bên trong forsẽ không được thực thi và chúng tôi giả định rằng độ phức tạp thực hiện C không đổi trên cơ thể của nó.

Bây giờ các tóm tắt có thể được đơn giản hóa bằng cách sử dụng một số quy tắc nhận dạng:

  1. Tổng (w từ 1 đến N) (C) = N * C
  2. Tổng kết (w từ 1 đến N) (A (+/-) B) = Tổng (w từ 1 đến N) (A) (+/-) Tổng kết (w từ 1 đến N) (B)
  3. Tổng (w từ 1 đến N) (w * C) = C * Tổng (w từ 1 đến N) (w) (C là hằng số, không phụ thuộc vào w)
  4. Tổng (w từ 1 đến N) (w) = (N * (N + 1)) / 2

Áp dụng một số đại số:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

Và BigOh là:

O(N²)

6
@arthur Đó sẽ là O (N ^ 2) vì bạn sẽ yêu cầu một vòng lặp để đọc qua tất cả các cột và một để đọc tất cả các hàng của một cột cụ thể.
Abhishek Dey Das

@arthur: Nó phụ thuộc. Đó là O(n)nơi ncó số lượng phần tử, hoặc O(x*y)vị trí xykích thước của mảng. Big-oh là "liên quan đến đầu vào", vì vậy nó phụ thuộc vào đầu vào của bạn là gì.
Vịt Mooing

1
Câu trả lời tuyệt vời, nhưng tôi thực sự bị mắc kẹt. Làm thế nào để Summation (i từ 1 đến N / 2) (N) biến thành (N ^ 2/2)?
Parsa

2
@ParsaAkbari Theo quy tắc chung, tổng (i từ 1 đến a) (b) là một * b. Đây chỉ là một cách khác để nói b + b + ... (một lần) + b = a * b (theo định nghĩa cho một số định nghĩa về phép nhân số nguyên).
Mario Carneiro

Không liên quan lắm, nhưng chỉ để tránh nhầm lẫn, có một lỗi nhỏ trong câu này: "chỉ số i lấy các giá trị: 0, 2, 4, 6, 8, ..., 2 * N". Chỉ số i thực sự tăng lên 2 * N - 2, vòng lặp sẽ dừng sau đó.
Albert

201

Big O đưa ra giới hạn trên cho độ phức tạp thời gian của thuật toán. Nó thường được sử dụng cùng với các bộ dữ liệu xử lý (danh sách) nhưng có thể được sử dụng ở nơi khác.

Một vài ví dụ về cách nó được sử dụng trong mã C.

Giả sử chúng ta có một mảng gồm n phần tử

int array[n];

Nếu chúng ta muốn truy cập phần tử đầu tiên của mảng thì đây sẽ là O (1) vì nó không quan trọng với mảng lớn như thế nào, nó luôn mất cùng một thời gian liên tục để có được mục đầu tiên.

x = array[0];

Nếu chúng tôi muốn tìm một số trong danh sách:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

Đây sẽ là O (n) vì nhiều nhất chúng ta sẽ phải xem qua toàn bộ danh sách để tìm số của mình. Big-O vẫn là O (n) mặc dù chúng ta có thể thấy số của mình là lần thử đầu tiên và chạy qua vòng lặp một lần vì Big-O mô tả giới hạn trên của thuật toán (omega dành cho giới hạn dưới và theta dành cho ràng buộc chặt chẽ) .

Khi chúng ta nhận được các vòng lặp lồng nhau:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

Đây là O (n ^ 2) vì với mỗi lần vượt qua vòng lặp bên ngoài (O (n)), chúng ta phải đi qua toàn bộ danh sách một lần nữa để n nhân cho chúng ta với n bình phương.

Điều này hầu như không làm trầy xước bề mặt nhưng khi bạn phân tích các thuật toán phức tạp hơn thì toán học phức tạp liên quan đến bằng chứng xuất hiện. Hy vọng điều này làm quen với bạn với những điều cơ bản ít nhất.


Giải thích tuyệt vời! Vì vậy, nếu ai đó nói rằng thuật toán của anh ta có độ phức tạp O (n ^ 2), điều đó có nghĩa là anh ta sẽ sử dụng các vòng lặp lồng nhau?
Navaneeth KN

2
Không thực sự, bất kỳ khía cạnh nào dẫn đến n lần bình phương sẽ được coi là n ^ 2
async chờ

@NavaneethKN: Bạn sẽ không luôn thấy vòng lặp lồng nhau, vì các lệnh gọi hàm có thể tự thực hiện O(1). Trong các API chuẩn C ví dụ, bsearchvốn đã O(log n), strlenđang O(n), và qsortO(n log n)(về mặt kỹ thuật nó không có bảo đảm, và quicksort chính nó có một độ phức tạp trường hợp tồi tệ nhất của O(n²), nhưng giả sử bạn libctác giả không phải là một moron, trường hợp phức tạp trung bình của nó là O(n log n)và nó sử dụng một chiến lược lựa chọn trục giúp giảm tỷ lệ trúng O(n²)kiện). Và cả hai bsearchqsortcó thể tồi tệ hơn nếu chức năng so sánh là bệnh lý.
ShadowRanger

95

Mặc dù biết cách tìm ra thời gian Big O cho vấn đề cụ thể của bạn là hữu ích, nhưng biết một số trường hợp chung có thể giúp bạn đưa ra quyết định trong thuật toán của mình.

Dưới đây là một số trường hợp phổ biến nhất, được lấy từ http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_fifts :

O (1) - Xác định xem một số chẵn hay lẻ; sử dụng bảng tra cứu kích thước không đổi hoặc bảng băm

O (logn) - Tìm một mục trong một mảng được sắp xếp với tìm kiếm nhị phân

O (n) - Tìm một mục trong danh sách chưa sắp xếp; thêm hai số n chữ số

O (n 2 ) - Nhân hai số có n chữ số bằng thuật toán đơn giản; thêm hai ma trận n × n; sắp xếp bong bóng hoặc sắp xếp chèn

O (n 3 ) - Nhân hai ma trận n × n bằng thuật toán đơn giản

O (c n ) - Tìm giải pháp (chính xác) cho vấn đề nhân viên bán hàng du lịch bằng lập trình động; xác định xem hai câu lệnh logic có tương đương bằng cách sử dụng lực lượng vũ phu

O (n!) - Giải quyết vấn đề nhân viên bán hàng du lịch thông qua tìm kiếm vũ phu

O (n n ) - Thường được sử dụng thay vì O (n!) Để rút ra các công thức đơn giản hơn cho độ phức tạp tiệm cận


Tại sao không sử dụng x&1==1để kiểm tra sự kỳ lạ?
Samy Bencherif

2
@SamyBencherif: Đó là một cách điển hình để kiểm tra (thực ra, chỉ cần kiểm tra x & 1là đủ, không cần kiểm tra == 1; trong C, x&1==1được đánh giá là x&(1==1) nhờ vào quyền ưu tiên của nhà điều hành , vì vậy nó thực sự giống như kiểm tra x&1). Tôi nghĩ rằng bạn đang đọc sai câu trả lời mặc dù; Có một dấu chấm phẩy ở đó, không phải dấu phẩy. Không nói rằng bạn cần một bảng tra cứu để kiểm tra chẵn / lẻ, nó nói rằng cả kiểm tra chẵn / lẻ kiểm tra bảng tra cứu đều O(1)hoạt động.
ShadowRanger

Tôi không biết về yêu cầu sử dụng trong câu cuối cùng, nhưng bất cứ ai làm điều đó sẽ thay thế một lớp bằng một lớp khác không tương đương. Lớp O (n!) Chứa, nhưng lớn hơn O (n ^ n). Tương đương thực tế sẽ là O (n!) = O (n ^ ne ^ {- n} sqrt (n)).
điều

43

Nhắc nhở nhỏ: big Oký hiệu được sử dụng để biểu thị độ phức tạp tiệm cận (nghĩa là khi kích thước của vấn đề tăng lên vô cùng) nó ẩn một hằng số.

Điều này có nghĩa là giữa thuật toán trong O (n) và một thuật toán trong O (n 2 ), nhanh nhất không phải luôn là thuật toán đầu tiên (mặc dù luôn tồn tại giá trị n sao cho các vấn đề về kích thước> n, thuật toán đầu tiên là nhanh nhất).

Lưu ý rằng hằng số ẩn rất nhiều phụ thuộc vào việc thực hiện!

Ngoài ra, trong một số trường hợp, thời gian chạy không phải là hàm xác định kích thước n của đầu vào. Hãy sắp xếp bằng cách sử dụng sắp xếp nhanh chẳng hạn: thời gian cần thiết để sắp xếp một mảng gồm n phần tử không phải là hằng số mà phụ thuộc vào cấu hình bắt đầu của mảng.

Có những phức tạp thời gian khác nhau:

  • Trường hợp xấu nhất (thường là đơn giản nhất để tìm ra, mặc dù không phải lúc nào cũng rất có ý nghĩa)
  • Trường hợp trung bình (thường khó hơn nhiều để tìm ra ...)

  • ...

Giới thiệu tốt là Giới thiệu về Phân tích thuật toán của R. Sedgewick và P. Flajolet.

Như bạn nói, premature optimisation is the root of all evilvà (nếu có thể) hồ sơ thực sự nên luôn luôn được sử dụng khi tối ưu hóa mã. Nó thậm chí có thể giúp bạn xác định độ phức tạp của các thuật toán của bạn.


3
Trong toán học, O (.) Có nghĩa là giới hạn trên và theta (.) Có nghĩa là bạn có một giới hạn trên và dưới. Là định nghĩa thực sự khác nhau trong CS, hay nó chỉ là một lạm dụng phổ biến của ký hiệu? Theo định nghĩa toán học, sqrt (n) là cả O (n) và O (n ^ 2), do đó không phải lúc nào cũng có một số n sau đó hàm O (n) nhỏ hơn.
Douglas Zare

28

Xem các câu trả lời ở đây tôi nghĩ rằng chúng ta có thể kết luận rằng hầu hết chúng ta thực sự gần đúng thứ tự của thuật toán bằng cách nhìn vào nó và sử dụng thông thường thay vì tính toán nó, ví dụ, phương pháp chính như chúng ta đã nghĩ ở trường đại học. Nói như vậy tôi phải nói thêm rằng ngay cả giáo sư cũng khuyến khích chúng tôi (sau này) thực sự nghĩ về nó thay vì chỉ tính toán nó.

Ngoài ra tôi muốn thêm cách nó được thực hiện cho các hàm đệ quy :

giả sử chúng ta có một hàm như ( mã lược đồ ):

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

trong đó tính toán đệ quy giai thừa của số đã cho.

Bước đầu tiên là thử và xác định đặc tính hiệu năng cho phần thân của hàm chỉ trong trường hợp này, không có gì đặc biệt được thực hiện trong phần thân, chỉ là phép nhân (hoặc trả về giá trị 1).

Vậy hiệu suất cho cơ thể là: O (1) (không đổi).

Tiếp theo hãy thử và xác định điều này cho số lượng các cuộc gọi đệ quy . Trong trường hợp này, chúng tôi có các cuộc gọi đệ quy n-1.

Vì vậy, hiệu suất cho các cuộc gọi đệ quy là: O (n-1) (thứ tự là n, khi chúng ta vứt bỏ các phần không đáng kể).

Sau đó đặt hai cái đó lại với nhau và sau đó bạn có hiệu suất cho toàn bộ hàm đệ quy:

1 * (n-1) = O (n)


Peter , để trả lời các vấn đề nêu lên của bạn; phương pháp tôi mô tả ở đây thực sự xử lý việc này khá tốt. Nhưng hãy nhớ rằng đây vẫn chỉ là một xấp xỉ và không phải là một câu trả lời đúng về mặt toán học. Phương pháp được mô tả ở đây cũng là một trong những phương pháp chúng tôi được dạy ở trường đại học và nếu tôi nhớ chính xác thì đã được sử dụng cho các thuật toán tiên tiến hơn nhiều so với yếu tố tôi đã sử dụng trong ví dụ này.
Tất nhiên tất cả phụ thuộc vào mức độ bạn có thể ước tính thời gian chạy của thân hàm và số lượng các cuộc gọi đệ quy, nhưng điều đó cũng đúng với các phương thức khác.


Sven, tôi không chắc rằng cách đánh giá sự phức tạp của hàm đệ quy sẽ có hiệu quả đối với các hàm phức tạp hơn, chẳng hạn như thực hiện tìm kiếm / tổng hợp từ trên xuống dưới / một cái gì đó trong cây nhị phân. Chắc chắn, bạn có thể lý do về một ví dụ đơn giản và đưa ra câu trả lời. Nhưng tôi nghĩ bạn thực sự phải làm một số phép toán cho những người đệ quy?
Peteter

3
+1 cho đệ quy ... Ngoài ra, điều này cũng rất hay: "... ngay cả giáo sư cũng khuyến khích chúng tôi nghĩ ..." :)
TT_

Vâng, điều này là rất tốt. Tôi có xu hướng nghĩ nó như thế này, thuật ngữ bên trong O (..) cao hơn, nhiều công việc bạn / máy đang làm. Suy nghĩ về nó trong khi liên quan đến một cái gì đó có thể là một xấp xỉ, nhưng những giới hạn này cũng vậy. Họ chỉ cho bạn biết làm thế nào để công việc được hoàn thành tăng lên khi số lượng đầu vào được tăng lên.
Abhinav Gauniyal

26

Nếu chi phí của bạn là một đa thức, chỉ cần giữ thời hạn đặt hàng cao nhất, không có hệ số nhân của nó. Ví dụ:

O ((n / 2 + 1) * (n / 2)) = O (n 2/4 + n / 2) = O (n 2/4 ) = O (n 2 )

Điều này không làm việc cho loạt vô hạn, nhớ bạn. Không có công thức duy nhất cho trường hợp chung, mặc dù đối với một số trường hợp phổ biến, áp dụng các bất đẳng thức sau:

O (log N ) <O ( N ) <O ( N log N ) <O ( N 2 ) <O ( N k ) <O (e n ) <O ( n !)


8
chắc chắn O (N) <O (NlogN)
jk.

22

Tôi nghĩ về nó về mặt thông tin. Bất kỳ vấn đề bao gồm học một số bit nhất định.

Công cụ cơ bản của bạn là khái niệm về các điểm quyết định và entropy của chúng. Entropy của một điểm quyết định là thông tin trung bình nó sẽ cung cấp cho bạn. Ví dụ: nếu một chương trình chứa một điểm quyết định có hai nhánh, thì entropy là tổng xác suất của mỗi nhánh nhân với log 2 của xác suất nghịch đảo của nhánh đó. Đó là bạn học được bao nhiêu bằng cách thực hiện quyết định đó.

Ví dụ: một ifcâu lệnh có hai nhánh, cả hai đều có khả năng như nhau, có entropy là 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Vậy entropy của nó là 1 bit.

Giả sử bạn đang tìm kiếm một bảng gồm N mục, như N = 1024. Đó là vấn đề 10 bit vì log (1024) = 10 bit. Vì vậy, nếu bạn có thể tìm kiếm nó với các câu lệnh IF có kết quả tương đương nhau, thì phải mất 10 quyết định.

Đó là những gì bạn nhận được với tìm kiếm nhị phân.

Giả sử bạn đang thực hiện tìm kiếm tuyến tính. Bạn nhìn vào yếu tố đầu tiên và hỏi xem đó có phải là yếu tố bạn muốn không. Xác suất là 1/1024, và 1023/1024 thì không. Entropy của quyết định đó là 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * khoảng 0 = khoảng 0,01 bit. Bạn đã học được rất ít! Quyết định thứ hai không tốt hơn nhiều. Đó là lý do tại sao tìm kiếm tuyến tính rất chậm. Trong thực tế, số mũ bạn cần học theo cấp số nhân.

Giả sử bạn đang làm chỉ mục. Giả sử bảng được sắp xếp trước thành rất nhiều thùng và bạn sử dụng một số tất cả các bit trong khóa để lập chỉ mục trực tiếp cho mục nhập bảng. Nếu có 1024 thùng, entropy là 1/1024 * log (1024) + 1/1024 * log (1024) + ... cho tất cả 1024 kết quả có thể xảy ra. Đây là 1/1024 * 10 lần 1024 kết quả, hoặc 10 bit entropy cho một hoạt động lập chỉ mục đó. Đó là lý do tại sao lập chỉ mục tìm kiếm là nhanh chóng.

Bây giờ hãy nghĩ về việc sắp xếp. Bạn có N mục, và bạn có một danh sách. Đối với mỗi mục, bạn phải tìm kiếm vị trí của mục trong danh sách, sau đó thêm nó vào danh sách. Vì vậy, việc sắp xếp mất khoảng N lần số bước của tìm kiếm cơ bản.

Vì vậy, sắp xếp dựa trên các quyết định nhị phân có kết quả gần như bằng nhau, tất cả đều thực hiện các bước O (N log N). Một thuật toán sắp xếp O (N) là có thể nếu nó dựa trên tìm kiếm lập chỉ mục.

Tôi đã thấy rằng gần như tất cả các vấn đề hiệu suất thuật toán có thể được xem xét theo cách này.


Ồ Bạn có bất kỳ tài liệu tham khảo hữu ích về điều này? Tôi cảm thấy công cụ này hữu ích cho tôi để thiết kế / tái cấu trúc / gỡ lỗi chương trình.
Jesvin Jose

3
@aitchnyu: Để biết giá trị của nó, tôi đã viết một cuốn sách về chủ đề đó và các chủ đề khác. Đã lâu không in, nhưng các bản sao sẽ có giá hợp lý. Tôi đã cố gắng để GoogleBooks lấy nó, nhưng hiện tại hơi khó để biết ai là người có bản quyền.
Mike Dunlavey

21

Hãy bắt đầu từ đầu.

Trước hết, chấp nhận nguyên tắc rằng một số thao tác đơn giản trên dữ liệu có thể được thực hiện O(1)kịp thời, nghĩa là, trong thời gian độc lập với kích thước của đầu vào. Các hoạt động nguyên thủy trong C bao gồm

  1. Các phép toán số học (ví dụ + hoặc%).
  2. Hoạt động logic (ví dụ: &&).
  3. Hoạt động so sánh (ví dụ: <=).
  4. Các hoạt động truy cập cấu trúc (ví dụ: lập chỉ mục mảng như A [i] hoặc con trỏ theo toán tử ->).
  5. Việc gán đơn giản như sao chép một giá trị vào một biến.
  6. Gọi các chức năng thư viện (ví dụ, scanf, printf).

Sự biện minh cho nguyên tắc này đòi hỏi một nghiên cứu chi tiết về các hướng dẫn máy (các bước nguyên thủy) của một máy tính thông thường. Mỗi thao tác được mô tả có thể được thực hiện với một số lượng nhỏ các hướng dẫn máy; thường chỉ cần một hoặc hai hướng dẫn. Kết quả là, một số loại câu lệnh trong C có thể được thực thi trong O(1)thời gian, nghĩa là, trong một số lượng thời gian không đổi độc lập với đầu vào. Những đơn giản này bao gồm

  1. Các câu lệnh gán không liên quan đến các lệnh gọi hàm trong biểu thức của chúng.
  2. Đọc báo cáo.
  3. Viết các câu lệnh không yêu cầu gọi hàm để đánh giá các đối số.
  4. Các câu lệnh nhảy ngắt, tiếp tục, goto và trả về biểu thức, trong đó biểu thức không chứa lệnh gọi hàm.

Trong C, nhiều vòng lặp for được hình thành bằng cách khởi tạo một biến chỉ số thành một giá trị nào đó và tăng biến đó lên 1 mỗi lần xung quanh vòng lặp. Vòng lặp for kết thúc khi chỉ số đạt đến một giới hạn nào đó. Chẳng hạn, vòng lặp for

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

sử dụng biến chỉ số i. Nó tăng i lên 1 mỗi lần xung quanh vòng lặp và các lần lặp dừng lại khi tôi đạt n - 1.

Tuy nhiên, hiện tại, tập trung vào hình thức vòng lặp đơn giản, trong đó sự khác biệt giữa giá trị cuối cùng và giá trị ban đầu, chia cho số lượng mà biến chỉ số được tăng cho chúng ta biết số lần chúng ta đi quanh vòng lặp . Số lượng đó là chính xác, trừ khi có nhiều cách để thoát khỏi vòng lặp thông qua câu lệnh nhảy; nó là giới hạn trên của số lần lặp trong mọi trường hợp.

Chẳng hạn, các vòng lặp for ((n − 1) − 0)/1 = n − 1 times, vì 0 là giá trị ban đầu của i, n - 1 là giá trị cao nhất đạt được bởi i (nghĩa là khi tôi đạt n 1, vòng lặp dừng và không xảy ra lặp lại với i = n− 1) và 1 được thêm vào i tại mỗi lần lặp của vòng lặp.

Trong trường hợp đơn giản nhất, trong đó thời gian dành cho thân vòng lặp là như nhau cho mỗi lần lặp, chúng ta có thể nhân giới hạn trên lớn cho cơ thể với số lần xung quanh vòng lặp . Nói một cách chính xác, sau đó chúng ta phải thêm thời gian O (1) để khởi tạo chỉ số vòng lặp và thời gian O (1) để so sánh lần đầu tiên của chỉ số vòng lặp với giới hạn , bởi vì chúng ta kiểm tra một lần nữa so với vòng lặp. Tuy nhiên, trừ khi có thể thực hiện vòng lặp 0 lần, thời gian để khởi tạo vòng lặp và kiểm tra giới hạn một lần là một thuật ngữ bậc thấp có thể được bỏ theo quy tắc tổng.


Bây giờ hãy xem xét ví dụ này:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

Chúng tôi biết rằng dòng (1) mất O(1)thời gian. Rõ ràng, chúng ta đi vòng quanh n lần, vì chúng ta có thể xác định bằng cách trừ giới hạn dưới khỏi giới hạn trên được tìm thấy trên dòng (1) và sau đó thêm 1. Vì thân, dòng (2), mất thời gian O (1), chúng ta có thể bỏ qua thời gian để tăng j và thời gian để so sánh j với n, cả hai đều là O (1). Như vậy, thời gian chạy của dòng (1) và (2) là sản phẩm của n và O (1) , đó là O(n).

Tương tự, chúng ta có thể giới hạn thời gian chạy của vòng lặp bên ngoài bao gồm các dòng (2) đến (4), đó là

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

Chúng tôi đã thiết lập rằng vòng lặp của dòng (3) và (4) mất thời gian O (n). Do đó, chúng ta có thể bỏ qua thời gian O (1) để tăng i và kiểm tra xem i <n trong mỗi lần lặp hay không, kết luận rằng mỗi lần lặp của vòng lặp bên ngoài sẽ mất thời gian O (n).

Việc khởi tạo i = 0 của vòng lặp bên ngoài và thử nghiệm (n + 1) của điều kiện i <n cũng mất thời gian O (1) và có thể bỏ qua. Cuối cùng, chúng tôi quan sát thấy rằng chúng tôi đi vòng quanh n vòng ngoài, lấy thời gian O (n) cho mỗi lần lặp, cho tổng O(n^2)thời gian chạy.


Một ví dụ thực tế hơn.

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


Điều gì sẽ xảy ra nếu một câu lệnh goto chứa một lệnh gọi hàm? Một cái gì đó giống như bước 3: if (M.step == 3) {M = step3 (xong, M); } bước 4: if (M.step == 4) {M = step4 (M); } if (M.step == 5) {M = step5 (M); goto bước 3; } if (M.step == 6) {M = step6 (M); goto bước 4; } return cut_matrix (A, M); Làm thế nào phức tạp sẽ được tính toán sau đó? nó sẽ là một phép cộng hay phép nhân? xem xét bước 4 là n ^ 3 và bước 5 là n ^ 2.
Taha Tariq

14

Nếu bạn muốn ước tính thứ tự mã của mình theo kinh nghiệm thay vì phân tích mã, bạn có thể sử dụng một loạt các giá trị tăng dần của n và thời gian mã của bạn. Vẽ thời gian của bạn trên một quy mô đăng nhập. Nếu mã là O (x ^ n), các giá trị sẽ nằm trên một đường dốc n.

Điều này có một số lợi thế so với việc chỉ nghiên cứu mã. Đối với một điều, bạn có thể xem liệu bạn có ở trong phạm vi mà thời gian chạy tiếp cận thứ tự tiệm cận của nó không. Ngoài ra, bạn có thể thấy rằng một số mã mà bạn nghĩ là thứ tự O (x) thực sự là thứ tự O (x ^ 2), chẳng hạn, vì thời gian dành cho các cuộc gọi thư viện.


Chỉ cần cập nhật câu trả lời này: vi.wikipedia.org/wiki/Analysis_of_alacticms , liên kết này có công thức bạn cần. Nhiều thuật toán tuân theo quy tắc sức mạnh, nếu bạn thực hiện, với 2 mốc thời gian và 2 thời gian chạy trên máy, chúng ta có thể tính độ dốc trên biểu đồ log-log. Đó là a = log (t2 / t1) / log (n2 / n1), điều này đã cho tôi số mũ của thuật toán trong, O (N ^ a). Điều này có thể được so sánh với tính toán thủ công bằng cách sử dụng mã.
Christopher John

1
Xin chào, câu trả lời tốt đẹp. Tôi đã tự hỏi nếu bạn biết bất kỳ thư viện hoặc phương pháp nào (tôi làm việc với python / R chẳng hạn) để khái quát phương pháp thực nghiệm này, có nghĩa là phù hợp với các hàm phức tạp khác nhau để tăng dữ liệu kích thước và tìm ra cái nào có liên quan. Cảm ơn
agenis

10

Về cơ bản, điều mà cây trồng tăng 90% thời gian chỉ là phân tích các vòng lặp. Bạn có vòng lặp đơn, đôi, ba lồng nhau? Các bạn có thời gian chạy O (n), O (n ^ 2), O (n ^ 3).

Rất hiếm khi (trừ khi bạn đang viết một nền tảng với một thư viện cơ sở rộng lớn (ví dụ như .NET BCL hoặc STL của C ++), bạn sẽ gặp bất cứ điều gì khó khăn hơn là chỉ nhìn vào các vòng lặp của bạn (đối với các câu lệnh, trong khi, goto, Vân vân...)


1
Phụ thuộc vào các vòng lặp.
kelalaka

8

Ký hiệu Big O rất hữu ích vì nó dễ làm việc và che giấu các biến chứng và chi tiết không cần thiết (đối với một số định nghĩa không cần thiết). Một cách hay để tìm ra sự phức tạp của các thuật toán phân chia và chinh phục là phương pháp cây. Giả sử bạn có một phiên bản quicksort với quy trình trung bình, vì vậy bạn chia mảng thành các phần con cân bằng hoàn hảo mỗi lần.

Bây giờ xây dựng một cây tương ứng với tất cả các mảng bạn làm việc với. Tại thư mục gốc bạn có mảng gốc, thư mục gốc có hai phần tử con. Lặp lại điều này cho đến khi bạn có các mảng phần tử đơn ở phía dưới.

Vì chúng ta có thể tìm trung vị trong thời gian O (n) và chia mảng thành hai phần trong thời gian O (n), nên công việc được thực hiện tại mỗi nút là O (k) trong đó k là kích thước của mảng. Mỗi cấp độ của cây chứa (nhiều nhất) toàn bộ mảng, vì vậy công việc trên mỗi cấp là O (n) (kích thước của các tập hợp con cộng với n và vì chúng ta có O (k) cho mỗi cấp nên chúng ta có thể thêm phần này) . Chỉ có các mức log (n) trong cây vì mỗi lần chúng ta giảm một nửa đầu vào.

Do đó, chúng ta có thể giới hạn số lượng công việc bằng O (n * log (n)).

Tuy nhiên, Big O ẩn một số chi tiết mà đôi khi chúng ta không thể bỏ qua. Hãy xem xét tính toán chuỗi Fibonacci với

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

và giả sử a và b là BigIntegers trong Java hoặc thứ gì đó có thể xử lý số lượng lớn tùy ý. Hầu hết mọi người sẽ nói đây là thuật toán O (n) mà không nao núng. Lý do là bạn có n lần lặp trong vòng lặp for và O (1) hoạt động bên vòng lặp.

Nhưng số Fibonacci là lớn, số Fibonacci thứ n là số mũ theo n nên chỉ cần lưu trữ nó sẽ theo thứ tự n byte. Thực hiện bổ sung với số nguyên lớn sẽ mất O (n) khối lượng công việc. Vì vậy, tổng số lượng công việc được thực hiện trong thủ tục này là

1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)

Vì vậy, thuật toán này chạy trong thời gian tứ giác!


1
Bạn không nên quan tâm đến cách các số được lưu trữ, điều đó không thay đổi khi thuật toán phát triển ở phía trên của O (n).
mikek3332002

8

Nói chung, ít hữu ích hơn, nhưng để hoàn thiện, cũng có một Omega lớn Ω , định nghĩa giới hạn dưới về độ phức tạp của thuật toán và Big Theta , định nghĩa cả giới hạn trên và dưới.


7

Chia nhỏ thuật toán thành các phần bạn biết ký hiệu O lớn và kết hợp thông qua các toán tử O lớn. Đó là cách duy nhất tôi biết.

Để biết thêm thông tin, hãy kiểm tra trang Wikipedia về chủ đề này.


7

Làm quen với các thuật toán / cấu trúc dữ liệu tôi sử dụng và / hoặc phân tích nhanh chóng về lồng lặp. Khó khăn là khi bạn gọi một chức năng thư viện, có thể nhiều lần - bạn thường không chắc chắn liệu bạn có đang gọi chức năng đó một cách không cần thiết vào thời điểm không hoặc họ đang sử dụng triển khai gì. Có lẽ các hàm thư viện nên có thước đo độ phức tạp / hiệu quả, cho dù đó là Big O hay một số liệu khác, có sẵn trong tài liệu hoặc thậm chí là IntelliSense .


6

Đối với "cách bạn tính toán" Big O, đây là một phần của lý thuyết phức tạp tính toán . Đối với một số (nhiều) trường hợp đặc biệt, bạn có thể đi kèm với một số phương pháp phỏng đoán đơn giản (như nhân số vòng lặp cho các vòng lặp lồng nhau), đặc biệt. khi tất cả những gì bạn muốn là bất kỳ ước tính ràng buộc trên và bạn không bận tâm nếu nó quá bi quan - mà tôi đoán có lẽ là câu hỏi của bạn.

Nếu bạn thực sự muốn trả lời câu hỏi của bạn cho bất kỳ thuật toán nào, tốt nhất bạn có thể làm là áp dụng lý thuyết. Bên cạnh phân tích "trường hợp xấu nhất" đơn giản, tôi đã thấy phân tích khấu hao rất hữu ích trong thực tế.


6

Đối với trường hợp 1, vòng lặp bên trong được thực hiện n-ilần, vì vậy tổng số các vụ hành quyết là tổng cho iđi từ 0đến n-1(vì thấp hơn, chứ không phải thấp hơn hoặc bằng) của n-i. Bạn nhận được cuối cùng n*(n + 1) / 2, vì vậy O(n²/2) = O(n²).

Đối với vòng lặp thứ 2, inằm giữa 0và được nbao gồm cho vòng lặp bên ngoài; sau đó vòng lặp bên trong được thực thi khi jlớn hơn n, điều đó là không thể.


5

Ngoài việc sử dụng phương thức chính (hoặc một trong các chuyên ngành của nó), tôi thử nghiệm các thuật toán của mình bằng thực nghiệm. Điều này không thể chứng minh rằng bất kỳ lớp phức tạp cụ thể nào đều đạt được, nhưng nó có thể đảm bảo rằng phân tích toán học là phù hợp. Để giúp đảm bảo điều này, tôi sử dụng các công cụ bảo hiểm mã kết hợp với các thử nghiệm của mình, để đảm bảo rằng tôi đang thực hiện tất cả các trường hợp.

Như một ví dụ rất đơn giản nói rằng bạn muốn kiểm tra độ chính xác về tốc độ sắp xếp danh sách của .NET framework. Bạn có thể viết một cái gì đó như sau, sau đó phân tích kết quả trong Excel để đảm bảo rằng chúng không vượt quá đường cong n * log (n).

Trong ví dụ này tôi đo số lượng so sánh, nhưng cũng cần thận trọng khi kiểm tra thời gian thực tế cần thiết cho mỗi cỡ mẫu. Tuy nhiên, sau đó bạn phải cẩn thận hơn nữa rằng bạn chỉ đang đo thuật toán và không bao gồm các tạo phẩm từ cơ sở hạ tầng thử nghiệm của bạn.

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);

4

Đừng quên cũng cho phép sự phức tạp không gian cũng có thể là một nguyên nhân gây lo ngại nếu một người có tài nguyên bộ nhớ hạn chế. Vì vậy, ví dụ bạn có thể nghe thấy ai đó muốn một thuật toán không gian không đổi, về cơ bản là một cách để nói rằng lượng không gian được sử dụng bởi thuật toán không phụ thuộc vào bất kỳ yếu tố nào trong mã.

Đôi khi sự phức tạp có thể đến từ số lần gọi là bao nhiêu lần, tần suất vòng lặp được thực thi, tần suất bộ nhớ được phân bổ, v.v là một phần khác để trả lời câu hỏi này.

Cuối cùng, chữ O lớn có thể được sử dụng cho trường hợp xấu nhất, trường hợp tốt nhất và trường hợp khấu hao trong đó nói chung đó là trường hợp xấu nhất được sử dụng để mô tả mức độ xấu của thuật toán.


4

Những gì thường bị bỏ qua là hành vi dự kiến của các thuật toán của bạn. Nó không thay đổi Big-O của thuật toán của bạn , nhưng nó không liên quan đến câu lệnh "tối ưu hóa sớm .. .."

Hành vi dự kiến ​​của thuật toán của bạn là - rất ngớ ngẩn - bạn có thể mong đợi thuật toán của mình hoạt động nhanh như thế nào trên dữ liệu mà bạn có thể thấy nhất.

Ví dụ: nếu bạn đang tìm kiếm một giá trị trong danh sách, thì đó là O (n), nhưng nếu bạn biết rằng hầu hết các danh sách bạn thấy đều có giá trị của bạn ở phía trước, hành vi điển hình của thuật toán của bạn sẽ nhanh hơn.

Để thực sự đóng nó xuống, bạn cần có thể mô tả phân phối xác suất của "không gian đầu vào" của bạn (nếu bạn cần sắp xếp một danh sách, danh sách đó có thường xuyên được sắp xếp không? Bao lâu thì hoàn toàn đảo ngược? nó thường được sắp xếp chủ yếu không?) Không phải lúc nào bạn cũng biết điều đó, nhưng đôi khi bạn làm vậy.


4

câu hỏi tuyệt vời!

Tuyên bố từ chối trách nhiệm: câu trả lời này có chứa các tuyên bố sai, xem các bình luận bên dưới.

Nếu bạn đang sử dụng Big O, bạn đang nói về trường hợp xấu hơn (nhiều hơn về điều đó có nghĩa là gì sau này). Ngoài ra, có vốn theta cho trường hợp trung bình và omega lớn cho trường hợp tốt nhất.

Kiểm tra trang web này để biết định nghĩa chính thức đáng yêu của Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html

f (n) = O (g (n)) có nghĩa là có hằng số dương c và k, sao cho 0 ≤ f (n) cg (n) với mọi n ≥ k. Các giá trị của c và k phải được cố định cho hàm f và không được phụ thuộc vào n.


Ok, vậy bây giờ chúng ta có ý nghĩa gì bởi sự phức tạp "trường hợp tốt nhất" và "trường hợp xấu nhất"?

Điều này có lẽ được minh họa rõ ràng nhất thông qua các ví dụ. Ví dụ: nếu chúng ta đang sử dụng tìm kiếm tuyến tính để tìm một số trong một mảng được sắp xếp thì trường hợp xấu nhất là khi chúng ta quyết định tìm kiếm phần tử cuối cùng của mảng vì điều này sẽ mất nhiều bước như có các mục trong mảng. Các trường hợp tốt nhất sẽ là khi chúng ta tìm kiếm các yếu tố đầu tiên kể từ khi chúng tôi sẽ được thực hiện sau khi kiểm tra đầu tiên.

Điểm chính của tất cả các phức tạp tính từ này là chúng tôi đang tìm cách vẽ biểu đồ lượng thời gian mà một chương trình giả định chạy đến khi hoàn thành về kích thước của các biến cụ thể. Tuy nhiên, đối với nhiều thuật toán, bạn có thể lập luận rằng không có một lần duy nhất cho một kích thước cụ thể của đầu vào. Lưu ý rằng điều này mâu thuẫn với yêu cầu cơ bản của hàm, bất kỳ đầu vào nào cũng không có nhiều hơn một đầu ra. Vì vậy, chúng tôi đưa ra nhiều hàm để mô tả độ phức tạp của thuật toán. Bây giờ, mặc dù việc tìm kiếm một mảng có kích thước n có thể mất nhiều thời gian khác nhau tùy thuộc vào những gì bạn đang tìm kiếm trong mảng và tùy theo tỷ lệ n, chúng ta có thể tạo một mô tả thông tin về thuật toán bằng cách sử dụng trường hợp tốt nhất, trường hợp trung bình và các trường hợp xấu nhất.

Xin lỗi điều này được viết quá kém và thiếu nhiều thông tin kỹ thuật. Nhưng hy vọng nó sẽ làm cho các lớp phức tạp về thời gian dễ suy nghĩ hơn. Khi bạn cảm thấy thoải mái với những điều này, việc phân tích chương trình của bạn trở nên đơn giản và tìm kiếm những thứ như vòng lặp phụ thuộc vào kích thước mảng và lý luận dựa trên cấu trúc dữ liệu của bạn, loại đầu vào nào sẽ dẫn đến trường hợp tầm thường và kết quả đầu vào nào sẽ dẫn đến trong trường hợp xấu nhất


1
Điều này là không chính xác. Big O có nghĩa là "giới hạn trên" không phải là trường hợp xấu nhất.
Samy Bencherif

1
Đó là một quan niệm sai lầm phổ biến rằng big-O đề cập đến trường hợp xấu nhất. Làm thế nào để O và Ω liên quan đến trường hợp xấu nhất và tốt nhất?
Bernhard Barker

1
Điều này là sai lệch. Big-O có nghĩa là giới hạn trên của hàm f (n). Omega có nghĩa là giới hạn dưới cho một hàm f (n). Nó hoàn toàn không liên quan đến trường hợp tốt nhất hoặc trường hợp xấu nhất.
TASneem Haider

1
Bạn có thể sử dụng Big-O làm giới hạn trên cho trường hợp tốt nhất hoặc xấu nhất, nhưng ngoài ra, có, không có quan hệ.
Samy Bencherif

2

Tôi không biết làm thế nào để giải quyết vấn đề này, nhưng điều đầu tiên mọi người làm là chúng tôi lấy mẫu thuật toán cho một số mẫu nhất định trong số lượng thao tác được thực hiện, giả sử 4n ^ 2 + 2n + 1 chúng tôi có 2 quy tắc:

  1. Nếu chúng ta có một tổng số thuật ngữ, thuật ngữ có tốc độ tăng trưởng lớn nhất sẽ được giữ nguyên, với các điều khoản khác được bỏ qua.
  2. Nếu chúng ta có một sản phẩm của một số yếu tố, các yếu tố không đổi được bỏ qua.

Nếu chúng ta đơn giản hóa f (x), trong đó f (x) là công thức cho số lượng thao tác được thực hiện, (4n ^ 2 + 2n + 1 đã giải thích ở trên), chúng ta thu được giá trị big-O [O (n ^ 2) trong phần này trường hợp]. Nhưng điều này sẽ phải tính đến phép nội suy Lagrange trong chương trình, điều này có thể khó thực hiện. Và điều gì xảy ra nếu giá trị O lớn thực sự là O (2 ^ n) và chúng ta có thể có một cái gì đó giống như O (x ^ n), vì vậy thuật toán này có thể sẽ không được lập trình. Nhưng nếu ai đó chứng minh tôi sai, hãy cho tôi mã. . . .


2

Đối với mã A, vòng lặp bên ngoài sẽ thực thi theo n+1thời gian, thời gian '1' có nghĩa là quá trình kiểm tra xem tôi có còn đáp ứng yêu cầu hay không. Và vòng lặp bên trong chạy nthời gian, n-2thời gian .... Vì vậy , 0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²).

Đối với mã B, mặc dù vòng lặp bên trong sẽ không bước vào và thực thi foo (), vòng lặp bên trong sẽ được thực thi trong n lần phụ thuộc vào thời gian thực hiện vòng lặp bên ngoài, đó là O (n)


1

Tôi muốn giải thích Big-O ở một khía cạnh khác một chút.

Big-O chỉ là để so sánh sự phức tạp của các chương trình, điều đó có nghĩa là chúng phát triển nhanh như thế nào khi đầu vào đang tăng và không phải là thời gian chính xác dành để thực hiện hành động.

IMHO trong các công thức big-O, tốt hơn hết bạn không nên sử dụng các phương trình phức tạp hơn (bạn có thể chỉ sử dụng các công thức trong biểu đồ sau.) Tuy nhiên, bạn vẫn có thể sử dụng công thức chính xác khác (như 3 ^ n, n ^ 3, .. .) nhưng nhiều hơn thế đôi khi có thể gây hiểu nhầm! Vì vậy, tốt hơn để giữ nó đơn giản nhất có thể.

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

Tôi muốn nhấn mạnh một lần nữa rằng ở đây chúng tôi không muốn có một công thức chính xác cho thuật toán của mình. Chúng tôi chỉ muốn cho thấy nó phát triển như thế nào khi các đầu vào đang phát triển và so sánh với các thuật toán khác theo nghĩa đó. Nếu không, bạn nên sử dụng các phương pháp khác nhau như đánh dấu băng ghế.

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.