Cse 3318 - W3 - 06172024
Cse 3318 - W3 - 06172024
Cse 3318 - W3 - 06172024
Week of 06/17/2024
1
Using Recursion
Any problem that can be solved recursively can also be solved iteratively.
2
Designing Algorithms
Insertion sort uses an incremental approach
having sorted the subarray A[1..j – 1], we inserted the single element A[j]
into its proper place, yielding the sorted subarray A[1..j]
3
Designing Algorithms
Many useful algorithms are recursive in structure
To solve a given problem, an algorithm calls itself recursively one or more times to deal
with closely related subproblems.
They break the problem into several subproblems that are similar to the
original problem but smaller in size, solve the subproblems recursively and
the combine these solutions to create a solution to the original problem.
4
The Divide and Conquer Approach
Divide the problem into a number of subproblems that are
smaller instances of the same problem.
6
Merge Sort
Merge Sort is a sorting algorithm based on divide and conquer.
Its worst-case running time has a lower order of growth than insertion
sort.
Divide by splitting into two subarrays A[p .. q] and A[q+1 .. r] where q is the
halfway point of A[p .. r]
Combine by merging the two sorted subarrays A[p .. q] and A[q+1 .. r] to produce
a single sorted subarray A[p .. r]. To accomplish this step, we’ll define a procedure
Merge(A, p, q, r)
The recursion bottoms out when the subarray has just 1 element, so that it’s trivially
sorted – can't divide 1 value.
8
Merge Sort
MergeSort(A, p, r)
q = (p + r)/2 // divide
return 0;
}
10
{12, 11, 5, 13, 7, 6}
mergeSort(arr, L, M);
merge(arr, L, M, R);
}
}
11
p = 1, q = 7, r = 13
n1 = q – p + 1 = 7-1+1 = 7
n2 = r – q = 13 – 7 = 6
L[1…n1 + 1] = L[1…8]
R[1…n2 + 1] = R[1…7] 12
Merge – Part 1
Merge – Part 3
void merge(int arr[], int left, int middle, int right) while (i < n1)
{ {
int i, j, k; Merge – Part 2 arr[k] = L[i];
int n1 = middle - left + 1; i = 0; i++;
int n2 = right - middle; j = 0;
k++;
k = left;
int L[n1], R[n2]; }
while (i < n1 && j < n2)
for (i = 0; i < n1; i++) {
L[i] = arr[left + i]; if (L[i] <= R[j]) while (j < n2)
for (j = 0; j < n2; j++) { {
R[j] = arr[middle + 1 + j]; arr[k] = L[i]; arr[k] = R[j];
i++; j++;
} k++;
else
}
{
arr[k] = R[j];
}
j++;
}
k++;
}
13
{12, 11, 5, 13, 7, 6} M = (L + R)/2
14
int main(void)
{
int arr[] = {12, 11, 5, 13, 7, 6};
int arr_size = sizeof(arr)/sizeof(arr[0]);
C-merge with L=3 M=3 R=4 E-merge with L=0 M=2 R=5 15
Merge – Part 1
20
i = 0; B-merge with
j = 0; Merge – Part 2 left=0 middle=1 right=2
k = left;
L = {11,12} R = {5} n1=2 n2=1
while (i < n1 && j < n2) {11, 12, 5, 13, 7, 6}
{
if (L[i] <= R[j]) {5, 11, 12, 13, 7, 6}
{ while (i < n1)
arr[k] = L[i]; {
i++; arr[k] = L[i];
i++;
}
k++;
else }
{ Merge – Part 3
arr[k] = R[j]; while (j < n2)
j++; {
arr[k] = R[j];
}
j++;
k++; k++;
} } 21
Merge – Part 1
22
i = 0; C-merge with
j = 0; Merge – Part 2 left=3 middle=3 right=4
k = left;
L = {13} R = {7} n1=1 n2=1
while (i < n1 && j < n2) {5, 11, 12, 13, 7, 6}
{
if (L[i] <= R[j]) {5, 11, 12, 7, 13, 6}
{ while (i < n1)
arr[k] = L[i]; {
i++; arr[k] = L[i];
i++;
}
k++;
else }
{ Merge – Part 3
arr[k] = R[j]; while (j < n2)
j++; {
arr[k] = R[j];
}
j++;
k++; k++;
} } 23
Merge – Part 1
24
i = 0; D-merge with
j = 0; Merge – Part 2 left=3 middle=4 right=5
k = left;
L = {7,13} R = {6} n1=2 n2=1
while (i < n1 && j < n2) {5, 11, 12, 7, 13, 6}
{
if (L[i] <= R[j]) {5, 11, 12, 6, 7, 13}
{ while (i < n1)
arr[k] = L[i]; {
i++; arr[k] = L[i];
i++;
}
k++;
else }
{ Merge – Part 3
arr[k] = R[j]; while (j < n2)
j++; {
arr[k] = R[j];
}
j++;
k++; k++;
} } 25
Merge – Part 1
26
i = 0; E-merge with
j = 0; Merge – Part 2 left=0 middle=2 right=5
k = left;
L = {5,11,12} R = {6,7,13} n1=3 n2=3
while (i < n1 && j < n2) {5, 11, 12, 6, 7, 13}
{
if (L[i] <= R[j]) {5, 6, 7, 11, 12, 13}
{ while (i < n1)
arr[k] = L[i]; {
i++; arr[k] = L[i];
i++;
}
k++;
else }
{ Merge – Part 3
arr[k] = R[j]; while (j < n2)
j++; {
arr[k] = R[j];
}
j++;
k++; k++;
} } 27
A merge {12} with {11} {12, 11, 5, 13, 7, 6}
{11, 12, 5, 13, 7, 6}
B merge {11,12} with {5} {11, 12, 5, 13, 7, 6}
{5, 11, 12, 13, 7, 6}
C merge {13} with {7} {5, 11, 12, 13, 7, 6}
{5, 11, 12, 7, 13, 6}
D merge {7,13} with {6} {5, 11, 12, 7, 13, 6}
{5, 11, 12, 6, 7, 13}
E merge {5,11,12} with {6,7,13} {5, 11, 12, 6, 7, 13}
{5, 6, 7, 11, 12, 13}
29
Asymptotic Notation
Sometimes, we want to say that an algorithm takes at least a certain
amount of time, without providing an upper bound.
If a running time is Ω(f(n)), then for large enough n, the running time is
at least k⋅f(n) for some constant k.
34
Asymptotic Notation
Here's how to think of a running time that Ω(f(n)
35
Asymptotic Notation
The running time is "big-Ω of f(n)"
For example, if you really do have a million dollars in your pocket, you can
truthfully say
Similarly, we can correctly but imprecisely say that the best-case running
time of binary search Ω(1) because we know that it takes at least constant
time.
37
Asymptotic Notation
f(n) = 5n2 + 2n + 1
if f(n) is O(g(n))
f(n) grows asymptotically no faster than g(n) then
g(n) = n2
if f(n) is Ω(g(n))
f(n) grows asymptotically no slower than g(n)
if f(n) is Θ(g(n))
f(n) grows asymptotically at the same rate as g(n)
38
39
Asymptotic Notation
Think of O as an upper bound and Ω as a lower bound.
These bounds can be tight or loose, but we prefer to make them tight
as possible.
If we have tight bounds where O and Ω have the same growth rate,
then we precisely know the growth rate.
Asymptotic Notation
For log2n… log2n
log2n also has O(n) since log2n grows asymptotically no faster than n
log2n also has O(log2n) since log2n grows asymptotically no faster than log2n
41
n2
n
log2n
42
Asymptotic Notation
log2n
For log2n… log2(log2n)
log2n has Ω(log2(log2n)) since log2n grows asymptotically no slower than log2(log2(n))
log2n has Ω(log2n) since log2n grows asymptotically no slower than log2n
43
log2n
log2(log2n)
1 44
n2
n
log2n
log2(log2n)
1 45
Asymptotic Notation
Since we have log2n has n2 n
O(log2n)
and
log2n
Ω(log2n)
log2(log2n)
What if your upper bound is 5 feet 4 inches and your lower bound is 5 feet 4
inches? 47
Asymptotic Notation
If we know the big Θ of a function, is there a reason that we would
want to know a different big O or big Ω for the function?
It seems that if you have the big Θ, you already know the best big-O
and big Ω.
Why would we want to know these other values? Are they significantly
easier to find than the big Θ?
Sometimes, we then can find a reasonable Ω value (again it doesn't have to be tight,
but it shouldn't be too loose).
49
Asymptotic Notation
Let's say we have some task that our program needs to perform.
Often, we don't care about which algorithm we use as long as the asymptotic
complexity of the algorithm is under some limit
If we write an algorithm that gets the job done, but it is hard to analyze, we can often
make simplifying assumptions that make the analysis easier but the bounds looser.
50
Asymptotic Notation
for (i = 1; i <= n; i++)
{
number = result_of_a_very_hard_to_analyze_calculation();
if number < 1
number = 1;
if number > 1
number = n;
for (k = 1; k <= number; k++)
some_simple_stuff
}
51
for (i = 1; i <= n; i++)
{
Asymptotic Notation
number = result_of_a_very_hard_to_analyze_calculation();
if number < 1
number = 1;
if number > 1
outer for loop will loop n times
number = n;
for (k = 1; k <= number; k++) inner for loop will loop
some_simple_stuff worst case = n times
} best case = 1 time
Since the calculation is hard to analyze,
we might not be able to tighten up Ω or O.
Running time is O(n2) and Ω(n)
To win, you must pick a box and eat all of the bugs inside and we are
going to assume that eating less bugs is better than eating more bugs.
53
Asymptotic Notation
Let's examine what we know about Box A
Box A contains between 10 and 20 bugs
While 5 and 10 are both valid lower bounds, we would rather use 10 since it is closer
than 5 to the actual number of bugs in the box.
54
Asymptotic Notation
Let's examine what we know about Box A
Box A contains between 10 and 20 bugs
While 50 and 20 are both valid upper bounds, we would rather use 20 since it is
closer than 50 to the actual number of bugs in the box.
55
Asymptotic Notation
The actual number of bugs in Box A must be between our lower bound of 10
and our upper bound of 20.
But…
Using the same technique we used to analyze Box B, we can determine that
Box B has an upper bound of 40 and a lower bound of 30. 56
Asymptotic Notation
Now we need to analyze the upper and lower bounds of the number of bugs
you will need to eat in the best and worst case scenarios.
Best case scenario is you pick Box A - the actual number of bugs in Box A must
be between our lower bound of 10 and our upper bound of 20.
Worst case scenario is you pick Box B – the actual number of bugs in Box B
must be between our lower bound of 30 and our upper bound of 40.
The best case scenario has both lower and upper bounds.
The worst case scenario has both lower and upper bounds. 57
Asymptotic Notation
Since we cannot distinguish Box A from Box B, then we are faced with
Can we get any closer to the actual number of bugs you have to eat to get that
million dollars?
58
Asymptotic Notation
Best case binary search
Finds search item on the first guess
f(n) = c1
Θ?
Lower bound on f(n) is Ω(1)
Upper bound on f(n) is O(1)
log2n is O(log8n)
This means that for large enough value of n, the running time is at
most k⋅log8n for some constant k. log8n is an upper bound.
62
Asymptotic Notation
Prove this is true log2n is O(log8n)
1
log8n = log2𝑛
log28
When we find two functions that differ by a constant multiplier, we know that we can
always find a k to serve as the upper bound.
Generally, we can effectively ignore constants in asymptotic notation and treat the
functions as equivalent.
63
Asymptotic Notation
Prove this is true log2n is Ω(log8n)
This means that for large enough value of n, the running time is at least k⋅log8n
for some constant k. log8n is an lower bound.
1
That constant, , is already a value for k that shows log8n to be a lower
log28
bound.
65
Asymptotic Notation
Prove this is true: log2n is Θ(log8n)
for a large enough value of n, the running time is at least k1⋅log8n and at
most k2⋅log8n for some constants k1 and k2
We have already found k1 and k2 that satisfies those constraints when we proved O and
Ω notation.
Since we established log2n is O(log8n) and Ω(log8n), we can conclude that it is also Θ(log8n).
66
67
68
Fill in the
values for
log2n for each
value of n
69
Divide and Conquer
Divide and Conquer
Divide and Conquer
Merge Sort
The recursion bottoms out when the subarray has just 1 element, so that it’s trivially
sorted – can't divide 1 value.
{14, 7, 3, 12, 9, 11, 6, 2}
The merge function merges two sorted arrays into a single sorted array.
If the two subarrays have a total of n elements, then how many times
did we examine each element in order to accomplish the merge?
𝑛
The conquer step, where we recursively sort two subarrays of approximately
2
elements each, takes some amount of time, but we'll account for that time when we
consider the subproblems.
The Θ(1) running time for the divide step is a low-order term when compared
with the Θ(n) running time of the combine step. As n increases, the amount of
constant time needed for the divide step will be trivial.
So let's eliminate the divide time as a separate consideration and just take the
divide and combine steps together as Θ(n) time.
To make things more concrete, let's say that the divide and combine steps
together take cn time for some constant c.
Analysis of Merge Sort
To keep things reasonably simple, let's assume that if n > 1, then n is always even.
Why?
𝑛
So that when we need to think about , it's an integer – integer division does not
2
lose anything.
Accounting for the case in which n is odd doesn't change the result in terms of
big-Θ notation.
Analysis of Merge Sort
So now we can think of the running time of Merge Sort on an n-element
subarray as being
the sum of
𝑛
twice the running time of Merge Sort on an -element subarray
2
(for the conquer step)
plus
cn
(for the divide and combine steps—really for just the merging).
Analysis of Merge Sort
𝑛
Now we have to figure out the running time of two recursive calls on
2
Each of these two recursive calls takes twice of the running time of Merge Sort
𝑛 𝑛 𝑐𝑛
on an -element subarray (because we have to halve ) plus to merge.
4 2 2
𝑛 𝑐𝑛
We have two subproblems of size and each takes time to merge.
2 2
𝑛 𝑐𝑛
The total time we spend merging for subproblems of size is 2 * = cn
2 2
Analysis of Merge Sort
n elements cn
𝑛 𝑛 𝑐𝑛
elements elements 2* = cn
2 2 2
𝑛 𝑛 𝑛 𝑛 𝑐𝑛
elements elements elements elements 4* = cn
4 4 4 4 4
𝑛 𝑐𝑛
What would be the merge time for the next level of ? 8* = cn
8 8
Analysis of Merge Sort
As the subproblems/arrays
get smaller, the number of
subproblems doubles at
each "level" of the
recursion, but the merging
time halves.
Z * cn
So what is Z?
cnlog2n + 1
Why + 1?
cnlog2n when n = 1 is 0
You could argue that it takes some time to check for that 1
element.
Analysis of Merge Sort
So we have calculated that the runtime of Merge Sort to be
cnlog2n or cnlog2n + 1
Θ(nlog2n)
void FunctionR(int Z[], int a, int b)
{
if(a > b) void PrintArray(int A[])
{
{ int i = 0;
PrintArray(Z);
for (i = 0; i < 5; i++)
Z[a] -= 10; printf("%d ", A[i]);
a--; printf("\n");
}
b++;
FunctionR(Z, a, b); int main(void)
{
PrintArray(Z); int A[] = {1032, 687, 648, 765, 981};
} int x = 4, y = 0;
} FunctionR(A, x, y);
return 0;
}
void FunctionR(int Z[], int a, int b)
1032 687 648 765 981
{
1032 687 648 765 971
if(a > b)
1032 687 648 755 971
{
1032 687 648 755 971
PrintArray(Z);
Z[a] -= 10;
a--;
b++;
FunctionR(Z, a, b); 0 1 2 3 4
PrintArray(Z); 1032 687 648 765
755 981
971
}
}