28

I know there are quite a bunch of questions about big O notation, I have already checked:

to name a few.

I know by "intuition" how to calculate it for n, n^2, n! and so, however I am completely lost on how to calculate it for algorithms that are log n , n log n, n log log n and so.

What I mean is, I know that Quick Sort is n log n (on average).. but, why? Same thing for merge/comb, etc.

Could anybody explain me in a not too math-y way how do you calculate this?

The main reason is that Im about to have a big interview and I'm pretty sure they'll ask for this kind of stuff. I have researched for a few days now, and everybody seem to have either an explanation of why bubble sort is n^2 or the unreadable explanation (for me) on Wikipedia

Community
  • 1
  • 1
Francisco Noriega
  • 13,725
  • 11
  • 47
  • 72

6 Answers6

41

The logarithm is the inverse operation of exponentiation. An example of exponentiation is when you double the number of items at each step. Thus, a logarithmic algorithm often halves the number of items at each step. For example, binary search falls into this category.

Many algorithms require a logarithmic number of big steps, but each big step requires O(n) units of work. Mergesort falls into this category.

Usually you can identify these kinds of problems by visualizing them as a balanced binary tree. For example, here's merge sort:

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

At the top is the input, as leaves of the tree. The algorithm creates a new node by sorting the two nodes above it. We know the height of a balanced binary tree is O(log n) so there are O(log n) big steps. However, creating each new row takes O(n) work. O(log n) big steps of O(n) work each means that mergesort is O(n log n) overall.

Generally, O(log n) algorithms look like the function below. They get to discard half of the data at each step.

def function(data, n):
    if n <= constant:
       return do_simple_case(data, n)
    if some_condition():
       function(data[:n/2], n / 2) # Recurse on first half of data
    else:
       function(data[n/2:], n - n / 2) # Recurse on second half of data

While O(n log n) algorithms look like the function below. They also split the data in half, but they need to consider both halves.

def function(data, n):
    if n <= constant:
       return do_simple_case(data, n)
    part1 = function(data[n/2:], n / 2)      # Recurse on first half of data
    part2 = function(data[:n/2], n - n / 2)  # Recurse on second half of data
    return combine(part1, part2)

Where do_simple_case() takes O(1) time and combine() takes no more than O(n) time.

The algorithms don't need to split the data exactly in half. They could split it into one-third and two-thirds, and that would be fine. For average-case performance, splitting it in half on average is sufficient (like QuickSort). As long as the recursion is done on pieces of (n/something) and (n - n/something), it's okay. If it's breaking it into (k) and (n-k) then the height of the tree will be O(n) and not O(log n).

Daniel Stutzbach
  • 74,198
  • 17
  • 88
  • 77
14

You can usually claim log n for algorithms where it halves the space/time each time it runs. A good example of this is any binary algorithm (e.g., binary search). You pick either left or right, which then axes the space you're searching in half. The pattern of repeatedly doing half is log n.

Jeremy L
  • 7,686
  • 4
  • 29
  • 36
  • 2
    yes exactly. It's worth mentioning that in CS `log` means logarithm *base 2* instead of base 10 which is normally assumed. *log n* means the number to which you would have to raise 2 to reach n. so *log 8* is 3, *log 16* is 4, etc... – Segfault Apr 12 '10 at 23:45
  • 11
    Actually that's somewhat misleading segfault. While log does refer to base 2 generally, in terms of big Oh notation it doesn't matter. O(log_2 (n) ) is equivalent to O(log_k (n) ), because log_k (n) = log_k (2) * log_2 (n). This is just a simplification of the change of base log formula: log_k(a)/log_k(b) = log_b (a). Then because log_k (2) is a constant the big oh is clearly equivalent. – JSchlather Apr 12 '10 at 23:52
  • 5
    @Segfault more to the point, big-O complexity does not take into account constant factors, and the difference between log_e and log_2 is only a constant factor. – Pascal Cuoq Apr 12 '10 at 23:54
  • Great answer. Considering how many times this post has been viewed, I'm surprised this hasn't had more upvotes. – i8abug Aug 08 '11 at 15:35
6

For some algorithms, getting a tight bound for the running time through intuition is close to impossible (I don't think I'll ever be able to intuit a O(n log log n) running time, for instance, and I doubt anyone will ever expect you to). If you can get your hands on the CLRS Introduction to Algorithms text, you'll find a pretty thorough treatment of asymptotic notation which is appropriately rigorous without being completely opaque.

If the algorithm is recursive, one simple way to derive a bound is to write out a recurrence and then set out to solve it, either iteratively or using the Master Theorem or some other way. For instance, if you're not looking to be super rigorous about it, the easiest way to get QuickSort's running time is through the Master Theorem -- QuickSort entails partitioning the array into two relatively equal subarrays (it should be fairly intuitive to see that this is O(n)), and then calling QuickSort recursively on those two subarrays. Then if we let T(n) denote the running time, we have T(n) = 2T(n/2) + O(n), which by the Master Method is O(n log n).

Ismail Badawi
  • 36,054
  • 7
  • 85
  • 97
4

Check out the "phone book" example given here: What is a plain English explanation of "Big O" notation?

Remember that Big-O is all about scale: how much more operation will this algorithm require as the data set grows?

O(log n) generally means you can cut the dataset in half with each iteration (e.g. binary search)

O(n log n) means you're performing an O(log n) operation for each item in your dataset

I'm pretty sure 'O(n log log n)' doesn't make any sense. Or if it does, it simplifies down to O(n log n).

Community
  • 1
  • 1
