en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en ja ru ja en fr en en en en en pl uk en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en en de en en en en en en en en en en fr
In this free email course, I'll teach you the right way of thinking for breaking down tricky algorithmic coding interview questions.
Get the coding interview crash course
7 days. One short-but-helpful email a day. Unsubscribe whenever.
No spam, ever. Easy unsubscribe.
You're in! Head over to your email inbox right now to read day one!
The idea behind big O notation
Big O notation is the language we use for talking about how long an algorithm takes to run. It's how we compare the efficiency of different approaches to a problem.
It's like math except it's an awesome, not-boring kind of math where you get to wave your hands through the details and just focus on what's basically happening.
With big O notation we express the runtime in terms of—brace yourself—how quickly it grows relative to the input, as the input gets arbitrarily large.
how quickly the runtime grows—It's hard to pin down the exact runtime of an algorithm. It depends on the speed of the processor, what else the computer is running, etc. So instead of talking about the runtime directly, we use big O notation to talk about how quickly the runtime grows.
relative to the input—If we were measuring our runtime directly, we could express our speed in seconds. Since we're measuring how quickly our runtime grows, we need to express our speed in terms of...something else. With Big O notation, we use the size of the input, which we call "n." So we can say things like the runtime grows "on the order of the size of the input" () or "on the order of the square of the size of the input" ().
as the input gets arbitrarily large—Our algorithm may have steps that seem expensive when n is small but are eclipsed eventually by other steps as n gets huge. For big O analysis, we care most about the stuff that grows fastest as the input grows, because everything else is quickly eclipsed as n gets very large. (If you know what an asymptote is, you might see why "big O analysis" is sometimes called "asymptotic analysis.")
If this seems abstract so far, that's because it is. Let's look at some examples.
This function runs in time (or "constant time") relative to its input. The input list could be 1 item or 1,000 items, but this function would still just require one "step."
def print_all_items(items): for item in items: print item
This function runs in time (or "linear time"), where n is the number of items in the list. If the list has 10 items, we have to print 10 times. If it has 1,000 items, we have to print 1,000 times.
def print_all_possible_ordered_pairs(items): for first_item in items: for second_item in items: print first_item, second_item
Here we're nesting two loops. If our list has n items, our outer loop runs n times and our inner loop runs n times for each iteration of the outer loop, giving us n^2 total prints. Thus this function runs in time (or "quadratic time"). If the list has 10 items, we have to print 100 times. If it has 1,000 items, we have to print 1,000,000 times.
N could be the actual input, or the size of the input
Both of these functions have runtime, even though one takes an integer as its input and the other takes a list:
def say_hi_n_times(n): for time in xrange(n): print "hi" def print_all_items(items): for item in items: print item
So sometimes n is an actual number that's an input to our function, and other times n is the number of items in an input list (or an input map, or an input object, etc.).
This is why big O notation rules. When you're calculating the big O complexity of something, you just throw out the constants. So like:
def print_all_items_twice(items): for item in items: print item # Once more, with feeling for item in items: print item
This is , which we just call .
def print_first_item_then_first_half_then_say_hi_100_times(items): print items[0] middle_index = len(items) / 2 index = 0 while index < middle_index: print items[index] index += 1 for time in xrange(100): print "hi"
This is , which we just call .
Why can we get away with this? Remember, for big O notation we're looking at what happens as n gets arbitrarily large. As n gets really big, adding 100 or dividing by 2 has a decreasingly significant effect.
Keep up the momentum! Sign up to get a data structures and algorithms practice question sent to you every week.
No spam, ever. Easy unsubscribe.
Cool, watch your inbox!
Drop the less significant terms
def print_all_numbers_then_all_pair_sums(numbers): print "these are the numbers:" for number in numbers: print number print "and these are their sums:" for first_number in numbers: for second_number in numbers: print first_number + second_number
Here our runtime is , which we just call . Even if it was , it would still be .
Again, we can get away with this because the less significant terms quickly become, well, less significant as n gets big.
We're usually talking about the "worst case"
Often this "worst case" stipulation is implied. But sometimes you can impress your interviewer by saying it explicitly.
Sometimes the worst case runtime is significantly worse than the best case runtime:
def contains(haystack, needle): # Does the haystack contain the needle? for item in haystack: if item == needle: return True return False
Here we might have 100 items in our haystack, but the first item might be the needle, in which case we would return in just 1 iteration of our loop.
In general we'd say this is runtime and the "worst case" part would be implied. But to be more specific we could say this is worst case and best case runtime. For some algorithms we can also make rigorous statements about the "average case" runtime.
Space complexity: the final frontier
Sometimes we want to optimize for using less memory instead of (or in addition to) using less time. Talking about memory cost (or "space complexity") is very similar to talking about time cost. We simply look at the total size (relative to the size of the input) of any new variables we're allocating.
This function takes space (we use a fixed number of variables):
def say_hi_n_times(n): for time in xrange(n): print "hi"
This function takes space (the size of hi_list scales with the size of the input):
def list_of_hi_n_times(n): hi_list = [] for time in xrange(n): hi_list.append("hi") return hi_list
Usually when we talk about space complexity, we're talking about additional space, so we don't include space taken up by the inputs. For example, this function takes constant space even though the input has n items:
def get_largest_item(items): largest = float('-inf') for item in items: if item > largest: largest = item return largest
Sometimes there's a tradeoff between saving time and saving space, so you have to decide which one you're optimizing for.
Big O analysis is awesome except when it's not
You should make a habit of thinking about the time and space complexity of algorithms as you design them. Before long this'll become second nature, allowing you to see optimizations and potential performance issues right away.
Asymptotic analysis is a powerful tool, but wield it wisely.
Big O ignores constants, but sometimes the constants matter. If we have a script that takes 5 hours to run, an optimization that divides the runtime by 5 might not affect big O, but it still saves you 4 hours of waiting.
Beware of premature optimization. Sometimes optimizing time or space negatively impacts readability or coding time. For a young startup it might be more important to write code that's easy to ship quickly or easy to understand later, even if this means it's less time and space efficient than it could be.
But that doesn't mean startups don't care about big O analysis. A great engineer (startup or otherwise) knows how to strike the right balance between runtime, space, implementation time, maintainability, and readability.
You should develop the skill to see time and space optimizations, as well as the wisdom to judge if those optimizations are worthwhile.
Don't miss part 2: logarithms, amortized analysis, and more.
Sign up for updates. Free practice problems every week!
No spam, ever. Easy unsubscribe.
You're in! Now what?
For coding interview prep, there's really no substitute for practice.
Check out our mock coding interview questions. They mimic a real interview by offering hints when you're stuck or you're missing an optimization.
Want more coding interview help?
Check out interviewcake.com for more advice, guides, and practice questions.
Subscribe to our weekly question email list »
Programming interview questions by company:
Programming interview questions by topic:
Testing and QA interview questions
Bit manipulation interview questions
Copyright © 2022 Cake Labs, Inc. All rights reserved.
228 Park Ave S #82632, New York, NY US 10003 (862) 294-2956
About | Privacy | Terms
With just a couple clicks.
| GitHub Auth | Google Auth | Facebook Auth
We'll never post on your wall or message your friends.
Where do I enter my password?
Actually, we don't support password-based login. Never have. Just the OAuth methods above. Why?
It's easy and quick. No "reset password" flow. No password to forget.
It lets us avoid storing passwords that hackers could access and use to try to log into our users' email or bank accounts.
It makes it harder for one person to share a paid Interview Cake account with multiple people.
“I got my dream job at Google! Thank you for this service. Your 4-layer answers were very instructive, it was well worth the price. — Yohan