-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathComplexity_of_Python_Operations
More file actions
390 lines (299 loc) · 17.8 KB
/
Copy pathComplexity_of_Python_Operations
File metadata and controls
390 lines (299 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
Complexity of Python Operations
In this lecture we will learn the complexity classes of various operations on
Python data types. Then we wil learn how to combine these complexity classes to
compute the complexity class of all the code in a function, and therefore the
complexity class of the function. This is called "static" analysis, because we
do not need to run any code to perform it (contrasted with Dynamir or Emperical
Analysis, when we do run code).
------------------------------------------------------------------------------
Python Complexity Classes
In ICS-46 we will write low-level implementations of all of Python's data types
and see/understand WHY these complexity classes apply. For now we just need to
try to absorb (not memorize) this information, with some -but minimal-
justification.
Binding a value to any name is O(1). Simple operators on integers (whose values
are small: e.g., under 12 digits) like + or == are also O(1).
In all these examples, N = len(data-type). The operations are organized by
increasing complexity
Lists:
Complexity
Operation | Example | Class | Notes
--------------+--------------+---------------+-------------------------------
Index | l[i] | O(1) |
Store | l[i] = 0 | O(1) |
Length | len(l) | O(1) |
Append | l.append(5) | O(1) |
Pop | l.pop() | O(1) | same as l.pop(-1), popping at end
Clear | l.clear() | O(1) | similar to l = []
Slice | l[a:b] | O(b-a) | l[1:5]:O(l)/l[:]:O(len(l)-0)=O(N)
Extend | l.extend(...)| O(len(...)) | depends only on len of extension
Construction | list(...) | O(len(...)) | depends on length of ...
check ==, != | l1 == l2 | O(N) |
Insert | l[a:b] = ... | O(N) |
Delete | del l[i] | O(N) |
Remove | l.remove(...)| O(N) |
Containment | x in/not in l| O(N) | searches list
Copy | l.copy() | O(N) | Same as l[:] which is O(N)
Pop | l.pop(i) | O(N) | O(N-i): l.pop(0):O(N)
Extreme value | min(l)/max(l)| O(N) |
Reverse | l.reverse() | O(N) |
Iteration | for v in l: | O(N) |
Sort | l.sort() | O(N Log N) | key/reverse doesn't change this
Multiply | k*l | O(k N) | 5*l is O(N): len(l)*l is O(N**2)
Tuples support all operations that do not mutate the data structure (and with
the same complexity classes).
Sets:
Complexity
Operation | Example | Class | Notes
--------------+--------------+---------------+-------------------------------
Length | len(s) | O(1) |
Add | s.add(5) | O(1) |
Containment | x in/not in s| O(1) | compare to list/tuple - O(N)
Remove | s.remove(5) | O(1) | compare to list/tuple - O(N)
Discard | s.discard(5) | O(1) |
Pop | s.pop(i) | O(1) | compare to list - O(N)
Clear | s.clear() | O(1) | similar to s = set()
Construction | set(...) | O(len(...)) | depends on length of ...
check ==, != | s != t | O((len(s)) | same as len(t): if different, False
<=/< | s <= t | O(len(s)) | issubset
>=/> | s >= t | O(len(t)) | issuperset s <= t == t >= s
Union | s | t | O(len(s)+len(t))
Intersection | s & t | O(len(s)+len(t))
Difference | s - t | O(len(s)+len(t))
Symmetric Diff| s ^ t | O(len(s)+len(t))
Iteration | for v in s: | O(N) |
Copy | s.copy() | O(N) |
Sets have many more operations that are O(1) compared with lists and tuples.
Not needing to keep values in a specific order (which lists/tuples require)
allows for faster operations.
Frozen sets support all operations that do not mutate the data structure (and
with the same complexity classes).
Dictionaries: dict and defaultdict
Complexity
Operation | Example | Class | Notes
--------------+--------------+---------------+-------------------------------
Index | d[k] | O(1) |
Store | d[k] = v | O(1) |
Length | len(d) | O(1) |
Delete | del d[k] | O(1) |
get/setdefault| d.method | O(1) |
Pop | d.pop(k) | O(1) |
Pop item | d.popitem() | O(1) |
Clear | d.clear() | O(1) | similar to s = {} or = dict()
Views | d.keys() | O(1) |
Construction | dict(...) | O(len(...)) | depends # (key,value) 2-tuples
Iteration | for k in d: | O(N) | all forms: keys, values, items
So, most dict operations are O(1).
defaultdicts support all operations that dicts support, with the same
complexity classes (because it inherits all the operations); this assumes that
calling the constructor when a values isn't found in the defaultdict is O(1) -
which is true for int(), list(), set(), ... (the things commonly used)
Note that for i in range(...) is O(len(...)); so for i in range(1,10) is O(1).
If len(alist) is N, then
for i in range(len(alist)):
is O(N) because it loops N times. Of course even
for i in range (len(alist)//2):
is O(N) because it loops N/2 times, and dropping the constant 1/2 makes
it O(N): the work doubles when the list length doubles.
Finally, when comparing two lists for equality, the complexity class above
shows as O(N), but in reality we would need to multiply this complexity by
O(==) where O(==) is the complexity class for checking whether two values in
the list are ==. If they are ints, O(==) would be O(1); if they are strings,
O(==) in the worst case it would be O(len(string)). This issue applies any
time an == check is done.
------------------------------------------------------------------------------
Composing Complexity Classes: Sequential and Nested Statements
In this section we will learn how to combine complexity class information about
simple operations into complexity information about complex operations
(composed from simple operations). The goal is to be able to analyze all the
statements in a functon/method to determine the complexity class of executing
the function/method.
------------------------------------------------------------------------------
Law of Addition for big-O notation
O(f(n)) + O(g(n)) is O( f(n) + g(n) )
That is, we when adding complexity classes we bring the two complexity classes
inside the O(...). Ultimately, O( f(n) + g(n) ) results in the bigger of the two
complexity class (because we drop the lower added term). So,
O(N) + O(Log N) = O(N + Log N) = O(N)
because N is the faster growing function.
This rule helps us understand how to compute the complexity of doing some
SEQUENCE of operations: executing a statement that is O(f(n)) followed by
executing a statement that is O(g(n)). Executing both statements SEQUENTAILLY
is O(f(n)) + O(g(n)) which is O( f(n) + g(n) ) by the rule above.
For example, if some function call f(...) is O(N) and another function call
g(...) is O(N Log N), then doing the sequence
f(...)
g(...)
is O(N) + O(N Log N) = O(N + N Log N) = O(N Log N). Of course, executing the
sequence (calling f twice)
f(...)
f(...)
is O(N) + O(N) which is O(N + N) which is O(2N) which is O(N).
Note that for an if statment like
if test: assume complexity of test is O(T)
block 1 assume complexity of block 1 is O(B1)
else:
block 2 assume complexity of block 2 is O(B2)
The complexity class for the if is O(T) + max(O(B1),O(B2)). The test is always
evaluated, and one of the blocks is always executed. In the worst case, the if
will execute the block with the largest complexity. So, given
if test: complexity is O(N)
block 1 complexity is O(N**2)
else:
block 2 complexity is O(N)
The complexity class for the if is O(N) + max (O(N**2),O(N))) = O(N) + O(N**2) =
O(N + N**2) = O(N**2). If the test had complexity class O(N**3), then the
complexity class for the if is O(N**3) + max (O(N**2),O(N))) =
O(N**3) + O(N**2) = O(N**3 + N**2) = O(N**3)
------------------------------------------------------------------------------
Law of Multiplcation for big-O notation
O(f(n)) * O(g(n)) is O( f(n) * g(n) )
If we repeat an O(f(N)) process O(N) times, the resulting complexity is
O(N)*O(f(N)) = O( Nf(N) ). An example of this is, if some function call f(...)
is O(N**2), then executing that call N times (in the following loop)
for i in range(N):
f(...)
is O(N)*O(N**2) = O(N*N**2) = O(N**3)
This rule helps us understand how to compute the complexity of doing some
statement INSIDE A BLOCK controlled by a statement that is REPEATING it. We
multiply the complexity class of the number of repetitions by the complexity
class of the statement(s) being repeated.
Compound statements can be analyzed by composing the complexity classes of
their constituent statements. For sequential statements the complexity classes
are added; for statements repeated in a loop the complexity classes are
multiplied.
Let's use the data and tools discussed above to analyze (determine their
complexity classes) three different functions that each compute the same
result: whether or not a list contains only unique values (no duplicates). We
will assume in all three examples that len(alist) is N.
1) Algorithm 1: A list is unique if each value in the list does not occur in any
later indexes: alist[i+1:] is a list containing all values after the one at
index i.
def is_unique1 (alist : [int]) -> bool:
for i in range(len(alist)): O(N)
if alist[i] in alist[i+1:]: O(N) - index+add+slice+in: O(1)+O(1)+O(N)+O(N) = O(N)
return False O(1) - never executed in worst case
return True O(1)
The complexity class for executing the entire function is O(N) * O(N) + O(1)
= O(N**2). So we know from the previous lecture that if we double the length of
alist, this function takes 4 times as long to execute.
Note that in the worst case, we never return False and keep executing the loop.
Also, in the worst case the list slice is aliset[1:] is O(N-1) = O(N).
2) Algorithm 2: A list is unique if when we sort its values, no ADJACENT values
are equal. If there were duplicate values, sorting the list would put these
duplicate values right next to each other (adjacent). Here we copy the list so
as to not mutate (change the order of the parameter's list) by sorting it:
copying the list does not increase the complexity class of the method.
def is_unique2 (alist : [int]) -> bool:
copy = list(alist) O(N)
copy.sort() O(N Log N) - for Python sorting
for i in range(len(alist)-1): O(N) - really N-1, but that is O(N)
if copy[i] == copy[i+1]: O(1): +, 2 [i],and == ints: all O(1)
return False O(1) - never executed in worst case
return True O(1)
The complexity class for executing the entire function is given by the sum
O(N) + O(N Log N) + O(N)*O(1) + O(1) = O(N + N Log N + O(N*1) + 1) =
O(N + N Log N + N + 1) = O(N Log N + 2N + 1) = O(N Log N). So the
complexity class for this algorithm/function is lower than the first algorithm,
the is_unique1 function. For large N unque2 will eventually be faster.
Notice that the complexity class for sorting is dominant in this code: it does
most of the work. If we double the length of alist, this function takes a bit
more than twice the amount of time. In N Log N: N doubles and Log N gets a tiny
bit bigger (i.e., Log 2N = 1 + Log N; e.g., Log 2000 = 1 + Log 1000 = 11, so
compared to 100 Log 1000, 2000 Log 2000 got 2.2 times bigger, or 10% bigger
than just doubling).
Looked at another way if T(N) = c*(N Log N), then T(2N) = c*(2N Log 2N) =
c*2N Log N + c*2N = 2*T(N) + c*2N.
3) Algorithm 3: A list is unique if when we turn it into a set, its length is
unchanged: if duplicate values were added to the set, its length would be
smaller than the length of the list by exactly the number of duplicates in the
list added to the set.
def is_unique3 (alist : [int]) -> bool:
aset = set(alist) O(N)
return len(aset) == len(alist) O(1): 2 len (each O(1)) and == ints O(1)
The complexity class for executing the entire function is O(N) + O(1) =
O(N + 1) = O(N). So the complexity class for this algortihm/function is lower
than both the first and second algorithms/functions. If we double the length of
alist, this function takes just twice the amount of time. We could write the
body of this function more simply as: return len(set(alist)) == len(alist),
where evaluating set(alist) takes O(N) and then computing the two len's and
comparing them for equality are all O(1).
So the bottom line here is that there might be many algorithms/functions to
solve some problem. If they are smalle, we can analyze them statically (looking
at th code, not running it) to determine their complexity classes. For large
problem sizes, the algorithm/function with the smallest complexity class would
be best. For small problem sizes, complexity classes don't determine which is
best, but we could run the functions (dynamic analysis, aka emperical analysis)
to test which is fastest.
------------------------------------------------------------------------------
Using a Class (implementable 3 ways) Example:
We will now look at the solution of a few problems (combining operations on a
priority queue: pq) and how the complexity class of the result is affected by
three different classes/implementations of priority queues.
In a priority queue, we can add values and remove values to the data structure.
A correctly working priority queue always removes the maximum value remaining in
the priority queue. Think of a line/queue outside of a Hollywood nightclub,
such that whenever space opens up inside, the most famous person in line gets
to go in (the "highest priority" person), no matter how long less famous people
have been standing in line (contrast this with first come/first serve, which is
a regular -non priority- queue).
For the problems below, all we need to know is the complexity class of the
"add" and "remove" operations.
add remove
+-------------+-------------+
Implementation 1 | O(1) | O(N) |
+-------------+-------------+
Implementation 2 | O(N) | O(1) |
+-------------+-------------+
Implementation 3 | O(Log N) | O(Log N) |
+-------------+-------------+
Implementation 1 adds the new value into the pq by appending the value at the
rear of a list or the front of a linked list: both are O(1); it removes the
highest priority value by scanning through the list or linked list to find the
highest value, which is O(N), and then removing that value, also O(N) in the
worst case (removing at the front of a list; at the rear of a linked list).
Implementation 2 adds the new value into the pq by scanning the list or linked
list for the right spot to put it and putting it there, which is O(N). Lists
store their highest priority at the rear (linked lists at the front); it
removes the highest priority value from the rear for lists (or the front for
linked lists), which is O(1).
Implementation 3, which is discussed in ICS-46, uses a binary heap tree (not a
binary search tree) to implement both operations with "middle" complexity
O(Log N): this complexity class greater than O(1) but less than O(N).
Problem 1: Suppose we wanted to use the priority queue to sort N values: we
add N values in the pq and then remove all N values (first the highest, next
the second highest, ...). Here is the complexity of these combined operations
for each implementation.
Implementation 1: O(N)*O(1) + O(N)*O(N) = O(N) + O(N**2) = O(N**2)
Implementation 2: O(N)*O(N) + O(N)*O(1) = O(N**2) + O(N) = O(N**2)
Implementation 3: O(N)*O(Log N) + O(N)*O(Log N) = O(NLogN) + O(NLogN) = O(NLogN)
Here, Implementation 3 has the lowest complexity class for the combined
operations. Implementations 1 and 2 each do one operation quickly but the other
slowly: both are done O(N) times. The slowest operation determines the
complexity class, and both are equally slow. The complexity class O(Log N) is
between O(1) and O(N); surprisingly, it is actually "closer" to O(1) than O(N),
even though it does grow -because it grows so slowly; yes, O(1) doesn't grow at
all, but O(Log N) grows very slowly: the known Universe has about 10**90
particles of matter, and Log 10**90 = Log (10**3)**30 = 300, which isn't very
big compared to 10**90.
Problem 2: Suppose we wanted to use the priority queue to find the 10 biggest
(of N) values: we would enqueue N values and then dequeue 10 values. Here is
the complexity of these combined operations for each implementation..
Implementation 1: O(N)*O(1) + O(10)*O(N) = O(N) + O(N) = O(N)
Implementation 2: O(N)*O(N) + O(10)*O(1) = O(N**2) + O(1) = O(N**2)
Implementation 3: O(N)*O(Log N) + O(10)*O(Log N) = O(NLogN) + O(LogN) = O(NLogN)
Here, Implementation 1 has the lowest complexity for the combined operations.
That makes sense, as the operation done O(N) times (add) is very simple (add to
the end of a list/the front of a linked list is O(1)) and the operation done a
constant number of times (10, independent of N) is the expensive operation
(remove, which is O(N)). It even beats the complexity of Implementation 3. So,
as N gets bigger, implementation 1 will eventually become faster than the other
two for the "find the 10 biggest" task.
So, the bottom line here is that sometimes there is NOT a "best all the time"
implementation for a data structure. We need to know what problem we are
solving (the complexity classes of all the operations in various
implementations and the number of times we must do these operations) to choose
the most efficient implementation for solving the problem.
------------------------------------------------------------------------------
Problems:
TBA