Các đặc điểm của thuật toán độ phức tạp thời gian


19

Đôi khi thật dễ dàng để xác định độ phức tạp thời gian của một thuật toán mà tôi kiểm tra nó một cách cẩn thận. Các thuật toán có hai vòng lặp lồng nhau của N rõ ràng là N2 . Thuật toán khám phá tất cả các kết hợp có thể có của N nhóm trong hai giá trị rõ ràng 2N .

Tuy nhiên tôi không biết làm thế nào để "phát" một thuật toán với Θ(NlogN) phức tạp. Một triển khai sáp nhập đệ quy, ví dụ, là một. Các đặc điểm chung của sáp nhập hoặc các thuật toán Θ(NlogN) khác sẽ cho tôi manh mối nếu tôi đang phân tích một thuật toán là gì?

Tôi chắc chắn rằng có nhiều hơn một cách thuật toán có thể có độ phức tạp , vì vậy bất kỳ và tất cả các câu trả lời đều được đánh giá cao. BTW Tôi đang tìm kiếm các đặc điểm chung và lời khuyên, không phải bằng chứng nghiêm ngặt.Θ(NlogN)


6
có nghĩa là cây. O(logn)
Pratik Deoghare


2
@PratikDeoghare: Không nhất thiết.
Raphael

3
@Raphael Ý tôi là chủ yếu! :)
Pratik Deoghare

Câu trả lời:


17

Archetypical bạn là một phân chia-và-chinh phục thuật toán, mà phân chia (và tái kết hợp) làm việc trong thời gian tuyến tính và recurses qua miếng. Merge sort hoạt động theo cách đó: chi tiêu O ( n ) thời gian tách đầu vào thành hai mảnh tương đương, đệ quy loại từng mảnh, và dành Θ ( n ) thời gian kết hợp hai sắp xếp nửa.Θ(nlogn)O(n)Θ(n)

Theo trực giác, tiếp tục ý tưởng phân chia và chinh phục, mỗi giai đoạn phân chia tổng thời gian tuyến tính, bởi vì sự gia tăng số lượng các mảnh để phân chia chính xác phù hợp với sự giảm kích thước của các mảnh, vì thời gian phân chia là tuyến tính. Tổng thời gian chạy là sản phẩm của tổng chi phí của một giai đoạn phân chia nhân với số lượng các giai đoạn phân chia. Vì kích thước của các mảnh được giảm một nửa mỗi lần, có các giai đoạn phân chia , do đó tổng thời gian chạy là n log ( n )log2(n)nlog(n) . (Lên đến hằng số nhân, cơ sở của logarit là không liên quan.)

Đặt nó trong phương trình (), một trong những cách để ước tính thời gian chạy của một thuật toán như vậy là để thể hiện nó một cách đệ quy: T ( n ) = 2 T ( n / 2 ) + Θ ( n ) . Rõ ràng là thuật toán này mất nhiều thời gian hơn tuyến tính và chúng ta có thể thấy nhiều hơn nữa bằng cách chia cho n : T ( n )T(n)T(n)=2T(n/2)+Θ(n)n Khinđôi,T(n)/ntăng một lượng không đổi:T(n)/ntăng theo hàm mũ, hay nói cách khác,T(n)=Θ(nlogn).

T(n)n=T(n/2)n/2+Θ(1)
nT(n)/nT(n)/nT(n)=Θ(nlogn)

Đây là một ví dụ của một mẫu tổng quát hơn: định lý tổng thể . Đối với bất kỳ thuật toán đệ quy mà phân chia đầu vào của nó kích thước thành một mảnh kích thước n / b và mất một thời gian f ( n ) để thực hiện việc phân chia và tái tổ hợp, thời gian chạy đáp ứng T ( n ) = một T ( n / b ) + f ( n ) . Điều này dẫn đến một dạng đóng phụ thuộc vào giá trị của ab và hình dạng củanan/bf(n)T(n)=aT(n/b)+f(n)ab . Nếuf f ( n ) = Θ ( n ) , định lý chủ nói rằng T ( n ) = Θ ( n log n ) .a=bf(n)=Θ(n)T(n)=Θ(nlogn)


1
To summarise: algorithms that do away with constant fractions of the search space at a time will exhibit logarithmic terms. The other factors depend on how long it takes to perform the doing away.
Raphael

1
example: quicksort average case O(nlogn)
vzn

11

