Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
11 views84 pages

Cse 3318 - W3 - 06172024

Download as pdf or txt
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 84

CSE 3318

Week of 06/17/2024

Instructor : Donna French

1
Using Recursion
Any problem that can be solved recursively can also be solved iteratively.

A recursive approach is normally chosen in preference to an iterative approach


when the recursive approach more naturally mirrors the problem and results in a
program that's easier to understand and debug.

Another reason to choose a recursive solution is that an iterative solution may


not be apparent.

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]

We are going to examine an alternative design approach called

divide and conquer

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.

These types of algorithms follow a divide and conquer approach

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.

Conquer the subproblems by solving them recursively. If the


subproblems are small enough, just solve them by brute force.

Combine the subproblem solutions to give a solution to the


original problem
5
The Divide and Conquer Approach

Insertion Sort n2 Merge Sort n log2n

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.

Because we are dealing with subproblems, we state each subproblem


as sorting a subarray A[p .. r]

Initially, p = 1 and r = n, but these values change as we recurse


through subproblems
7
Merge Sort
To sort A[p .. r]

Divide by splitting into two subarrays A[p .. q] and A[q+1 .. r] where q is the
halfway point of A[p .. r]

Conquer by recursively sorting the two subarrays A[p .. q] and A[q+1 .. 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)

if p < r // check for base case

q = (p + r)/2 // divide

MergeSort (A, p, q) // conquer left


MergeSort(A, q+1, r) // conquer right

Merge (A, p, q, r) // combine


9
int main()
{
int arr[] = {12, 11, 5, 13, 7, 6};
int arr_size = sizeof(arr)/sizeof(arr[0]);

printf("Given array is \n");


printArray(arr, arr_size);

mergeSort(arr, 0, arr_size - 1);

printf("\nSorted array is \n");


printArray(arr, arr_size);

return 0;
}

10
{12, 11, 5, 13, 7, 6}

void mergeSort(int arr[], int L, int R)


{
if (L < R)
{
int M = (L+R)/2;

mergeSort(arr, L, M);

mergeSort(arr, M+1, R);

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]);

printf("Given array is \n"); void mergeSort(int arr[], int L, int R)


printArray(arr, arr_size); {
if (L < R)
mergeSort(arr, 0, arr_size - 1); {
int M = (L+R)/2;
printf("\nSorted array is \n");
printArray(arr, arr_size); mergeSort(arr, L, M);

return 0; mergeSort(arr, M+1, R);


}
merge(arr, L, M, R);
A-merge with L=0 M=0 R=1 }
}
B-merge with L=0 M=1 R=2 D-merge with L=3 M=4 R=5

C-merge with L=3 M=3 R=4 E-merge with L=0 M=2 R=5 15
Merge – Part 1

void merge(int arr[], int left, int middle, int right)


