Complexity of Python Operations


SUBMITTED BY: smart4two

DATE: Jan. 1, 2018, 12:08 p.m.

FORMAT: Text only

SIZE: 19.8 kB

HITS: 198

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

comments powered by Disqus