keithjgrant
  • 12,421
  • 6
  • 54
  • 88
  • :D Im pretty the super short "generally means" snippet will be very useful for quick on the spot "analysis" about the n log log n... I havent really seen those algorithms, but I stumble across that order while in my search.. apparently han's and thorup's are an example of that http://en.wikipedia.org/wiki/Sorting_algorithm – Francisco Noriega Apr 13 '10 at 00:34
  • 3
    `O(n log log n)` algorithms do exist. For example: http://portal.acm.org/citation.cfm?id=975984 – David Johnstone Apr 13 '10 at 04:33
  • O(n log log n) algorithms are usually simplified down to O(n), as log log n is so incredible small - for example, log log (2^64) = 6. – Niki Yoshiuchi Apr 13 '10 at 15:11
  • 3
    @Niki While the speed hit for the the log log n term is tiny, it's incorrect to describe a O(n log log n) algorithm as O(n). That's like saying that 3 equals 4 for sufficiently large values of 3. – Daniel Stutzbach Apr 13 '10 at 18:03
  • 1
    Yes, I don't mean they are equal, just that O(loglogn) can, in most cases, be treated like O(1). Going from N=2^32 to N=2^64 increases loglogn from 5 to 6. To get to 7 you have to increase N to 2^128. That kind of growth is so slow that it looks flat. For the purposes of academic research and general knowledge you are of course right, but in application the loglogn factor is usually ignored. – Niki Yoshiuchi Apr 13 '10 at 18:42
3

I'll attempt to do an intuitive analysis of why Mergesort is n log n and if you can give me an example of an n log log n algorithm, I can work through it as well.

Mergesort is a sorting example that works through splitting a list of elements repeatedly until only elements exists and then merging these lists together. The primary operation in each of these merges is comparison and each merge requires at most n comparisons where n is the length of the two lists combined. From this you can derive the recurrence and easily solve it, but we'll avoid that method.

Instead consider how Mergesort is going to behave, we're going to take a list and split it, then take those halves and split it again, until we have n partitions of length 1. I hope that it's easy to see that this recursion will only go log (n) deep until we have split the list up into our n partitions.

Now that we have that each of these n partitions will need to be merged, then once those are merged the next level will need to be merged, until we have a list of length n again. Refer to wikipedia's graphic for a simple example of this process http://en.wikipedia.org/wiki/File:Merge_sort_algorithm_diagram.svg.

Now consider the amount of time that this process will take, we're going to have log (n) levels and at each level we will have to merge all of the lists. As it turns out each level will take n time to merge, because we'll be merging a total of n elements each time. Then you can fairly easily see that it will take n log (n) time to sort an array with mergesort if you take the comparison operation to be the most important operation.

If anything is unclear or I skipped somewhere please let me know and I can try to be more verbose.

Edit Second Explanation:

Let me think if I can explain this better.

The problem is broken into a bunch of smaller lists and then the smaller lists are sorted and merged until you return to the original list which is now sorted.

When you break up the problems you have several different levels of size first you'll have two lists of size: n/2, n/2 then at the next level you'll have four lists of size: n/4, n/4, n/4, n/4 at the next level you'll have n/8, n/8 ,n/8 ,n/8, n/8, n/8 ,n/8 ,n/8 this continues until n/2^k is equal to 1 (each subdivision is the length divided by a power of 2, not all lengths will be divisible by four so it won't be quite this pretty). This is repeated division by two and can continue at most log_2(n) times, because 2^(log_2(n) )=n, so any more division by 2 would yield a list of size zero.

Now the important thing to note is that at every level we have n elements so for each level the merge will take n time, because merge is a linear operation. If there are log(n) levels of the recursion then we will perform this linear operation log(n) times, therefore our running time will be n log(n).

Sorry if that isn't helpful either.

JSchlather
  • 1,564
  • 2
  • 13
  • 22
  • Thanks, I like your explanation since it gives me ways to do this for other types of algorithms.. however I in this part: Then you can fairly easily see that it will take n log (n) time to sort an array with mergesort if you take the comparison operation to be the most important operation. it wasnt so easy to see for me.. but it complemented with others people answer of (when you have to apply log(n) operation to each of you n elements)... that is how you "easily saw"that it becomes n*logn? – Francisco Noriega Apr 13 '10 at 00:45
0

When applying a divide-and-conquer algorithm where you partition the problem into sub-problems until it is so simple that it is trivial, if the partitioning goes well, the size of each sub-problem is n/2 or thereabout. This is often the origin of the log(n) that crops up in big-O complexity: O(log(n)) is the number of recursive calls needed when partitioning goes well.

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • Yes, but divide and conquer algorithms are generally n log(n), because while you divide the problem into smaller and smaller bits there's generally an operation taking n time in the length of the partition that must be performed at each step. – JSchlather Apr 12 '10 at 23:49
  • @Liberalkid - I don't think that's right. The work done in each iteration generally has no relation to `n`, it's just a (more-or-less) constant amount of processing. Since constants are ignored in Big-O notation, a divide-and-conquer algorithm like a binary search is O(log n). – keithjgrant Apr 13 '10 at 00:19
  • Binary search isn't really divide and conquer, it's more what would be called decrease and conquer. Algorithms like Mergesort, Quicksort, FFT, etc are divide and conquer. It's not really divide and conquer unless you're breaking the problem down into smaller subproblems and solving those, then using those solutions to solve larger problems. – JSchlather Apr 13 '10 at 04:12
  • @Liberalkid, @keithjgrant: Some textbooks include a separate "decrease and conquer" category. Other textbooks group both kinds of algorithms together into the "divide and conquer" category. – Daniel Stutzbach Apr 13 '10 at 15:30