Problem DS-02-07 (b) (e) Suppose you need to generate a random

I Problem DS-02-07 (b) (e) Suppose you need to generate a random permutation of
the first N integers. For example, {4, 3, 1, 5, 2} and {3, 1, 4, 2, 5} are legal permutation,
but {5, 4, 1, 2, 1} is not, because one number {1} is duplicated and another {3} is missing.
This routine is often used in simulation of algorithms. We assume the existence of a
random number generator, RandInt(i, j), which generates integers between i and j with
equal probability. Here are three algorithms:
1. Fill the array A from A[0] to A[N − 1] as follows: To fill A[i], generate random
numbers until you get one that is not already in A[0], A[1], . . . , A[i − 1].
2. Same as algorithm (1), but keep an extra array called U sed array. When a random
number, Ran, is first put in the array A, set U sed[Ran] = 1. This means that
when filling A[i] with a random number, you can test in one step to see whether
the random number has been used, instead of the (possibly) i steps in the first
algorithm.
3. Fill the array such that A[i] = i + 1. Then
for( i=1; i<N; i++)
Swap( &A[i], &A[RandInt(0,i)]);
(b) Give as accurate (Big-Oh) an analysis as you can of the expected running time of each
algorithm.
(e) What is the worst-case running time of each algorithm?
Solution. (b) We assume that the running time for each call of RandInt(i, j) takes
a constant time. For the first algorithm, the time to decide if a random number to be
placed in A[i] has not been used earlier is O(i) because it needs to compare the result
of A[i] generated by random number and every place before A[i] (i.e., between A[0] and
A[i − 1]). The expected number of random numbers that need to be tried is N/(N − i)
and the probability of success is (N − i)/N . Thus the expected number of independent
trials is N/(N − i). The time bound is thus
N
−1
X
i=0
N
−1
N
−1
N
X
X
X
N
N2
1
1
2
2
i·
<
=N
=N
= O(N 2 log N ).
N −i
N
−
i
N
−
i
j
i=0
i=0
j=1
The second algorithm saves a factor of i for each random number, and thus reduces the
time bound to O(N log N ) on average. The third algorithm is clearly linear.
(e) The worst-case running time of algorithms 1 and 2 cannot be bounded because
there is always a finite probability that the program will not terminate by some given
time T . The worst-case running time of the third algorithm is linear because its running
time does not depend on the sequence of random numbers.
1
I Problem DS-02-11 Given an efficient algorithm to determine if there exists an
integer i such that Ai = i in an array of integers A1 < A2 < A3 < · · · < AN . What is the
running time of your algorithm?
Solution. Due to the facts that all integers in the array are distinct and permute in
increasing order, we first give the following observation. For any given i between 1 and N ,
if we check Ai and obtain Ai > i, then we have the result that Aj 6= j for all j > i (e.g.,
suppose we have A5 = 8, it implies that A6 ≥ 9, A7 ≥ 10, A8 ≥ 11, . . . etc.) Oppositely,
if Ai < i, by the similar reasoning we have Aj 6= j for all j < i. Therefore, we can design
the algorithm by using the binary search technique.
#include<math.h>
#include<stdio.h>
int find(int A[], int N)
{
int lower=1, upper=N, mid;
while (lower <= upper)
{
mid = (lower+upper)/2;
if (A[mid]>mid)
upper = mid-1;
else if (A[mid]<mid)
lower = mid+1;
else
return mid;
}
return NOT_FOUND;
}
The worest-case running time of the program is O(log2 N ).
1
I Problem DS-02-13 (a) (b)
(a) Write a program to determine if a positive integer, N , is prime.
(b) In terms of N , what is the worst-case running time of your program? (You should be
√
able to do this in O( N ).)
Solution. (a)
#include<math.h>
#include<stdio.h>
void main()
{
int N,j,f=0;
printf("Please input a positive integer N for testing prime.");
scanf("%d",&N);
for(j=2; j<=sqrt(N); j++)
if (N%j == 0) { f=1; break; }
if (f == 0)
printf("N is a prime")
else
printf("N is not a prime")
}
√
(b) The worest-case running time of the program is O( N ).
1
I Problem DS-02-13 (c) (d) (e)
(a) Write a program to determine if a positive integer, N , is prime.
(b) In terms of N , what is the worst-case running time of your program? (You should be
√
able to do this in O( N ).)
(c) Let B equal the number of bits in the binary representation of N . What is the value
of B?
(d) In therms of B, what is the worst-case running time of your program?
(e) Compare the running times to determine if a 20-bits number and a 40-bits number
are prime.
Solution. For (a) and (b), see DS-02-13 (a) (b).
(c) Since B = dlog2 (N + 1)e, we have B = O(log2 N ).
√
(d) Since N ≤ 2B − 1, we have N ≤ (2B − 1)1/2 < 2B/2 . Thus, the worst-case running
time of the algorithm is O(2B/2 ).
(e) Suppose that a 20-bit number can be tested in time T = O(220/2 ) = O(210 ). Then, a
40-bit number would require about T 0 = O(240/2 ) = O(220 ) = T 2 time.
1
I Supplementary Problem 02-01 Show directly that T (N ) = N 2 + 3N 3 = Θ(N 3 ).
That is, use the definitions of O and Ω to show that T (N ) is in both O(N 3 ) and Ω(N 3 ).
Proof. For proving T (N ) = Θ(h(N )), we need to show that both T (N ) = O(h(N )) and
T (N ) = Ω(h(N )). Since T (N ) = N 2 + 3N 3 , we consider h(N ) = N 3 . By definition, we
can find c1 = 3, c2 = 4, and n0 = 1 such that c1 · h(N ) ≤ T (N ) ≤ c2 · h(N ) holds. We
verify as follows:
T (N ) = N 2 + 3N 3 ≤ 4N 3 = O(N 3 )
if N ≥ n0 = 1
T (N ) = N 2 + 3N 3 ≥ 3N 3 = Ω(N 3 )
if N ≥ n0 = 1
and
Thus, T (N ) = Θ(N 3 ).
1
I Supplementary Problem 02-02 Using the definitions of O and Ω, show that
T (N ) = 6N 2 + 20N = O(N 3 ) but T 0 (N ) = 6N 2 + 20N 6= Ω(N 3 ).
Proof. To show T (N ) = 6N 2 + 20N = O(N 3 ), by definition, we can find c = 1 and
n0 = 9 such that 6N 2 + 20N < N 3 if N ≥ n0 = 9. Thus, T (N ) = O(N 3 ).
On the other hand, for proving T 0 (N ) 6= Ω(g(N )), we need to show that for any given
positive constant c, there exists an integer n0 > 0 such that T 0 (N ) < c · g(N ) when
N ≥ n0 . We now consider T 0 (N ) = 6N 2 + 20N and g(N ) = N 3 . Then, for any given
constant c > 0, we choose n0 as follows:
(
d9/ce, if 0 < c < 1
n0 =
9,
if c ≥ 1.
It is easy to verify that 6N 2 + 20N < c · N 3 if N ≥ n0 (e.g., if c = 9/10, then n0 = 10.
In this case, 6N 2 + 20N <
9
10
· N 3 if N ≥ n0 = 10). Thus, T 0 (N ) 6= Ω(N 3 ).
1
I Supplementary Problem 02-03 Given an algorithm for the following problem.
Given a set of n distinct positive integers, partition the set into two subsets, each of
size n/2, such that the difference between the sums of the integers in the two subsets is
minimized. Determine the time complexity of your algorithm. You may assume that n
is a multiple of 2.
1
I Problem DS-03-03 Swap two adjacent elements by adjusting only the pointers (and
not the data) using: (a) singly linked lists (b) doubly linked lists.
Solution. (a) Suppose that BeforeP points to the node before the two adjacent nodes
that are to be swapped.
void SwapTwoAdjacentNodes_Single(Position BeforeP, List L)
{
Position P, AfterP;
P = BeforeP->Next;
AfterP = P->Next;
P->Next = AfterP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
}
(b) Suppose that the declaration of node for doubly linked list is as follows:
struct Node
{
ElementType Element;
Position Prev;
Position Next;
}
The following procedure can be used to swap node pointed by P and the node after P .
void SwapTwoAdjacentNodes_Double(Position P, List L)
{
Position BeforeP, AftertP;
BeforeP = P->Prev;
AfterP = P->Next;
P->Next = AftertP->Next;
BeforeP->Next = AfterP;
AfterP->Next = P;
P->Next->Prev = P;
P->Prev = AfterP;
AfterP->Prev = BeforeP;
}
1
I Problem DS-03-16 (b) (d) Suppose we have an array-based list A[0 . . . N − 1] and
we want to delete all duplicates. LastPosition is initially N − 1, but get smaller as
elements are deleted. Consider the pseudocode program fragment in the following figure.
The procedure Delete deletes the element in position j and collapses the list.
for(i=0; i<LastPosition; i++)
{
j = i+1;
while(j<LastPosition)
if (A[i] == A[j])
Delete(j);
else
j++;
}
(b) Rewrite this procedure using general list operations.
(d) what is the running time using a linked list implementation?
Solution. (b) Suppose that L is a linked list and the function IsLast(P, L) can be used
to test whether the position P is in the end of L in a constant time (See Page 47 for
definition). Let P, Q, PrevQ be variables with type Position.
P = L->Next;
while(!IsLast(P,L))
{
PrevQ = P;
while(!IsLast(PrevQ,L))
{
Q = PrevQ->Next;
if (P->Element == Q->Element)
{
PrevQ->Next = Q->Next;
free(Q)
}
else
{
PrevQ = Q;
Q = Q->Next;
}
}
}
(d) It is trivial that the running time is O(N 2 ).
1
I Problem DS-03-16 (e) Suppose we have an array-based list A[0 . . . N − 1] and
we want to delete all duplicates. LastPosition is initially N − 1, but get smaller as
elements are deleted. Consider the pseudocode program fragment in the following figure.
The procedure Delete deletes the element in position j and collapses the list.
for(i=0; i<LastPosition; i++)
{
j = i+1;
while(j<LastPosition)
if (A[i] == A[j])
Delete(j);
else
j++;
}
(e) Give an algorithm to solve this problem in O(N log N ) time.
Solution. Sort the list, and make a scan to remove duplicates (which must now be
adjacent).
void swap( int &a, int &b )
{
int temp = a;
a = b;
b = temp;
}
void Quick_Sort( int p[], int left, int right )
{
int divided,i,j;
if( left < right )
{
divided = left;
do{
for( i=left+1; p[i]<p[divided] && i<=right; i++ );
for( j=right; p[j]>p[divided] && j>=left; j-- );
if( i<j ) swap(p[i],p[j]);
}while( i<j );
swap( p[divided], p[j] );
Quick_Sort( p, left, j-1 );
Quick_Sort( p, j+1, right );
}
}
1
void Remove_Duplicate( const int A[], int N )
{
Quick_Sort(A[],0,N-1);
int last = 0;
for( k=1; k<N; k++ )
if( A[last] != A[k] )
{
A[last+1] = A[k];
last = k;
}
}
2
I Problem DS-03-17 An alternative to the deletion strategy we have given is to use
lazy deletion. To delete an element, we merely mark it deleted (using an extra bit field).
The number of deleted and nondeleted elements in the list is kept as part of the data
structure. If there are as many deleted elements as nondeleted elements, we traverse the
entire list, performing the standard deletion algorithm on all marked nodes.
(a) List the advantages and disadvantages of lazy deletion.
(b) Write routines to implement the standard linked list operations using lazy deletion.
Solution. (a) The advantages are that it is simpler to code, and there is a possible
savings if deleted keys are subsequently reinserted (in the same place). The disadvantage
is that it uses more space, because each node needs an extra bit (which is typically a
byte), and unused nodes are not freed.
(b) Let the structure of nodes be defined as follows:
struct Node
{
ElementType Element;
Position
Next;
char
Marked;
}
// set to 1 if deleted, and 0 otherwise
To check the number of deleted elements and the number of nondeleted elements, we
define the following two variable. Initially, both variables are set to be zero.
int Total_Count = 0; // the total number of elements in the list
int Deleted_Count = 0;
// the total number of deleted elements
The following are the relevant functions:
Position Find(ElementType X, List L)
{
Position P = L->Next;
while ( P != NULL && (P->Element != X || P->Marked) )
P = P->Next;
return P;
}
int IsLast(Position P, List L)
{
return P->Next == NULL;
}
1
void Lazy_Deletion(ElementType X, List L)
{
Position P;
P = Find( X, L);
if (P)
{
P->Marked = 1;
Delete_Count++;
if( Total_Count - Deleted_Count == Deleted_Count )
// the number of deleted elements is the same as the number
// of nondeleted elements in the list
Deletion(List L);
}
}
void Deletion(List L)
{
Position P, Provious, TmpCell;
Provious = L;
P = L->Next;
while ( !IsLast(P,L) )
{
if ( P->Marked )
{
TmpCell = P;
P = P->Next;
Previous->Next = P;
free( TmpCell );
Total_Count--;
Delete_Count--;
}
else
{
Provious = P;
P = P->Next;
}
}
}
2
I Problem DS-03-18 (b) Write a program to check for balancing symbols in the
following languages: (b) C languages (/* */, ( ), [ ], { } ).
Solution. For the input C language file F , we suppose that all symbols appeared in
F are valid. Also, the function Read Next Symbol(F ) can be used to read the next
symbol from the current reading position of F , and the status of “end of file” for F can
be checked by using the function EOF(F ). In addition, we suppose that the stack used
in the following program is similar to that defined in page 68 of the textbook excepting
that the data type ElementType is replaced with Symbol.
int Check_Balance(File F)
{
Stack S = CreateStack();
Symbol sym = Read_Next_Symbol(F);
while ( !EOF(F) )
{
switch(sym)
{
case ’/*’:
case ’(’:
case ’[’:
case ’{’:
Push(sym, S);
break;
case ’*/’:
if (Top(S)==’/*’) Pop(S);
else return false;
break;
case ’)’:
if (Top(S)==’(’) Pop(S);
else return false;
break;
case ’]’:
if (Top(S)==’[’) Pop(S);
else return false;
break;
case ’}’:
if (Top(S)==’(’) Pop(S);
else return false;
break;
}
sym = Read_Next_Symbol(F);
}
if ( IsEmpty(S) )
return true;
else
return false;
}
1
I Problem DS-03-22 (a) Propose a data structure that supports the stack Push and
Pop operations and a third operation FindMin, which returns the smallest element in the
data structure, all in O(1) worst case time.
Solution. We suppose that the stack used in the program has structure similar to that
defined in page 68 of the textbook. The idea is the use of the so-called extended stack.
Let E be our extended stack. We will implement E with two stacks. One stack, which
we’ll call S, is used to keep track of the P ush and P op operations, and the other, M ,
keeps track of the minimum. To implement P ush(X, E), we perform P ush(X, S). If X is
smaller than or equal to the top element in stack M , then we also perform P ush(X, M ).
To implement P op(E), we perform P op(S). If X is equal to the top element in stack M ,
then we also P op(M ). F indM in(E) is performed by examining the top of M . All these
operations are clearly O(1).
struct StackRecord
{
int Capacity;
int TopOfStack;
ElementType *Array;
}
typedef struct StackRecord *Stack;
struct ExtendedRecord
{
Stack S;
Stack M;
}
typedef struct ExtendedRecord *ExtendedStack
int IsFull(Stack S)
{
return S->TopOfStack == S->Capacity - 1;
}
int IsEmpty(Stack S)
{
return S->TopOfStack == -1;
}
1
ElementType Top(stack M)
{
if ( !IsEmpty(M) )
return M->Array[M->TopOfStack];
retuen MaxValue;
// return a maximum integer to avoid warning
}
void Push(ElementType X, ExtendedStack E)
{
if ( IsFull(E->S) )
Error( "Full stack" );
else
{
E->S->Array[++E->S->TopOfStack] = X;
if ( X <= Top(E->M) )
E->M->Array[++E->M->TopOfStack] = X;
}
}
//Push(X,E)
ElementType Pop(ExtendedStack E)
{
ElementType temp;
//Pop(E)
if ( IsEmpty(E->S) )
Error( "Empty stack" );
else
{
temp = E->S->Array[E->S->TopOfStack];
E->S->TopOfStack--;
if (Top(E->M) == temp)
E->M->TopOfStack-return temp;
}
}
ElementType FindMin(ExtendedStack E)
{
if ( IsEmpty(E->M) )
Error( "Empty stack" );
else
return Top(E->M);
}
2
//Push(X,S)
//Push(X,M)
//Pop(S)
//Pop(M)
I Problem DS-03-26 A deque is a data structure consisting of a list of items, on which
the following operations are possible:
• Puch(X,D): Insert item X on the front end of deque D.
• Pop(D): Remove the front item from deque D and return it.
• Inject(X,D): Insert item X on the rear end of deque D.
• Eject(D): Remove the rear item from deque D and return it.
Write routines to support the deque that take O(1) time per operation.
Solution. We suppose that the informations about the deque are stored in the following
record:
struct DequeRecord
{
int Capacity;
int Front;
int Rear;
ElementType *Array;
};
typedef struct DequeRecord *Deque;
For convenience, we always assume that there exists sufficient memory to support the
design of the structure. We first need the following relevant functions:
Deque CreateDeque(int MaxElements)
{
Deque D;
D = malloc( sizeof( struct DequeRecord ) );
D->Array = malloc( sizeof( ElementType ) * MaxElements );
D->Capacity = MaxElements;
D->Front = D->Capacity / 2;
D->Rear = D->Capacity / 2 - 1;
return D;
}
We now give the functions Push(X,D), Pop(D), Inject(X,D) and Eject(D) as follows:
void Push(ElementType X, Deque D)
{
if ( D->Front == 0 )
Error( "Full deque" );
else
{
D->Front--;
D->Array[D->Front] = X;
}
}
1
ElementType Pop(Deque D)
{
ElementType temp;
if ( D->Rear < D->Front )
Error( "Empty deque" );
else
{
temp = D->array[D->Front];
D->Front++;
return temp;
}
}
void Inject(ElementType X, Deque D)
{
if ( D->Rear == D->Capacity - 1)
Error( "Full deque" );
else
{
D->Rear++;
D->Array[D->Rear] = X;
}
}
ElementType Eject(Deque D)
{
ElementType temp;
if ( D->Rear < D->Front )
Error( "Empty deque" );
else
{
temp = D->array[D->Rear];
D->Rear--;
return temp;
}
}
2
I Problem DS-04-07 Suppose a binary tree has leaves l1 , l2 , . . . , lM at depths d1 , d2 , . . . , dM ,
P
−di
respectively. Prove that M
≤ 1 and determine when the equality is true.
i=1 s
P
−di
Proof. Before the proof, we first give an example to see the inequality M
≤ 1.
i=1 s
Consider a tree with five leaves (i.e., M = 5) shown below.
depth 0
depth 1
depth 2
l1
l4
depth 3
depth 4
l5
l3
l2
Then, d1 = depth(l1 ) = 2, d2 = depth(l2 ) = 4, d3 = depth(l3 ) = 3, d4 = depth(l4 ) = 2,
and d5 = depth(l5 ) = 2. Thus,
5
X
i=1
2−di =
1
1
1
1
4+1+2+4+4
15
1
+
+
+
+
=
=
≤1
22 24 23 22 22
16
16
For any tree T with M leaves, we denote by F (T ) =
PM
i=1
s−di where di = depth(li ) for
each leaf li in T . The proof is by induction on the number of nodes in a tree. Clearly, in
a tree T with no nodes, we have F (T ) = 0, and in a one-node tree T , the root is a leaf at
depth zero, so F (T ) = 1 and the claim is true. Suppose the result is true for all trees with
at most N nodes. Consider any tree T with N + 1 nodes. Without loss of generality, we
assume that such a tree consists of a left subtree TL with k node and a right subtree TR
with N − k node. By the inductive hypothesis, F (TL ) ≤ 1 and F (TR ) ≤ 1. Because all
leaves are one deeper with respect to the original tree T than with respect to the subtrees
TL or TR , F (T ) = 12 F (TL ) + 12 F (TR ) ≤ 1, proving the result.
The equality is true if and only if there are no nodes with one child. If there is a
node with one child, the equality cannot be true because adding the second child would
increase the sum to higher than 1. If no nodes have one child, then we can find and
remove two sibling leaves, creating a new tree. It is easy to see that this new tree has the
same sum as the old. Applying this step repeatedly, we arrive at a single node, whose
sum is 1. Thus the original tree had sum 1.
1
I Problem DS-04-12
Suppose you want to perform an experiment to verify the
problem that can be caused by random Insert/Delete pairs. Here is a strategy that is not
perfectly random, but close enough. You build a tree with N elements by inserting N
elements chosen at random from the range 1 to M = αN . You than perform N 2 pairs of
insertions followed by deletions. Assume the existence of a routine, RandomInteger(A, B),
which returns a uniform random integer between A and B inclusive.
(a) Explain how to generate a random integer between 1 and M that is not already in
the tree (so a random inset can be performed). In term of N and α, what is the
running time of this operation?
(b) Explain how to generate a random integer between 1 and M that is already in
the tree (so a random delete can be performed). In term of N and α, what is the
running time of this operation?
(c) What is a good choice of α? Why?
Solution. (a) Keep a bit array B. If i is in the tree, then B[i] is true; otherwise, it is
false. Repeatedly generate random integers until an unused one is found. If there are N
elements already in the tree, then M − N are not, and the probability of finding one of
these is (M − N )/M . Thus the expected number of trials is M/(M − N ) = α/(α − 1).
(b) To find an element that is in the tree, repeatedly generate random integers until
an already-used integer is found. The probability of finding one is N/M , so the expected
number of trials is M/N = α.
1
(c) The total cost for one insert and one delete is α/(α − 1) + α = 1 + α + α−1
. Setting
α = 2 minimizes this cost.
1
I Problem DS-04-21
(a) How many bits are required per node to store the height of a node in an N -node
AVL tree?
(b) What is the smallest AVL tree that overflows an 8-bit height counter?
Solution. (a) The answer is log(dlog N e + 1) bits.
From pages 110–111 of the textbook, it tell us that the height of an AVL tree is at most
roughly 1.44 log(N + 2) − 1.328, but in practice it is only slightly more than log N . Thus,
the height of an AVL tree with N nodes is at about dlog N e. It is well-known that the
largest unsigned integer represented by k bits is 2k − 1. Thus, if dlog N e = 2k − 1 then
k = log(dlog N e + 1). So, it requires log(dlog N e + 1) bits of memory to store an integer
(i.e., the height) with the value dlog N e.
(b) Let h be the height of the smallest AVL tree that uses a k-bits counter to store its
height, where k ≥ 8. From (a), we know that h = dlog N e and log(dlog N e + 1) = k ≥ 8.
Thus, we obtain log(h + 1) ≥ 8, and it implies h ≥ 28 − 1 = 255. So, the desired AVL
tree is an AVL tree with height at least 255.
1
I Problem DS-04-42 Two trees, T1 and T2 , are isomorphic if T1 can be transformed
into T2 by swapping left and right children of (some of the) nodes in T1 . For instance, the
two trees in the following figure are isomorphic because they are the same if the children
of A, B, and G, but not the other nodes, are swapped.
A
A
B
D
C
G
E
F
C
B
E
G
H
H
D
F
(a) Give a polynomial time algorithm to decide if two trees are isomorphic.
(b) What is the running time of your program (there is a linear solution)?
Solution. (a)
#define TRUE 1
#define FALSE 0
int Isomorphic(BinaryTree T1, BinaryTree T2)
{
if ( T1 == NULL && T2 == NULL )
return TRUE;
else if ( T1 == NULL && T2 != NULL )
return FALSE;
else if ( T1 != NULL && T2 == NULL)
return FALSE;
else if (T1->Element != T2->Element )
return FALSE;
else
return (Isomorphic(T1->Left,T2->Left) && Isomorphic(T1->Right,T2->Right)) ||
(Isomorphic(T1->Left,T2->Right) && Isomorphic(T1->Right,T2->Left));
}
(b) The function shown above is clearly a linear time routine because in the worst
case it does a traversal on both T1 and T2 at most twice.
1