Table of Contents
Chapter 2
2.1: Computational Tractability
The goal of this course is to write correct and efficient algorithms and this sections goes through how we define efficiency.
Intial attempt at defining efficiancy: “An algorithm is efficient if when, when implemented, it runs quickly on real input instances” What works:
- Good initial intro to what we want. Need to get things done quickly!
What doesn't work:
- Too vague: need where and how well
- Will it be fast on more than just small cases?
- What are real input instances?
Worst-Case Running Times and Brute-Force Search: “An algorith is efficient if it achieves qualitatviely better worst-case performance, at an analytical level, than brute-force search” Why it works:
- brute-force search is just going through each option so imporving is needed.
- shows intrinsic structure and computational tractability about the problem
Why it doesn't works:
- qualitatively better performance is too vague
Polynomial Time as a Definition of Efficiency “An algorithm is efficient if it has a polynomial running time.” Why it works:
- very specific
- makes it easier to prove there is no efficient algorithm
Why it doesn't work
- Too specific
2.2 Asymptotic Order of Growth
This section discuss how to measure the order of an algorithm and looks at how to find a lower bound, an upper bound, and a tight bound. Our goal is to express the growth rate of running times of algorithms.
Upper bounds:
- If T(n) is ⇐ c*f(n) for some c > 0 then T is asymptotically bounded above by f.
- We will denote an upper bound as O. There can be multiple upper bounds.
Lower bound:
- IF T(n) is >=ξ*f(n) for some ξ>0 then T is asymptotically bounded below by f.
- we will denote this as Ω
Tight bounds:
- If T(n) is both O(f(n)) and Ω(f(n)) then we've found a asymptically tight bound
- T is bound tightly by f(n)
- our tight bound is denoted as Θ
- another way to find Θ is through the limit!
Properties of Asymptotic Growth Rates
1. Transitivity: * If f = O(g) and g = O(h) then f = O(h) * If f = Ω(g) and g = Ω(h) then f = Ω(h) * If f = Θ(g) and g = Θ(h) then f = Θ(h) 2. Sum of Functions * Suppose that f and g are two functions such that for some other function h, we have f=O(h) and g = O(h) then f+g=O(h) * The same can be said for a finite number of functions * g = O(f) then f+g = O(f)
Asymptotic bounds for the some common fuctions
- polynomials: f is o degree d then f = O(n^d)
- logs: log_b(n) = O(n^x) where x > 0 and b > 1
- Exponentials: f(n)=r^n where r > 1 and d > 0 → O(r^n) = n^d
Helpful to have the bounds for common functions, but I am wondering how will we find them in the future?
2.3: Implementing the Stable Matching Algorithm Using Lists and Arrays
Moving from general discussion of GS algorithm to actual implementation. First need to figure out what Data Structures to use.
Arrays:
- Arrays have length n and A[i] is the ith element in the list.
- Give direct access to ith elements - there for O(1) time to get value A[i]
- check if e is in A, (e = A[i] for some i), then it will take O(n) time
- if elements are sorted check for e in O(logn) using binary search.
- “An array is less good for dynamically mainting a list of elements that changes over time, such as a l ist of free men”
Lists:
- can delete or add
- should use a linked list
- possible option is doubly linked list:
- deletion: constant time
- insertion: constant time
- ith access: O(i) time. No direct access.
We will need to use both in the stable matching alogorithm
- identify a free man - use a linked list
- for a man m identify his highest ranked woman who he has not proposed to - array
- for a woman w, decide if w is currently engaged and who her current partner is - use an array.
- for a woman w and two men m and m' we need to be able to check in constant time which W prefers m or m'. - inialize with an n x n array of preferences then we can use constant time access through out algorithm
This gives O(n^2) running time.
Getting into this stuff is more difficult for me. I don't remember running times as much and I try to think of it logically and actually go through the process, but it doesn't always work. Any step by step process would be helpful, but I am guessing it's something you get better at with time.
2.4: A Survey of Common Running Times
This section was easier to read since we just spent so much time on it and I felt pretty comfortable with it.
Linear time
- O(n)
- takes a constant times n amount of time
- computing the maximum - have to go through all n objects
- Merging Two Sorted Lists - basically have to check the min in each list against the other min. pop off the smaller one and add it to a new list.
- The book has more info on why this is O(n) look it over if confused: p. 50
O(n*logn) time
- very common
- Mergesort algorithm the set of input #s into two equal-sized pieces than solves each half recursively.
- common because most expensive step is sorting.
Quadratic time:
- O(n^2)
- trying to find all the pairs
- two nested loops
Cubic time
- O(n^3)
- trying to find all three tuples
- “elaborate sets of nested loops”
O(n^k)
- similar to quadratic and cubic times
Beyond polynomial time
- find maximum independent set in graph of k nodes
- talked about this in class
- n! is huge and usually away to find smaller bounds
Sublinear time
- Binary search = logn
- O(logn) arises when we are doing constant amount of work in order to throw away a fraction of the input.
This all makes sense to me, I think the biggest struggle will be when I need to design an alogirthm with a certain running time, but these are the notes I should look at!
2.5: A More Complex Data Structure Priority Queues
Priority Queue:
- Designed for applications in which elements have a priority value, or key, and each time we need to select an element from S, we want to take the one with highest priority.
- A priority queue is a data structure that maintains a set of elements S, where each element v in S has an associated value key(v) that denotes the priority of element v; smaller keys represent higher priorities.
Use a heap to implement a priority queue
- gets us the O(log n)
- For every element v, at a node i, the element w at i's parents satisfies key(w) ⇐ key(v)
- leftchild(i) = 2i, rightchild(i) = 2i+1
- Heapify up and heapify down - allows for log n
Use these operations:
- StartHeap(N)
- Insert(H,V)
- FindMin(H)
- Delete(H,i)
- ExtractMin(H)
I think heaps are soooo fun! I enjoyed this chapter because it layed out the code in a pretty readabe way.