Uploaded by marie knowlton

Time and Space Complexity

advertisement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Mutable and Immutable Objects
- Mutable Objects - Objects that are mutable can have their value changed after they are created.
- Objects of built-in types like (list, set, dict, user defined classes) are mutable. Example:
my_list = [1, 2, 3]
print(my_list)
- Solution output: [1, 2, 3]
``````````````````````````````````````````````````````````````````````````````````````
# you can change the integer in the list like this
my_list[1] = 7
Print(my_list)
- Solution output: [1, 7, 3]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Immutable Objects
- An Object is immutable if its value cannot be changed after it is created.
- Objects of built-in types like (int, float, bool, str, tuple, unicode, decimals, ranges) are immutable. Example:
my_tuple = (1, 2, 3)
print(my_tuple)
- Solution output: (1, 2, 3)
``````````````````````````````````````````````````````````````````````````````````````
# you will run into an error because the tuple object doesn't support item assignment
- this means we cannot change the value itself
my_tuple[1] = 7
print(my_tuple)
- Solution output: returns an error tuple object doesn't support item assignment
``````````````````````````````````````````````````````````````````````````````````````
- Even though we cannot change the value itself, we can still access it in memory
- every single object in python has a place in memory and you can actually determine where
that place of memory is using using the function called id()
- id()
tells the identity of an object in Python
``````````````````````````````````````````````````````````````````````````````````````
# so if you print id of my_tuple
print(id(my_tuple))
- Solution output: 140657764146688
- this is the address in which it stores in memory
- every single object in python has this
- Solution output: 139693624819008
``````````````````````````````````````````````````````````````````````````````````````
# so if you print id of my_list
print(id(my_list))
- Solution output: 1396936244884488
``````````````````````````````````````````````````````````````````````````````````````
- strings are immutable
my_string = "Hello"
Print(my_string)
- Solution output: returns an error str object doesn't support item assignment
- Strings are not mutable in Python
``````````````````````````````````````````````````````````````````````````````````````
- if we want to change the secound letter in Hello
my_string[1] = "y"
Print(my_string)
- Solution output: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Compare the time complexity of different approaches to a problem using Big O notation
- What is Big O notation?
- Big-O notation is a metrics used to find algorithm complexity.
- Big-O notation signifies the relationship between the input to the algorithm and the steps required to execute the algorithm.
Good resource for Big O notation:
https://stackabuse.com/big-o-notation-and-algorithm-analysis-with-python-examples/#:~:text=Big%2DO%20notation%20is%20a%20metrics%20used%20to%20find%20algorithm,by%20opening%20and%20closing%20parenthesis.
``````````````````````````````````````````````````````````````````````````````````````
- What is an Algorithm?
- Set of instructions that executes and solves a problem
- Any code you write is an algorithm
- If you have two sets of algorithms that solve the exact same problem ->
- How do we know which one's the better of the two
- In computer science we usually look at two things
- time
- is the algorithm fast
- space
- does the algorithm don't take up too much computer memory
``````````````````````````````````````````````````````````````````````````````````````
- Time -> how do we measure how fast an algorithm is
- you may thing timing it will give us the solution
- however, every computer runs at a different speed so that isn't the best practice
- The best way to measure the time complexity of an algorithm or how how fast an algorithm is
- is to use Big O notation
Big O notation -> measures how fast an algorithm grows based on how the input itself grows
- if an input grows to a million and the algorithom also grows to a million
- this is called Linear runtime
``````````````````````````````````````````````````````````````````````````````````````
—Linear Time:
Linear Time Complexity describes an algorithm or program who’s complexity will grow in direct proportion to the size of the input data. As a rule of thumb, it is best to try and keep your functions running below or within this range of time-complexity, but obviously it won’t always be possible.
``````````````````````````````````````````````````````````````````````````````````````
- Another resource explaining Big O notation and Linear Runtime https://medium.com/@ariel.salem1989/an-easy-to-use-guide-to-big-o-time-complexity-5dcf4be8a444
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Common Big O Run Times
`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
| Classification:|Description: |
`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
|||
| - Constant O(1) | The runtime is entirely unaffected by the input size. This is the ideal solution.|
|||
| - Logarithmic O(log n)| As the input size increases, the runtime will grow slightly slower. This is a pretty good solution.|
|||
| - Linear O(n)|As the input size increases, the runtime will grow at the same rate. This is a pretty good solution.|
|||
| - Polynomial O(n^c)|As the input size increases, the runtime will grow at a faster rate. This might work for small inputs |
||but is not a scalable solution.|
|||
| - Exponential O(c^n)|As the input size increases, the runtime will grow at a much faster rate. This solution is inefficient.|
|||
| - Factorial O(n!)|As the input size increases, the runtime will grow astronomically, even with relatively small inputs. |
||This solution is exceptionally inefficient.|
|||
`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
| | |
`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
In our warmup notes checkout the graph showing the curves of these different runtimes
- LowerCase n is the size of the data
- UpperCase N represents the number of operations
***Note: Big O only matters for large data sets. An O(n^3) solution is adequate, as long as you can guarantee that your datasets will always be small.****
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Examples of a few different examples of Python functions that print something to output
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Constant Time
def print_one_item(items):
print(items[0])# O(1)
- print_one_item -> How fast is it
- Items is a list and we are going to print "items" to "0"
- How many operation will it take if "items" the length of two -> we are only doing one operation because items is "0"
- What if "items" was size one million when we had a list that was a million "items" -> again, we are only doing one operation because items is "0"
- No matter how big "items" is --> we are just going to do one operation. So it is constant no matter how big items is.
- If it is a constant runtime we would go ahead and call that "O(1)"
``````````````````````````````````````````````````````````````````````````````````````
- Linear Time
def print_every_item(items):
for item in items:
print(item)# O(N)
- We are taking in the list of values called "items"
- For every single value we are going to print it
- What if "items" is the lenght of "10" --> We would print "10" values
- If "item" was the length of a million --> We would print "1,000,000" values
- This would grow respectively with the size of the input
- This would be called Linear runtime --> because it grows in the same fashion - With Linear runtime we would call it "O(N)"
`````````````````````````````````````````````````````````````````````````````````````` - Quadratic Time
def print_pairs(items):
for item_one in items:
for item_two in items:
print(item_one, item_two)
- print_pairs -> We have a list of values called "items"
- For every "item" -> We are going to go through "items" -> print both of those "items"
- We know that with onefor loop it takes "O(N)" -> - But looks for every single "n" -> we are going to have to do that "n" times
- So we will have to do "N" * "N"
- Calls "O(N*N)" -> "N" * "N" = N^2
- This is a Quadratic runtime and we would call it "O(N^2)
`````````````````````````````````````````````````````````````````````````````````````` - What about Constants?
def do_a_bunch_of_stuff(items): # O(1 + n/2 + 2000)
last_idx = len(items) - 1
print(items[last_idx]) # O(1)
middle_idx = len(items) / 2
idx = 0
while idx < middle_idx: # O(n/2)
print(items[idx])
idx = idx + 1
for num in range(2000): # O(2000)
print(num) # O(N)
beg * - This function does a bunch of stuff "do_a_bunch_of_stuff"
1st * - This first part "last_idx" takes constant time "O(1)"
- We are going to print just the "items" of the last index -> we said that this takes a constant runtime
- Then looking at the middle_idx we are going to print every value
- middle_idx is going to take half the length of my list -> since we are going to the middle of the list
and printing it 2nd *- the second part "middle_idx" takes "O(N/2)"
- then we will go ahead and go from the range(2000) print
3rd *- and the third part "range(2000)" takes "O(2000)"
- So what is the overall runtime of this algorithm?
- We will need to base it on the size of the input -> As the input grows -> What matters?
- As the input grows up to 2000 -> this will take the longest part
- However, we need to think about infinite growth
- Once N is at "1,000.000" -> parts "first" and "third" will not really matter that much "O(1)" & "O(2000)"
- We only care about the middle section - part "second" - Where it is "O(n/2)"
- We would actually say that this algorithm runs in "O(N/2)"
- But we don't care actually about constants whe doing "Big O Notation"
- We would need to get rid of these constants and say "O(N)"
`````````````````````````````````````````````````````````````````````````````````````` - Most Significant Term
def do_different_things(items): # O(n + n^2)
for item in items: # O(n)
print(item)
for item_one in items: # O(n * n) = O(n^2)
for item_two in items:
print(item_one, item_two)
- This does do_differnt_things
1st * - We are going to pring every "item" -> where it takes "O(N)" linear runtime
2nd *- Then we will do the double for loop and that takes "O(N^2)"
- We know the first part takes "N"
- And the second part takes "N^2"
- So whenever we are analyzing the runtime of an algorithm, we always just take the larger value
because that is going to grow much faster
- So we would say "O(N^2)" because this is bigger than "O(N)"
`````````````````````````````````````````````````````````````````````````````````````` - Big O represents the worse-case
def search_for_thing(items, thing):
for item in items:
if item == thing:
return True
return False
- For "item" and "items" -> if "item" = "thing" return "True"
- If "thing" is the first value in this list of "items" -> we are gonna be done after the first for loop
that is jsut constant runtime
- But we should always think about the worst case scenario
- If "thing" is at the end of the "items" list
- then we don't have what exactly -> we have to go through every single value in "items" so it would be "O(N)"
- `````````````````````````````````````````````````````````````````````````````````````` - Logarithmic runtime
def foo(n):
i = 1
while i < n
print(i)
i *= 2
foo(16)
answer is: 1
2
4
8
foo(32)
answer is: 1
2
4
8
16
foo(64)
answer is: 1
2
4
8
16
32
- foo function
- "i" equals "1", while "i" is less then "n"
- print "i" and then "i" is itself -> multiplied by "2" - Call foo of 16
- Call foo of 32
- we doubled the size of "N" but only increased our operation by "1"
- Call foo of 32
- every time we double our input, we are only increasing our input by "1"
- We call this Logarithmic runtime
- So this would be "O(log N)"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Compare the space complexity of different approached to a problem using "Big O Notation"
- The second aspect of determining how good an algorithm is, is space complexity
- Space Complexity:
- Efficiency of memory usage instead of the number of operations
- The space complexity is basically the amount of memory space required to solve a problem in relation to the input size.
- Rather than focusing on number of operations -> we are focusing on the efficiency of memory usage
`````````````````````````````````````````````````````````````````````````````````````` def print_lambda_n_times(n):
for i in range(n):
print("lambda") # O(1)
- print_lambda_n_times
- "n" is the input - Then for "i" range "n"
- print lambda
- printing does not take up any memory whatsoever -> because we are printing to the terminal
and we are not storing anything. - This will take "O(1)" in terms of space complexity. - Because we are not storing anything, this is "O(1)"
- No matter how big "n" is we are going to get where we always take "O(1)" -> again this is just for space complexity
`````````````````````````````````````````````````````````````````````````````````````` def append_to_list_n_times(n):
my_list = []
for _ in range(n):
my_list.append("lambda")
return my_list
- append_to_list_n_times -> we are taking some value called "n"
- Then we set an empty list called my_list
- Then append lambda "n" times
- By appending we are storing a value in a list
- It looks like if "n" is "100", we are going to store "lambda" "100" times
- And if "n" is "1,000,000", we are going to store "lambda" "1,000,000" times - So this would take "O(N)" in terms of space complexity
`````````````````````````````````````````````````````````````````````````````````````` def get_the_max(items_list):
maximum = float("-inf")
for item in items_list:
if item > maximum:
maximum = item
return maximum# O(1)
- function called get_the_max -> we are going to go through all the "items_list"
- see if it compares to our maximum
- And if it does, we are going to go ahead and return the maximum
- "items_list" -> we are going to be storing that value
- "items_list" can be any sort of input -> such as "1,000,000" or "10,000,000"
- But we are not storing any additional values besides the maximum
- So no matter how large the list is, the space complexity will remain the same
- because we are not storing anything additional
- so no matter if "items_list" is a hundred or hundred million -> we are only storing the maximum
- Therefore, the space complexity is "O(1)"
- The time complexity is "O(N)" because the number of operations will increase as the list increases
- But the actual space complexity will remain constant because we are not adding anything additional to the memory
`````````````````````````````````````````````````````````````````````````````````````` *** Remember Space Complexity is no determined on the size of the input itself, but rather on what additional memory is being used. ****
Download