— notes — 36 min read
Pet Peeve: The author sometimes skips steps without an explaination (like the integer truncation in "stop and think: incremental correctness"). Some examples are hard to follow for an international student (like understanding the lottery system in "war story: pschic modeling").
A computational problem is a specification of a desired input-output relationship. e.g.
Computational problem: Sorting
Input: A sequence of n values a1, ..., an.
Output: The permutation (reordering) of the input sequence such that a1′≤a2′ ... an−1′≤an′.
An instance of a problem is all the inputs needed to compute a solution to the problem. Alternatively, a computational problem is the set of all (problem) instances and the desired output. e.g.
To sort the permutation
{8, 3, 6, 7}
is an instance of the general sorting problem and{3, 6, 7, 8}
is the desired output.
An algorithm is a well-defined computational procedure that halts with a desired output when given any instance of the given computational problem. An algorithm is an abstract (idea) and can have multiple implementations (programs). e.g.
1/*2Insertion sort is an algorithm to the sorting problem: Start with a single3element (thus forming a trivially sorted list) and then incrementally inserts4the remaining elements so that the list stays sorted.5*/6insertion_sort(item s[], int n)7{8 int i,j; /* counters */9 for (i=0; i<n; i++) {10 j=i;11 12 while ((j>0) && (s[j] < s[j-1])) {13 swap(&s[j],&s[j-1]);14 j = j-1;15 }16 }17}
Real-world applications involve real-world objects. However, most algorithms are designed to work on rigorously defined abstract structures. Modeling is the art of formulating your application in terms of procedures on such fundamental abstract structures so as to exploit the existing algorithms literature.
Determining that you are dealing with an instance of a general problem allows you to solve it using general well-known algorithms.
Certain problems can be modeled in several different ways, some much better than others. Modeling is only the first step in designing an algorithm for a problem. Be alert but not dismissive for how the details of your applications differ from a candidate model.
{1,4,3,2}
and {4,3,2,1}
are two distinct permutations of the same set of four integers.{1,3,4}
and {2}
are two distinct subsets of the first four integers. Order does not matter in subsets the way it does with permutations.Learning to think recursively is learning to look for big things that are made from smaller things of exactly the same type as the big thing.
If you think of houses as sets of rooms, then adding or deleting a room still leaves a house behind.
n
things {1, ..., n}
and you get a permutation of the remaining n-1
things. Basis case: {}{1, ..., n}
contains a subset of (1, ..., n - 1)
obtained by deleting element n
. Basis case: {}Recursive decompositions of combinatorial objects. (left column) Permutations, subsets, trees, and graphs. (right column) Point sets, polygons, and stringsRecursive descriptions of objects require both decomposition rules and basis cases, namely the specification of the smallest and simplest objects where the decomposition stops.
An algorithmic problem is specified by describing the complete set of instances it must work on and of its output after running on one of these instances.
There is a distinction between a general problem and an instance of a problem. E.g:
Problem: Sorting
Input: A sequence ofn
keys a1,...,an.
Output: The permutation (reordering) of the input sequence such that: a′1 ≤ a′2 ≤ ··· ≤ a′n−1 ≤ a′n.
Instance of sorting problem: { Mike, Bob, Sally}; { 154, 245 }
An algorithm is a procedure that takes any of the possible input instances and transforms it to the desired output.
Three desirable properties for a good algorithm:
Insertion sort is an algorithm to the sorting problem: English description:
Start with a single element (thus forming a trivially sorted list) and then incrementally inserts the remaining elements so that the list stays sorted.
Pseudocode:
1array = input sequence2n = array size3i = 045while i < n6 j = i7 while j > 0 AND array[j] < array[j - 1]8 swap element at `j` with element at `j - 1` in array9 decrement j by 110 increment i by 1
Code:
An animation of the logical flow of this algorithm on a particular instance (the letters in the word “INSERTIONSORT”
)
Problem: Robot Tour Optimization (aka: Traveling Salesman Problem [TSP]).
Input: A setS
ofn
points in the plane.
Output: What is the shortest cycle tour that visits each point in the setS
?
Starting from some point
p0
, we walk first to its nearest neighborp1
Fromp1
, we walk to its nearest unvisited neighbor. Repeat this process until we run out of unvisited points After which we return top0
to close off the tour.
1NearestNeighbor(P)2 Pick and visit an initial point p₀ from P3 p = p₀4 i = 05 6 While there are still unvisited points7 i = i + 18 Select pᵢ to be the closest unvisited point to pᵢ₋₁9 Visit pᵢ10 Return to p₀ from pₙ₋₁
Repeatedly connect the closest pair of endpoints whose connection will not create a problem, such as premature termination of the cycle. Each vertex begins as its own single vertex chain. After merging everything together, we will end up with a single chain containing all the points in it. Connecting the final two endpoints gives us a cycle. At any step during the execution of this closest-pair heuristic, we will have a set of single vertices and vertex-disjoint chains available to merge.
Correct algorithms usually come with a proof of correctness, which is an explanation of why we know that the algorithm must take every instance of the problem to the desired result.
proof
.QED
at the bottom to denote that you have finished, representing the Latin phrase for "thus it is demonstrated."1* The set of allowed input instances.2* The required properties of the algorithm's
1* Asking an ill-defined question. E.g: Asking for the best route between two places on a map is a silly question, unless you define what best means.2* Creating compound goals. E.g: A goal like find the shortest route from **a** to **b** that doesn't use more than twice as many turns as necessary is perfectly well defined, but complicated to reason about and solve.
n
(that is, an integer n ≥ 0
) holds for all values of n
. The proof consists of two steps:n = k
, and prove that the statement holds for n = k + 1
.n = k
, is called the induction/inductive hypothesis. You're doing a thought experiment of what would happen if it was true for n = k
. It might be clearer to use the phrase "suppose true when n = k
" rather than "assume true when n = k
" to emphasise this.n = k
and then uses this assumption to prove that the statement holds for n = k + 1
. We try to manipulate the statement for n = k + 1
so that it involves the case for n = k
(which we assumed to be true).n - 1
elements of array A
are completely sorted after n - 1
iterations of insertion sort.x
to A
, we find where it goes, namely the unique
spot between the biggest element less than or equal to x
and the smallest element greater than x
. This is done by moving all the greater elements
back by one position, creating room for x
in the desired location. ■m
, which can be listed as p₁,..., pₘ
. So let's assume this is the case and work with it.N
has the property that it is divisible by each and every one of the known primes, because of how it was built.N + 1
. It can't be divisible by p₁ = 2
, because N
is.p₂ = 3
and every other listed prime. Since a +1 can’t be evenly split by any of the prime numbers because they are bigger.N + 1
doesn't have any non-trivial factor, this means it must be prime.m
prime numbers, none of which are N + 1
, because N + 1 > m
.Show that a + b
can be less than min(a, b)
.
When:
a and b < 0
(i.e: negative)
a <= b
Then:
min(a, b) = a
a + b < a
Example:
min(-6, -5) = -6
-6 + -5 = -6 -5 = -11
-11 < -6
Show that a × b
can be less than min(a, b)
.
When:
a < 0
(i.e: negative)
b > 0
(i.e: positive)
Then:
min(a, b) = a
a * b < a
Example:
min(-3, 4) = -3
-3 * 4 = -12
-12 < -3
Design/draw a road network with two points a and b such that the fastest route between a and b is not the shortest route.
1a──────c──────b2│ │3│ │4└────d────────┘56Route `acb` have a toll gate, is in development and have a speed limit.7Route `adb` is longer, but has none of these impediment.89Route `adb` will be faster than the shorter `acb` route.
Design/draw a road network with two points a and b such that the shortest route between a and b is not the route with the fewest turns.
1a────┐ ┌────b2│ └─c─┘ │3│ │4└──────d──────┘56Route `acb` is the shortest but has 4 turns.7Route `adb` is the longest and has only 2 turns.
The analysis of algorithms is the process of finding the computational complexity of algorithms.
For an ordered list of size n (33 in the example above), binary search takes at most log2n check steps (5 steps in the example above); while linear search takes at most n check steps (28 steps in the example above).
The computational complexity or simply complexity of an algorithm is the amount of resources required to run it.
Common types of resources include:
The computational complexity of an algorithm can be measured only when given a model of computation.
The most commonly used model of computation is the Random Access Machine (RAM) which is a hypothetical computation model where:
+
, *
, -
, =
, if
, call
, etc) takes exactly unit-time (i.e. 1 step).The RAM is a simple model of how computers perform. It allows the analysis of algorithms in a machine-independent way by assuming that simple operations take a constant amount of time on a given computer and change only by a constant factor when run on a different computer.
It doesn’t capture the full complexity of real computers (e.g. multiplying two numbers takes more time than adding two numbers on most processors). However, the RAM is still an excellent model for understanding how an algorithm will perform on a real computer.
A model is a simplification or approximation of reality and hence will not reflect all of reality. […] "all models are wrong, but some are useful." — Model Selection and Multimodel Inference
Under the RAM model, we measure the computational complexity by counting the number of simple operations (also called steps) an algorithm takes on a given problem instance.
As the amount of resources required to run an algorithm generally varies with the size of the input, the complexity is typically expressed as a function n→f(n), where n is the size of the input and f(n) is the computational complexity.
Aside: We can also use any other characteristic of the input influencing the computational complexity.
However, the complexity of an algorithm may vary dramatically for different inputs of the same size. Therefore, three complexity functions are commonly used:
There are three problems with the complexity functions above:
Complexity theory seeks to quantify the intrinsic time requirements of algorithms (independent of any external factors), that is, the basic time constraints an algorithm would place on any computer .
For these reasons, one generally focuses on the behavior of the complexity for large n, that is on its asymptotic behavior when n tends to infinity. Therefore, the complexity is generally expressed by using big O notation.
Big O notation is a mathematical notation that describes the limiting behavior of a function when the argument tends towards infinity. The formal definition of the big O notations are:
Notation: f(n)=O(g(n))
Meaning: There exists some constant c such that ∣f(n)∣≤c⋅∣g(x)∣ for large enough n (i.e. for all n≥n0, for some constant n0).
Notation: f(n)=Ω(g(n))
Meaning: Means there exists some constant c such that ∣f(n)∣≥c⋅∣g(x)∣ for large enough n (i.e. for all n≥n0, for some constant n0).
Notation: f(n)=Θ(g(n))
Meaning: Means there exists some constants c1 and c2 such that ∣f(n)∣≤c1⋅∣g(x)∣ and ∣f(n)∣≥c2⋅∣g(x)∣ for large enough n (i.e. for all n≥n0, for some constant n0).
The best/average/worst-case computational complexity is a function of the size of the input for a certain algorithm.
The O, Ω, and Θ notations are used to describe the relationship between two functions.
Hence, each best/average/worst case computational complexity function has its corresponding O, Ω, and Θ notation.
Conflating the big O notations with the computational complexity functions likely stems from the fact that in everyday use, “the big O of the worst-case computational complexity function” is used interchangeably with just “big O”, “time complexity”, “complexity”, etc.
There are two accepted abuses of notation in the computer science industry:
Abuse of the equal sign: Technically, the appropriate notation is f(n)∈O(g(n)), because the equal sign does not imply symmetry.
Abuse of the Big O notation: The big O notation is often used to describe an asymptotic tight bound where using big Theta Θ notation would be more factually appropriate. Big O notation provides an upper bound, but it doesn't necessarily provide a tight upper bound. For example, 2n2+3n+1 is O(n2), but it is also O(n3), O(n4), etc. For contrast, the equation is only Θ(n2).
The function g(x) appearing within the O(⋅) is typically chosen to be as simple as possible, omitting constant factors and lower-order terms.
Constant functions:
Logarithmic functions:
Linear functions:
Superlinear functions:
Quadratic functions:
Cubic functions:
Exponential functions:
Factorial functions:
Addition:
Multiplication by a constant:
Multiplication by non-constant:
Transitivity:
1// Selection sort repeatedly identifies the smallest remaining unsorted element2// and puts it at the end of the sorted portion of the array.3void selectionSort(int arr[], int n) {4 int i, j, minimum_element_index;56 // One by one move the boundary of the unsorted subarray7 for (i = 0; i < n; i++) {89 // Find the minimum element in the unsorted array10 minimum_element_index = i; // Assume the current element is the minimum11 12 for (j = i + 1; j < n; j++) {13 // If we find a smaller element, update the minimum_element_index14 if (arr[j] < arr[minimum_element_index]) {15 minimum_element_index = j;16 }17 }1819 // Swap the found minimum element with the first element20 swap(&arr[minimum_element_index], &arr[i]);21 }22}
Analysis of the selection sort algorithm:
if
statement is executed is:T(n)=i=0∑n−1j=i+1∑n−11A logarithm is an inverse exponential function:
bx=yx=logbyb(logby)=yExponential functions grow at a fast rate while logarithmic functions grow at a slow rate. Logarithms arise in any process where things are repeatedly halved. e.g.
Logarithms and binary search:
Logarithms and trees:
Logarithms and bits:
A direct consequence of the product property is the power property:
logb(ac)=c⋅logb(a)Since c is a constant, this implies raising the logarithm’s argument to a power doesn't affect the growth rate:
logb(ac)=Θ(logb(a))Changing the base of logb(a) to base-c simply involves multiplying by logc(b):
logb(a)=logc(b)logc(a)logc(a)=logb(a)⋅logc(b)Since c and b are constants, this property implies that the base of a logarithm has no impact on the growth rate:
log2(1,000,000)=19.9316log3(1,000,000)=12.5754log100(1,000,000)=3Aside: In practice, an asymptotically inefficient algorithm may be more efficient for small input sizes. This is particularly used in hybrid algorithms, like Timsort, which use an asymptotically efficient algorithm (here merge sort, with time complexity nlogn), but switch to an asymptotically inefficient algorithm (here insertion sort, with time complexity n2) for small data, as the simpler algorithm is faster on small data.
int
in a programming language sense is a fixed-width data structure. An integer in a mathematical sense is an ADT. For most purposes the user can work with the abstraction rather than the concrete choice of representation, and can simply use the data as if the type were truly abstract.t(i) defines the time it takes to perform the i-th addition
.
The first case of t(i):
added
into the new array. This represents the +1.The second case of t(i):
addition
is performed.e.g. Given a dynamic array initialized with a capacity of 1:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
t(i) | 1 | 2 | 3 | 1 | 5 | 1 | 1 | 1 | 9 | 1 |
Capacity | 1 | 2 | 4 | 4 | 8 | 8 | 8 | 8 | 16 | 16 |
e.g. Given n=4:
Amortized cost=n∑i=14t(i)=1+(20+1)+(21+1)+1We see that in both cases of the function, there is a +1. So summation from 1 to n results in:
i=1∑nt(i)=n+k=0∑log2(n−1)2kThe second operand represents the total cost of the copy operation that occurs on each resizing. For n addition
, this happens log2(n−1)+1 times. e.g.
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
log2(n−1) | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 3 | |
∑k=0log2(n−1)2k | 1 | 3 | 3 | 7 | 7 | 7 | 7 | 15 | 15 |
The second operand is the partial sum of a geometric series, which has the formula:
k=0∑nxk=x−1xn+1−1Applying the formula to the second operand:
k=0∑log2(n−1)2k=2−12log2(n−1)+1−1=12log2(n−1)+1−1=2log2(n−1)+1−1=2log2(n−1)⋅21−1log2(n−1) is the number to which 2 must be raised to obtain n−1. Raising that number then to 2 results in n−1. Hence, the last expression can be simplified to:
=(n−1)⋅21−1=2(n−1)−1=2n−3The summation can be re-expressed again as:
i=1∑nt(i)=n+2n−3The −3 is inconsequencial in the time analysis, hence the summation can be simplified into:
i=1∑nt(i)≤3nInterpretation: A sequence of n
add
operations costs at most 3n, hence eachadd
in the sequence costs at most 3 (constant time) on average, which is the amortized cost according to the aggregate method.
Conclusion: This proves that the amortized cost of insertion to a dynamic array with doubling is O(1).
Pointers represent the address of a location in memory. Pointers are the connections that hold the nodes (i.e. elements) of linked data structures together.
In C:
*p
denotes the item that is pointed to by pointer p
&p
denotes the address of (i.e. pointer to) a particular variable p
.NULL
pointer value is used to denote unassigned pointers.All linked data structures share these properties:
Example linked-list displaying these properties:
1typedef struct list {2 data_type data; /* Data field */3 struct list *next; /* Pointer field */4} list;
The linked-list is the simplest linked structure.
Singly linked-list has a pointer to only the successor whereas a doubly linked-list has a pointer to both the predecessor and successor.
Searching for data x
in a linked-list recursively:
1list *search_list(list *my_list, data_type x) {2 if (my_list == NULL) {3 return(NULL);4 }56 // If `x` is in the list, it's either the first element or located in the rest of the list.7 if (my_list->data == x) {8 return(my_list);9 } else {10 return(search_list(my_list.next, x));11 }12}
Inserting into a singly linked-list at the head:
1list *insert_list(list **my_list, data_type x) {2 list *p; /* temporary pointer */3 p = malloc(sizeof(list));4 p->data = x;5 p->next = *my_list;6 // `**my_list` denotes that `my_list` is a pointer to a pointer to a list node.7 // This line copies `p` to the place pointed to by `my_list`,8 // which is the external variable maintaining access to the head of the list.9 *my_list = p;10}
Deletion from a list:
1// Used to find the predecessor of the item to be deleted.2list *item_ahead(list *my_list, list *x) {3 if ((my_list == NULL) || (my_list->next == NULL) {4 return(NULL);5 }67 if ((my_list->next) == x) {8 return(my_list);9 } else {10 return(item_ahead(my_list->next, x));11 }12}1314// This is called only if `x` exists in the list.15void *delete_list(list **my_list, list **x) {16 list *p; /* element pointer */17 list *pred; /* predecessor pointer */1819 p = *my_list;20 pred = item_ahead(*my_list, *x);2122 // Given that we assume `x` exists in the list,23 // `pred` is only null when the first element is the target.24 if (pred == NULL) { /* splice out of list */25 // Special case: resets the pointer to the head of the list26 // when the first element is deleted.27 *my_list = p->next;28 } else {29 pred->next = (*x)->next30 }3132 free(*x) /* free memory used by node */33}
The advantages of linked structures over static arrays include:
Both arrays and lists can be thought of as recursive objects:
An array is only recursive conceptually: Every array "contains" a sub-array. However, an array isn't recursive in code: A structure is recursive if while navigating through it, you come across "smaller" versions of the whole structure. While navigating through an array, you only come across the elements that the array holds, not smaller arrays.
push(x)
— Inserts item x
at the top of the stack.pop()
— Return and remove the top item of the stack.enqueue(x)
— Inserts item x
at the back of the queue.dequeue()
— Return and remove the front item from the queue.(key, value) pairs
, such that each possible key
appears at most once in the collection.put(key, value)
remove(key)
get(key)
1typedef struct tree {2 data_type data; /* Data field */3 struct tree *parent; /* Pointer to parent */4 struct tree *left; /* Pointer to left chid */5 struct tree *right; /* Pointer to right child */6} tree;
priority
.add()
— an element with a given priority,delete(x)
— an element,pull()
— Get the the highest priority element and remove it, andpeek()
— Get the the highest priority element without removing it.Hash tables are a data structure that efficiently implements a dictionary. They exploit the fact that looking an element up in an array takes constant time once you have its index.
The basic idea is to pick a hash function h that maps every possible key x to a small integer h(x). Then we store x and its value in an array at index h(x); the array itself is essentially the hash table.
A hash function h maps the universe U of keys to array indices within the hash table.
h:U→{0,…,n−1}Properties of a good hash function:
A hash function for an arbitrary key x (like a string) is typically defined as:
h(x)=toInteger(x)modnThis is the polynomial function that Java uses to convert strings to integers:
s[0]∗31n−1+s[1]∗31n−2+...+s[n−1]Which translates into the following code:
1int hashcode = 0;2for (int i = 0; i < s.length(); i++) {3 hashcode = (hashcode * 31) + s.charAt(i);4}
A collision occurs when:
h(j)=h(k)∧j=kCollisions can be minimized but cannot be eliminated (see Pigeon hole principle). It’s impossible to eliminate collisions without knowing the U ahead of time.
The two common methods for collision resolution are separate chaining and open addressing.
In separate chaining, the values of the hash-table’s array is a linked-list.
In open addressing, every key and its value is stored in the hash-table’s array itself, and the resolution is performed through probing
.
Search algorithms that use hashing consist of two separate parts: hashing and collision resolution.
Other uses of hashing (or a hash table):
1* Plagiarism detection using Rabin-Karp string matching algorithm2* English dictionary search3* Finding distinct elements4* Counting frequencies of items
Time complexity in big O notation
Operation | Average | Worst case |
---|---|---|
Search | Θ(1) | O(n) |
Insert | Θ(1) | O(n) |
Delete | Θ(1) | O(n) |
Space complexity is O(n).
A common problem for compilers and text editors is determining whether the parentheses in a string are balanced and properly nested. For example, the string ((())())()
contains properly nested pairs of parentheses, while the strings )()(
and ())
do not. Give an algorithm that returns true if a string contains properly nested and balanced parentheses, and false if otherwise. For full credit, identify the position of the first offending parenthesis if the string is not properly nested and balanced.
1fun test() {2 System.out.println(areParentheseProperlyBalanced("((())())()"))3 System.out.println(areParentheseProperlyBalanced(")()("))4 System.out.println(areParentheseProperlyBalanced("())"))5 System.out.println(areParentheseProperlyBalanced(")))"))6 System.out.println(areParentheseProperlyBalanced("(("))7 }89/**10 * @return -1 if [string] is valid, else a positive integer11 * that provides the position of the offending index.12 */13fun areParentheseProperlyBalanced(string: String): Int {14 val stack = Stack<Pair<Char, Int>>()1516 string.forEachIndexed { index, char ->17 if (char == '(') {18 stack.push(char to index)19 } else if (char == ')') {20 if (stack.empty()) {21 return index22 } 23 stack.pop()24 } else {25 throw IllegalArgumentException("Only parenthesis are supported")26 }27 } 28 29 return if (stack.empty()) -1 else stack.peek().second30 }
Give an algorithm that takes a string S consisting of opening and closing parentheses, say )()(())()()))())))(
, and finds the length of the longest balanced parentheses in S, which is 12 in the example above. (Hint: The solution is not necessarily a contiguous run of parenthesis from S)
1fun test() {2 System.out.println(lengthOfLongestBalancedParentheses("((())())()"))3 System.out.println(lengthOfLongestBalancedParentheses(")()(())()()))())))("))4 System.out.println(lengthOfLongestBalancedParentheses(")()("))5 System.out.println(lengthOfLongestBalancedParentheses("())"))6 System.out.println(lengthOfLongestBalancedParentheses(")))"))7 System.out.println(lengthOfLongestBalancedParentheses("(("))8}910fun lengthOfLongestBalancedParentheses(string: String): Int {11 val stack = Stack<Char>()12 var numBalancedParenthesis = 01314 string.forEachIndexed { index, char ->15 if (char == '(') {16 stack.push(char)17 } else if (char == ')') {18 if (!stack.empty()) {19 stack.pop()20 numBalancedParenthesis++21 }22 }23 }2425 // Multiplied by 2 because each balanced pair has a length of 2.26 return numBalancedParenthesis * 227}
Give an algorithm to reverse the direction of a given singly linked list. In other words, after the reversal all pointers should now point backwards. Your algorithm should take linear time.
1fun test() {2 val node1 = Node("Elvis", null)3 4 val node2 = Node("Chidera", null)5 node1.next = node26 7 val node3 = Node("Nnajiofor", null)8 node2.next = node39 10 System.out.println(node1)11 System.out.println(reverse(node1))12}1314data class Node(15 val element: String,16 var next: Node?17)1819/**20 * Work through an example:21 * reverse(Node1 -> Node2 -> Node3)22 * 23 * Node1 level:24 * val last = reverse(Node2 -> Node3) 🛑25 * 26 * Node2 level:27 * val last = reverse(Node3) 🛑28 * 29 * Node3 level:30 * return Node331 * 32 * Back to Node2 level:33 * val last = Node3 🟢34 * Node3.next = Node235 * Node2.next = null36 * return last37 * 38 * Back to Node1 level:39 * val last = Node3 🟢40 * Node2.next = Node141 * Node1.next = null42 * return last43*/44fun reverse(node: Node): Node {45 // Base case46 if (node.next == null) return node47 48 val last = reverse(node.next!!)49 node.next!!.next = node50 node.next = null51 return last52}
Design a stack S that supports S.push(x)
, S.pop()
, and S.findmin()
, which returns the minimum element of S. All operations should run in constant time.
1fun test() {2 val stack = Stack()3 stack.push(50)4 stack.push(40)5 stack.push(30)6 stack.push(20)7 stack.push(10)8 9 System.out.println(stack.findMin())10 stack.pop()11 System.out.println(stack.findMin())12 stack.pop()13 System.out.println(stack.findMin())14 stack.pop()15 System.out.println(stack.findMin())16 stack.pop()17 System.out.println(stack.findMin())18 stack.pop()19}2021data class Element(22 val num: Int,23 internal val minNumSoFar: Int24)2526class Stack {27 28 private val list = mutableListOf<Element>()29 30 fun push(num: Int) {31 list += Element(32 num = num,33 minNumSoFar = Math.min(num, list.lastOrNull()?.minNumSoFar ?: num)34 ) 35 }36 37 fun pop(): Int {38 return list.removeLast().num39 }40 41 fun findMin(): Int {42 return list.last().minNumSoFar43 }44}
We have seen how dynamic arrays enable arrays to grow while still achieving constant-time amortized performance. This problem concerns extending dynamic arrays to let them both grow and shrink on demand.
a. Consider an underflow strategy that cuts the array size in half whenever the array falls below half full. Give an example sequence of insertions and deletions where this strategy gives a bad amortized cost.
b. Then, give a better underflow strategy than that suggested above, one that achieves constant amortized cost per deletion.
a. A sequence of insert
, insert
, delete
, insert
, delete
, insert
will lead to a bad amortized cost because the array will be busy resizing for most of the operations.
b. A better underflow strategy is to cut the array size in half whenever the array falls below 1/4. 1/4 is arbitrary, but it gives the array good "slack" space. The goal is to select a number such that the array is not busy resizing for most of the operation. The smaller the cut-off ratio, the smaller the number of resizing but the more space the array uses.
Suppose you seek to maintain the contents of a refrigerator so as to minimize food spoilage. What data structure should you use, and how should you use it?
Use a priority queue. The expiry date is the priority of each element. Elements with the lowest expiry date are served first (minElement
).
Work out the details of supporting constant-time deletion from a singly linked list as per the footnote from page 79, ideally to an actual implementation. Support the other operations as efficiently as possible.
1fun test() {2 val node3 = Node(3)3 val node2 = Node(2, node3)4 val node1 = Node(1, node2)56 println(node1)7 println(deleteInConstantTime(node1, node2))8 println(deleteInConstantTime(node1, node3))9 println(deleteInConstantTime(node1, node1))10}1112fun deleteInConstantTime(head: Node, search: Node): Node? {13 if (head == search) {14 // Handle head case15 return head.next16 }1718 var node: Node? = head19 var prevNode: Node? = null20 while (node != null) {21 if (node == search) {22 if (node.next == null) {23 // Handle tail case24 prevNode?.next = null25 } else {26 node.element = node.next!!.element27 node.next = node.next?.next28 }2930 break31 } else {32 prevNode = node33 node = node.next34 }35 }3637 return head38}3940data class Node(41 var element: Int,42 var next: Node? = null43)
Tic-tac-toe is a game played on an n∗n board (typically n=3) where two players take consecutive turns placing “O” and “X” marks onto the board cells. The game is won if n consecutive “O” or “X” marks are placed in a row, column, or diagonal. Create a data structure with O(n) space that accepts a sequence of moves, and reports in constant time whether the last move won the game.
1fun test() {2 var tickTacToe = TickTacToe(3)3 assertFalse(tickTacToe.playX(1, 1))4 assertFalse(tickTacToe.playO(1, 2))5 assertFalse(tickTacToe.playX(1, 3))67 tickTacToe = TickTacToe(3)8 assertFalse(tickTacToe.playO(2, 1))9 assertFalse(tickTacToe.playX(2, 2))10 assertFalse(tickTacToe.playO(2, 3))1112 tickTacToe = TickTacToe(3)13 assertFalse(tickTacToe.playX(3, 1))14 assertFalse(tickTacToe.playX(3, 2))15 assertTrue(tickTacToe.playX(3, 3))1617 tickTacToe = TickTacToe(3)18 assertFalse(tickTacToe.playX(1, 1))19 assertFalse(tickTacToe.playO(2, 1))20 assertFalse(tickTacToe.playX(3, 1))2122 tickTacToe = TickTacToe(3)23 assertFalse(tickTacToe.playO(1, 2))24 assertFalse(tickTacToe.playX(2, 2))25 assertFalse(tickTacToe.playO(3, 2))2627 tickTacToe = TickTacToe(3)28 assertFalse(tickTacToe.playX(1, 3))29 assertFalse(tickTacToe.playX(2, 3))30 assertTrue(tickTacToe.playX(3, 3))3132 tickTacToe = TickTacToe(3)33 assertFalse(tickTacToe.playO(1, 1))34 assertFalse(tickTacToe.playO(2, 2))35 assertTrue(tickTacToe.playO(3, 3))3637 tickTacToe = TickTacToe(3)38 assertFalse(tickTacToe.playO(1, 3))39 assertFalse(tickTacToe.playO(2, 2))40 assertTrue(tickTacToe.playO(3, 1))41}4243private fun assertTrue(value: Boolean) {44 require(value)45 println("Won!!!")46}4748private fun assertFalse(value: Boolean) {49 require(!value)50 println("Not won yet")51}5253class TickTacToe(private val n: Int) {5455 private val columns = Array(n) {56 Slot(n)57 }5859 private val rows = Array(n) {60 Slot(n)61 }6263 private val diagonal = Slot(n)6465 private val antiDiagonal = Slot(n)6667 fun playX(rowPosition: Int, columnPosition: Int): Boolean {68 return play('X', rowPosition, columnPosition)69 }7071 fun playO(rowPosition: Int, columnPosition: Int): Boolean {72 return play('O', rowPosition, columnPosition)73 }7475 private fun play(char: Char, rowPosition: Int, columnPosition: Int): Boolean {76 return rows[rowPosition.toIndex].play(char) ||77 columns[columnPosition.toIndex].play(char) ||78 (rowPosition == columnPosition && diagonal.play(char)) ||79 ((rowPosition + columnPosition) == (n + 1) && antiDiagonal.play(char))80 }8182 private val Int.toIndex get() = this - 18384 class Slot(private val n: Int) {8586 private var number = 08788 fun play(char: Char): Boolean {89 val increment = if (char == 'X') 1 else -190 val target = if (char == 'X') n else -n9192 number += increment93 return number == target94 }95 }96}
Write a function which, given a sequence of digits 2–9 and a dictionary of n words, reports all words described by this sequence when typed in on a standard telephone keypad. For the sequence 269 you should return any, box, boy, and cow, among other words.
1fun test() {2 println(words(arrayOf(2, 6, 9)))3 println(words(arrayOf(7, 6, 7, 7)))4}56fun words(inputDigits: Array<Int>): List<String> {7 val words = setOf(8 "pops",9 "any",10 "box",11 "boy",12 "cow",13 "dad",14 "mom",15 )16 val charToDigitMapping = mapOf(17 'a' to 2,18 'b' to 2,19 'c' to 2,20 'd' to 3,21 'e' to 3,22 'f' to 3,23 'g' to 4,24 'h' to 4,25 'i' to 4,26 'j' to 5,27 'k' to 5,28 'l' to 5,29 'm' to 6,30 'n' to 6,31 'o' to 6,32 'p' to 7,33 'q' to 7,34 'r' to 7,35 's' to 7,36 't' to 8,37 'u' to 8,38 'v' to 8,39 'w' to 9,40 'x' to 9,41 'y' to 9,42 'z' to 9,43 )4445 val matchingWords = mutableListOf<String>()46 words.forEach { word ->47 word.forEachIndexed { index, char ->48 val charDigit = charToDigitMapping[char] ?: return@forEach49 val inputDigitAtIndex = inputDigits.getOrNull(index) ?: return@forEach50 if (charDigit != inputDigitAtIndex) {51 return@forEach52 }53 }5455 matchingWords += word56 }5758 return matchingWords59}
Two strings X and Y are anagrams if the letters of X can be rearranged to form Y. For example, silent/listen, and incest/insect are anagrams. Give an efficient algorithm to determine whether strings X and Y are anagrams.
1fun test() {2 println(isAnagram("silent", "listen"))3 println(isAnagram("silence", "listen"))4 println(isAnagram("incest", "insect"))5 println(isAnagram("incest", "insects"))6}78fun isAnagram(s1: String, s2: String): Boolean {9 val map = mutableMapOf<Char, Int>()10 11 s1.forEach { char ->12 map[char] = map.getOrPut(char) { 0 } + 113 }14 15 s2.forEach { char ->16 if (map.containsKey(char)) {17 map[char] = map.getValue(char) - 118 } else {19 return false20 }21 }22 23 map.values.forEach { number ->24 if (number != 0) {25 return false26 }27 }28 29 return true30}
Design a dictionary data structure in which search
, insertion
, and deletion
can all be processed in O(1) time in the worst case. You may assume the set elements are integers drawn from a finite set 1,2,...,n and initialization can take O(N) time.
1fun test() {2 val dictionary = Dictionary(10)3 dictionary.insert(4)4 println(dictionary.search(4))5 dictionary.delete(4)6 println(dictionary.search(4))7}89class Dictionary(val capacity: Int) {10 11 private val array = Array<Int?>(capacity) { null }12 13 fun search(element: Int): Int? {14 return array[element.toIndex]15 }1617 fun delete(element: Int) {18 array[element.toIndex] = null19 }2021 fun insert(element: Int) {22 array[element.toIndex] = element23 }24 25 private val Int.toIndex get() = this - 126}
The maximum depth of a binary tree is the number of nodes on the path from the root down to the most distant leaf node. Give an O(n) algorithm to find the maximum depth of a binary tree with n nodes.
1fun test() {2 val root = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = BNode(7 element = 2,8 left = BNode(9 element = 1,10 left = null,11 right = null12 ),13 right = null14 ),15 right = null16 ),17 right = BNode(18 element = 7,19 left = null,20 right = BNode(21 element = 8,22 left = null,23 right = null24 )25 )26 )2728 println(maxDepth(root))29}3031fun maxDepth(root: BNode?): Int {32 if (root == null) {33 return 034 }3536 val leftDepth = maxDepth(root.left)37 val rightDepth = maxDepth(root.right)3839 return max(leftDepth, rightDepth) + 140}
Two elements of a binary search tree have been swapped by mistake. Give an O(n) algorithm to identify these two elements so they can be swapped back.
1fun test() {2 val root = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = BNode(7 element = 8, // Swapped8 left = null,9 right = null10 ),11 right = null12 ),13 right = BNode(14 element = 7,15 left = null,16 right = BNode(17 element = 2, // Swapped18 left = null,19 right = null20 )21 )22 )2324 println(root)25 val recoverer = SwappedNodeRecoverer()26 recoverer.recover(root)27 println(root)28}2930class SwappedNodeRecoverer {3132 private var prev: BNode? = null33 private var first: BNode? = null34 private var second: BNode? = null3536 fun recover(root: BNode?) {37 recoverInternal(root)3839 if (first != null) {40 val firstElement = first!!.element41 first!!.element = second!!.element42 second!!.element = firstElement43 }44 }4546 private fun recoverInternal(root: BNode?) {47 if (root == null) {48 return49 }5051 recoverInternal(root.left)5253 if (prev != null && root.element < prev!!.element) {54 if (first == null) {55 // This handles adjacent node case56 first = prev57 second = root58 } else {59 second = root60 }61 }6263 prev = root6465 recoverInternal(root.right)66 }67}6869data class BNode(70 var element: Int,71 var left: BNode? = null,72 var right: BNode? = null,73)
Given two binary search trees, merge them into a doubly linked list in sorted order.
1fun test() {2 val s1 = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = null,7 right = null8 ),9 right = BNode(10 element = 7,11 left = null,12 right = null13 )14 )1516 val s2 = BNode(17 element = 10,18 left = BNode(19 element = 8,20 left = null,21 right = null22 ),23 right = BNode(24 element = 12,25 left = null,26 right = null27 )28 )2930 println(merge(s1, s2))31}3233fun merge(tree1: BNode, tree2: BNode): Node {34 val tree1Nodes = toList(tree1)35 val tree2Nodes = toList(tree2)3637 var i1 = 038 var i2 = 039 val list = Node(Integer.MIN_VALUE) // Sentinel40 var lastListNode = list41 val addListNode = { node: Node ->42 lastListNode.next = node43 lastListNode = node44 }4546 while (i1 < tree1Nodes.size || i2 < tree2Nodes.size) {47 val tree1Node = tree1Nodes.getOrNull(i1)48 val tree2Node = tree2Nodes.getOrNull(i2)4950 if (tree1Node == null && tree2Node != null) {51 addListNode(Node(tree2Node.element))52 i2++53 } else if (tree2Node == null && tree1Node != null) {54 addListNode(Node(tree1Node.element))55 i1++56 } else if (tree1Node!!.element < tree2Node!!.element) {57 addListNode(Node(tree1Node.element))58 i1++59 } else if(tree1Node.element > tree2Node.element) {60 addListNode(Node(tree2Node.element))61 i2++62 } else {63 addListNode(Node(tree1Node.element))64 i1++65 addListNode(Node(tree2Node.element))66 i2++67 }68 }6970 return list.next!!71}7273fun toList(tree: BNode?): MutableList<BNode> {74 if (tree == null) {75 return mutableListOf()76 }7778 return (toList(tree.left) + tree + toList(tree.right)).toMutableList()79}8081data class BNode(82 val element: Int,83 var left: BNode?,84 var right: BNode?,85)8687data class Node(88 val element: Int,89 var next: Node? = null90)
Describe an O(n)-time algorithm that takes an n-node binary search tree and constructs an equivalent height-balanced binary search tree. In a height-balanced binary search tree, the difference between the height of the left and right subtrees of every node is never more than 1.
1fun test() {2 val root = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = BNode(7 element = 2,8 left = BNode(9 element = 1,10 left = null,11 right = null12 ),13 right = null14 ),15 right = null16 ),17 right = BNode(18 element = 7,19 left = null,20 right = BNode(21 element = 8,22 left = null,23 right = null24 )25 )26 )2728 println("Unbalanced tree: $root")29 println("Balanced tree: " + toBalancedTree(root))30}3132fun toBalancedTree(root: BNode): BNode {33 val nodes = toList(root)34 return sortedListToBinaryTree(nodes)!!35}3637fun sortedListToBinaryTree(list: List<BNode>): BNode? {38 if (list.isEmpty()) {39 return null40 }4142 val mid = list.size / 243 val root = BNode(list[mid].element)44 root.left = sortedListToBinaryTree(list.slice(0 until mid))45 root.right = sortedListToBinaryTree(list.slice((mid + 1) until list.size))4647 return root48}4950fun toList(tree: BNode?): MutableList<BNode> {51 if (tree == null) {52 return mutableListOf()53 }5455 return (toList(tree.left) + tree + toList(tree.right)).toMutableList()56}5758data class BNode(59 val element: Int,60 var left: BNode? = null,61 var right: BNode? = null,62)
Find the storage efficiency ratio (the ratio of data space over total space) for each of the following binary tree implementations on n nodes:
Storage efficiency ratio = /fracSpace used to store dataSpace used to store data and pointers For option A: Space taken by a single node = (2 pointers 4 bytes) + (1 pointer 4 bytes) + (4 bytes) = 16 bytes Given n nodes Storage efficiency ratio = 16n4n=41
For option B: Space taken by a single internal node = 2 pointers * 2 bytes = 4 bytes Space taken by a single leaf node = 4 bytes In a full tree, given n leaf nodes, there are n−1 internal nodes. Storage efficiency ratio = 4n+4(n−1)4n=4n+4n−44n=8n−44n=8n4n=/12 (As n gets larger, the constant 4 doesn't matter, hence why it was dropped)
Option B has a higher storage efficiency to option A.
Give an O(n) algorithm that determines whether a given n-node binary tree is height-balanced (see Problem 3-15).
1fun test() {2 val root = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = BNode(7 element = 2,8 left = BNode(9 element = 1,10 left = null,11 right = null12 ),13 right = null14 ),15 right = null16 ),17 right = BNode(18 element = 7,19 left = null,20 right = BNode(21 element = 8,22 left = null,23 right = null24 )25 )26 )2728 println(isBalanced(root))29}3031fun isBalanced(root: BNode?): Pair<Boolean, Int> {32 if (root == null) {33 return true to 034 }3536 val (isLeftBalanced, leftHeight) = isBalanced(root.left)37 if (!isLeftBalanced) {38 return false to 039 }4041 val (isRightBalanced, rightHeight) = isBalanced(root.right)42 if (!isRightBalanced) {43 return false to 044 }4546 if (abs(leftHeight - rightHeight) > 1) {47 return false to 048 }4950 return true to (max(leftHeight, rightHeight) + 1)51}5253data class BNode(54 val element: Int,55 val left: BNode?,56 val right: BNode?,57)
Describe how to modify any balanced tree data structure such that search, insert, delete, minimum, and maximum still take O(logn) time each, but successor and predecessor now take O(1) time each. Which operations have to be modified to support this?
TODO
Suppose you have access to a balanced dictionary data structure that supports each of the operations search, insert, delete, minimum, maximum, successor, and predecessor in O(logn) time. Explain how to modify the insert and delete operations so they still take O(logn) but now minimum and maximum take O(1) time. (Hint: think in terms of using the abstract dictionary operations, instead of mucking about with pointers and the like.)
Use two variables to maintain the maximum and minimum element. On:
Insert(x)
: If x<minimum, set x as new minimum; If x>maximum, set x as new maximum.Delete(x)
: If x=minimum, set minimum=sucessor(x); If x=maximum, set maximum=predecessor(x).Design a data structure to support the following operations:
insert(x,T)
– Insert item x into the set T.delete(k,T)
– Delete the kth smallest element from T.member(x,T)
– Return true iff x∈T.All operations must take O(logn) time on an n-element set.
g
A concatenate operation takes two sets S1 and S2, where every key in S1 is smaller than any key in S2, and merges them. Give an algorithm to concatenate two binary search trees into one binary search tree. The worst-case running time should be O(h), where h is the maximal height of the two trees.
1fun test() {2 val s1 = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = null,7 right = null8 ),9 right = BNode(10 element = 7,11 left = null,12 right = null13 )14 )1516 val s2 = BNode(17 element = 10,18 left = BNode(19 element = 8,20 left = null,21 right = null22 ),23 right = BNode(24 element = 12,25 left = null,26 right = null27 )28 )2930 println(concat(s1, s2))31}3233fun concat(s1: BNode, s2: BNode): BNode {34 val s1RightMostNode = rightMostNode(s1)3536 s1RightMostNode.right = s23738 return s139}4041fun rightMostNode(node: BNode): BNode {42 if (node.right == null) {43 return node44 }4546 return rightMostNode(node.right!!)47}4849data class BNode(50 val element: Int,51 var left: BNode?,52 var right: BNode?,53)
Design a data structure that supports the following two operations:
insert(x)
– Insert item x from the data stream to the data structure.median()
– Return the median of all elements so far.All operations must take O(logn) time on an n-element set.
1fun test() {2 val ds = DataStructure()34 ds.insert(5)5 ds.insert(2)6 ds.insert(3)78 println("Median: ${ds.median()}")9 10 ds.insert(4)11 println("Median: ${ds.median()}")12}1314class DataStructure {1516 /* The head of this queue is the least element with respect to the specified ordering. */17 private val minHeap = PriorityQueue<Int>() // Represents the upper half18 private val maxHeap = PriorityQueue<Int>() // Represents the lower half1920 fun insert(x: Int) {21 if (maxHeap.isEmpty() || x <= -maxHeap.peek()) {22 maxHeap.offer(-x)23 } else {24 minHeap.offer(x)25 }2627 // Balance the heaps to ensure the size difference is at most 128 if (maxHeap.size > minHeap.size + 1) {29 minHeap.offer(-maxHeap.poll())30 } else if (minHeap.size > maxHeap.size) {31 maxHeap.offer(-minHeap.poll())32 }33 }3435 fun median(): Double {36 return if (minHeap.size == maxHeap.size) {37 // If the number of elements is even, take the average of the middle two38 (minHeap.peek() + (-maxHeap.peek())) / 2.039 } else {40 -maxHeap.peek().toDouble()41 }42 }43}
Assume we are given a standard dictionary (balanced binary search tree) defined on a set of n strings, each of length at most l. We seek to print out all strings beginning with a particular prefix p. Show how to do this in O(m⋅l⋅logn) time, where m is the number of strings.
g
An array A is called k-unique if it does not contain a pair of duplicate elements within k positions of each other, that is, there is no i and j such that A[i]=A[j] and ∣j−i∣≤k. Design a worst-case O(n⋅logk) algorithm to test if A is k-unique.
g
In the bin-packing problem, we are given n objects, each weighing at most 1 kilogram. Our goal is to find the smallest number of bins that will hold the n objects, with each bin holding 1 kilogram at most.
g
Suppose that we are given a sequence of n values x1,x2,...,xn and seek to quickly answer repeated queries of the form: given i and j, find the smallest value in xi,...,xj. a. Design a data structure that uses O(n2) space and answers queries in O(1) time. b. Design a data structure that uses O(n) space and answers queries in O(logn) time. For partial credit, your data structure can use O(nlogn) space and have O(logn) query time.
1fun test() {2 val numbers1 = listOf(1, 3, 9, 2, 5)3 println(solutionA(numbers1, 2, 5))4 println(solutionA(numbers1, 1, 3))5 println(solutionB(numbers1, 2, 5))6 println(solutionB(numbers1, 1, 3))7}89fun solutionA(numbers: List<Int>, i: Int, j: Int): Int? {10 val list = Array<Array<Int?>>(numbers.size) { Array(numbers.size) { null } }1112 for (p in numbers.indices) {13 var minimumSoFar = Int.MAX_VALUE1415 for (q in (p+1) until numbers.size) {16 val qthNumber = numbers[q]17 if (qthNumber < minimumSoFar) {18 minimumSoFar = qthNumber19 }2021 list[p][q] = minimumSoFar22 }23 }2425 return list[i-1][j-1]26}2728fun solutionB(numbers: List<Int>, i: Int, j: Int): Int? {29 TODO()30}
Suppose you are given an input set S of n integers, and a black box that if given any sequence of integers and an integer k instantly and correctly answers whether there is a subset of the input sequence whose sum is exactly k. Show how to use the black box O(n) times to find a subset of S that adds up to k.
1fun test() {2 println(findSubset(listOf(3, 5, 8, 2, 1), 6))3 println(findSubset(listOf(2, 3, 5, 8, 6, 4), 20))4}56fun findSubset(s: List<Int>, k: Int): List<Int>? {7 if (!blackBox(s, k)) return null89 var subset = s10 for (i in s.indices) {11 val subsetWithoutIthNumber = subset.filter { it != s[i] }1213 if (blackBox(subsetWithoutIthNumber, k)) {14 subset = subsetWithoutIthNumber15 }16 }1718 return subset19}2021fun blackBox(integers: List<Int>, k: Int): Boolean {22 // We were not told to implement the black box, so we will hack it for the tests23 return if (k == 6) {24 integers.containsAll(listOf(1, 2, 3))25 } else return if (k == 20) {26 integers.containsAll(listOf(2, 8, 6, 4)) || integers.containsAll(listOf(3, 5, 8, 4))27 } else {28 throw IllegalArgumentException()29 }30}
Let A[1..n] be an array of real numbers. Design an algorithm to perform any sequence of the following operations:
Add(i,y)
– Add the value y to the ith number.Partial-sum(i)
– Return the sum of the first i numbers, that is, ∑j=1iA[j].There are no insertions or deletions; the only change is to the values of the numbers. Each operation should take O(logn) steps. You may use one additional array of size n as a work space.
g
Extend the data structure of the previous problem to support insertions and deletions. Each element now has both a ''key'' and a ''value''. An element is accessed by its key, but the addition operation is applied to the values. The ''Partial_sum'' operation is different.
Add(k,y)
– Add the value y to the item with key k.Insert(k,y)
– Insert a new item with key k and value y.Delete(k)
– Delete the item with key k.Partial-sum(k)
– Return the sum of all the elements currently in the set whose key is less than k, that is, ∑i<kxi. The worst-case running time should still be O(nlogn) for any sequence of O(n) operations.
g
You are consulting for a hotel that has n one-bed rooms. When a guest checks in, they ask for a room whose number is in the range [l,h]. Propose a data structure that supports the following data operations in the allotted time:
Initialize(n)
: Initialize the data structure for empty rooms numbered 1,2,...,n, in polynomial time.Count(l, h)
: Return the number of available rooms in [l,h], in O(logn) time.Checkin(l, h)
: In O(logn) time, return the first empty room in [l,h] and mark it occupied, or return NIL if all the rooms in [l,h] are occupied.Checkout(x)
: Mark room x as unoccupied, in O(logn) time.1fun test() {2 val ds = DataStructure()3 ds.initialize(10)4 println(ds.root)56 println("Available in [2, 5]: ${ds.count(2, 5)}")7 val checkIn1 = ds.checkIn(2, 5)8 println("Available in [2, 5]: ${ds.count(2, 5)}")9 val checkIn2 = ds.checkIn(2, 5)10 println("Available in [2, 5]: ${ds.count(2, 5)}")1112 ds.checkOut(checkIn2!!)13 println("Available in [2, 5]: ${ds.count(2, 5)}")14 ds.checkOut(checkIn2)15 ds.checkOut(checkIn1!!)16 println("Available in [2, 5]: ${ds.count(2, 5)}")17}1819class DataStructure() {2021 lateinit var root: BNode2223 fun initialize(n: Int) {24 root = sortedListToBinaryTree((1..n).toList())!!25 }2627 fun sortedListToBinaryTree(list: List<Int>): BNode? {28 if (list.isEmpty()) {29 return null30 }3132 val mid = list.size / 233 val root = BNode(list[mid])34 root.left = sortedListToBinaryTree(list.slice(0 until mid))35 root.right = sortedListToBinaryTree(list.slice((mid + 1) until list.size))3637 return root38 }3940 fun count(l: Int, h: Int): Int {41 return countInRange(l, h, root)42 }4344 fun checkIn(l: Int, h: Int): Int? {45 val first = findFirstInRange(l, h, root)46 first?.isCheckedIn = true47 return first?.element48 }4950 fun checkOut(x: Int) {51 find(x, root)?.isCheckedIn = false52 }5354 fun countInRange(l: Int, h: Int, node: BNode?): Int {55 if (node == null) {56 return 057 }5859 val count = if (node.element in l..h && !node.isCheckedIn) 1 else 06061 return if (node.element in l..h) {62 count + countInRange(l, h, node.left) + countInRange(l, h, node.right)63 } else if (node.element < l) {64 count + countInRange(l, h, node.right)65 } else {66 count + countInRange(l, h, node.left)67 }68 }6970 fun findFirstInRange(l: Int, h: Int, node: BNode?): BNode? {71 if (node == null) {72 return null73 }7475 if (node.element in l..h && !node.isCheckedIn) {76 return node77 }7879 return if (node.element < l) {80 findFirstInRange(l, h, node.right)81 } else if (node.element > h) {82 findFirstInRange(l, h, node.left)83 } else {84 findFirstInRange(l, h, node.left) ?: findFirstInRange(l, h, node.right)85 }86 }8788 fun find(x: Int, node: BNode?): BNode? {89 if (node == null) {90 return null91 }9293 return if (node.element == x) {94 node95 } else if (node.element < x) {96 find(x, node.right)97 } else {98 find(x, node.left)99 }100 }101}102103data class BNode(104 val element: Int,105 var isCheckedIn: Boolean = false,106 var left: BNode? = null,107 var right: BNode? = null,108)
Design a data structure that allows one to search, insert, and delete an integer X in O(1) time (i.e., constant time, independent of the total number of integers stored). Assume that 1≤X≤n and that there are m+n units of space available, where m is the maximum number of integers that can be in the table at any one time. (Hint: use two arrays A[1..n] and B[1..m].) You are not allowed to initialize either A or B, as that would take O(m) or O(n) operations. This means the arrays are full of random garbage to begin with, so you must be very careful.
1fun test() {2 val ds = DataStructure(m = 5, n = 10)3 ds.insert(7)4 ds.insert(3)5 ds.insert(6)6 ds.insert(5)7 ds.insert(9)89 println(ds.search(6))10 println(ds.search(9))1112 ds.delete(6)13 ds.delete(9)1415 println(ds.search(6))16 println(ds.search(9))17}1819class DataStructure(20 val m: Int,21 val n: Int22) {2324 val indices = arrayOfNulls<Int>(n + 1)25 val values = arrayOfNulls<Int>(m + 1)26 var k = 02728 fun insert(x: Int) {29 k++30 indices[x] = k31 values[k] = x32 }3334 fun search(x: Int): Int? {35 val index = indices[x] ?: return null36 return values[index]37 }3839 fun delete(x: Int) {40 val xIndex = indices[x] ?: return41 if (k == 0) return4243 val lastValue = values[k]!!44 indices[lastValue] = xIndex45 values[xIndex] = lastValue46 values[k] = null47 indices[x] = null4849 k--50 }51}
What method would you use to look up a word in a dictionary?
Use a hash table: Store the word as the key and the definition as the value.
Imagine you have a closet full of shirts. What can you do to organize your shirts for easy retrieval?
Sort them based on specific property like color.
Write a function to find the middle node of a singly linked list.
1fun test() {2 val node1 = Node("Elvis", null)34 val node2 = Node("Chidera", null)5 node1.next = node267 val node3 = Node("Nnajiofor", null)8 node2.next = node3910 val node4 = Node("Jollof", null)11 node3.next = node41213 val node5 = Node("List", null)14 node4.next = node51516 println(middleElement(node1))17}1819data class Node(20 val element: String,21 var next: Node?22)2324fun middleElement(head: Node): Node {25 var hare: Node? = head26 var tortoise: Node = head2728 while (hare?.next != null) {29 hare = hare.next?.next30 tortoise = tortoise.next!!31 }3233 return tortoise34}
Write a function to determine whether two binary trees are identical. Identical trees have the same key value at each position and the same structure.
1fun test() {2 val s1 = BNode(3 element = 5,4 left = BNode(5 element = 3,6 left = null,7 right = null8 ),9 right = BNode(10 element = 7,11 left = null,12 right = null13 )14 )1516 val s2 = BNode(17 element = 10,18 left = BNode(19 element = 8,20 left = null,21 right = null22 ),23 right = BNode(24 element = 12,25 left = null,26 right = null27 )28 )2930 println(isIdentical(s1, s2))31 println(isIdentical(s1, s1))32 println(isIdentical(s2, s2))33}3435fun isIdentical(tree1: BNode?, tree2: BNode?): Boolean {36 if (tree1 == null || tree2 == null) {37 return tree1 == tree238 }3940 return tree1.element == tree2.element &&41 isIdentical(tree1.left, tree2.left) &&42 isIdentical(tree1.right, tree2.right)43}4445data class BNode(46 val element: Int,47 var left: BNode?,48 var right: BNode?,49)
Write a program to convert a binary search tree into a linked-list.
See Problem 14: Part of the solution involves converting a tree into a linked-list.
Implement an algorithm to reverse a linked list. Now do it without recursion.
1fun test() {2 val node1 = Node("Elvis", null)34 val node2 = Node("Chidera", null)5 node1.next = node267 val node3 = Node("Nnajiofor", null)8 node2.next = node3910 println(reverse(node1))11}1213fun reverse(head: Node): Node {14 var currentNode: Node? = head15 var previousNode: Node? = null1617 while (currentNode != null) {18 val nextNode = currentNode.next19 currentNode.next = previousNode20 previousNode = currentNode21 currentNode = nextNode22 }2324 return previousNode!!25}2627data class Node(28 val element: String,29 var next: Node?30)
What is the best data structure for maintaining URLs that have been visited by a web crawler? Give an algorithm to test whether a given URL has already been visited, optimizing both space and time.
A dictionary that acts as a set:
You are given a search string and a magazine. You seek to generate all the characters in the search string by cutting them out from the magazine. Give an algorithm to efficiently determine whether the magazine contains all the letters in the search string.
1fun test() {2 val node1 = Node("Elvis", null)34 println(containsAllCharacters("Elvis", "The Elfs are very happy"))5 println(containsAllCharacters("Elvis", "The Elfs are very happy because it is christmas"))6}78fun containsAllCharacters(string: String, magazine: String): Boolean {9 val map = mutableMapOf<Char, Int>()1011 string.forEach { char ->12 map[char] = map.getOrDefault(char, 0) + 113 }1415 var matched = 016 magazine.forEach { char ->17 val count = map[char] ?: 01819 if (count > 0) {20 matched++21 map[char] = count - 122 }2324 if (matched == string.length) {25 return true26 }27 }2829 return false30}
Reverse the words in a sentence—that is, “My name is Chris” becomes “Chris is name My.” Optimize for time and space.
1fun test() {2 println(reverse("My name is Chris"))3}45fun reverse(sentence: String): String {6 val words = sentence.split(" ")7 var reversedSentence = ""89 var i = words.lastIndex10 while (i >= 0) {11 val word = words[i]12 reversedSentence += word13 i--1415 if (i >= 0) {16 reversedSentence += " "17 }18 }1920 return reversedSentence21}
Determine whether a linked list contains a loop as quickly as possible without using any extra storage. Also, identify the location of the loop.
1fun test() {2 val node1 = Node("Elvis", null)34 val node2 = Node("Chidera", null)5 node1.next = node267 val node3 = Node("Nnajiofor", null)8 node2.next = node3910 val node4 = Node("Jollof", null)11 node3.next = node41213 val node5 = Node("List", null)14 node4.next = node51516 node5.next = node31718 println(detectLoopLocation(node1)?.element)19}2021data class Node(22 val element: String,23 var next: Node?24)2526/**27* https://en.wikipedia.org/wiki/Cycle_detection#:~:text=Floyd's%20cycle%2Dfinding%20algorithm%20is,The%20Tortoise%20and%20the%20Hare.28*/29fun detectLoopLocation(head: Node): Node? {30 var hare: Node? = head31 var tortoise: Node? = head3233 while (hare != null) {34 hare = hare.next?.next35 tortoise = tortoise?.next3637 if (hare == tortoise) {38 break39 }40 }4142 if (head.next != null && hare == tortoise) {43 hare = head44 while (hare != tortoise) {45 hare = hare?.next46 tortoise = tortoise?.next47 }4849 return tortoise!!50 }5152 return null53}
You have an unordered array X of n integers. Find the array M containing n elements where Mi is the product of all integers in X except for Xi. You may not use division. You can use extra memory. (Hint: there are solutions faster than O(n2).)
1fun test() {2 println(transform(arrayOf(3, 5, 4)).toList())3 println(transform(arrayOf(2, 3, 4, 5, 6)).toList())4}56fun transform(x: Array<Int>): Array<Int> {7 val prefixProducts = Array(x.size) { 0 }8 val suffixProducts = Array(x.size) { 0 }910 var prefixProduct = 111 x.forEachIndexed { i, xi ->12 prefixProducts[i] = prefixProduct13 prefixProduct *= xi14 }1516 var suffixProduct = 117 var i = x.lastIndex18 while (i >= 0) {19 val xi = x[i]20 suffixProducts[i] = suffixProduct21 suffixProduct *= xi22 i--23 }2425 return Array(x.size) {26 prefixProducts[it] * suffixProducts[it]27 }28}
Give an algorithm for finding an ordered word pair (e.g. “New York”) occurring with the greatest frequency in a given webpage. Which data structures would you use? Optimize both time and space.
1fun test() {2 println(findOrderedWordPairWithMaxFrequency("New york is a great city. I love new york."))3 println(findOrderedWordPairWithMaxFrequency("My name is Elvis Chidera. Elvis Chidera is me."))4}56/*7Punctuation marks can mess up the algorithm. Only "." is handled.8 */9fun findOrderedWordPairWithMaxFrequency(text: String): Pair<String, String> {10 val words = text.replace(".", "").split(" ")11 if (words.size <= 1) throw IllegalArgumentException("Text is empty or only has one word")1213 val wordPairs = mutableMapOf<Pair<String, String>, Int>()1415 var i = 116 while (i < words.size) {17 val previousWord = words[i - 1]18 val currentWord = words[i]19 val wordPair = previousWord to currentWord2021 wordPairs[wordPair] = wordPairs.getOrDefault(wordPair, 1) + 122 i++23 }2425 var maxFrequency = 026 var wordPairWithMaxFrequency: Pair<String, String>? = null27 wordPairs.forEach { (wordPair, frequency) ->28 if (frequency > maxFrequency) {29 maxFrequency = frequency30 wordPairWithMaxFrequency = wordPair31 }32 }3334 return wordPairWithMaxFrequency!!35}
This chapter discusses sorting, algorithm design paradigms espoused by sorting algorithms and how sorting can be applied in solving other problems.
The first algorithm design paradigm is the application of sorting as an initial step to simplify another computational problem. e.g.
Pairwise comparison (i.e. is a > or < or = to b) is typical abstracted away into a comparison function that is then passed to the sorting algorithm. This allows the sorting algorithm to be independent of application-specific comparison concerns like:
An example comparison function:
1/**2* Returns a negative integer if `e1` is less than `e2`.3* Returns zero if `e1` is equal `e2`.4* Returns a positive integer if `e1` is greater than `e2`.5*/6fun compare(e1: Element, e2: Element): Int {78}
Stable sorting algorithms maintain the relative order of elements with equal keys. That is, a sorting algorithm is stable if whenever there are two elements R and S with the same key and with R appearing before S in the original list, R will appear before S in the sorted list.
Few fast algorithms are naturally stable. Stability can be achieved for any sorting algorithm by adding the initial position as a secondary key.
When equal elements are indistinguishable, such as with integers, or more generally, any data where the entire element is the key, stability is not an issue.
A heap is a tree-based data structure that satisfies the heap property:
The heap is one maximally efficient implementation of a priority queue ADT. In a heap, the highest (or lowest) priority element is always stored at the root. A heap is a useful data structure when it is necessary to repeatedly remove the object with the highest (or lowest) priority.
However, a heap is not a sorted structure; it can be regarded as being partially ordered: there is no implied ordering between siblings or cousins and no implied sequence for an in-order traversal. The heap relation mentioned above applies only between nodes and their parents, grandparents, etc.
Heaps are usually implemented with an array:
For a binary heap, in the array:
This can be generalized into:
Given a node at index i,
Its left child is at index 2i+1
Its right child is at index 2i+2
And its parent is at index ⌊(i−1)/2⌋
This simple indexing scheme makes it efficient to move "up" or "down" the tree.
This implicit structure trades space efficiency for flexibility. e.g. With pointers, we can move subtrees around by changing a single pointer.
To avoid potentially requiring a 2n−1 array to store an n-element heap, it's required that the binary heap is complete with its element inserted from left to right. This structural constraint allows us to store the n-element heap with the first n indices of the array. When a heap is a complete binary tree, it has the smallest possible height — a heap with N nodes and a branches for each node always has logaN height.
1/**2 * Put elements of [array] in heap order, in-place.3 *4 * Consider the array in reverse order, starting from the last (nth) position. It represents a leaf of the tree and5 * so trivial obeys the heap property (i.e. it's larger than its non-existent children). The same is the case for6 * the last n/2 positions in the array, because all are leaves.7 *8 * If we continue to walk backwards through the array we will eventually encounter an internal node with children.9 * This element may not obey the heap property (because it might be smaller than its children),10 * but its children represent well-formed (if small) heaps. This situation is exactly what [siftDown] was designed11 * to do: given a "damaged" binary heap, where the max-heap property (no child is greater than its parent) holds12 * everywhere except possibly between the root node and its children, repair it to produce an undamaged heap.13 *14 * To see that this takes O(n) time, count the worst-case number of [siftDown] iterations.15 * The last half of the array requires zero iterations, the preceding quarter requires at most one iteration,16 * the eighth before that requires at most two iterations, the sixteenth before that requires at most three, and so on.17 */18private fun heapify(array: ArrayList<Int>) {19 // Start is initialized to the first leaf node20 // Find the parent of the last element.21 var start = iParent(array.lastIndex) + 12223 while (start > 0) {24 // Go to the last non-heap node25 start -= 126 // Sift down the node at index 'start' to the proper place such that all nodes below the start index are in heap order27 siftDown(array, start, array.lastIndex)28 }2930 // After sifting down the root all nodes/elements are in heap order.31}3233private fun siftUp(array: ArrayList<Int>, index: Int) {34 var index = index3536 while (index > 0) {37 val parentIndex = iParent(index)3839 if (array[parentIndex] < array[index]) {40 swap(array, parentIndex, index)41 index = parentIndex42 } else {43 return44 }45 }46}4748/**49 * Repair the heap whose root element is at index 'start', assuming the heaps rooted at its children are valid.50 *51 * The number of iterations in any one [siftDown] call is bounded by the height of the tree, which is ⌊log2 n⌋ = O(log n).52 */53fun siftDown(array: ArrayList<Int>, rootIndex: Int, endIndex: Int) {54 var rootIndex = rootIndex55 while (iLeftChild(rootIndex) <= endIndex) { // While the root has at least one child56 var biggerChildIndex = iLeftChild(rootIndex)5758 // If there is right child and that child is greater59 if (iRightChild(rootIndex) <= endIndex && array[biggerChildIndex] < array[iRightChild(rootIndex)]) {60 biggerChildIndex = iRightChild(rootIndex)61 }6263 if (array[rootIndex] < array[biggerChildIndex]) {64 swap(array, rootIndex, biggerChildIndex)65 rootIndex = biggerChildIndex66 // Repeat to continue sifting down the child now.67 } else {68 // The root holds the largest element. Since we may assume the heaps rooted at the children are valid,69 // this means that we are done.70 return71 }72 }73}7475private fun swap(array: ArrayList<Int>, i: Int, j: Int) {76 val temp = array[i]77 array[i] = array[j]78 array[j] = temp79}8081fun iParent(i: Int): Int {82 return floor(i / 2.0).toInt()83}8485fun iLeftChild(i: Int): Int {86 return (i * 2) + 187}8889fun iRightChild(i: Int): Int {90 return (i * 2) + 291}9293data class Heap(private val array: ArrayList<Int>) {9495 init {96 heapify(array)97 }9899 /**100 * Add the new element at the end of the heap, in the first available free space.101 * If this violates the heap property, sift up the new element until the heap property has been reestablished.102 * Since the heap is balanced, the height is $log_2 n$ and hence each insertion costs at most $O(log_2 n)$.103 */104 fun insert(element: Int) {105 array.add(element)106 siftUp(array, array.lastIndex)107 }108109 /**110 * Remove the root and insert the last element of the heap in the root.111 * If this violates the heap property, sift down the new root to reestablish the heap property.112 */113 fun pop(): Int {114 val root = array[0]115 val lastChild = array.removeLast()116 array[0] = lastChild117 siftDown(array, 0, array.lastIndex)118 return root119 }120121 /**122 * Remove the root and put the new element in the root and sift down.123 * When compared to pop followed by insert, this avoids a sift up step.124 */125 fun replaceRoot(element: Int) {126 array[0] = element127 siftDown(array, 0, array.lastIndex)128 }129}
Heapsort is an in-place comparison-based sorting algorithm which can be thought of as "an implementation of selection sort using the right data structure." Like selection sort, heapsort divides its input into a sorted and an unsorted region, and it iteratively shrinks the unsorted region by extracting the largest element from it and inserting it into the sorted region. Unlike selection sort, heapsort does not waste time with a linear-time scan of the unsorted region; rather, heap sort maintains the unsorted region in a heap data structure to efficiently find the largest element in each step.
Although somewhat slower in practice on most machines than a well-implemented quicksort, it has the advantages of very simple implementation and a more favorable worst-case O(nlogn) runtime. Most real-world quicksort variants include an implementation of heapsort as a fallback should they detect that quicksort is becoming degenerate.
Heapsort is an example of the second algorithm design paradigm: Use an appropriate data structure.
1/**2 * The steps are:3 *4 * 1. Call the [heapify] function on the array. This builds a heap from an array in O(n) operations.5 * 2. Swap the first element of the array (the largest element in the heap) with the final element of the heap. Decrease the considered range of the heap by one.6 * 3. Call the [siftDown] function on the array to move the new first element to its correct place in the heap.7 * 4. Go back to step (2) until the remaining array is a single element.8 *9 * The [heapify] operation is run once, and is O(n) in performance.10 * The [siftDown] function is called n times and requires O(log n) work each time, due to its traversal starting from the root node.11 * Therefore, the performance of this algorithm is O(n + n log n) = O(n log n).12 */13fun heapsort(array: ArrayList<Int>): ArrayList<Int> {14 // Build the heap in the array so that largest value is at the root15 heapify(array)16 var endIndex = array.lastIndex1718 // The following loop maintains the invariants that every element between 0 to endIndex is a heap, and19 // every element beyond endIndex is greater than everything before it (in sorted order).20 while (endIndex > 0) {21 // [0] is the root and largest value. The swap moves it in front of the sorted elements.22 swap(array, 0, endIndex)23 // The heap size is reduced by one.24 endIndex--25 // The swap ruined the heap property, so restore it.26 siftDown(array, 0, endIndex)27 }2829 return array30}