Two other categories of algorithms that take Θ(nlogn) time:

Algorithms where each item is processed in turn, and it takes logarithmic time to process each item (e.g. HeapSort or many of the plane sweep computational geometry algorithms).

Các thuật toán trong đó thời gian chạy bị chi phối bởi một bước xử lý trước sắp xếp. (Ví dụ: trong thuật toán của Kruskal cho cây bao trùm tối thiểu, chúng ta có thể sắp xếp các cạnh theo trọng lượng là bước đầu tiên).


Nâng cao cho đoạn đầu tiên. Thứ hai có vẻ dư thừa, vì các thuật toán sắp xếp tuyến tính mà chúng ta biết là phân chia & chinh phục hoặc heapsort. Một ví dụ cho danh mục đầu tiên là tìm kiếm đối tượng trong cây tìm kiếm nhị phân (hoặc mảng được sắp xếp) có kích thước n ; ở một mức độ trừu tượng hơn, đó cũng có thể được coi là sự phân chia và chinh phục. nn
Raphael

@Raphael I know that sorting is redundant with the categories of running times already given. The point is that sorting is sometimes the "bottleneck" and algorithms where at first blush the question isn't about sorting may still have the run-time because sorting is required.
Joe

9

Another category: Algorithms in which the output has size Θ(nlogn), and therefore Θ(nlogn) running time is linear in the output size.

Although the details of such algorithms often use divide-and-conquer techniques, they don't necessarily have to. The run-time fundamentally comes from the question being asked, and so I think it is worth mentioning separately.

This comes up in data structures which are based on an augmented binary search tree, where each node stores a linear size data structure to search over the leaves in that node's subtree. Such data structures come up often in geometric range searching, and are often based on a decomposition scheme. See Agarwal's Survey.

For a concrete example, consider the range-tree, built to answer two dimensional orthogonal range queries. Although the space was later reduced using some compression techniques to pack multiple objects into a single word, the textbook (and most intuitive) version of the data structure requires O(nlogn) space (each leaf is stored in an auxiliary structure at each node on the path from the leaf to the root, or in O(logn) places), and the construction algorithm takes time linear in the space requirement.


An example would be finding the n-th prime using a sieve, because you'd need a sieve of size θ(nlogn).
gnasher729

6

A complexity of O(nlogn) arises from divide and conquer algorithms which divide their input into k pieces of roughly equal size in time O(n), operate on these pieces recursively, and then combine them in time O(n). If you only operate on some of the pieces, the running time drops to O(n).


5

Those are typically algorithms of the "divide and conquer" variety, where the cost of dividing and combining subsolutions isn't "too large". Take a look at this FAQ to see what kinds of recurrences give rise to this behaviour.



-1

A generic example of a loop (not an algorithm per se) that runs in O(nlogn) is the following:

for (i = 0; i < constant; i++){
    for(j = 0; j < n; j++){
        // Do some O(1) stuff on the input
    }
}

// Alternative Variant:
for (i = 0; i < constant; i++){
    for(j = n; j < constant; j++){
        // Do some O(1) stuff on the input
    }
}

(note that this nested loops are not O(n2). Also note that it isn't divide-and-conquer nor recursive.)

I can't give a concrete example of an algorithm that uses this loop, but it comes up often when coding custom algorithms.


Yes, these are both O(nlogn) but for the trivial reason that the bound is not tight. The first one is Θ(n) and the second is Θ(1) (or Θ(|n|) if n can be negative).
David Richerby

I thought it useful to mention, since it does come up in practice but every search for "O(nlogn)" is mostly "divide-and-conquer" examples like e.g. merge sort.
Nicolas Miari

additionally, the original question didn't ask for big-theta.
Nicolas Miari

1
Sure but any algorithm that runs in linear or constant time is O(nlogn), as well as being O(22n) and O(almost anything else). Your answer has nothing to do with logarithms. If you want to make the point that the question should be asking about Θ(nlogn) rather than O(nlogn) then either make that point explicitly as comment to the question or, even better, edit the question.
David Richerby

I wanted to make the point that I actually searched the internet for O(nlogn) to see if the loop in my answer, that I had to come up with for an actual answer to an actual programming problem that was required to run in O(nlogn), was actually O(nlogn) or not. And most information around deals with divide-and-conquer algorithms, so it would be worth mentioning. I do not pretend my answer to be the archetypical O(nlogn) algorithm.
Nicolas Miari
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.