{
int i, j, k; A-merge with
int n1 = middle - left + 1; left=0 middle=0 right=1
int n2 = right - middle;

int L[n1], R[n2];


{12, 11, 5, 13, 7, 6}
for (i = 0; i < n1; i++)
L[i] = arr[left + i]; L = {12} R = {11}
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
n1 = 1 n2 = 1
18
i = 0; A-merge with
j = 0; Merge – Part 2 left=0 middle=0 right=1
k = left;
L = {12} R = {11} n1=1 n2=1
while (i < n1 && j < n2) {12, 11, 5, 13, 7, 6}
{
if (L[i] <= R[j]) {11, 12, 5, 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++;
} } 19
Merge – Part 1

void merge(int arr[], int left, int middle, int right)


{
int i, j, k; B merge with
int n1 = middle - left + 1; left=0 middle=1 right=2
int n2 = right - middle;
{11, 12, 5, 13, 7, 6}
int L[n1], R[n2];

for (i = 0; i < n1; i++)


L = {11,12} R = {5}
L[i] = arr[left + i];
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
n1 = 2 n2 = 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

void merge(int arr[], int left, int middle, int right)


{
int i, j, k; C-merge with
int n1 = middle - left + 1; left=3 middle=3 right=4
int n2 = right - middle;

int L[n1], R[n2]; {5, 11, 12, 13, 7, 6}


for (i = 0; i < n1; i++)
L[i] = arr[left + i]; L = {13} R = {7}
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
n1 = 1 n2 = 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

void merge(int arr[], int left, int middle, int right)


{
int i, j, k; D-merge with
int n1 = middle - left + 1; left=3 middle=4 right=5
int n2 = right - middle;

int L[n1], R[n2]; {5, 11, 12, 7, 13, 6}


for (i = 0; i < n1; i++)
L[i] = arr[left + i]; L = {7,13} R = {6}
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
n1 = 2 n2 = 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

void merge(int arr[], int left, int middle, int right)


{
int i, j, k; E-merge with
int n1 = middle - left + 1; left=0 middle=2 right=5
int n2 = right - middle;

int L[n1], R[n2]; {5, 11, 12, 6, 7, 13}


for (i = 0; i < n1; i++)
L[i] = arr[left + i]; L = {5,11,12} R = {6,7,13}
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
n1 = 3 n2 = 3

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.

We use big-Ω notation Greek letter "omega."

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)"

Big-Ω notation is used for asymptotic lower bounds, since it bounds


the growth of the running time from below for large enough input
sizes.

Just as Θ(f(n)) automatically implies O(f(n)), it also automatically


implies Ω(f(n)).

So we can say that the worst-case running time of binary search


has Ω(log2n).
36
Asymptotic Notation
We can also make correct, but imprecise, statements using big-Ω notation.

For example, if you really do have a million dollars in your pocket, you can
truthfully say

"I have an amount of money in my pocket, and it's at least 10 dollars."

That is correct, but certainly not very precise.

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.

If we can precisely give the growth rate, then we know Θ.


40
n2
n

Asymptotic Notation
For log2n… log2n

log2n has O(n2) since log2n grows asymptotically no faster than n2

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

We went from loose upper bounds to a tight upper bound

41
n2
n

log2n

42
Asymptotic Notation
log2n
For log2n… log2(log2n)

log2n has Ω(1) since log2n grows asymptotically no slower than 1

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

We went from loose lower bounds to a tight lower bound

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)

we can say that log2n is Θ(log2n)


1
46
Asymptotic Notation
How would you describe your height using asymptotic notation?

Do you have an upper bound of 1 million feet tall?


Do you have a lower bound of 1 inch tall?

Would it be more informative to have an upper bound of 6 feet and a lower


bound of 5 feet?

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 Θ?

Does big Θ give the best big O and big Ω possible?


48
Asymptotic Notation
Typically, if we design an algorithm and perform some quick analysis on it, we will
find a reasonable O value – meaning it doesn't have to be tight, but it shouldn't be
too loose.

Often, that's all we care about.

Sometimes, we then can find a reasonable Ω value (again it doesn't have to be tight,
but it shouldn't be too loose).

If it is critical to have tight bounds, or if the algorithm is easy to analyze, we can


tighten up the bounds on O and Ω until they are the same and then we have Θ.

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

the algorithm is fast enough for our needs.

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)

If we can't tighten up the bounds on Ω or


O, then we can't figure out Θ. 52
Asymptotic Notation
Suppose you are on a game show and are given the following challenge
with the prize being a million dollars.

You are shown 2 identical boxes and are told


Box A contains between 10 and 20 bugs
Box B contains between 30 and 40 bugs

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

We can say that Box A cannot have less than 5 bugs.


That would make 5 a lower bound on the number of bugs in Box A.
This lower bound does not really help us make a decision.
We would rather take 10 as the lower bound.

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

We can say that Box A cannot have more than 50 bugs.


That would make 50 an upper bound on the number of bugs in Box A.
This upper bound does not really help us make a decision.
We would rather take 20 as the upper bound.

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…

we also have Box B to analyze.

Remember that Box B contains between 30 and 40 bugs.

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

overall worst case scenario is the upper bound of all cases


40 bugs
overall best case scenario is the lower bound of all cases
10 bugs

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)

Worst case binary search


Search item not found
f(n) = c1 * log2n + c2
Lower bound on f(n) is Ω(log2n)
Upper bound on f(n) is O(log2n)
59
Asymptotic Notation
Prove this is true

log2n is O(log8n)

This is saying that log2n has an asymptotic upper bound of 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.

Can we come up with a k to multiply log8n by so that it always bounds log2n?


60
61
Asymptotic Notation
Prove this is true So log8n can be written as

log2n is O(log8n) log2𝑛


log8n = which is
log28

Let's remember that 1


log8n = log2𝑛
log28
log𝑏𝑛
logan =
log𝑏𝑎
So log8n is just log2n times a constant

62
Asymptotic Notation
Prove this is true log2n is O(log8n)

1
log8n = log2𝑛
log28

So log8n is just log2n times a constant

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 is saying that log2n has an asymptotic lower bound of Ω(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.

Can we come up with a k to multiply log8n by so that it always lower


bounds log2n?
64
Asymptotic Notation
Prove this is true: log2n is Ω(log8n)

We have already rewritten log8n as log2n


log8n
1
log8n = log2𝑛
log28

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)

If log2n is Θ(log8n), then log2n is "tightly bound" by Θ(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

When using divide-and-conquer to sort, we need to decide what our


subproblems are going to look like.

The full problem is to sort an entire array.

The subproblem is to sort a subarray.


Divide and Conquer
Merge Sort
To sort A[p .. r]

Divide by splitting into two subarrays A[p .. q] and A[q+1 .. r] where q is


the halfway point of A[p .. r]

Conquer by recursively sorting the two subarrays A[p .. q] and A[q+1 .. 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].

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}

{7, 14, 3, 12, 9, 11, 6, 2}

{7, 14, 3, 12, 9, 11, 6, 2}

{3, 7, 12, 14, 9, 11, 6, 2}

{3, 7, 12, 14, 9, 11, 6, 2}

{3, 7, 12, 14, 9, 11, 2, 6}

{3, 7, 12, 14, 2, 6, 9, 11}

{2, 3, 6, 7, 9, 11, 12, 14}


Analysis of Merge Sort
Let's examine the merge function from Merge Sort.

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?

We have to examine each of the elements in order to merge them


together which gives us a merging time of Θ(n).
Analysis of Merge Sort
Given that the merge function runs in Θ(n) time when merging n
elements, how to demonstrate that Merge Sort runs in Θ(nlog2n) time?

We start by thinking about the three parts of divide-and-conquer and


how to account for their running times.

We assume that we are sorting a total of n elements in the entire array.


Analysis of Merge Sort
The divide step takes constant time, regardless of the subarray size.
The divide step just computes the midpoint q of the indices p and r.
(middle = (left+right)/2)
Using 1 with Θ indicates no
In big-Θ notation, we indicate constant time by Θ(1). growth

𝑛
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 combine step merges a total of n elements, taking Θ(n) time.


Analysis of Merge Sort
Let's put the divide and combine steps together.

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

Total merge time for array of this size

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.

The doubling and halving


cancel each other out so
the total merging time is cn
at each level of recursion.
Analysis of Merge Sort
Eventually, we get down to subproblems of size 1 (the base case)

We have to spend Θ(1) time to sort subarrays of size 1


we have to test whether p < r (left < right)
this test takes time but that time is constant/the same every time

How many subarrays of size 1 are there?


Since we started with n elements, there must be n base cases.
Since each base case takes Θ(1) time, the base cases take cn time
Analysis of Merge Sort
The total time merging in
Merge Sort is the sum of the
merging times for all the levels.

If there are Z levels in the tree,


then the total merging time is

Z * cn

So what is Z?

Does this pattern look familiar?


cnlog2n Analysis of Merge Sort
Analysis of Merge Sort
You could argue that this runtime should be

cnlog2n + 1

Why + 1?

What happens when we have an array of 1 element?

cnlog2n when n = 1 is 0

Does it take 0 time to sort an array of 1 element?

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

Using Θ notation allows us to discard the constant coefficient c and the


lower order term (+ 1) if we included it . This gives us a runtime of

Θ(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
}
}

main calls Pass 1 with 4 and 0

Pass 1 Pass 2 Pass 3


a=4 a=3 a=2
b=0 b=1 b=2
Z[a]=Z[4] Z[a]=Z[3]
void InsertionSort(int A[], int n)
{
int i, key, j;
{13,5,6,11,12,0}
for (j = 1; j < n; j++) j=1 i=0 key = 5
{
key = A[j]; j=2 i=1 key = 6
i = j - 1;

while (i >= 0 && A[i] > key) j=3 i=2 key = 11


{
A[i + 1] = A[i];
i = i - 1; j=4 i=3 key = 12
}
A[i + 1] = key; j=5 i=4 key = 0
}
} j=6

You might also like