-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathcs240.tex
2757 lines (2465 loc) · 170 KB
/
cs240.tex
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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[]{article}
\usepackage{etex}
\usepackage[margin = 1.5in]{geometry}
\setlength{\parindent}{0in}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{amsthm}
\usepackage{mathtools}
\usepackage{pgfplots}
\usepackage[lined]{algorithm2e}
\usepackage{qtree}
\usepackage{xytree}
\usepackage{float}
\usepackage[T1]{fontenc}
\usepackage{ae,aecompl}
\usepackage[pdftex,
pdfauthor={Chris Thomson},
pdftitle={CS 240: Data Structures and Data Management},
pdfsubject={Lecture notes from CS 240 at the University of Waterloo},
pdfkeywords={CS 240, course notes, notes, Waterloo, University of Waterloo, algorithms, data structures},
pdfproducer={LaTeX},
pdfcreator={pdflatex}]{hyperref}
\usepackage{cleveref}
\theoremstyle{definition}
\newtheorem*{defn}{Definition}
\newtheorem{ex}{Example}[section]
\newtheorem*{theorem}{Theorem}
\setlength{\marginparwidth}{1.5in}
\setlength{\algomargin}{0.75em}
\DeclarePairedDelimiter{\set}{\lbrace}{\rbrace}
\definecolor{darkish-blue}{RGB}{25,103,185}
\usepackage{hyperref}
\hypersetup{
colorlinks,
citecolor=darkish-blue,
filecolor=darkish-blue,
linkcolor=darkish-blue,
urlcolor=darkish-blue
}
\newcommand{\lecture}[1]{\marginpar{{\footnotesize $\leftarrow$ \underline{#1}}}}
\makeatletter
\def\blfootnote{\gdef\@thefnmark{}\@footnotetext}
\makeatother
\begin{document}
\let\ref\Cref
\title{\bf{CS 240: Data Structures and Data Management}}
\date{Winter 2013, University of Waterloo \\ \center Notes written from Alejandro L\'opez-Ortiz's lectures.}
\author{Chris Thomson}
\blfootnote{I'd love to hear your feedback. Feel free to email me at \href{mailto:[email protected]}{[email protected]}.}
\blfootnote{See \href{http://cthomson.ca/notes}{cthomson.ca/notes} for updates.
\ifdefined\sha % Also, \commitDateTime should be defined.
Last modified: \commitDateTime{} ({\href{https://github.com/christhomson/lecture-notes/commit/\sha}{\sha}}).
\fi
}
\maketitle
\newpage
\tableofcontents
\newpage
\section{Introduction \& Code Optimization} \lecture{January 8, 2013}
\subsection{Course Structure}
The grading scheme is 50\% final, 25\% midterm, and 25\% assignments. There are five assignments, usually due on Wednesday mornings at 9:30 am. There are several textbooks for the course, all of which are optional but recommended. The textbooks cover roughly 80\% of the course content. There are also \href{https://www.student.cs.uwaterloo.ca/~cs240/w13/lectures.phtml}{some course notes available online} from previous terms, however the lectures will not necessarily follow those notes strictly.
\\ \\
See the \href{https://www.student.cs.uwaterloo.ca/~cs240/w13/info.phtml}{course syllabus} for more information.
\subsection{CS as the Science of Information}
So far in our undergraduate careers, computer science has meant programming. However, programming is only a subset of computer science. Computer science is the \textbf{science of information}.
\\ \\
What do we want to do with information? We want to:
\begin{itemize}
\item \textbf{Process it}. Programs $\equiv$ algorithms.
\item \textbf{Store it}. We want to encode it. This also leads to information theory. Storing information involves data structures, databases, (file) systems, etc., all of which are searchable in some way.
\item \textbf{Transmit it}. We want to transmit information over networks. This process involves coding theory.
\item \textbf{Search it}. First, we have to structure it with data structures and/or SQL databases. Information retrieval is the process of searching for textual information instead of numerical information.
\item \textbf{Mine it}. This involves artificial intelligence and machine learning.
\item \textbf{Display it}. Information needs to be displayed using computer graphics (CG) and user interfaces (UI, partially related to psychology).
\item \textbf{Secure it}. Encryption and cryptography are important. Information should also be stored redundantly to prevent harm from catastrophic events.
\end{itemize}
\subsection{Objectives of the Course}
\begin{itemize}
\begin{item}
\textbf{Study efficient methods of storing, accessing, and performing operations on \underline{large} collections of data.} \\ \\
``Large" is subjective -- your data needs to be large enough to justify the additional mental complexity. \\ \\
Typical operations:
\begin{itemize}
\item Insert new item.
\item ``Deleting" data (flagging data as ``deleted," not actually deleting the data).
\item Searching for data.
\item Sorting the data. \\
\end{itemize}
Examples of ``large data":
\begin{itemize}
\item The web.
\item Facebook data.
\item DNA data.
\item LHC (Large Hadron Collider) measurements (terabytes per day).
\item All the music from iTunes, for finding duplicate songs, etc.
\end{itemize}
\end{item}
\begin{item}
\textbf{There is a strong emphasis on mathematical analysis.} \\ \\
The performance of algorithms will be analyzed using order notation.
\end{item}
\begin{item}
\textbf{The course will involve abstract data types (objects) and data structures.} \\ \\
We will see examples that have to do with:
\begin{itemize}
\item Databases.
\item Geographic databases.
\item Text searching.
\item Text compression.
\item Web searching.
\end{itemize}
\end{item}
\end{itemize}
\subsection{Code Optimization \& Measuring Performance}
Richard Feynman was in charge of the computer group on the Manhattan Project. He made his code run 10x faster.
\\ \\
The initial method to measure performance was to use a \textbf{wall clock}. Initial studies looked like this:
\begin{center}
\begin{tabular}{|c|c|c|}
\hline
Data size & A & B \\ \hline
3 & 1 & 3 \\
5 & 2 & 9 \\
10 & 4 & 10 \\
20 & 16 & 11 \\ \hline
\end{tabular}
\end{center}
However, computers were getting better, so we couldn't use wall clocks anymore. Results were not robust enough due to the quick progression of performance increases in terms of computing power. Moreover, because of the differences in architecture, the same program may have a different execution time on two different computer models.
\\ \\
\underline{Idea}: rather than comparing algorithms using seconds, we should compare algorithms using the number of operations required for the algorithm to run.
\begin{itemize}
\item Express algorithm using pseudocode.
\item Count the number of primitive operations.
\item Plot the number of operations vs. the input size. \\
\end{itemize}
\begin{figure}
\centering
\begin{tikzpicture}[domain=0:10]
\begin{axis}[
xlabel = Input size (n),
ylabel = Number of operations,
legend entries = {A,B},
legend pos = south east]
\addplot{5*x};
\addplot{0.5*x+35};
\end{axis}
\end{tikzpicture}
\caption{A comparison of two algorithms $A$ and $B$.}
\end{figure}
Note that $A$ and $B$ are plotted as continuous functions, however they are not actually continuous -- we join all of the points for readability purposes only.
\\ \\
In the long run, you may want to use algorithm $B$ even if algorithm $A$ is better in some cases, because the benefits of $A$ will be short lived as $n$ grows.
\\ \\
Hence, comparing the programs has been transformed into comparing functions. We use order notation to measure the long-term growth of functions, allowing us to choose the smaller function, which corresponds to the faster algorithm.
\section{Order Notation} \lecture{January 10, 2013}
The time for algorithm $A$ on input of size $n$ is:
\begin{align*}
\underbrace{2n \log n}_\text{sort} + \underbrace{1.5n^3}_\text{mult} + \underbrace{22n^2 - 3n}_\text{additions} + \underbrace{7}_\text{setup}
\end{align*}
On a different machine, the same algorithm $A$ may be $2n \log n + 9n^3 + 10n^2 - 3n + 7$. These give a \textbf{false sense of precision}. These specifics can also be quite costly to determine. In the 1960s, Don Knuth proposed \textbf{order notation} as a way to analyze the general quality of algorithms in an accurate, cost- and time-efficient way by ignoring precise runtimes.
\subsection{Formal Definitions}
Order notation represents algorithms as functions. We say a function $f(n)$ relates to a certain order $g(n)$ using different notation depending on which relation we're interested in.
\begin{center}
\begin{tabular}{|c|c|}
\hline
Relation & Functions \\ \hline
$3 \le 7$ & $f(n) = O(g(n))$ \\
$8 \ge 7$ & $f(n) = \Omega(g(n))$ \\
$7 = 7$ & $f(n) = \Theta(g(n))$ \\
$3 < 7$ & $f(n) = o(g(n))$ \\
$7 > 3$ & $f(n) = \omega(g(n))$ \\ \hline
\end{tabular}
\end{center}
Here's an easy way to remember the correspondence between a relation and its order notation symbol: the greedy operators, $\le$ and $\ge$, are uppercase letters $O$ and $\Omega$. The less greedy operators, $<$ and $>$, are the same letters but in lowercase ($o$ and $\omega$). \\
Order notation only cares about the long run. An algorithm can violate the relation early -- we're interested in its asymptotic behavior.
\begin{defn}[$\le$]
$f(n) = O(g(n))$ if there exist constants $c > 0, n_0 > 0$ such that $f(n) \le c \cdot g(n)$ for all $n \ge n_0$.
\end{defn}
\begin{defn}[$\ge$]
$f(n) = \Omega(g(n))$ if there exist constants $c > 0, n_0 > 0$ such that $f(n) \ge c \cdot g(n)$ for all $n \ge n_0$.
\end{defn}
\begin{defn}[$=$]
$f(n) = \Theta(g(n))$ if there exists constants $c_1, c_2 > 0$ such that $c_1 \cdot g(n) \le f(n) \le c_2 \cdot g(n)$.
\end{defn}
The equality relation is effectively sandwiching the algorithm $f(n)$ between two other functions (the squeeze theorem). If it's possible to sandwich the algorithm between two multiples of the same $g(n)$, then $f(x)$ is said to be equal to the order $g(n)$.
\begin{defn}[$<$]
$f(n) = o(g(n))$ if for all $c > 0$ there exists constant $n_0 > 0$ such that $f(n) < c \cdot g(n)$ for all $n \ge n_0$.
\end{defn}
\begin{defn}[$>$]
$f(n) = \omega(g(n))$ if for all $c > 0$ there exists constant $n_0 > 0$ such that $f(n) > c \cdot g(n)$ for all $n \ge n_0$.
\end{defn}
\subsection{Order Notation Examples}
\begin{ex}
$2n^2 + 3n + 11 = O(n^2)$
\\ \\
We need to show that there exists constants $c > 0$ and $n_0 > 0$ such that $2n^2 + 3n + 11 \le cn^2$ for all $n \ge n_0$.
\\ \\
Let $c = 3$. Simplifying gets us $3n + 11 \le n^2$. This holds for $n_0 = 1000$, for instance.
\end{ex}
\begin{ex}
$2n^2 + 3n + 11 1 = \Theta(n^2)$
\\ \\
In order to show equality ($\Theta$), we need to show both the $\le$ and $\ge$ cases. In the previous example, the $\le$ case was shown. For the $\ge$ case, we need to show $n^2 \le c \cdot (2n^2 + 3n + 11)$.
\\ \\
Let $c = 1$. Simplifying gets us $n^2 \le 2n^2 + 3n + 11$, which gives us $0 \le n^2 + 3n + 11$. This holds for $n_0 = 0$.
\end{ex}
\begin{ex}
$2010n^2 + 1388 = o(n^3)$
\\ \\
We must show that for all $c > 0$, there exists $n_0 > 0$ such that $2010n^2 + 1388 \le c \cdot n^3$ for all $n \ge n_0$.
\begin{align*}
\frac{2010n^2+1388}{n^3} \stackrel{?}{\le} \frac{c \cdot n^3}{n^3} \implies \frac{2010}{n} + \frac{1388}{n^3} \stackrel{?}{\le} c
\end{align*}
There is a \textbf{trick to prove f(n) = o(g(n))}: show that $\lim_{n \to \infty}{}\frac{f(n)}{g(n)} = 0$.
\end{ex}
\subsection{A note about the use of =}
\begin{align*}
\underbrace{2n^2}_\text{specific function} \in \underbrace{O(n^2))}_\text{set of functions}
\end{align*}
The use of the equality operator ($=$) in order notation is not the same as in most other areas of mathematics, since a single element of a set cannot strictly be equal to the set itself (even $a \ne \set{a}$). The $=$ in order notation is not a true equality -- it's just notation. The $\in$ operator is semantically more correct.
\subsection{Performance of Algorithms}
We have made some progression in how we determine the performance of algorithms.
\begin{itemize}
\item Comparing single runs.
\item Comparing measured curves (multiple runs).
\item Produce analytical expressions for the curves.
\item Simplify using order notation.
\end{itemize}
Let's say we have the problem of integrating a mathematical function. Our program numerically integrates the function, and we're trying to analyze the algorithm behind that process. The timing would vary depending on the mathematical function and preciseness required.
\\ \\
If we conduct multiple runs of our algorithm, we'll get different times. Plotting all of the results (time vs. input size) will result in data that is not a function, because the data fails the vertical line test, since for a given input size we have multiple times.
\\ \\
We need to decide on a convention for these cases where we have multiple time values. In this course, we'll usually look at the worst case, however in some situations examining the best or average cases could be useful.
\\ \\
Finding the average case is hard. To produce an appropriate average, we need an input distribution.
\section{Formalism} \lecture{January 15, 2013}
\begin{defn}
A \textbf{problem} is a collection of questions and (correct) answer pairs.
\end{defn}
\begin{ex}
Informally, the problem is ``multiplying two numbers.'' More formally, the problem could be represented as: (2 x 3, 6), (3 x 4, 12), (1 x 5, 5), etc.
\end{ex}
\begin{defn}
An \textbf{instance} of a problem is a specific question and answer pair, (Q, A).
\end{defn}
\begin{defn}
The \textbf{input} of a problem is the question. Example: 3 x 4.
\end{defn}
\begin{defn}
The \textbf{output} is the only correct answer. Note: there is only one correct answer under this definition. ``Multiple'' correct answers can always be reduced to some canonical form that represents the one correct answer.
\end{defn}
\begin{defn}
An \textbf{algorithm} is a mechanical method to produce the answer to a given question of a problem.
\end{defn}
\begin{defn}
The \textbf{size of the input} is the number of bits, characters, or elements in the question. This definition will vary depending on what's most appropriate for the given question.
\end{defn}
Long division in elementary school was the first time a problem's complexity was directly related to the input size. That's not always the case, however.
\begin{defn}
We say an algorithm \textbf{solves} a problem if for every question $Q$ it produces the correct answer, $A$.
\end{defn}
\begin{defn}
A \textbf{program} is an implementation of an algorithm using a specified computer language.
\end{defn}
\begin{ex}
We want to sort $n$ numbers. One instance of this problem $Q$ is:
\begin{align*}
(\underbrace{(5, 1, 3, 2, 4)}_\text{Q}, \underbrace{(1, 2, 3, 4, 5)}_\text{A})
\end{align*}
For a problem in this form, we'll say the size of $Q$ is $|I| = 5$. Why? Counting the number of elements is the most logical definition in this case.
\end{ex}
This course emphasizes algorithms rather than programs. We're computer scientists, so we care about the algorithm and the speed/efficiency of that algorithm.
\\ \\
A problem $\Pi$ can have several correct algorithms that solve it. Our goal is to find efficient solutions to $\Pi$. How do we do that?
\begin{enumerate}
\item \textbf{Algorithm design}. Find a solution(s) to $\Pi$.
\item \textbf{Algorithm analysis}. Assess the correctness and efficiency of the algorithm.
\end{enumerate}
In this course, we're mostly interested in algorithm analysis. Algorithm design will be covered in more detail in CS 341.
\subsection{Timing Functions}
A timing function is a function $T_{\mathcal{A}}$ such that:
\begin{align*}
T_{\mathcal{A}}: \set{Q } \to \mathbb{R}^{+}
\end{align*}
where $\mathbb{R}^{+}$ is the set of positive real numbers.
\\ \\
$T_{\mathcal{A}}(Q)$ is the time taken by algorithm $\mathcal{A}$ to compute the answer to $Q$.
\\ \\
We also want to define a general timing function for a particular problem, regardless of algorithm:
\begin{align*}
T(n) &= max \set{T_{ \mathcal{A}}(Q) } \\
T_{avg}(n) &= avg \set{T_{\mathcal{A}}(Q) } \\
T_{min}(n) &= min \set{T_{\mathcal{A}}(Q) } \\
\vdots&
\end{align*}
If we had two solutions (algorithms) $T_A(n)$ and $T_B(n)$, we could use order notation to compare the quality of $A$ and $B$.
\\ \\
Note that some problem instances are easier than others to solve, even with the same input size. Most people find it easier to multiply $0 \times 7$ than $8 \times 7$, for instance, despite those two problems having the same input size.
\section{Analysis of Algorithms}
In order to analyze an algorithm, we count the number of \underline{basic} operations that the algorithm performs.
\begin{ex}
Let's say our algorithm is to compute $x^2 + 4x$ and assign it to a variable called \verb+result+. That involves seven basic operations:
\begin{enumerate}
\item Read $x$ from memory.
\item Compute $x \cdot x$.
\item Assign the result of $x \cdot x$ to a variable, \verb+pr1+.
\item Compute $4 \cdot x$.
\item Assign the result of $4 \cdot x$ to a variable, \verb+pr2+.
\item Compute \verb+pr1+ + \verb+pr2+.
\item Assign the result of \verb+pr1+ + \verb+pr2+ to a variable, \verb+result+.
\end{enumerate}
\end{ex}
The number of operations remains the same across machines, but the actual running time of an algorithm will differ from machine to machine (which one of the reasons why we use order notation).
\\ \\
We only count the number of \underline{basic} operations. There are various definitions of what a ``basic'' operation actually is (especially with regards to variable assignments), but these are some common operations that are considered to be basic:
\begin{itemize}
\item Add numbers of reasonable size (numbers that can be added primitively).
\item Multiply numbers of reasonable size (numbers that can multiplied primitively).
\item Access an index in an array.
\item Store a value in memory (variable assignments).
\item Read a value from memory.
\end{itemize}
Be careful. Some languages disguise complexity well. Just because the syntax is simple doesn't necessarily mean it's a basic operation, and doesn't guarantee that the operation runs in constant time.
\subsection{Techniques for Algorithm Analysis}
\begin{itemize}
\item Straight line programs (no loops). Simply tally up the basic operation count.
\item Loops (\verb+for+/\verb+while+). You need to add the number of operations for each pass on the body of the loop.
\end{itemize}
\begin{ex}
Analyze the following algorithm. \\
\begin{algorithm}[H]
\For{i = a to b}{
< straight line program > $T_L$
}
\end{algorithm}
This program will run with $\sum_{i = a}^{b} T_L(i)$ operations.
\end{ex}
Whenever you have a loop in your algorithm, you should expect to get a summation in your timing function.
\begin{ex}
Analyze the following algorithm. \\
\begin{algorithm}[H]
\For{x = 0 to 10}{
pr1 = x * x\;
pr2 = 4 * x\;
result = pr1 + pr2\;
print result\;
}
\end{algorithm}
This program will run with $\sum_{i = 0}^{10} 4 = 44$ operations (depending on your definition of ``basic'' operations).
\end{ex}
Operations like addition and multiplication are primitive for numbers of a reasonable size. Once numbers get large enough to exceed certain limits, we have to resort to some trickery to perform those operations, which adds additional complexity, making them no longer ``basic'' operations.
\\ \\
We can be a bit sloppy and use the worst case, then say the actual algorithm is $\le$ to what we calculated.
\begin{ex}
Analyze the following algorithm. Test 1 ($n$). \\
\begin{algorithm}[H]
sum = 0\;
\For{i = 1 to n}{
sum = sum + i
}
\Return sum
\end{algorithm}
This program will run with $1 + (\sum_{i = 1}^n 1) + 1 = 2 + n = \Theta(n)$ operations.
\end{ex}
\begin{ex}
Analyze the following algorithm. \\
\begin{algorithm}[H]
sum = 0\;
\For{i = 1 to n}{
\For{j = 1 to i}{
$sum = sum + (i - j)^2$\;
$sum = sum^2$\;
}
}
\Return sum
\end{algorithm}
This program will run with time:
\begin{align*}
& 1 + \sum_{i = 1}^{n} \bigg[ \sum_{j = 1}^{i} 4 \bigg] + 1 \\
&= 2 + \sum_{i = 1}^{n} 4i \\
&= 2 + 4 \sum_{i = 1}^{n} i \\
&= 2 + 4 \cdot \frac{n(n+1)}{2} \text{ (Gauss)} \\
&= 2 + 2n^2 + 2n \\
&= \Theta(n^2)
\end{align*}
We can be pessimistic, assume worst-case scenario, and say that it runs with time:
\begin{align*}
&2 + \sum_{i = 1}^{n} n \\
&= O(n^2)
\end{align*}
Note that $O(n^2)$ is a less precise result than $\Theta(n^2)$, but in some cases that's good enough.
\end{ex}
Order notation helps make algorithm analysis easier by allowing us to throw away any specific constants, as long as the order remains untouched. The entire analysis process happens within the context of order notation, so you can just start dropping constants immediately.
\\ \\
Keep in mind, it is possible to create a bad over-estimation. You have to be smart about it.
\begin{ex}
Analyze the following algorithm. Test 2 (A, n). \\
\begin{algorithm}[H]
max = 0\;
\For{i = 1 to n}{
\For{j = 1 to n}{
sum = 0\;
\For{k = i to j}{
sum = A[k] + sum\;
\If{sum > max}{
max = sum\;
}
}
}
}
\Return max
\end{algorithm}
This is the \textbf{maximum subsequence problem}. The input is an array of integers $A[1\dots n]$, and the output is the consecutive run with the largest sum.
\\ \\
Sample sequence:
\begin{tabular}{|c|c|c|c|c|c|c|}
\hline 2 & -4 & 1 & 3 & -2 & 8 & -1 \\ \hline
\end{tabular}
\\ \\
The running time of this program is $max \set{\sum_{i = 1}^{j} A[k] \big| 1 \le i \le j \le n}$.
\end{ex}
\begin{ex}
\lecture{January 17, 2013}
Analyze the following algorithm.
\begin{algorithm}
\For{i = 1 to n}{
\For($L_1[i]$){j = 1 to i}{
\For($L_2[i, j]$){k = 1 to j}{
x = x + 1
}
}
}
\end{algorithm}
\begin{align*}
\sum_{i = 1}^{n} L_1(i) &= \sum_{i = 1}^{n} \bigg[\sum_{j = 1}^{i} L_2(i, j)\bigg] \\
&= \sum_{i = 1}^{n} \bigg[ \sum_{j = 1}^{i} \bigg[ \sum_{k = 1}^{j} \Theta(1) \bigg] \bigg] \\
&= \sum_{i = 1}^{n} \sum_{j = 1}^{i} \sum_{k = 1}^{j} 1 \\
&= \sum_{i = 1}^{n} \sum_{j = 1}^{i} j \\
&= \sum_{i = 1}^{n} \frac{i(i+1)}{2} \\
&= \frac{1}{2} \sum_{i = 1}^{n}(i^2 + i) \\
&= \frac{1}{2} \bigg[ \sum_{i = 1}^{n} i^2 + \sum_{i = 1}^{n} i \bigg] \\
&= \frac{1}{2} \bigg[ \frac{n(n+1)(2n+1)}{6} + \frac{n(n+1)}{2} \bigg] \\
&= \Theta(n^3)
\end{align*}
Alternatively, we could use a lazier process to determine that this algorithm is $O(n^3)$, which is less precise than saying the algorithm is $\Theta(n^3)$. The lazy way is to say each of the nested loops will run in $\le n$ operations in the worst case, which can be multiplied out to $n^3$.
\end{ex}
\subsection{Math Review}
\subsubsection{Exponentiation}
Exponents have a number of important properties, including:
\begin{align*}
b^0 &= 1 \\
b^1 &= b \\
b^{1/2} &= b^{0.5} = \sqrt{b} \\
b^{-1} &= \frac{1}{b} \\
b^a \cdot b^c &= b^{a + c} \\
(b^a)^c &= b^{ac}
\end{align*}
\subsubsection{Logarithms}
\begin{defn}
$\log_b a = c$ if and only if $a = b^c$. If $b = 2$, we write $\lg a$.
\end{defn}
There are a number of log identities you should be aware of:
\begin{align}
\log_b (a \cdot c) &= \log_b a + \log_b c \\
\log \left( \frac{a}{c} \right) &= \log_b a - \log_b c \\
\log(a^c) &= c \cdot \log_b a \\
b^{\log_c a} &= a^{\log_c b} \\
\log_b a &= \frac{\log_c a}{\log_c b}
\end{align}
Identity (1) is useful because it allows you to go from multiplication to addition. You can avoid nasty multiplication operations by using this identity.
\\ \\
Identity (4) is the prof's favorite. If $b$ and $c$ are constants, $\log_b n = \Theta(\log_c n)$ \textendash{} that is, we don't care about the base of the logarithm in the context of order notation.
\subsubsection{Recursive Definitions}
\begin{defn}
The \textbf{factorial} of a number $n$ is represented by $n!$. Informally, $n! = 1 \cdot 2 \cdot 3 \cdot \ldots \cdot n$. More formally:
\begin{align*}
n! = \begin{cases}
1 & n = 0 \\
n \cdot (n - 1)! & n > 0
\end{cases}
\end{align*}
\end{defn}
\begin{defn}
The \textbf{fibonacci numbers} are defined by:
\begin{align*}
F_i = \begin{cases}
0 & i = 0 \\
1 & i = 1 \\
F_{i - 2} + F_{i - 1} & i > 1
\end{cases}
\end{align*}
\end{defn}
\subsubsection{Summations}
There are a few common summations you should be aware of:
\begin{align*}
\sum_{i = 1}^{n} i &= \frac{n(n+1)}{2} \\
\sum_{i = 1}^{n} i^2 &= \frac{n(n+1)(2n+1)}{6} \\
\sum_{i = 1}^{n} i^k &\approx \frac{n^{k + 1}}{k + 1} = \Theta(n^{k + 1})
\end{align*}
You should also be familiar with the \textbf{geometric series}:
\begin{align*}
\sum_{i = 0}^{n} a^i &= \frac{a^{n + 1} - 1}{a - 1} \\
\sum_{i = 0}^{\infty} a^i &= \frac{1}{1 - a} \text{ where } a < 1
\end{align*}
\subsection{Useful Properties of Order Notation}
\begin{enumerate}
\item $f(n) = \Theta(a \cdot f(n))$ for $a > 0$.
\item Transitivity. If $f(n) = O(g(n))$ and $g(n) = O(h(n))$, then $f(n) = O(h(n))$.
\item $[f(n) + g(n)] = \Theta(\max\set{f(n), g(n) })$
\item $a_0 + a_1 x^1 + a_2 x^2 + \ldots + a_n x^n = \Theta(x^n)$, where $a_i$ are constants, $x > 1$, and $a_n > 0$. This is just a common case of (3).
\item $n^k = O(a^n)$ for $a > 1$.
\item $\log_k n = o(n^b)$ for $k > 0$ and $b > 0$ (where $b \in \mathbb{R}$, not necessarily the base of the logarithm).
\end{enumerate}
You can use these properties in proofs \emph{unless} you're requested to write a proof from first principles.
\subsection{Orders We Aim For}
When we are designing an algorithm, we aim for:
\begin{enumerate}
\item Constant time: $\Theta(1)$
\item Logarithmic time: $\Theta(\lg n)$
\item Poly log: $\Theta((\lg n)^{k})$
\item Linear complexity: $\Theta(n)$
\item $n \log n$ complexity: $\Theta(n \lg n)$
\item Quadratic: $\Theta(n^2)$
\item Cubic: $\Theta(n^3)$
\item Polynomial: $\Theta(n^k)$
\item Exponential: $\Theta(b^n)$
\end{enumerate}
\subsection{Maximum Subsequence Problem}
Recall the maximum subsequence problem from earlier: \\
\begin{algorithm}[H]
max = $-\infty$\;
\For{i = 1 to n}{
\For{j = 1 to n}{
total = 0\;
\For{k = i to j}{
total = total + A[k]\;
\If{total > max}{
max = total\;
}
}
}
}
\Return max
\end{algorithm}
Also, recall our sample sequence $A$:
\begin{tabular}{|c|c|c|c|c|c|c|}
\hline 2 & -4 & 1 & 3 & -2 & 8 & -1 \\ \hline
\end{tabular}
\\ \\
Note that the longest run (the subsequence with the largest sum) in this sample sequence is from 1 to 8, which is a subsequence that sums to 10.
\\ \\
The maximum subsequence problem has many applications. It is used for DNA matching, for instance. There is a score for accuracy / closeness of the match. You get a score for various runs within a DNA sequence. If there is a long enough run(s) then you have probably identified the correct person. Google may also use the maximum subsequence problem as part of its algorithm for determining typos or other terms to match a particular search query (such as the word's plural form).
\\ \\
Our na\"ive solution is not good if $n$ is sufficiently large, such as $10,000$ or $1,000,000,000,000$. Notice that the innermost loop recalculates the sum unnecessarily every time $j$ is incremented. You really just need to add the new element to the previous result. That gives us this more efficient algorithm: \\
\begin{algorithm}[H]
max = $-\infty$\;
\For{i = 1 to n}{
total = 0\;
\For{j = i to n}{
total = total + A[j]\;
\If{total > max}{
max = total\;
}
}
}
\Return max
\end{algorithm}
This algorithm runs in $O(n^2)$ time. We could do better, but that's okay for now. Our code is now 10,000 times faster on an input of size 10,000.
\section{Recursive Analysis}
We are going to analyze \textbf{mergesort}. Recall: mergesort involves sorting the two halves separately, then merging the sorted items into a single list again. We merge items $(1 \ldots n/2)$ and $(n/2 + 1 \ldots n)$ separately, then merge the two lists.
\\ \\
The number of operations mergesort requires is:
\begin{align*}
T_{ms}(n) &= T_{ms}\left( \frac{n}{2} \right) + T_{ms}\left( \frac{n}{2} \right) + n \\
&= 2T_{ms}\left( \frac{n}{2} \right) + n
\\ \\
T_{ms}(1) &= 1 \text{ (base case)}
\end{align*}
The method you use here is \textbf{guess and check}. For example, for $n = 4$, we have:
\begin{align*}
T_{ms} (4) &= 2T_{ms}(2) + 4 \\
&= 2(2 T_{ms}(1) + 2) + 4 \\
&= 4T_{ms}(1) + 4 + 4 \\
&= 4 + 4 + 4
\\ \\
T_{ms}(8) &= 8 + 8 + 8 + 8
\\ \\
T_{ms}(n) &= n \cdot \lg n
\end{align*}
You can check this with induction. That is, assuming this holds for $n = 2$, show that it holds for any $n$. If $T_{ms}(n) = n \lg n$, then:
\begin{align*}
T_{ms}(n) &= 2 T_{ms}\left( \frac{n}{2} \right) + n \\
&= 2 \cdot \frac{n}{2} \cdot \lg \frac{n}{2} + n \\
&= n \cdot \lg \frac{n}{2} + n \\
&= n \cdot (\lg n - \lg 2) + n \text{ by log identity} \\
&= n(\lg n - 1) + n \\
&= n \lg n - n + n \\
&= n \lg n
\end{align*}
Therefore, mergesort has time $\Theta(n \lg n)$.
\section{Abstract Data Types}
\subsection{Stack}
A \textbf{stack} is a collection of items, which supports the operations \verb+push+ (insert an item), \verb+pop+ (remove most recently inserted item), \verb+peek+ (look at the last item), and \verb+isEmpty+. \verb+push+ and \verb+pop+ are the most commonly supported operations.
\subsubsection{Stack Implementations}
Common ways to implement a stack are:
\begin{itemize}
\item \textbf{Linked List}. You maintain a pointer to the top of the stack. When you \verb+push+ or \verb+pop+, you change your pointer to the next or previously item as appropriate. This method is more flexible, but pointers can be a pain.
\item \textbf{Array}. You keep track of where the last item is in the array. You may need to re-size your array, but at least you don't have to worry about pointers.
\end{itemize}
\subsection{Queue}
A \textbf{queue} is a data structure where you \verb+insert+ at the end and remove (\verb+dequeue+) from the front, just like the way a queue (a lineup) works in real life.
\\ \\
Implementations are similar to implementing a stack. You maintain pointers to the start and end of the array or linked list.
\subsection{Priority Queue} \lecture{January 22, 2013}
A priority queue is a collection of elements similar to a queue, except each element has a priority assigned to it and elements are \verb+pop+ped in order of priority (not in order of arrival).
\\ \\
A priority queue supports these operations:
\begin{itemize}
\item \verb+insert(x, p)+ \textendash{} inserts item $x$ with priority $p$.
\item \verb+deleteMin()+ \textendash{} deletes. Used when the queue is defined to be such that lower numbers indicate higher priority. This deletes the element with the lowest $p$.
\item \verb+deleteMax()+ \textendash{} deletes. Used when the queue is defined to be such that higher numbers indicate higher priority. This deletes the element with the highest $p$.
\item \verb+peek()+ \textendash{} view the top element without deleting it.
\end{itemize}
\subsubsection{Applications of Priority Queues}
\begin{itemize}
\item To-do lists in real life.
\item To-do lists in the operating system (used for multitasking; the processor can only really do one thing at a time, so it does a bit of each task as determined by priority).
\item Sorting. Insert the elements from an array $A$ and then when you delete them you'll get them back in sorted order.
\end{itemize}
Priority queues can be used for sorting purposes. It allows us to sort without worrying about the underlying sorting implementation. However, if we want to know how good of a sorting algorithm it is, we would need to examine the implementation of \verb+insert+ and \verb+deleteMin+. Here's some pseudocode for sorting with a priority queue: \\
\begin{algorithm}[H]
\For{i = 0 to n - 1}{
PQ.insert(A[i], A[i]); // key and priority are the same
}
\For{i = 0 to n - 1}{
A[i] = PQ.deleteMin();
}
\end{algorithm}
\subsubsection{Implementations of Priority Queues}
\begin{enumerate}
\item Use \textbf{unsorted arrays}.
\begin{itemize}
\item \verb+insert(x, p)+ takes O(1) time because you simply place the element at the end of the array.
\item \verb+deleteMin()+ takes O(n) time since you need to walk the array (keeping track of the current min you've seen so far), then replace the deleted element with the last element of the array. O(n) + O(1) = O(n) time.
\end{itemize}
Sorting with unsorted arrays takes O($n^2$) time, since for each element you're deleting you need to walk the entire array. Clearly, this is not a good sorting algorithm.
\item Use \textbf{heaps}. A heap is a \underline{complete} binary tree that has the \underline{heap property}.
\\ \\
Recall: a \textbf{complete (or perfect) binary tree} is defined as a binary tree such that:
\begin{itemize}
\item Every node has 0 or 2 children, except the rightmost leaf in the bottom level, which may have one only child.
\item All the leafs appear consecutively in the bottom two levels.
\item Deeper leaves are leftmost in the tree.
\end{itemize}
You can think of a complete binary tree as trying to fill the tree from left-to-right, going deeper and deeper as necessary.
\begin{figure}[H]
\Tree [.0
[.1 [.3 [.7 ] [.8 ]].3 [.4 [.9 ] [.10 ] ].4 ].1
[.2 [.5 [.11 ] ].5 [.6 ] ].2 ].0
\caption{\label{fig:completeBinaryTree} A complete binary tree.}
\end{figure}
The \textbf{heap property} is the idea that the priority of a node is higher than that of all of its children (if any). In a min-PQ, this means the $p$ value of all children is a larger number than that of the parent. In a max-PQ, the childrens' $p$ values must all be smaller than that of their parent.
\begin{figure}[H]
\Tree [.5
[.10 [.12 ] [.14 ]].10
[.7 [.13 ] [.8 ]].7 ].5
\caption{\label{fig:minPQheap} A min-PQ tree that satisfies the heap property.}
\end{figure}
You can have multiple elements with the same priority. You settle those disputes arbitrarily.
\\ \\
This is \underline{not} a binary search tree, so the larger element does not need to be on the right. There is no order among siblings, as seen in \ref{fig:minPQheap}. There is also no relationship with the parent's siblings \textendash{} it's only a relationship with the parent.
\\ \\
Next, we'll look into \textbf{filling holes}. If you delete an element from the tree, you delete the element at the top, but then you have a hole that needs to be filled. But how? We take the last element in the tree (the rightmost element at the bottom level) and swap it into the top position, then we get it to \textbf{boogie down}.
\\ \\
The process of \textbf{boogieing down} is where an element (in this case, the top element) looks at its children and swaps with a child that has higher priority than itself (if any, where ``higher priority'' differs depending on if it's a min- or max-heap). It does this recursively. It continues to recurse until the child is of higher priority than its parent, or until you reach a leaf node. Swap places with the child with the highest priority.
\\ \\
Insertion uses the process of \textbf{bubbling up}. We place the new element in the first available spot, which will be in the bottom level. The new element exchanges with their parent if the new element has higher priority than the parent, and that continues as far as needed, until it doesn't swap or until it reaches the top.
\\ \\
\textbf{Implementing a heap as an array} (also known as a \textbf{l33t hacking trick}):
\\ \\
You can actually implement a heap without pointers, using an array instead. The array indices follow the pattern as indicated in \ref{fig:completeBinaryTree} by inserting at the end of the array.
\\ \\
The parent of a node at index $m$ is $\lfloor \frac{m - 1}{2} \rfloor$. The children for a node at index $m$ are located at index $2 \cdot m + 1$ and $2 \cdot m + 2$.
\\ \\
To perform a \verb+deleteMin+, we erase element 0 and replace it with the last element in the array. We then perform a boogie down on element 0. Before computing children, we must perform boundary checking to ensure there actually are children (that is, we haven't run off the end of the array).
\\ \\
Here's the pseudocode for the bubble up process: \\
\begin{algorithm}[H]
\While( [parent exists and has higher priority] ){$\lfloor \frac{v - 1}{2} \rfloor \ge 0$ and $A[\lfloor \frac{v - 1}{2} \rfloor].p > A[v].p$}{
swap(v, $\lfloor \frac{v - 1}{2} \rfloor$)\;
v = $\lfloor \frac{v - 1}{2} \rfloor$\;
}
\end{algorithm}
Here's the pseudocode for the boogie down process: \\
\begin{algorithm}[H]
\While( [v has at least one child] ){$2v + 1 \le$ last}{
u = argmin$\set{A[2v + 1], A[2v + 2]}$; // argmin returns the array index of the min \\
\eIf{$A[u].p < A[v].p$}{
swap(u, v)\;
v = u\;
}{
break\;
}
}
\end{algorithm}
When implementing boogieing down, you'll have to handle boundary cases where there is only one child, etc., which were not handled in this pseudocode.
\end{enumerate}
\section{Sorting}
\subsection{PQ Sort}
We can sort data using a priority queue. \\ \\
Both \verb+insert+ and \verb+deleteMin+ take O(lg n) time where $n$ is the number of elements in the tree. Bubble up and boogie down each take O(h) = O(lg n) time, where $h$ is the height of the tree.
\\ \\
Note that the number of elements in a tree is between $2^{h - 1}$ and $2^h$. PQ sort takes O(n lg n) time, which is the same as mergesort.
\subsection{Heap Sort} \lecture{January 24, 2013}
Heap sort is a specialization of PQ sort. We can actually insert things even faster into the heap, though.
\\ \\
\textbf{Claim}: if all the elements to be inserted are given at once we can build the heap in $\Theta(n)$ time, which is faster than $\Theta(n \lg n)$.
\begin{proof} We're going to show that we can run this algorithm in $\Theta(n)$ time. \\
\begin{algorithm}[H]
Heapify(A); // A is an array \\
n = size(A) - 1\;
\For{i = $\lfloor \frac{n - 1}{2} \rfloor$ down to 0}{
boogie\_down(A, i)\;
}
\end{algorithm}
The number of comparisons for boogie down is:
\begin{align*}
\frac{n}{2} + 1& + \frac{n}{4} \cdot 2 + \frac{n}{8} \cdot 3 + \cdots + 1 \cdot \lg n \\
&\sum_{i = 1}^{\lg n} \frac{n}{2^i} \cdot i
\end{align*}
The cost of boogieing down $n$ elements is:
\begin{align*}
T(n) &= T\bigg(\frac{n}{2}\bigg) + \frac{n}{4} = T\bigg(\frac{n}{4}\bigg) + \frac{n}{8} + \frac{n}{4}
\end{align*}
This is the case because there are $\frac{n}{4}$ elements that can go one level further down. So we have:
\begin{align*}
T\bigg(\frac{n}{2}\bigg) &= T\bigg(\frac{\frac{n}{2}}{2}\bigg) + \frac{\frac{n}{2}}{4} \\
&= T\bigg(\frac{n}{4}\bigg) + \frac{n}{8} \\
&= \frac{n}{4} + \frac{n}{8} + \frac{n}{16} + \frac{n}{32} + \cdots \\
&= n\bigg(\frac{1}{4} + \frac{1}{8} + \frac{1}{16} + \frac{1}{32} + \cdots\bigg)
\end{align*}
This is true because $\sum_{i = 0}^{\infty} \frac{1}{2^i} = 1 + \frac{1}{2} + \frac{1}{4} + \cdots = 2$ was proven by Zeno.
\\ \\
\underline{Aside}: Zeno's proof of this involved a human and a turtle walking. The human walked at a pace of 1 metre per hour and the turtle moved at a pace of a $\frac{1}{2}$ metre per hour. The turtle was given a head start. The human would catch up with the turtle at point 2, but not any sooner because both the human and the turtle will have made progress on their race by the time the human reaches a point where the turtle was previously. This is known as \textbf{Zeno's paradox}. Unrelated: someone has called the dust that you can never sweep into a dust pan zenodust.
\\ \\
An alternate way to prove this is as follows:
\begin{align*}
\sum_{i = 0}^{\lg n} \frac{n}{x^i} \cdot i &= n \sum_{i = 0}^{\lg n} \frac{1}{x^i} \cdot i \\
&= n \sum_{i = 0}^{\lg n} \tau^i \cdot i \text{ where } \tau = \frac{1}{x}
\end{align*}
What we now have looks a bit like a derivative. Differentiating gives:
\begin{align*}
n \cdot \tau \sum_{i = 0}^{\lg n} i \cdot 2 \tau^{i - 1}
\end{align*}
We can integrate and then differentiate.
\\ \\
Therefore, we can heapify in $\Theta(n)$ time, so heap sort is $\Theta(n \lg n)$ and the constant will be lower than other methods, which is preferable.
\end{proof}
In practice, it's better to insert one by one, because our analysis is in the worst-case. When comparing the average case, inserting one by one is faster than heapifying. The worst case is really rare, so you should insert one by one (and pay more attention to the average case analysis in this situation).
\\ \\
A heap implemented using an array is called an \textbf{in-place implicit data structure}. You avoid pointers by using trickery in the organization of the data itself.
\subsection{Quick Sort}
\textbf{Selection}: suppose we want to find the minimum or maximum element in an unsorted array. The easy solution is to scan the array and report the answer in $\Theta(n)$ time. However, what if you're asked to find the $k$-th largest element? The na\"ive solution would be to sort the entire array, which would take $\Theta(n \lg n)$ time.
\\ \\
We can do better. We can find the $k$-th largest element in an array in linear time in the worst case, using a method called quick select.
\subsubsection{Quick Select}
Given an array $A$, we want to find the $k$-th smallest (or $k$-th largest) element. For example, let's say we're looking for the 3rd smallest element and we're given this array $A$: \\ \\
\begin{tabular}{|c|c|c|c|c|c|c|c|}
\hline 20 & 8 & 9 & 2 & 1 & 7 & 15 & 12 \\ \hline
\end{tabular}
\\ \\
We now want to partition the array into two smaller sets of elements. We will first sort the elements onto the proper side of 20 (the first element):
\\ \\
\begin{tabular}{|c|c|c|c|c|c|c|c|}
\hline 8 & 9 & 2 & 1 & 7 & 15 & 12 & 20 \\ \hline
\end{tabular}
\\ \\
Then, we'll continue on the side of 20 that happens to contain the element we're looking for. This time, we'll sort elements onto the proper side of 8 (which is the first element).
\\ \\
\begin{tabular}{|c|c|c|c|c|c|c|c|}
\hline 2 & 1 & 7 & 8 & 9 & 15 & 12 & 20 \\ \hline
\end{tabular}
\\ \\
If we were to continue further, we would proceed on the left side of 8 because there are fewer than (or equal to) 3 elements on that side.
\\ \\
We use a random element from the array called the \textbf{pivot} to partition the elements into those smaller than the pivot and those larger than the pivot. In general:
\begin{tabular}{|c|c|c|}
\hline < p & p & > p \\ \hline
\end{tabular}
\\ \\
Here's some pseudocode for quick select: \\
\begin{algorithm}[H]
p = choose random pivot in $A$\;
partition $(A, p)$ obtaining position $i$ of $p$\;
\uIf{k < i}{
quick\_select(A[0, i - 1], k)\;
}
\uElseIf{k > 1}{
quick\_select(A[i + 1, \ldots, n - 1], k - i - 1)\;
}
\Else{
return p\;
}
\end{algorithm}
In quick select, you only continue to recurse in the ``half'' that contains what we're looking for (unlike in quick sort where we continue to recurse on both sides).
\\ \\
In the worst case, this runs in $\Theta(n^2)$ time, since the random element chosen is always max($A$), and we are searching for $k$ = first element:
\begin{tabular}{|c|c|c|c|}
\hline $\ldots$ & $p_3$ & $p_2$ & $p_1$ \\ \hline
\end{tabular}
\\ \\
Partitioning with two arrays is easy (i.e. creating a new array and inserting). Partitioning within the existing array is a bit more complicated, however that is a key advantage to quick select. Here's the pseudocode for partitioning within the same array: \\
\begin{algorithm}[H]
swap(A[0], A[p])\;
i = 1\;
j = n - 1\;
\While{true}{
\While{i < n and A[i] < A[0]}{
i = i + 1\;
}
\While{j $\ge$ 1 and A[j] > A[0]}{
j = j - 1\;
}
\eIf{j < i}{
break\;
}{
swap(A[i], A[j])\;
}
}
\end{algorithm}
A sample run of this algorithm is as follows:
\lecture{January 29, 2013} \begin{verbatim}
44 4 10 52 86 57 97 19 18 88 31 82 59 39
19 i -->> 44 <<-- j
19 4 10 18 86 57 97 44 52 88 31 82 59 39
i -->>-<>-<<-- j
4 10 18 19 86 57 97 44 52 88 31 82 59 39 (final output)
\end{verbatim}
In-place partitioning is one the reasons why quick sort (and quick select) is so good. Recall the selection problem, where we have to find a way to return the $k$-th element in the sorted order. Quick select is one solution to this problem.
\\ \\
\textbf{Worst case}: quick select runs in $T(n) = n + T(n - 1) = n + (n - 1) + (n - 2) + \ldots + 1 = \Theta(n^2)$ time.
\\ \\
\textbf{Average case}: assume all permutations are equally likely. This is a probabilistic assumption on the input. The probability of choosing a ``good'' pivot is $\frac{1}{2}$. After $r$ recursive calls, about half the pivots are good, which means the array was halved approximately $\frac{r}{2}$ times. After $4 \lg n$ recursive calls, we must be left with an array of size 1. Note that this is $4 \lg n$ and not $2 \lg n$ because we aren't dividing perfectly in half each time. We're also assuming that randomness is somehow preserved on recursive calls.
\\ \\
\textbf{Expected case}: probabilities come from the algorithm itself. For example: select $p$ at random in A[0\ldots n - 1], which preserves randomness. Regardless of the input distribution or partition algorithm, a pivot is ``good'' with probability $\approx \frac{1}{2}$. The expected case is that this algorithm runs in $T(n, k)$ time as follows:
\begin{align*}
T(n, k) &= cn + \frac{1}{n} T(n - 1, k) + \frac{1}{n} T(n - 2, k) + \frac{1}{n} T(n - 3, k) + \ldots + \frac{1}{n}T(n + 1, k - 1) + \frac{1}{n}T(n + 2, k - 2) + \ldots \\
&= cn + \frac{1}{n} \left[ \sum_{i = 0}^{k - 1} T(n - i - 1, k - i - 1) + \sum_{i = k + 1}^{n - 1} T(i, k) \right]
\end{align*}
We could hand-wave and say:
\begin{align*}
T(n) &\le \frac{1}{2} T(n) + \frac{1}{2} T\left(\frac{3n}{4}\right) + cn \\
&\le 2cn + 2cn\left(\frac{3n}{4}\right) + 2c\left(\frac{9n}{16}\right) + \ldots + T(1) \\
&\le \frac{1}{2} \bigg[\frac{1}{2}T(n) + \frac{1}{2}T\left(\frac{3n}{4}\right) + cn \bigg] + \frac{1}{2}\bigg[ \frac{1}{2} T\left(\frac{3n}{4}\right) + \frac{1}{2}T\left(\frac{9n}{16}\right) + c \frac{3n}{4} \bigg] \\
&\le \frac{1}{4}T(n) + \frac{1}{2}T\left(\frac{3n}{4}\right) + \frac{1}{2} cn + \frac{1}{4}T\left(\frac{9n}{16}\right) + c\frac{3n}{8} + cn \\
&\le \underbrace{T(1)}_{\text{constant}} + 2cn \underbrace{\sum_{i = 0}^{\infty} \left(\frac{3}{4}\right)^i}_{\text{constant}} \\
&= \Theta(n)
\end{align*}
$\therefore$ We can quick select in linear time in the expected case.
\\ \\
We really don't need highly random bits for quick select \textendash{} at least nowhere near as random as in cryptography. You generally need a good random source, however for this application (since security is not an issue), the current time may suffice as a random seed. If we can \emph{easily} produce a sequence mechanically with a program, then it is not considered random. True randomness cannot be compressed.
\subsubsection{Quick Sort Algorithm and Analysis}
The general algorithm for quick sort is as follows. \\
\begin{algorithm}[H]
\lIf{n = 0}{\Return}
p = choose random\_pivot(0 \ldots n - 1)\;
i = partition(A, p)\;
QS(A[0 \ldots i - 1])\;
QS(A[i + 1 \ldots n])\;
\end{algorithm}
In quick select, we only recursed on the side containing the target, but in quick sort, we want to recurse continuously on both sides until we every side contains only one element.
\\ \\
\textbf{Worst case}: $T(n) = n + T(n - 1) = n + (n - 1) + T(n - 2) + \ldots = \Theta(n^2)$.
\\ \\
\textbf{Best case}: $T(n) = n + 2T\left(\frac{n}{2}\right) = \Theta(n \lg n)$.
\\ \\
\textbf{Average case}:
\begin{align*}
T(n) &= cn + \frac{1}{n}(T(n - 1) + T(1)) + \frac{1}{n}(T(n - 2) + T(2)) + \ldots + \frac{1}{n}(T(1) + T(n - 1)) \\
&= \frac{1}{n} \sum_{i = 0}^{n - 1}(T(i) + T(n - i - 1)) + \underbrace{\Theta(n)}_{\text{partition step}} \\
&\le \frac{1}{n} \sum_{i = 0}^{n - 1} 2 \max \set{T(i), T(n - i - 1) } + \Theta(n) \\
&= \frac{1}{n} \cdot \frac{1}{2}(T\left(\frac{3i}{4}\right)) + \frac{1}{n} \cdot \frac{1}{2} T(i) + cn
\end{align*}
Take note of two important observations here:
\begin{enumerate}
\item At each level of the recursion, the total work adds up to $n$ (roughly).
\item This continues until we reach the end of the recursion at $n = 1$.
\end{enumerate}