Algorithms I, Princeton University, P1
For me, great algorithms are the poetry of computation. — Donald Knuth
Presented by Robert Sedgwick & Kevin Wayne,普林斯顿大名鼎鼎的算法课程,在 Coursera 上接近满分的评价:这么多 buff 加身的神课,不认真刷一下都对不起我自己。
我想通过学习这门课得到什么?
- 想将之前参加 OI 掌握的算法知识系统化;
- 采用 Java 进行授课,想通过这门课入门这个著名的纯 OOP 语言;
- 这门课有 10 个超高质量的 Projects,实际问题背景,丰富的测试样例,竟然还配有教授工业级别高效实现的源码!即使抄下来做板子都是十分宝贵的收获。
关于笔记的组织形式,曾经想了很多种方案,但最终都否决了。本课的大纲遵循一种很自然的,难以分类的顺序;如果强制割裂开来,会显得很 inconsistent。于是最终还是采用了最简单的时间顺序记录;除了 P4,它记录的是这门课中包含的 Java 语言知识。
另,本课程 projects 的代码实现存放在 ThisisXXZ/Algorithms-Princeton - GitHub 中,仅供参考。
这是 Algorithms I & II, Princeton 系列笔记的 P1 部分。这一节将介绍:
- 并查集。
- 算法复杂度分析的基本方法与 notations。
- 栈与队列——链表与动态数组实现。
- \(\sim N^2\) 的基础排序——选择排序,插入排序,希尔排序 (最优可达 \(\sim N\log{N}\))。
This article is a self-administered course note.
It will NOT cover any exam or assignment related content.
Union-Find
把并查集作为课程中第一个介绍的算法实在是独具匠心。
我现在还记得第一次学到并查集时的惊喜:它像是一篇精彩的短文,读完让人大呼过瘾。其实现是那么的精巧而简洁,以至于在之后在学习 BST,LCT 等复杂晦涩的算法时,我时常会怀念这种 Eureka 的感觉。
Problem Model: Dynamic Connectivity
Given a set of \(N\) objects.
- Union command: connect two objects.
- Find/connected query: is there a path connecting the two objects?
Primitive method: Given a unique \(id\) for each connected component.
- Quick-Find: \(O(n)\) updates the \(id\) as an array. \(O(1)\) query by checking \(id\).
- Quick-Union: \(O(1)\) updates the \(id\) as a forest. \(O(n)\) query by checking the \(id\) of root.
Improvements
Improvement 1: Weighting
- Modify Quick-Union to avoid tall trees.
- Keep track of size of each tree (number of objects).
- Balance by linking the root of smaller tree to the root of larger tree. (Reasonable alternatives: union by height or "rank")
Improvement 2: path compression
Just after computing the root of \(p\), set the \(id\) of each examined node to point of that root.
Code Implementation
Weighted quick-union with path compression: in practice, WQUPC is linear.
1 | public class UF { |
Analysis of Algorithms
学到这里才发现之前用 Big-Oh notation \(O(n)\) 描述时间复杂度是一个常见的误区......!
Mathematical Models
The total running time of an algorithm is the sum of cost \(\times\) frequency for all operations. In principle, we could always develop an accurate mathematical models for a particular algorithm.
However, taking exactly all operations into consideration seems too tedious.
Simplification 1: cost model
When a proper cost model is chosen, other operations could be ignored.
Simplification 2: tilde notation
We use tilde notation \(\sim\) to estimate running time (or memory) as a function of input size \(N\). It will ignore all lower-order terms.
- When \(N\) is large, lower-order terms are negligible.
- When \(N\) is small, we simply don't care.
Technically, \(f(N)\sim g(N)\) means \(\lim_{N\to\infty} \frac{f(N)}{g(N)}=1\).
Order-of-Growth Classifications
Here are the common order-of-growth classifications:
- constant \(1\): statement
- logarithmic \(\log N\): divide in half
- linear \(N\): loop
- linearithmic \(N\log N\): divide and conquer
- quadratic \(N^2\): double loops
- cubic \(N^3\): triple loops
- exponential \(2^N\): exhaustive search
Typically, better order of growth \(\to\) faster in practice.
Commonly Used Notations
notation | provides | used to |
---|---|---|
Tilde \(\sim\) | leading term | provide approximate model |
Big Theta \(\Theta(\cdot)\) | asymptotic growth rate | classify algorithms |
Big Oh \(O(\cdot)\) | \(\Theta(\cdot)\) and smaller | develop upper bounds |
Big Omega \(\Omega(\cdot)\) | \(\Theta(\cdot)\) and larger | develop lower bounds |
Example: 1-SUM problem \(=\) "Is there a 0 in the array?"
Upper bound. A specific algorithm.
Brute-force algorithm for 1-SUM: Look at every array entry. Running time of the optimal algorithm for 1-SUM is \(O(N)\).
Lower bound. Proof that no algorithm can do better.
Have to examine all \(N\) entries (any unexamined one might be \(0\)). Running time of the optimal algorithm for 1-SUM is \(\Omega(N)\).
If for a specific problem, upper bound \(O(N)=\) lower bound \(\Omega(N)\), we say there exists an optimal algorithm with \(\Theta(N)\) running time.
Algorithm design aims to reduce the gap between upper bound \(O(N)\) and lower bound \(\Omega(N)\).
Stack & Queue
No need to give a detailed introduction on these two famous data structures. We will focus on how to implement them in two different approaches - linked lists or resizing arrays.
- Approach 1. linked lists. (ez)
- Approach 2. resizing arrays.
Resizing-Array Implementation
Consider an array-based stack. The implementation is trivial; however, we need to know the size \(N\) of the input data to avoid stack overflow.
Therefore, we introduce the resizing arrays to this problem: the resizing arrays grow and shrink dynamically according to the current data size.
- How to grow array? (operation
push()
) If array is full, create a new array of twice (repeated doubling) the size, and copy items. - How to shrink array? (operation
pop()
) If array is one-quarter full, create a new array of half the size, and copy items.
It is proven that the amortized running time of a single push/pop is constant. (in particular, \(3\))
Linked Lists Versus Resizing Arrays
implementation/cost | time | space |
---|---|---|
Linked Lists | Every operation takes constant time in the worst case. | Use extra space to deal with the links. |
Resizing Arrays | Every operation takes constant amortized time. | Less wasted space. |
Code Implementation
Here is a resizing-array implementation for a queue.
1 | public class ResizingArrayQueue<Item> implements Iterable<Item> { |
Elementary Sorts
获益良多,对 \(\sim N^2\)
的基础排序算法 (插排,希尔排序) 有了新的认识。之前一直觉得有快排就够了
(N
方的排序算法是什么臭鱼烂虾),学完之后才发现这样的想法是多么傲慢。这些算法里蕴含的排序思想,和无数计算机科学家
(包括教授 Sedgewick) 尝试改进优化所做的努力,都是难能可贵的。
排序算法在 Java 中的实现离不开 Comparable
接口。有关这部分的内容 (包括下文所用到的 less
与
exch
辅助方法) 将在 P4 中进行记录。
Selection Sort
In iteration \(i\), find index \(min\) of smallest remaining entry. Then swap \(a_i\) and \(a_{min}\).
1 | public static void sort(Comparable[] a) { |
Knuth's shuffle is structurally similar to selection sort. It is used to generate random shuffles.
Insertion Sort
In iteration \(i\), swap \(a_i\) with each larger entry to its left.
1 | public static void sort(Comparable[] a) { |
Insertion sort is NOT intensive to input.
- Best case. ascending arrays: it makes \(N-1\) compares and \(0\) exchanges.
- Worst case. descending arrays: it makes \(\sim \frac{1}{2}N^2\) compares and \(\sim \frac{1}{2}N^2\) exchanges.
Note that for partially-sorted arrays, insertion sort runs in linear time.
This is because number of exchanges equals the number of inversions, and numbers of compares \(=\) exchanges \(+(N-1)\).
Shellsort
Idea. Move entries more than one position at a time by \(h\)-sorting the array.
We use insertion sort with stride length \(h\) to \(h\)-sort an array. why choosing insertion sort:
- Big increments \(\to\) small subarray.
- Small increments \(\to\) nearly in order. (insertion sort is fast when array is partially sorted)
Note the fact that a \(g\)-sorted array remains \(g\)-sorted after \(h\)-sorting it.
An efficient shellsort scheme depends on a proper increment sequence.
- \(3x+1\). (most commonly used) \(1,4,13,40,121,364,...\)
- Sedgewick. (tough to beat in empirical studies) \(1,5,19,41,109,...\)
However, accurate model for shellsort has NOT yet been discovered. Maybe some better increment sequences are still left for discover......!
1 | public class Shell { |
Also note that though shellsort uses insertion sort to \(h\)-sort the array, it is NOT stable.
Insertion sort and merge sort are stable; Selection sort and shellsort are not.
Reference
This article is a self-administered course note.
References in the article are from corresponding course materials if not specified.
Course info:
Algorithms, Part 1, Princeton University. Lecturer: Robert Sedgewick & Kevin Wayne.
Course textbook:
Algorithms (4th edition), Sedgewick, R. & Wayne, K., (2011), Addison-Wesley Professional, view