lecturer notes

203
CS9212 DATA STRUCTURES AND ALGORITHMS UNIT I COMPLEXITY ANALYSIS & ELEMENTARY DATA STRUCTURES Asymptotic notations – Properties of big oh notation asymptotic notation with several parameters – conditional asymptotic notation – amortized analysis – NP-completeness – NP-hard – recurrence equations – solving recurrence equations – arrays – linked lists – trees. UNIT II HEAP STRUCTURES Min-max heaps – Deaps – Leftist heaps –Binomial heaps – Fibonacci heaps – Skew heaps - Lazy-binomial heaps. UNIT III SEARCH STRUCTURES Binary search trees – AVL trees – 2-3 trees – 2-3-4 trees – Red- black trees – B-trees – splay trees – Tries. UNIT IV GREEDY & DIVIDE AND CONQUER Quicksort – Strassen’s matrix multiplication Convex hull - Tree-vertex splitting – Job sequencing with deadlines – Optimal storage on tapes UNIT V DYNAMIC PROGRAMMING AND BACKTRACKING Multistage graphs – 0/1 knapsack using dynamic programming – Flow shop scheduling – 8-queens problem – graph coloring – knapsack using backtracking REFERENCES: 1. E. Horowitz, S.Sahni and Dinesh Mehta, Fundamentals of Data structures in C++, Galgotia, 1999. 2. E. Horowitz, S.Sahni and S. Rajasekaran, Computer Algorithms / C++, Galgotia, 1999. 3. Adam Drozdex, Data Structures and algorithms in C++, Second Edition, Thomson learning – 1

Upload: priya-vadhana

Post on 29-Oct-2014

83 views

Category:

Documents


5 download

DESCRIPTION

me

TRANSCRIPT

Page 1: lecturer notes

CS9212 DATA STRUCTURES AND ALGORITHMS

UNIT I COMPLEXITY ANALYSIS & ELEMENTARY DATA STRUCTURES

Asymptotic notations – Properties of big oh notation – asymptotic notation with several

parameters – conditional asymptotic notation – amortized analysis – NP-completeness – NP-hard – recurrence equations – solving recurrence equations – arrays – linked lists – trees.

UNIT II HEAP STRUCTURES

Min-max heaps – Deaps – Leftist heaps –Binomial heaps – Fibonacci heaps – Skew heaps -

Lazy-binomial heaps.

UNIT III SEARCH STRUCTURES

Binary search trees – AVL trees – 2-3 trees – 2-3-4 trees – Red-black trees – B-trees – splay

trees – Tries.

UNIT IV GREEDY & DIVIDE AND CONQUER

Quicksort – Strassen’s matrix multiplication – Convex hull - Tree-vertex splitting – Job

sequencing with deadlines – Optimal storage on tapes

UNIT V DYNAMIC PROGRAMMING AND BACKTRACKING

Multistage graphs – 0/1 knapsack using dynamic programming – Flow shop scheduling – 8-queens problem – graph coloring – knapsack using backtracking

REFERENCES:

1. E. Horowitz, S.Sahni and Dinesh Mehta, Fundamentals of Data structures in C++, Galgotia,

1999.

2. E. Horowitz, S.Sahni and S. Rajasekaran, Computer Algorithms / C++, Galgotia, 1999.

3. Adam Drozdex, Data Structures and algorithms in C++, Second Edition, Thomson learning –

vikas publishing house, 2001.

4. G. Brassard and P. Bratley, Algorithmics: Theory and Practice, Printice –Hall, 1988.

5. Thomas H.Corman, Charles E.Leiserson, Ronald L. Rivest, ”Introduction to Algorithms”,

Second Edition, PHI 2003.

1

Page 2: lecturer notes

UNIT I COMPLEXITY ANALYSIS & ELEMENTARY DATA STRUCTURES

Asymptotic Notation

Introduction

A problem may have numerous algorithmic solutions. In order to choose the best algorithm for a particular task, you need to be able to judge how long a particular solution will take to run. Or, more accurately, you need to be able to judge how long two solutions will take to run, and choose the better of the two. You don't need to know how many minutes and seconds they will take, but you do need some way to compare algorithms against one another.

Asymptotic complexity is a way of expressing the main component of the cost of an algorithm, using idealized units of computational work. Consider, for example, the algorithm for sorting a deck of cards, which proceeds by repeatedly searching through the deck for the lowest card. The asymptotic complexity of this algorithm is the square of the number of cards in the deck. This quadratic behavior is the main term in the complexity formula, it says, e.g., if you double the size of the deck, then the work is roughly quadrupled.

The exact formula for the cost is more complex, and contains more details than are needed to understand the essential complexity of the algorithm. With our deck of cards, in the worst case, the deck would start out reverse-sorted, so our scans would have to go all the way to the end. The first scan would involve scanning 52 cards, the next would take 51, etc. So the cost formula is 52 + 51 + ..... + 2. generally, letting N be the number of cards, the formula is 2 + ... + N, which equals

. But the N^2 term dominates the expression, and this is what is key for comparing algorithm costs. (This is in fact an expensive algorithm; the best sorting algorithms run in sub-quadratic time.)

Asymptotically speaking, in the limit as N tends towards infinity, 2 + 3 + .,,,.. + N gets closer and closer to the pure quadratic function (1/2)N^2. And what difference does the constant factor of 1/2 make, at this level of abstraction? So the behavior is said to be O( ).

Now let us consider how we would go about comparing the complexity of two algorithms. Let f(n) be the cost, in the worst case, of one algorithm, expressed as a function of the input size n, and g(n) be the cost function for the other algorithm. E.g., for sorting algorithms, f(10) and g(10) would be the maximum number of steps that the algorithms would take on a list of 10 items. If, for all values of n >= 0, f(n) is less than or equal to g(n), then the algorithm with complexity function f is strictly faster. But, generally speaking, our concern for computational cost is for the cases with large inputs; so the comparison of f(n) and g(n) for small values of n is less significant than the "long term" comparison of f(n) and g(n), for n larger than some threshold.

2

Page 3: lecturer notes

Note that we have been speaking about bounds on the performance of algorithms, rather than giving exact speeds. The actual number of steps required to sort our deck of cards (with our naive quadratic algorithm) will depend upon the order in which the cards begin. The actual time to perform each of our steps will depend upon our processor speed, the condition of our processor cache, etc., etc. It's all very complicated in the concrete details, and moreover not relevant to the essence of the algorithm.

Big-O Notation

Definition

Big-O is the formal method of expressing the upper bound of an algorithm's running time. It's a measure of the longest amount of time it could possibly take for the algorithm to complete.

More formally, for non-negative functions, f(n) and g(n), if there exists an integer and a constant c > 0 such that for all integers , f(n) ≤ cg(n), then f(n) is Big O of g(n). This is denoted as "f(n) = O(g(n))". If graphed, g(n) serves as an upper bound to the curve you are analyzing, f(n).

Note that if f can take on finite values only (as it should happen normally) then this definition implies that there exists some constant C (potentially larger than c) such that for all values of n, f(n) ≤ Cg(n).

An appropriate value for C is the maximum of c and .

Theory Examples

So, let's take an example of Big-O. Say that f(n) = 2n + 8, and g(n) = . Can we find a constant , so that 2n + 8 <= ? The number 4 works here, giving us 16 <= 16. For any number n greater than 4, this will still work. Since we're trying to generalize this for large values of n, and small values (1, 2, 3) aren't that important, we can say that f(n) is generally faster than g(n); that is, f(n) is bound by g(n), and will always be less than it.

It could then be said that f(n) runs in O( ) time: "f-of-n runs in Big-O of n-squared time".

To find the upper bound - the Big-O time - assuming we know that f(n) is equal to (exactly) 2n + 8, we can take a few shortcuts. For example, we can remove all constants from the runtime; eventually, at some value of c, they become irrelevant. This makes f(n) = 2n. Also, for convenience of comparison, we remove constant multipliers; in this case, the 2. This makes f(n) = n. It could also be said that f(n) runs in O(n) time; that lets us put a tighter (closer) upper bound onto the estimate.

Practical Examples

O(n): printing a list of n items to the screen, looking at each item once.

O(ln n): also "log n", taking a list of items, cutting it in half repeatedly until there's only one item left.3

Page 4: lecturer notes

O( ): taking a list of n items, and comparing every item to every other item.

Big-Omega Notation

For non-negative functions, f(n) and g(n), if there exists an integer and a constant c > 0 such that for all integers , f(n) ≥ cg(n), then f(n) is omega of g(n). This is denoted as "f(n) = Ω(g(n))".

This is almost the same definition as Big Oh, except that "f(n) ≥ cg(n)", this makes g(n) a lower bound function, instead of an upper bound function. It describes the best that can happen for a given data size.

Theta Notation

For non-negative functions, f(n) and g(n), f(n) is theta of g(n) if and only if f(n) = O(g(n)) and f(n) = Ω(g(n)). This is denoted as "f(n) = Θ(g(n))".

This is basically saying that the function, f(n) is bounded both from the top and bottom by the same function, g(n).

Little-O Notation

For non-negative functions, f(n) and g(n), f(n) is little o of g(n) if and only if f(n) = O(g(n)), but f(n) ≠ Θ(g(n)). This is denoted as "f(n) = o(g(n))".

This represents a loose bounding version of Big O. g(n) bounds from the top, but it does not bound the bottom.

Little Omega Notation

For non-negative functions, f(n) and g(n), f(n) is little omega of g(n) if and only if f(n) = Ω(g(n)), but f(n) ≠ Θ(g(n)). This is denoted as "f(n) = ω(g(n))".

Much like Little Oh, this is the equivalent for Big Omega. g(n) is a loose lower boundary of the function f(n); it bounds from the bottom, but not from the top.

How asymptotic notation relates to analyzing complexity

Temporal comparison is not the only issue in algorithms. There are space issues as well. Generally, a trade off between time and space is noticed in algorithms. Asymptotic notation empowers you to make that trade off. If you think of the amount of time and space your algorithm uses as a function of your data over time or space (time and space are usually analyzed separately), you can analyze how the time and space is handled when you introduce more data to your program.

This is important in data structures because you want a structure that behaves efficiently as you increase the amount of data it handles. Keep in mind though that algorithms that are efficient

4

Page 5: lecturer notes

with large amounts of data are not always simple and efficient for small amounts of data. So if you know you are working with only a small amount of data and you have concerns for speed and code space, a trade off can be made for a function that does not behave well for large amounts of data.

5

Page 6: lecturer notes

Amortized Analysis

Overview

This lecture discusses a useful form of analysis, called amortized analysis, for problems in which one must perform a series of operations, and our goal is to analyze the time per operation. The

motivation for amortized analysis is that looking at the worst-case time per operation can be too

pessimistic if the only way to produce an expensive operation is to “set it up” with a large number

of cheap operations beforehand.

6

Page 7: lecturer notes

We also introduce the notion of a potential function which can be a useful aid to performing this type of analysis. A potential function is much like a bank account: if we can take our cheap

operations (those whose cost is less than our bound) and put our savings from them in a bank

account, use our savings to pay for expensive operations (those whose cost is greater than our

bound), and somehow guarantee that our account will never go negative, then we will have proven

an amortized bound for our procedure.As in the previous lecture, in this lecture we will avoid use of asymptotic notation as much as possible, and focus instead on concrete cost models and bounds.

Introduction

So far we have been looking at static problems where you are given an input (like an array of n objects) and the goal is to produce an output with some desired property (e.g., the same objects,

but sorted). For next few lectures, we’re going to turn to problems where we have a series of

operations, and goal is to analyze the time taken per operation. For example, rather than being

given a set of n items up front, we might have a series of n insert, lookup, and remove requests to

some database, and we want these operations to be efficient.

Today, we’ll talk about a useful kind of analysis, called amortized analysis for problems of this sort.

The definition of amortized cost is actually quite simple:

Definition : The amortized cost of n operations is the total cost of the operations divided by n.

Analyzing the amortized cost, however, will often require some thought if we want to do it well.We will illustrate how this can be done through several examples.

Example #1: Implementing a stack as an array Say we want to use an array to implement a stack. We have an array A, with a variable top that points to the top of the stack (so A[top] is the next free cell). This is pretty easy:

• To implement push(x), we just need to perform:

A[top] = x;

top++;

• To implement x=pop(), we just need to perform:

top--;

x = A[top];

(first checking to see if top==0 of course...)

7

Page 8: lecturer notes

However, what if the array is full and we need to push a new element on? In that case we can

allocate a new larger array, copy the old one over, and then go on from there. This is going to be

an expensive operation, so a push that requires us to do this is going to cost a lot. But maybe

we can “amortize” the cost over the previous cheap operations that got us to this point. So, on

average over the sequence of operations, we’re not paying too much. To be specific, let us define

the following cost model.

Cost model: Say that inserting into the array costs 1, taking an element out of the array costs 1,

and the cost of resizing the array is the number of elements moved. (Say that all other operations,

like incrementing or decrementing “top”, are free.)

Question 1: What if when we resize we just increase the size by 1. Is that a good idea?

Answer 1: Not really. If our n operations consist of n pushes then even just considering the

array-resizing cost we will incur a total cost of at least 1 + 2 + 3 + 4 + . . . + (n − 1) = n(n − 1)/2.

That’s an amortized cost of (n − 1)/2 per operation just in resizing.

Question 2: What if instead we decide to double the size of the array when we resize?

Answer 2: This is much better. Now, in any sequence of n operations, the total cost for resizing

is 1 + 2 + 4 + 8 + . . . + 2i for some 2i < n (if all operations are pushes then 2i will be the largest

power of 2 less than n). This sum is at most 2n − 1. Adding in the additional cost of n for

inserting/removing, we get a total cost < 3n, and so our amortized cost per operation is < 3.

Piggy banks and potential functions

Here is another way to analyze the process of doubling the array in the above example. Say that every time we perform a push operation, we pay $1 to perform it, and we put $2 into a piggy bank. So, our out-of-pocket cost per push is $3. Any time we need to double the array, from size L to

2L, we pay for it using money in the bank. How do we know there will be enough money ($L) in

the bank to pay for it? Because after the last resizing, there were only L/2 elements in the array

and so there must have been at least L/2 new pushes since then contributing $2 each. So, we can

pay for everything by using an out-of-pocket cost of at most $3 per operation. Putting it another

way, by spending $3 per operation, we were able to pay for all the operations plus possibly still

have money left over in the bank. This means our amortized cost is at most 3.1

This “piggy bank” method is often very useful for performing amortized analysis. The piggy bank is also called a potential function, since it is like potential energy that you can use later. The

8

Page 9: lecturer notes

potential function is the amount of money in the bank. In the case above, the potential is 2 times

the number of elements in the array after the midpoint. Note that it is very important in this

analysis to prove that the bank account doesn’t go negative. Otherwise, if the bank account can

slowly drift off to negative infinity, the whole proof breaks down.

Definition: A potential function is a function of the state of a system, that generally should

be non-negative and start at 0, and is used to smooth out analysis of some algorithm or process.

Observation: If the potential is non-negative and starts at 0, and at each step the actual cost

of our algorithm plus the change in potential is at most c, then after n steps our total cost is at

most cn. That is just the same thing we were saying about the piggy bank: our total cost for the

n operations is just our total out of pocket cost minus the amount in the bank at the end.

Sometimes one may need in an analysis to “seed” the bank account with some initial positive

amount for everything to go through. In that case, the kind of statement one would show is that

the total cost for n operations is at most cn plus the initial seed amount.

Recap: The motivation for amortized analysis is that a worst-case-per-operation analysis can

give overly pessimistic bound if the only way of having an expensive operation is to have a lot of

cheap ones before it. Note that this is different from our usual notion of “average case analysis”: we

are not making any assumptions about the inputs being chosen at random, we are just averaging

over time.

Example #2: a binary counter

Imagine we want to store a big binary counter in an array A. All the entries start at 0 and at

each step we will be simply incrementing the counter. Let’s say our cost model is: whenever we

increment the counter, we pay $1 for every bit we need to flip. (So, think of the counter as an

1In fact, if you think about it, we can pay for pop operations using money from the bank too, and even have $1

left over. So as a more refined analysis, our amortized cost is $3 per push and $−1 per successful pop (a pop from a

nonempty stack).

EXAMPLE #3: WHAT IF IT COSTS US 2K TO FLIP THE KTH BIT?

array of heavy stone tablets, each with a “0” on one side and a “1” on the other.) For instance,

9

Page 10: lecturer notes

here is a trace of the first few operations and their cost:

A[m] A[m-1] ... A[3] A[2] A[1] A[0] cost

0 0 ... 0 0 0 0 $1

0 0 ... 0 0 0 1 $2

0 0 ... 0 0 1 0 $1

0 0 ... 0 0 1 1 $3

0 0 ... 0 1 0 0 $1

0 0 ... 0 1 0 1 $2

In a sequence of n increments, the worst-case cost per increment is O(log n), since at worst we flip

lg(n) + 1 bits. But, what is our amortized cost per increment? The answer is it is at most 2. Here

are two proofs.

Proof 1: Every time you flip 0 ! 1, pay the actual cost of $1, plus put $1 into a piggy bank. So

the total amount spent is $2. In fact, think of each bit as having its own bank (so when you turn

the stone tablet from 0 to 1, you put a $1 coin on top of it). Now, every time you flip a 1 ! 0, use

the money in the bank (or on top of the tablet) to pay for the flip. Clearly, by design, our bank

account cannot go negative. The key point now is that even though different increments can have

different numbers of 1 ! 0 flips, each increment has exactly one 0 ! 1 flip. So, we just pay $2

(amortized) per increment.

Equivalently, what we are doing in this proof is using a potential function that is equal to the

number of 1-bits in the current count. Notice how the bank-account/potential-function allows us

to smooth out our payments, making the cost easier to analyze.

Proof 2: Here is another way to analyze the amortized cost. First, how often do we flip A[0]?

Answer: every time. How often do we flip A[1]? Answer: every other time. How often do we flip

A[2]? Answer: every 4th time, and so on. So, the total cost spent on flipping A[0] is n, the total

cost spent flipping A[1] is n/2, the total cost flipping A[2] is n/4, etc. Summing these up, the

total cost spent flipping all the positions in our n increments is at most 2n.

Example #3: What if it costs us 2k to flip the kth bit?

Imagine a version of the counter we just discussed in which it costs 2k to flip the bit A[k]. (Suspend

10

Page 11: lecturer notes

disbelief for now — we’ll see shortly why this is interesting to consider). Now, in a sequence of n

increments, a single increment could cost as much as n, but the claim is the amortized cost is only

O(log n) per increment. This is probably easiest to see by the method of “Proof 2” above: A[0]

gets flipped every time for cost of $1 each (a total of $n). A[1] gets flipped every other time for

cost of $2 each (a total of at most $n). A[2] gets flipped every 4th time for cost of $4 each (again,

a total of at most $n), and so on up to A[blg nc] which gets flipped once for a cost at most $n. So,

the total cost is O(n log n), which is O(log n) amortized per increment.

Example #4: A simple amortized dictionary data structure

One of the most common classes of data structures are the “dictionary” data structures that

support fast insert and lookup operations into a set of items. In the next lecture we will look at

balanced-tree data structures for this problem in which both inserts and lookups can be done with

cost only O(log n) each. Note that a sorted array is good for lookups (binary search takes time

only O(log n)) but bad for inserts (they can take linear time), and a linked list is good for inserts

(can do them in constant time) but bad for lookups (they can take linear time). Here is a method

that is very simple and almost as good as the ones in the next lecture. This method has O(log2 n)

search time and O(log n) amortized cost per insert.

The idea of this data structure is as follows. We will have a collection of arrays, where array i has

size 2i. Each array is either empty or full, and each is in sorted order. However, there will be no

relationship between the items in different arrays. The issue of which arrays are full and which are

empty is based on the binary representation of the number of items we are storing. For example,

if we had 11 items (where 11 = 1 + 2 + 8), the data structure might look like this:

A0: [5]

A1: [4,8]

A2: empty

A3: [2, 6, 9, 12, 13, 16, 20, 25]

To perform a lookup, we just do binary search in each occupied array. In the worst case, this takes

time O(log(n/2) + log(n/4) + log(n/8) + . . . + 1) = O(log2 n).

What about inserts? We’ll do this like mergesort. To insert the number 10, we first create an array

11

Page 12: lecturer notes

of size 1 that just has this single number in it. We now look to see if A0 is empty. If so we make

this be A0. If not (like in the above example) we merge our array with A0 to create a new array

(which in the above case would now be [5, 10]) and look to see if A1 is empty. If A1 is empty, we

make this be A1. If not (like in the above example) we merge this with A1 to create a new array

and check to see if A2 is empty, and so on. So, inserting 10 in the example above, we now have:

A0: empty

A1: empty

A2: [4, 5, 8, 10]

A3: [2, 6, 9, 12, 13, 16, 20, 25]

Cost model: To be clear about costs, let’s say that creating the initial array of size 1 costs 1,

and merging two arrays of size m costs 2m (remember, merging sorted arrays can be done in linear

time). So, the above insert had cost 1 + 2 + 4.

For instance, if we insert again, we just put the new item into A0 at cost 1. If we insert again, we

merge the new array with A0 and put the result into A1 at a cost of 1 + 2.

EXAMPLE #4: A SIMPLE AMORTIZED DICTIONARY DATA STRUCTUREClaim 7.1 The above data structure has amortized cost O(log n) per insert.

Proof: With the cost model defined above, it’s exactly the same as the binary counter with cost

2k for counter k.

NP-complete problems

Hard problems, easy problems

In short, the world is full of search problems, some of which can be solved efciently, while

others seem to be very hard. This is depicted in the following table.

12

Page 13: lecturer notes

This table is worth contemplating. On the right we have problems that can be solved

effciently. On the left, we have a bunch of hard nuts that have escaped effcient solution over

many decades or centuries. The various problems on the right can be solved by algorithms that are specialized and diverse: dynamic programming, network ow, graph search, greedy. These problems are easy

for a variety of different reasons. In stark contrast, the problems on the left are all diffcult for the same reason! At their core, they are all the same problem, just in different disguises! They are all equivalent: each of them can be reduced to any of the others.and back.

P and NP

It's time to introduce some important concepts. We know what a search problem is: its dening

characteristic is that any proposed solution can be quickly checked for correctness, in the

sense that there is an efcient checking algorithm C that takes as input the given instance I

(the data specifying the problem to be solved), as well as the proposed solution S, and outputs

true if and only if S really is a solution to instance I. Moreover the running time of C(I; S)

is bounded by a polynomial in jIj, the length of the instance. We denote the class of all search

problems by NP.

We've seen many examples of NP search problems that are solvable in polynomial time.

In such cases, there is an algorithm that takes as input an instance I and has a running time

polynomial in jIj. If I has a solution, the algorithm returns such a solution; and if I has no

13

Page 14: lecturer notes

solution, the algorithm correctly reports so. The class of all search problems that can be solved

in polynomial time is denoted P. Hence, all the search problems on the right-hand side of the

table are in P.

Why P and NP?

Okay, P must stand for .polynomial.. But why use the initials NP (the common chatroom

abbreviation for .no problem.) to describe the class of search problems, some of which are

terribly hard? NP stands for .nondeterministic polynomial time,. a term going back to the roots of

complexity theory. Intuitively, it means that a solution to any search problem can be found

and veried in polynomial time by a special (and quite unrealistic) sort of algorithm, called a

nondeterministic algorithm. Such an algorithm has the power of guessing correctly at every

step. Incidentally, the original denition of NP (and its most common usage to this day) was

not as a class of search problems, but as a class of decision problems: algorithmic questions

that can be answered by yes or no. Example: .Is there a truth assignment that satises this

Boolean formula?. But this too reects a historical reality: At the time the theory of NPcompleteness

was being developed, researchers in the theory of computation were interested

in formal languages, a domain in which such decision problems are of central importance.

Are there search problems that cannot be solved in polynomial time? In other words,

is P not equal to NP? Most algorithms researchers think so. It is hard to believe that exponential

search can always be avoided, that a simple trick will crack all these hard problems, famously

unsolved for decades and centuries. And there is a good reason for mathematicians to believe

that P not equal to NP.the task of nding a proof for a given mathematical assertion is a search

problem and is therefore in NP (after all, when a formal proof of a mathematical statement is

written out in excruciating detail, it can be checked mechanically, line by line, by an efcient

algorithm). So if P = NP, there would be an efcient method to prove any theorem, thus

eliminating the need for mathematicians! All in all, there are a variety of reasons why it is

widely believed that P not equal to NP. However, proving this has turned out to be extremely difcult, one of the deepest and most important unsolved puzzles of mathematics.

Reductions, again

Even if we accept that P not equal to NP, what about the specic problems on the left side of the table? On the basis of what evidence do we believe that these particular problems have no

14

Page 15: lecturer notes

efcient algorithm (besides, of course, the historical fact that many clever mathematicians

and computer scientists have tried hard and failed to nd any)? Such evidence is provided

by reductions, which translate one search problem into another. What they demonstrate is

that the problems on the left side of the table are all, in some sense, exactly the same problem,

except that they are stated in different languages. What's more, we will also use reductions to

show that these problems are the hardest search problems in NP.if even one of them has a

polynomial time algorithm, then every problem in NP has a polynomial time algorithm. Thus

if we believe that P not equal to NP, then all these search problems are hard.

We dened reductions in Chapter 7 and saw many examples of them. Let's now specialize

this denition to search problems. A reduction from search problem A to search problem B

is a polynomial-time algorithm f that transforms any instance I of A into an instance f(I) of

B, together with another polynomial-time algorithm h that maps any solution S of f(I) back

into a solution h(S) of I; see the following diagram. If f(I) has no solution, then neither does

I. These two translation procedures f and h imply that any algorithm for B can be converted

into an algorithm for A by bracketing it between f and h.

And now we can nally dene the class of the hardest search problems.

A search problem is NP-complete if all other search problems reduce to it.

This is a very strong requirement indeed. For a problem to be NP-complete, it must be useful

in solving every search problem in the world! It is remarkable that such problems exist.

But they do, and the rst column of the table we saw earlier is lled with the most famous

examples. In Section 8.3 we shall see how all these problems reduce to one another, and also

why all other search problems reduce to them.

15

Page 16: lecturer notes

The two ways to use reductions

So far in this book the purpose of a reduction from a problem A to a problem B has been

straightforward and honorable: We know how to solve B efciently, and we want to use this

knowledge to solve A. In this chapter, however, reductions from A to B serve a somewhat

perverse goal: we know A is hard, and we use the reduction to prove that B is hard as well!

If we denote a reduction from A to B by

A --> B

then we can say that difculty ows in the direction of the arrow, while efcient algorithms

move in the opposite direction. It is through this propagation of difculty that we know

NP-complete problems are hard: all other search problems reduce to them, and thus

each NP-complete problem contains the complexity of all search problems. If even one

NP-complete problem is in P, then P = NP.

Reductions also have the convenient property that they compose.

If A --> B and B --> C, then A ---> C .

To see this, observe rst of all that any reduction is completely specied by the pre- and

postprocessing functions f and h (see the reduction diagram). If (fAB; hAB) and (fBC; hBC)

dene the reductions from A to B and from B to C, respectively, then a reduction from A to

C is given by compositions of these functions: fBC o fAB maps an instance of A to an instance

of C and hAB hBC sends a solution of C back to a solution of A.

16

Page 17: lecturer notes

This means that once we know a problem A is NP-complete, we can use it to prove that

a new search problem B is also NP-complete, simply by reducing A to B. Such a reduction

establishes that all problems in NP reduce to B, via A.

NP-Hard Problems

‘Efficient’ Problems

A generally-accepted minimum requirement for an algorithm to be considered ‘efficient’ is that its running time is polynomial: O(nc) for some constant c, where n is the size of the input.1 Researchers recognized early on that not all problems can be solved this quickly, but we had a hard time figuring out exactly which ones could and which ones couldn’t. there are several so-called NP-hard problems, which most people believe cannot be solved in polynomial time, even though nobody can prove a super-polynomial lower bound.

Circuit satisfiability is a good example of a problem that we don’t know how to solve in polynomial time. In this problem, the input is a boolean circuit: a collection of AND, OR, and NOT gates connected by wires. We will assume that there are no loops in the circuit (so no delay lines or flip-flops). The input to the circuit is a set of m boolean (TRUE/FALSE) values x1, . . . , xm. The output is a single boolean value. Given specific input values, we can calculate the output of the circuit in polynomial (actually, linear) time using depth-first-search, since we can compute the output of a k-input gate in O(k) time.

The circuit satisfiability problem asks, given a circuit, whether there is an input that makes the

17

Page 18: lecturer notes

circuit output TRUE, or conversely, whether the circuit always outputs FALSE. Nobody knows how to solve this problem faster than just trying all 2m possible inputs to the circuit, but this requires exponential time. On the other hand, nobody has ever proved that this is the best we can do; maybe there’s a clever algorithm that nobody has discovered yet!

P, NP, and co-NP

A decision problem is a problem whose output is a single boolean value: YES or NO.2 Let me define three classes of decision problems:

¤ P is the set of decision problems that can be solved in polynomial time.3 Intuitively, P is the set of problems that can be solved quickly.

¤ NP is the set of decision problems with the following property: If the answer is YES, then there is a proof of this fact that can be checked in polynomial time. Intuitively, NP is the set of decision problems where we can verify a YES answer quickly if we have the solution in front of us.

¤ co-NP is the opposite of NP. If the answer to a problem in co-NP is NO, then there is a proof of this fact that can be checked in polynomial time.

For example, the circuit satisfiability problem is in NP. If the answer is YES, then any set of m input values that produces TRUE output is a proof of this fact; we can check the proof by evaluating the circuit in polynomial time. It is widely believed that circuit satisfiability is not in P or in co-NP, but nobody actually knows.

Every decision problem in P is also in NP. If a problem is in P, we can verify YES answers in polynomial time recomputing the answer from scratch! Similarly, any problem in P is also in co-NP.

One of the most important open questions in theoretical computer science is whether or not P = NP. Nobody knows. Intuitively, it should be obvious that P 6= NP; the homeworks and exams in this class and others have (I hope) convinced you that problems can be incredibly hard to solve, even when the solutions are obvious in retrospect. But nobody knows how to prove it.

A more subtle but still open question is whether NP and co-NP are different. Even if we can verify every YES answer quickly, there’s no reason to think that we can also verify NO answers quickly. For example, as far as we know, there is no short proof that a boolean circuit is not satisfiable. It is generally believed that NP != co-NP, but nobody knows how to prove it.

18

Page 19: lecturer notes

NP-hard, NP-easy, and NP-complete

A problem is NP-hard if a polynomial-time algorithm for would imply a polynomial-time algorithm for every problem in NP. In other words:

Intuitively, if we could solve one particular NP-hard problem quickly, then we could quickly solve any problem whose solution is easy to understand, using the solution to that one special problem as a

subroutine. NP-hard problems are at least as hard as any problem in NP.4

Saying that a problem is NP-hard is like saying ‘If I own a dog, then it can speak fluent English.’ You

probably don’t know whether or not I own a dog, but you’re probably pretty sure that I don’t own a

talking dog. Nobody has a mathematical proof that dogs can’t speak English—the fact that no one has

ever heard a dog speak English is evidence, as are the hundreds of examinations of dogs that lacked the proper mouth shape and braiNPower, but mere evidence is not a mathematical proof. Nevertheless, no sane person would believe me if I said I owned a dog that spoke fluent English. So the statement ‘If I own a dog, then it can speak fluent English’ has a natural corollary: No one in their right mind should believe that I own a dog! Likewise, if a problem is NP-hard, no one in their right mind should believe it can be solved in polynomial time.

Finally, a problem is NP-complete if it is both NP-hard and an element of NP (or ‘NP-easy’). Npcomplete problems are the hardest problems in NP. If anyone finds a polynomial-time algorithm for even one NP-complete problem, then that would imply a polynomial-time algorithm for every NP-complete problem. Literally thousands of problems have been shown to be NP-complete, so a polynomial-timealgorithm for one (that is, all) of them seems incredibly unlikely.

19

Page 20: lecturer notes

It is not immediately clear that any decision problems are NP-hard or NP-complete. NP-hardness is already a lot to demand of a problem; insisting that the problem also have a nondeterministic polynomial-time algorithm seems almost completely unreasonable. The following remarkable theorem was first published by Steve Cook in 1971 and independently by Leonid Levin in 1973.5 I won’t even sketch the proof, since I’ve been (deliberately) vague about the definitions.

What is a recurrence relation?

A recurrence relation, T(n), is a recursive function of integer variable n.

Like all recursive functions, it has both recursive case and base case.

Example:

The portion of the definition that does not contain T is called the base case of the recurrence relation; the portion that contains T is called the recurrent or recursive case. Recurrence relations are useful for expressing the running times (i.e., the number of basic operations executed) of recursive algorithms.

Forming Recurrence Relations

For a given recursive method, the base case and the recursive case of its recurrence relation

20

Page 21: lecturer notes

correspond directly to the base case and the recursive case of the method.

Example 1: Write the recurrence relation for the following method.

public void f (int n) {

if (n > 0) {

System.out.println(n);

f(n-1);

}

}

The base case is reached when n == 0. The method performs one comparison. Thus, the number of operations when n == 0, T(0), is some constant a.

When n > 0, the method performs two basic operations and then calls itself, using ONE recursive call, with a parameter n – 1.

Therefore the recurrence relation is:

Example 2: Write the recurrence relation for the following method.

public int g(int n) {

if (n == 1)

return 2;

else

return 3 * g(n / 2) + g( n / 2) + 5;

}

The base case is reached when n == 1. The method performs one comparison and one return statement. Therefore, T(1), is constant c.

When n > 1, the method performs TWO recursive calls, each with the parameter n / 2, and some constant # of basic operations.

Hence, the recurrence relation is:

21

Page 22: lecturer notes

Solving Recurrence Relations

To solve a recurrence relation T(n) we need to derive a form of T(n) that is not a recurrence relation. Such a form is called a closed form of the recurrence relation.

There are four methods to solve recurrence relations that represent the running time of recursive methods:

Iteration method (unrolling and summing)

Substitution method

Recursion tree method

Master method

In this course, we will only use the Iteration method.

Iteration method

Steps:

Expand the recurrence

Express the expansion as a summation by plugging the recurrence back into itself until you see a pattern.  

Evaluate the summation

In evaluating the summation one or more of the following summation formulae may be used:

Arithmetic series:

Geometric Series:

Geometric Series:(special case)

22

Page 23: lecturer notes

Harmonic Series:

Others:

Analysis Of Recursive Factorial method

Example1: Form and solve the recurrence relation for the running time of factorial method and hence determine its big-O complexity:

long factorial (int n) {

if (n == 0) return 1; else return n * factorial (n – 1); }

T(0) = cT(n) = b + T(n - 1) = b + b + T(n - 2) = b +b +b + T(n - 3) …

23

Page 24: lecturer notes

= kb + T(n - k)When k = n, we have: T(n) = nb + T(n - n) = bn + T(0)

= bn + c.Therefore method factorial is O(n).

Analysis Of Recursive Selection Sort

public static void selectionSort(int[] x) {

selectionSort(x, x.length - 1);}

private static void selectionSort(int[] x, int n) {

int minPos;

if (n > 0) {

minPos = findMinPos(x, n);

swap(x, minPos, n);

selectionSort(x, n - 1);

}

}

private static int findMinPos (int[] x, int n) {

int k = n;

for(int i = 0; i < n; i++)

if(x[i] < x[k]) k = i;

return k;

}

private static void swap(int[] x, int minPos, int n) {

int temp=x[n]; x[n]=x[minPos]; x[minPos]=temp;}

findMinPos is O(n), and swap is O(1), therefore the recurrence relation for the running time of the selectionSort method is:

T(0) = a

T(n) = T(n – 1) + n + c n > 0

= [T(n-2) +(n-1) + c] + n + c = T(n-2) + (n-1) + n + 2c

= [T(n-3) + (n-2) + c] +(n-1) + n + 2c= T(n-3) + (n-2) + (n-1) + n + 3c

= T(n-4) + (n-3) + (n-2) + (n-1) + n + 4c

= ……

= T(n-k) + (n-k + 1) + (n-k + 2) + …….+ n + kc24

Page 25: lecturer notes

When k = n, we have :

Therefore, Recursive Selection Sort is O(n

2

)

Analysis Of Recursive Binary Search

public int binarySearch (int target, int[] array,

int low, int high) {

if (low > high)

return -1;

else {

int middle = (low + high)/2;

if (array[middle] == target)

return middle;

else if(array[middle] < target)

return binarySearch(target, array, middle + 1, high);

else

return binarySearch(target, array, low, middle - 1);

}

}

The recurrence relation for the running time of the method is:

T(1) = a if n = 1 (one element array)

T(n) = T(n / 2) + b if n > 1

Expanding:

T(n) = T(n / 2) + b

= [T(n / 4) + b] + b = T (n / 2

2

) + 2b

25

Page 26: lecturer notes

= [T(n / 8) + b] + 2b = T(n / 2

3

) + 3b

= ……..

= T( n / 2

k

) + kb

When n / 2

k

= 1 --> n = 2

k

--> k = log

2

n, we have:

T(n) = T(1) + b log

2

n

= a + b log

2

n

Therefore, Recursive Binary Search is O(log n)

Analysis Of Recursive Towers of Hanoi Algorithm

public static void hanoi(int n, char from, char to, char temp){

if (n == 1)

System.out.println(from + " --------> " + to);

else{

hanoi(n - 1, from, temp, to);

System.out.println(from + " --------> " + to);

hanoi(n - 1, temp, to, from);

}

}

The recurrence relation for the running time of the method hanoi is:

T(n) = a if n = 1

T(n) = 2T(n - 1) + b if n > 1

Expanding:

T(n) = 2T(n – 1) + b

= 2[2T(n – 2) + b] + b = 2

2

T(n – 2) + 2b + b

26

Page 27: lecturer notes

= 2

2

[2T(n – 3) + b] + 2b + b = 2

3

T(n – 3) + 2

2

b + 2b + b

= 2

3

[2T(n – 4) + b] + 2

2

b + 2b + b = 2

4

T(n – 4) + 2

3

b + 2

2

b + 2

1

b + 2

0

b

= ……

= 2

k

T(n – k) + b[2

k- 1

+ 2

k– 2

+ . . . 2

1

+ 2

0

]

When k = n – 1, we have:

Therefore, The method hanoi is O(2

n

)

UNIT II HEAP STRUCTURES

Min-Max Heaps and Generalized Priority Queues

INTRODUCTION

27

Page 28: lecturer notes

A (single-ended) priority queue is a data type supporting the following operations on an ordered set of

values:

1) find the maximum value (FindMax);

2) delete the maximum value (DeleteMax);

3) add a new value x (Insert(x)).

Obviously, the priority queue can be redefined by substituting operations 1) and 2) with FindMin and

DeleteMin, respectively. Several structures, some implicitly stored in an array and some using more complex data structures, have been presented for implementing this data type, including max-heaps (or min-heaps)

Conceptually, a max-heap is a binary tree having the following properties:

a) heap-shape: all leaves lie on at most two adjacent levels, and the leaves on the last level occupy

the leftmost positions; all other levels are complete.

b) max-ordering: the value stored at a node is greater than or equal to the values stored at its children.

A max-heap of size n can be constructed in linear time and can be stored in an n-element array; hence

it is referred to as an implicit data structure [g]. When a max-heap implements a priority queue,

FindMax can be performed in constant time, while both DeleteMax and Insert(x) have logarithmic time.

We shall consider a more powerful data type, the double-ended priority queue, which allows both

FindMin and FindMax, as well as DeleteMin, DeleteMax, and Insert(x) operations. An important

application of this data type is in external quicksort . A traditional heap does not allow efficient implementation

of all the above operations; for example, FindMin requires linear (instead of constant) time in a max-heap. One approach to overcoming this intrinsic limitation of heaps, is to place a max-heap “back-to-back” with a min-heap as suggested by Williams. This leads to constant time Find either extremum and logarithmic time to Insert an element or Delete one of the extrema, but is somewhat trickier to implement than the method following.

MIN-MAX HEAPS

Given a set S of values, a min-max heap on S is a binary tree T with the following properties:

28

Page 29: lecturer notes

1) T has the heap-shape

2) T is min-max ordered: values stored at nodes on even (odd) levels are smaller (greater) than or equal

to the values stored at their descendants (if any) where the root is at level zero. Thus, the smallest

value of S is stored at the root of T, whereas the largest value is stored at one of the root’s children;

an example of a min-max heap is shown in Figure 1

A min-max heap on n elements can be stored in an array A[1 . . . n]. The ith location in the array will

correspond to a node located on level L(log,i)l in the heap. A max-min heap is defined analogously; in

such a heap, the maximum value is stored at the root, and the smallest value is stored at one of the

root’s children. It is interesting to observe that the Hasse diagram for a min-max heap (i.e., the diagram representing the order relationships implicit within the structure) is rather complex in contrast with the one for a

traditional heap (in this case, the Hasse diagram is the heap itself); Figure 2 (p. 998) shows the Hasse

diagram for the example of Figure 1. Algorithms processing min-max heaps are very similar to those corresponding to conventional heaps. Creating a min-max heap is accomplished by an adaption of Floyd’s [4] linear-time heap construction algorithm. Floyd’s algorithm builds a heap in a bottom-up fashion. When the algorithm examines the subtree rooted at A[i] then both subtrees of A[i] are max-ordered, whereas the subtree itself may not necessarily be max-ordered. The TrickleDown step of his algorithm exchanges the value at A[i] with the maximum of its children. This step is then applied recursively to this maximum child to maintain the

max-ordering. In min-max heaps, the required ordering must be established between an element, its

children, and its grandchildren. The procedure must differentiate between min- and max-levels. The resulting

description of this procedure follows:

procedure TrickleDown

- - i is the position in the array

if i is on a min level then

TrickleDownMin(i)

else

TrickleDownMax(i)

endif

procedure TrickleDownMin(i)

29

Page 30: lecturer notes

if A[i] has children then

m := index of smallest of the

children and grandchildren

(if any) of A[i]

if A[m] is a grandchild of A[i] then

if A[m] < A[i] then

swap A[i] and A[m]

if A[m] > A[parent(m)] then

swap A[m] and A[parent(m)]

endif

TrickleDownMin(m)

endif

else {A[m] is a child of A[i]]

if A[m] < A[i] then

swap A[i] and A[m]

endif

endif

30

Page 31: lecturer notes

The procedure TrickleDownMax is the same except that the relational operators are reversed. The operations DeleteMin and DeleteMax are analogous to deletion in conventional heaps. Specifically, the required element is extracted and the vacant position is filled with the last element of the heap. The minmax ordering is maintained after applying the TrickleDown procedure. An element is inserted by placing it into the first available leaf position and then reestablishing the ordering on the path from this element to the root.An efficient algorithm to insert an element can be designed by examining the Hasse diagram (recall Figure 2). The leaf-positions of the heap correspond to the nodes lying on the middle row in the Hasse diagram. To reestablish the min-max ordering, the

new element is placed into the next available leaf position, and must then move up the diagram toward

the top, or down toward the bottom, to ensure that all paths running from top to bottom remain

sorted. Thus the algorithm must first determine whether the new element should proceed further

down the Hasse diagram (i.e., up the heap on maxlevels) or up the Hasse diagram (i.e., up the heap on

successive min-levels). Once this has been determined, only grandparents along the path to the root

of the heap need be examined-either those lying on min-levels or those lying on max-levels.

The algorithms are as follows:

procedure BubbleUp

- - i is the position in the array

if i is on a min-level then

if i has a parent cand A[i] >

A[parent(i)] then

swap A[i] and A[parent(i)]

BubbleUpMax(parent(i))

else

BubbleUpMin(i)

endif

else

if i has a parent cand A[i] <

A[parent(i)] then

swap A[i] and A[parent(i)]

BubbleUpMin(parent(i))

else

BubbleUpMax(i)

endif

31

Page 32: lecturer notes

endif

procedure BubbleUpMin(i)

if A[i] has grandparent then

if A[i] < A[grandparent(i)] then

swap A[i] and A[grandparent(i)]

BubbleUpMin(grandparent(i))

endif

endif

The cand (conditional and) operator in the above code evaluates its second operand only when the

first operand is true. BubbleUpMax is the same as BubbleUpMin except that the relational operators

are reversed. From the similarity with the traditional heap algorithms, it is evident that the min-max heap algorithms will exhibit the same order of complexity (in terms of comparisons and data movements). The

only difference rests with the actual constants: for construction and deletion the constant is slightly

higher, and for insertion the constant is slightly lower. The value of the constant for each operation is summarized in Table I: the reader is referred to [2] for a detailed derivation of these values. All logarithms are base 2.

Slight improvements in the constants can be obtained by employing a technique similar to the one used by Gonnet and Munro [S] for traditional heaps. The resulting new values are shown in Table II; again, details of the derivation can be found in [2]. In Table II the function g(x) is defined as follows: g(x) = 0 for x< 1 and g(a) = g(log(n)l) + 1.

Double Ended Priority Trees

There are 2 double ended priority structures called min-max heap and deaps which perform

Insertion, Deletion of the minimum element and Deletion of the maximum element in o(logn) time;

Reporting of the maximum and minimum element in constant time.

Min-Max heap: Min-Max heap: It has the following properties:32

Page 33: lecturer notes

1. It is a complete binary tree.

2. A node at an odd (respectively, even) level is called a minimum (respectively, maximum) node. If a node x is a minimum nodex is a minimum node (respectively, maximum) node then all elements in its subtrees all elements in its subtrees have values largerhave values larger (respectively, smaller) than xthan x..

DEAPS

The deap is a doubly ended queue.

A deap is a complete binary tree data structure, which is either empty or satisfies the following properties:

1)     The root contains no element.

2)  The left subtree is a minimum heap data structure.

3)    The right subtree is a maximum heap data structure.

4)   If the right subtree is not empty, then i is any node in the left (minimum) subtree and j is the corresponding node in the right (maximum) subtree. If such a j does not exists, then j is the node in the right subtree that corresponds to the parent of i. The key value in node i is less than or equal to that in node j. The nodes i and j are called partners.

Example of a Deap

33

-5

7 12

8811 19 18

30

1520

10Node i

Node j

Corresponding nodes

Page 34: lecturer notes

1 2 3 4 5 6 7 8 9 10 11

-5 30 7 12 15 20 8 11 19 18 10

Properties of Deap Data Structure

We see that the maximum and the minimum element can be determined in constant time .

The element j corresponding to element i (described in property 4) is computed as follows:

if element j exists, otherwise j is set to (j-1)/2. (which is its parent).

For the value 8 (at index i=7) the j partner is at index 11 and contains the value 10 .

For the value 11 at index i=8 the j partner is evaluated to be 12 , and since index 12 does not since index 12 does not exist, the j partner is at index 5 ( its parent ), containing the value 15exist, the j partner is at index 5 ( its parent ), containing the value 15 .

InsertionIntoDeap (Deap, maxElements, x)

if Deap is not full

maxElements = maxElements + 1;

if maxElements belongs to maximum Heap of the Deap

i = maxElements -

if x < Deap[i]

Deap[maxElements] = Deap[i];

BubbleUpMin (Deap, maxElements, x)

else

BubbleUpMax (Deap, maxElements, x)

else

34

Array RepresentationArray Representation

Page 35: lecturer notes

j = (maxElements + 2

log maxElements-1

) / 2

if j > maxElements

j = j / 2;

if x > Deap[j]

Deap[maxElements] = Deap[j]

BubbleUpMax (Deap, maxElements, x)

else

BubbleUpMin (Deap, maxElements, x)

DeleteMin (Deap)Output Deap[1]Deap[1] <-- Deap[maxElements]t <-- Deap[1]maxElements <-- maxElements – 1HeapifyMin (Deap, 1)i <-- index of value t in Deapj <-- maximum partner of iif (Deap[i] > Deap[j])swap Deap[i] and Deap[j]bubbleUpMax (Deap, j, Deap[j]

LEFTIST HEAPS

Definition 1: The distance of a node m in a tree, denoted dist[m], is the length of the

shortest path from m down to a descendant node that has at most one child.

Definition 2: A leftist heap is a binary tree such that for every node m,

(a) key[m] £ key[lchild[m]] and key(m) £ key[rchild[m]], and

(b) dist[lchild[m]] ³ dist[rchild[m]].

In the above definition, key[m] is the key stored at node m. We assume that there is a

total ordering among the keys. We also assume that key[nil] =¥ and dist[nil] = −1.

Definition 3: The right path of a tree is the path m1, m2, . . . , mk where m1 is the root of

the tree, mi+1 = rchild[mi] for 1 £ i < k, and rchild[mk] = nil.

Figure 1 below shows two examples of leftist heaps.

Here are a few simple results about leftist heaps that you should be able to prove easily:35

Page 36: lecturer notes

Fact 1: The left and right subtrees of a leftist heap are leftist heaps.

Fact 2: The distance of a leftist heap’s root is equal to the length of the tree’s right path.

Fact 3: For any node m of a leftist heap, dist[m] = dist[rchild[m]] +1 (where, as usual,

we take dist[nil] = −1).

Figure 1. In each node we record its key at the top half and its distance at the

bottom half. The right path of T1 is 1, 5, 8 while the right path of T2 is 1.

In the examples above, T2 illustrates the fact that leftist heaps can be unbalanced. However,

in INSERT, DELETEMIN and UNION, all activity takes place along the right path which, the following theorem shows, is short.

Theorem: If the length of the right path in a leftist tree T is k then T has at least 2k+1 −1 nodes.

Proof: By induction on the height h of T.

Basis (h=0): Then T consists of a single node and its right path has length k=0. Indeed, T has 1 ³ 21 −1 nodes, as wanted.

Inductive Step (h>0): Suppose the theorem holds for all leftist heaps that have height < h and let T be a leftist heap of height h. Further, let k be the length of T’s right path and n

be the number of nodes in T. Consider two cases:

Case 1: k=0 (i.e. T has no right subtree). But then clearly n ³1=21 −1, as wanted.

36

Page 37: lecturer notes

Case 2: k>0. Let TL, TR be the left and right subtrees of T; nL, nR be the number of

nodes in TL and TR; and kL, kR be the lengths of the right paths in TL and TR respectively.

By Fact 1, TR and TL are both leftist heaps. By Facts 2 and 3, kR = k −1, and by

definition of leftist tree kL ³ kR. Since TL, TR have height < h we get, by induction

hypothesis, nR ³ 2k −1 and nL ³ 2k −1. But n = nL + nR +1 and thus,

n ³ 2k −1 + 2k −1 +1=2k+1 −1. Therefore n ³ 2k+1 −1, as wanted.

From this we immediately get

Corollary: The right path of a leftist heap with n nodes has length £ ëlog(n +1)û − 1.

Now let’s examine the algorithm for joining two leftist heaps. The idea is simple: if one

of the two trees is empty we’re done; otherwise we want to join two non-empty trees T1

and T2 and we can assume, without loss of generality, that the key in the root of T1 is £

the key in the root of T2. Recursively we join T2 with the right subtree of T1 and we

make the resulting leftist heap into the right subtree of T1. If this has made the distance of

the right subtree’s root longer than the distance of the left subtree’s root, we simply interchange

the left and right children of T1’s root (thereby making what used to be the right

subtree of T1 into its left subtree and vice-versa). Finally, we update the distance of T1’s

root. The following pseudo-code gives more details.

We assume that each node of the leftist heap is represented as a record with the following

format

where the fields have the obvious meanings. A leftist heap is specified by giving a pointer

to its root.

/* The following algorithm joins two leftist heaps whose roots are pointed at by r1 and r2, and returns a pointer to the root of the resulting leftist heap. */

function UNION (r1 , r2)

if r1 = nil then return r2

else if r2 = nil then return r1

else

if key[r1] > key[r2] then r1 r2

rchild[r1] UNION ( rchild[r1] , r2 )

37

Page 38: lecturer notes

if d(rchild[r1]) > d(lchild[r1])

then rchild[r1] lchild[r1]

dist[r1]d(rchild[r1]) 1

return r1

end {UNION}

function d(x) /* returns dist(x) */

if x = nil then return −1

else return dist[x]

end {d}

What is the complexity of this algorithm? First, observe that there is a constant number of

steps that must be executed before and after each recursive call to UNION. Thus the complexity

of the algorithm is proportional to the number of recursive calls to UNION. It is

easy to see that, in the worst case, this will be equal to p1 + p2 where p1 (respectively p2)

is 1 plus the length of the right path of the leftist heap whose root is pointed at by r1

(respectively r2). Let the number of nodes in these trees be n1, n2. By the above Corollary

we have p1 £ ëlog (n1 +1) û, p2 £ ëlog (n2 +1) û. Thus p1 + p2 £ log n1 + log n2 + 2. Let

n=max(n1, n2). Then p1 + p2 £ 2 log n + 2. Therefore, UNION is called at most

2 log n +2 times and the complexity of the algorithm is O(log (max(n1, n2))) in the worst

case.

Figure 2 below shows an example of the UNION operation.

Armed with the UNION algorithm we can easily write algorithms for INSERT and

DELETEMIN:

38

Page 39: lecturer notes

Figure 2. The UNION operation.

INSERT(e, r) {e is an element, r is pointer to root of tree}

1. Let r´ be a pointer to the leftist heap containing only e

2. return UNION (r´ , r).

DELETEMIN(r)

1. min ¬ element stored at r (root of leftist heap)

2. r ¬ UNION(lchild[r] , rchild[r])

3. return min.

By our analysis of the worst case time complexity of UNION it follows immediately that

the complexity of both these algorithms is O(log n) in the worst case, where n is the number

of nodes in the leftist heap.

In closing, we note that INSERT can be written as in the heap representation of priority queues, by adding the new node at the end of the right path, percolating its value up (if necessary), and switching

39

Page 40: lecturer notes

right and left children of some nodes (if necessary) to maintain the properties of the leftist heap after the insertion. As an exercise, write the INSERT algorithm for leftist heaps in this fashion. On the other hand, we cannot use the idea of percolating values down to implement DELETEMIN in leftist heaps the way we did in standard heaps: doing so would result in an algorithm with O(n) worst case complexity. As an exercise, construct a leftist heap where this worst case behaviour would occur.

Binomial Heaps

The binomial tree is the building block for the binomial heap. A binomial tree

is an ordered tree - that is, a tree where the children of each node are ordered. Binomial trees are de¯ned recursively, building up from single nodes. A single tree of degree k is constructed from two trees of degree k ¡ 1 by making the root of one tree the leftmost child of the root of the other tree. This process is shown in Figure 1.

A binomial heap H consists of a set of binomial trees. Such a set is a binomial

heap if it satisfies the following properties:

1. For each binomial tree T in H, the key of every node in T is greater than or equal to the key of its parent.

2. For any integer k >= 0, there is no more than one tree in H whose root has degree k.

The algorithms presented later work on a particular representation of a binomial

heap. Within the heap, each node stores a pointer to its leftmost child (if any) and its rightmost sibling (if any). The heap itself is a linked list of the roots of its constituent trees, sorted by ascending number of children. The following data is maintained for each non-root node x:

1. key[x] - the criterion by which nodes are ordered,

2. parent[x] - a pointer to the node's parent, or NIL if the node is a root

node.

3. child[x] - a pointer to the node's leftmost child, or NIL if the node is

childless,

4. sibling[x] - a pointer to the sibling immediately to the right of the node,

or NIL if the node has no siblings,

5. degree[x] - the number of children of x.

Figure 1: The production of a binomial tree. A shows two binomial trees of

40

Page 41: lecturer notes

degree 0. B Shows the two trees combined into a degree-1 tree. C shows two

degree-1 trees combined into a degree-2 tree.

Binomial Heap Algorithms

Creation

A binomial heap is created with the Make-Binomial-Heap function, shown below. The Allocate-Heap procedure is used to obtain memory for the new heap.

Make-Binomial-Heap()

1 H <--- Allocate-Heap()

2 head[H] <--- NIL

3 return H

Finding the Minimum

To find the minimum of a binomial heap, it is helpful to refer back to the binomial heap properties. Property one implies that the minimum node must be a root node. Thus, all that is needed is a loop over the list of roots.

Binomial-Heap-Minimum(Heap)

1 best node <--- head[Heap]

2 current <--- sibling[best node]

3 while current != NIL

4 do if key[current] < key[best node]

5 best node <--- current

6 return best node

Unifying two heaps

The rest of the binomial heap algorithms are written in terms of heap unification.

In this section, the unification algorithm is developed. Conceptually, the algorithm consists of two parts. The heaps are first joined together into one data structure, and then this structure is is manipulated into satisfying the binomial heap properties.

To address the second phase first, consider two binomial heaps H1 and H2, which are to be merged into H = H1 U H2. Both H1 and H2 obey the binomial heap properties, so in each of them, there is at most one tree whose root has degree k, for k >= 0. In H, however, there may be up to two such trees. To recover the second binomial heap property, such duplicates must be merged. This merging process may result in additional work: when merging a root of degree m with one of degree n, the operation involves adding one as a child of the other - this creates a root with degree p, where p = n + 1 or p = m + 1. However, it is perfectly possible for there to already be a node with degree p,

and so another merge is needed. This second merge has the same problem. If root nodes are considered in some arbitrary order, then after every merge, the entire list must be rechecked in case a new conflict has arisen. However, by requiring the list of roots to be in a monotonically increasing order, it is possible to scan through it in a linear fashion. This restriction is enforced by

41

Page 42: lecturer notes

the auxiliary routine Binomial-Heap-Merge:

Binomial-Heap-Merge(H1;H2)

1 H <--- Make-Binomial-Heap()

2 if key[head[H2]] < key[head[H1]]

3 then head[H] <--- head[H2]

4 current2 <--- sibling[head[H2]]

5 current1 <--- head[H1]

6 else head[H] <--- head[H1]

7 current1 <--- sibling[head[H1]]

8 current2 <--- head[H2]

9 current <--- head[H]

10 while current1 != NIL and current2 != NIL

11 do if key[current1] > key[current2]

12 then sibling[current] <--- current2

13 current <--- sibling[current]

14 current2 <--- sibling[current2]

15 else

16 sibling[current] <--- current1

17 current <--- sibling[current]

18 current1 <--- sibling[current1]

19 if current1 = NIL

20 then tail <--- current2

21 else tail <--- current1

22 while tail != NIL

23 do sibling[current] <--- tail

24 current <--- sibling[current]

25 tail <--- sibling[tail]

26 return head[H]

This routine starts by creating and initialising a new heap, on lines 1 through 9. The code maintains three pointers. The pointer current, stores the root of the tree most recently added to the heap. For the two input heaps, current1 and current2 record their next unprocessed root nodes. In the while loop, these pointers are used to add trees to the new heap, while maintaining the desired monotonic

42

Page 43: lecturer notes

ordering within the resulting list. Finally, the case where the two heaps have di®ering numbers of trees must be handled - this is done on lines 19 through 25. Before the whole algorithm is given, one more helper routine is needed. The Binomial-Link routine joins two trees of equal degree:

Binomial-Link(Root;Branch)

1 parent[Branch] = Root

2 sibling[Branch] = child[Root]

3 child[Root] = Branch

4 degree[Root] = degree[Root] + 1

And now, the full algorithm:

Binomial-Heap-Unify(Heap1,Heap2)

1 head[Final Heap] <--- Binomial-Heap-Merge(Heap1,Heap2)

2 if head[Final Heap] = NIL

3 then return Final Heap

4 previous <--- NIL

5 current <--- head[Final_Heap]

6 next <--- sibling[current]

7 while next != NIL

8 do need merge <--- TRUE

9 if (degree[current] != degree[next])

10 then need merge <--- FALSE

11 if (sibling[next] !=NIL and

12 degree[sibling[next]] = degree[next])

13 then need merge <--- FALSE

14 if (need merge)

15 then if (key[current] · key[next]

16 then sibling[current] <--- sibling[next]

17 Binomial-Link(current, next)

18 else if (previous !=NIL)

19 then sibling[previous] <--- next

20 Binomial-Link(next,current)

21 else head[Final Heap] <--- next

22 Binomial-Link(next,current)

23 else previous <--- current

43

Page 44: lecturer notes

24 current <--- next

25 next <--- sibling[current]

26 return Final_Heap

The first line creates a new heap, and populates it with the contents of the old heaps. At this point, all the data are in place, but the heap properties (which are relied on by other heap algorithms) may not hold. The remaining lines restore these properties. The first property applies to individual trees, and so is preserved by the merging operation. As long as Binomial-Link is called with the arguments in the correct order, the first property will never be violated. The second property is restored by repeatedly merging trees whose roots have the same degree.

Insertion

To insert an element x into a heap H, simply create a new heap containing x and unify it with H:

Binomial-Heap-Insert(Heap,Element)

1 New <--- Make-Binomial-Heap()

2 head[New] <--- Element

3 parent[New] <--- Element

4 sibling[New] <--- NIL

5 child[New] <--- NIL

6 degree[New] <--- 1

7 Binomial-Heap-Unify(Heap,New)

Extracting the Minimum

Extracting the smallest element from a binomial heap is fairly simple, due to the recursive manner in which binomial trees are constructed.

Binomial-Heap-Extract-Min(Heap)

1 min <--- Binomial-Heap-Minimum(Heap)

2 rootlist <--- null ;

3 current <--- child[min]

4 while current != NIL

5 parent[current] <--- NIL

6 rootlist <--- current + rootlist

7 new <--- Make-Binomial-Heap()

8 head[new] <--- rootlist[0]

9 Heap <--- Binomial-Heap-Unify(Heap;new)

10 return min

The only subtlety in the above pseudocode is on line six, where the next element is added to the front of the list. This is because, within a heap, the list of roots is ordered by increasing degree. (This

44

Page 45: lecturer notes

assumption is behind, for example, the implementation of the Binomial-Heap-Merge algorithm.) However, when a binomial tree is built, the children will be ordered by decreasing degree. Thus, it is necessary to reverse the list of children when said children are promoted to roots.

Decreasing a key

Decreasing the key of a node in a binomial heap is also simple. The required

node has its key adjusted, and is then moved up through the tree until it is

no less than its parent, thus ensuring the resulting structure is still a binomial

heap.

Binomial-Heap-Decrease-Key(Heap; item; key)

1 key[item] <--- key

2 current <--- item

3 while parent[current] != NIL and key[parent[current]] >

key[current]

4 tmp <--- data[current]

5 data[current] <--- data[parent[current]]

6 data[parent[current]] <--- tmp

7 current <--- parent[current]

Deletion

Deletion is simple, given the routines already discussed:

Binomial-Heap-Delete(Heap, item)

1 min <--- Binomial-Heap-Minimum(Heap)

2 Binomial-Heap-Decrease-Key(Heap,item,min - 1)

3 Binomial-Heap-Extract-Min(Heap)

FIBONACCI HEAPS

Introduction

Priority queues are a classic topic in theoretical computer science. The search

for a fast priority queue implementation is motivated primarily by two net- work optimization algorithms: Shortest Path (SP) and Minimum Spanning Tree (MST), i.e., the connector problem. As we shall see, Fibonacci Heaps provide a fast and elegant solution.

The following 3-step procedure shows that both Dijkstra’s SP-algorithm or Prim’s MST-algorithm can be implemented using a priority queue:

1. Maintain a priority queue on the vertices V (G).

2. Put s in the queue, where s is the start vertex (Shortest Path) or any vertex (MST). Give s a key of 0. Add all other vertices and set their key to infinity.

45

Page 46: lecturer notes

3. Repeatedly delete the minimum-key vertex v from the queue and mark it scanned. For each neighbor w of v do: If w is not scanned (so far), decrease its key to the minimum of the value calculated below and w’s current key:

• SP: key(v) + length(vw),

• MST: weight(vw).

The classical answer to the problem of maintaining a priority queue on the vertices is to use a binary heap, often just called a heap. Heaps are commonly used because they have good bounds on the time required for the following

operations: insert O(log n), delete-min O(log n), and decrease-key O(log n),

where n reflects the number of elements in the heap. If a graph has n vertices and e edges, then running either Prim’s or Dijkstra’s algorithms will require O(n log n) time for inserts and deletes. However, in the worst case, we will also perform e decrease-keys, because we may have to perform a key update every time we come across a new edge. This will take O(e log n) time. Since the graph is connected, e ≥ n, and the overall time bound is given by O(e log n). As we shall see, Fibonacci heaps allow us to do much better.

Definition and Elementary Operations

The Fibonacci heap data structure invented by Fredman and Tarjan in 1984 gives a very efficient implementation of the priority queues. Since the goal is to find a way to minimize the number of operations needed to compute the MST or SP, the kind of operations that we are interested in are insert, decrease-key, link, and delete-min (we have not covered why link is a useful operation yet, but this will become clear later on). The method to achieve this minimization goal is laziness - do work only when you must, and then use it to simplify the structure as much as possible so that your future work is easy. This way, the user is forced to do many cheap operations in order to make the data structure complicated. Fibonacci heaps make use of heap-ordered trees. A heap-ordered tree is one that maintains the heap property, that is, where key(parent) ≤ key(child) for all nodes in the tree.

Definition: A Fibonacci heap H is a collection of heap-ordered trees that have the following properties:

1. The roots of these trees are kept in a doubly-linked list (the root list of H),

2. The root of each tree contains the minimum element in that tree (this follows from being a heap-ordered tree),

3. We access the heap by a pointer to the tree root with the overall minimum key,

4. For each node x, we keep track of the degree (also known as the order or rank) of x, which is just the number of children x has; we also keep track of the mark of x, which is a Boolean value whose role will be explained later.

46

Page 47: lecturer notes

Fig.: A detailed view of a Fibonacci Heap. Null pointers are omitted for clarity.

For each node, we have at most four pointers that respectively point to the node’s parent, to one of its children, and to two of its siblings. The sibling pointers are arranged in a doubly-linked list (the child list of the parent node). We have not described how the operations on Fibonacci heaps are implemented, and their implementation will add some additional properties to H. The following are some elementary operations used in maintaining Fibonacci heaps:

Inserting a node x: We create a new tree containing only x and insert it into the root list of H; this is clearly an O(1) operation.

Linking two trees x and y: Let x and y be the roots of the two trees we want to link; then if key(x) ≥ key(y), we make x the child of y; otherwise, we make y the child of x. We update the appropriate node’s degrees and the appropriate child list; this takes O(1) operations.

Cutting a node x: If x is a root in H, we are done. If x is not a root in H, we remove x from the child list of its parent, and insert it into the root list of H, updating the appropriate variables (the degree of the parent of x is decremented, etc.). Again, this takes O(1) operations. We assume that when we want to cut/find a node, we have a pointer hanging around that accesses it directly, so actually finding the node takes O(1) time.

47

Page 48: lecturer notes

Fig. The Cleanup algorithm executed after performing a delete-min

Marking a node x: We say that x is marked if its mark is set to true, and that it is unmarked if its mark is set to false. A root is always unmarked. We mark x if it is not a root and it loses a child (i.e., one of its children is cut and put into the root-list). We unmark x whenever it becomes a root. We shall see later on that no marked node will lose a second child before it is cut itself.

The delete-min Operation

Deleting the minimum key node is a little more complicated. First, we remove the minimum key from the root list and splice its children into the root list. Except for updating the parent pointers, this takes O(1) time. Then we scan through the root list to find the new smallest key and update the parent

pointers of the new roots. This scan could take O(n) time in the worst case. To bring down the amortized deletion time (see further on), we apply a Cleanup algorithm, which links trees of equal degree until there is only one root node of any particular degree.

Let us describe the Cleanup algorithm in more detail. This algorithm maintains a global array B[1 . . . ⌊log n⌋], where B[i] is a pointer to some previously-visited root node of degree i, or Null if there is no such previously- visited root node. Notice, the Cleanup algorithm simultaneously resets the parent pointers of all the new roots and updates the pointer to the minimum key. The part of the algorithm that links possible nodes of equal degree is given in a separate subroutine LinkDupes, see Figure The subroutine

Fig.: The Promote algorithm

48

Page 49: lecturer notes

ensures that no earlier root node has the same degree as the current. By the possible swapping of the nodes v and w, we maintain the heap property. We shall analyze the efficiency of the delete-min operation further on. The fact that the array B needs at most ⌊log n ⌋ entries is proven in Section , where we prove that the degree of any (root) node in an n-node Fibonacci heap is bounded by ⌊log n⌋.

The decrease-key Operation

If we also need the ability to delete an arbitrary node. The usual way to do this is to decrease the node’s key to −∞ and then use delete-min. We start by describing how to decrease the key of a node in a Fibonacci heap; the algorithm will take O(log n) time in the worst case, but the amortized time will be only O(1). Our algorithm for decreasing the key at a node v follows two simple rules:

1. If newkey(v) < key(parent(v)), promote v up to the root list (this moves the whole subtree rooted at v).

2. As soon as two children of any node w have been promoted, immediately promote w.

In order to enforce the second rule, we now mark certain nodes in the Fibonacci heap. Specifically, a node is marked if exactly one of its children has been promoted. If some child of a marked node is promoted, we promote (and unmark) that node as well. Whenever we promote a marked node, we

unmark it; this is the only way to unmark a node (if splicing nodes into the root list during a delete-min is not considered a promotion). A more formal description of the Promote algorithm is given in Figure. This algorithm is executed if the new key of the node v is smaller than its parent’s key.

SKEW HEAP

Skew heaps are one of the possible implementations of priority queues. A skew heap is a self-adjusting form of a leftist heap, which may grow arbitrarily unbalanced because they do not maintain balancing information.

Definition

Skew Heaps are a self-adjusting form of Leftist Heap. By unconditionally swapping all nodes in the merge path Skew Heaps attempt to maintain a short right path from the root. The following diagram is a graphical representation of a Skew Heap.

From the above diagram it can be seen that a Skew Heap can be “heavy” at times on the

right side of the tree. Depending on the order of operations Skew Heaps can have long or

short, right hand side path lengths. As the following diagram shows by inserting the

49

Page 50: lecturer notes

element ‘45’ into the above heap a short right hand side is achieved:

Algorithm

Skew Heaps are of interest as they do not maintain any balancing information but still can achieve amortized log n time in the Union Operation.

The following operations can be executed on Skew Heaps:

1. MakeHeap (Element e)

2. FindMin (Heap h)

3. DeleteMin (Heap h)

4. Insert (Heap h, Element e)

5. Union (Heap h1, Heap h2)

The only difference between a skew heap and a leftist heap is the union operation is treated differently in skew heaps. The swapping of the children of a visited node on the right path is performed unconditionally; the dist value is not maintained.

The purpose of the swapping is to keep the length of the right path bounded, even though the length of the right path can grow to Omega (n), it is quite effective. The reasoning behind this is that insertions are made on the right side and therefore creating a “heavy” right side. Then by swapping everything unconditionally a relatively “light” right side is created. So, the good behavior of skew heaps is due to always inserting to the right and unconditionally swapping all nodes. Refer to Bibliography for reference to Proof. Note that the operation Union is used for Inserts and DeleteMin as specified below.

MakeHeap (Element e)

return new Heap(e);

FindMin(Heap h)

if (h == null)

return null;

50

Page 51: lecturer notes

else

return h.key

DeleteMin(Heap h)

Element e = h.key;

h = Union (h.left, h.right);

return e;

Insert (Heap h, Element e)

z = MakeHeap(e);

h = Union (h, z);

Union (Heap h1, heap h2)

Heap dummy;

if (h1 == null)

return h2;

else if (h2 == null)

return h1;

else

{

// Assure that the key of h1 is smallest

if (h1.key > h2.key){

Node dummy = h1;

h1 = h2;

h2 = dummy;

}}

if (h1.right == null) // Hook h2 directly to h1

h1.right = h2;

else // Union recursively

h1.right = Union (h1.right, h2);

// Swap children of h1

dummy = h1.right;

h1.right = h1.left;

h1.left = dummy;

return h1;

Applications

51

Page 52: lecturer notes

Because of the self-adjusting nature of this Heap the Union Operation has sufficiently

enhanced and runs in O (log n) amortized time.

In some applications, pairs of heaps are repeatedly unioned; spending linear time per

union operation would be out of the question. Skew Heap supports union in O (log n)

amortized time and therefore are a good candidate for applications that will repeatedly

call the Union operation.

The self-adjustment in skew heaps has important advantages over the balancing in leftist

heaps:

Reduction of memory consumption (no balancing information needs to be stored)

Reduction of time consumption (saving the updates of the dist information)

Unconditional execution (saving one test for every processed node

Binomial heaps with Lazy Meld

Definition A lazy meld binomial heap is a collection of heap-ordered binomial trees B(i)’s such that when n items are stored in the heap, the largest tree is B(⌊(log n)⌋); the reason is that, as we have seen before, each B(i) has 2i nodes. Thus since a B(i) has no empty node, and no value is duplicated, in order to store n values, we need n nodes. Thus the biggest tree with n nodes is a B(log n). This kind of heap is implemented as a doubly linked circular list of binomial trees. It is then possible to have multiple B(i)’s for the same i in a single heap. This increases the cost of the previously defined delete-min operation, since it has to find the new minimum value among a bigger number of trees than before. We thus have to redefine delete-min so as to make sure that after a delete-min operation, there is at most one tree per rank again in the heap.

Operations on binomial heaps with lazy meld

Definition . Insert(h, i) = meld(h, make-heap(i)). As you can see, this kind of heap is very lazy at insertion.

Algorithm Meld

Require: h, h′

Ensure: h′′ = meld(h, h’)

concatenate list of h and h′

update min pointer

This algorithm runs in O(1) time.

Algorithm Delete-min

Require: h

Ensure: h′′ = delete-min(h)

delete the min from its binomial tree and break this binomial tree into a heap h′

52

Page 53: lecturer notes

create array of ceil((log n)) empty pointers

for all trees in h and h′ do

insert the tree B(i) into the array at position i, linking and carrying if necessary so as to have at most one tree per rank

compare root of inserted tree to min pointer and update min pointer suitably.

end for

Example Sequence of insert and one delete-min

If we consider the following sequence of insert : insert(1), insert(3), insert(5), this will create the following heap h, made of doubly linked list with three B(0), with the min pointer on the first B(0) tree :

Then a delete-min will first remove the B(0) which has the min pointer on it. Since this tree is a B(0), then no new heap is made out of its other nodes. Then an array of empty pointers of size n is ⌊ ⌋created. Now the first tree of h is inserted in the array, at the position 0, since it is a B(0). Then the same is done for the second tree of h. We want to place it at the position 0 of the array, but since it already contains a tree, then a link is performed, creating a B(1).

Thus the resulting heap is

Amortized analysis

The amortized analysis with banker’s view is identical as before, except that we place now 2 ∗ c coins at each new

tree’s root, with c defined below. The amortized analysis with physicist view is as follows : the potential function is

= 2c trees in doubly linked list, where c is a suitable constant chosen below.

53

Page 54: lecturer notes

Now we have the following amortized costs :

• Meld : amortized cost = O(1) + 0 = 0(1)

• Insert : amortized cost = O(1) + 2c = 0(1)

• Delete : let m be the number of trees of h and h′ right after the step 1 of the algorithm; let k be the number of link operations in the step 3. Then

– at the end of the algorithm, there are ≤ log n trees, since there is at most one tree per rank, and the biggest one has rank ⌊(log n)⌋. Since each link operation removes one tree, then m − k ≤ log n.

– let c be a constant such that the acutal cost is ≤ c(m + k). Since m ≤ log n + k, then acutal cost

≤ c ∗ (log n + 2k) = c ∗ log n + 2ck. This is the c we use in the potential function.

– the change in is as follows : the heap h′ containging at most log n trees, it adds at most log n trees. As each link removes one tree, then the change is ≤ 2c(log n − k) = 2c ∗ log n − 2ck. → thus the amortized cost is ≤ c ∗ log n + 2ck + 2c log n − 2ck = 3c log n.

Summary

54

Page 55: lecturer notes

UNIT III SEARCH STRUCTURES

BinarySearchTree : An Unbalanced Binary Search Tree

A BinarySearchTree is a special kind of binary tree in which each node, u, also stores a data value, u:x, from some total order. The data values in a binary search tree obey the binary search tree property: For a node, u, every data value stored in the subtree rooted at u:left is less than u:x and every data value stored in the subtree rooted at u:right is greater than u:x. An example of a BinarySearchTree is shown in Figure

Searching

The binary search tree property is extremely useful because it allows us to quickly locate

a value, x, in a binary search tree. To do this we start searching for x at the root, r. When

examining a node, u, there are three cases:

Figure : A binary search tree

1. If x < u:x then the search proceeds to u:left;

2. If x > u:x then the search proceeds to u:right;

55

Page 56: lecturer notes

3. If x = u:x then we have found the node u containing x.

The search terminates when Case 3 occurs or when u = nil. In the former case, we found

x. In the latter case, we conclude that x is not in the binary search tree.

BinarySearchTree

T findEQ(T x) {Node *w = r;while (w != nil) {int comp = compare(x, w->x);if (comp < 0) {w = w->left;} else if (comp > 0) {w = w->right;} else {return w->x;}}return null;}

Two examples of searches in a binary search tree are shown in Figure . As the second example shows, even if we don’t find x in the tree, we still gain some valuable information. If we look at the last node, u, at which Case 1 occurred, we see that u:x is the smallest value in the tree that is greater than x. Similarly, the last node at which Case 2

Figure : An example of (a) a successful search (for 6) and (b) an unsuccessful search

(for 10) in a binary search tree.

occurred contains the largest value in the tree that is less than x. Therefore, by keeping track of the last node, z, at which Case 1 occurs, a BinarySearchTree can implement the find(x) operation that returns the smallest value stored in the tree that is greater than or equal to x

BinarySearchTree

T find(T x) {

56

Page 57: lecturer notes

Node *w = r, *z = nil;while (w != nil) {int comp = compare(x, w->x);if (comp < 0) {z = w;w = w->left;} else if (comp > 0) {w = w->right;} else {return w->x;}}return z == nil ? null : z->x;}

Addition

To add a new value, x, to a BinarySearchTree, we first search for x. If we find it, then there is no need to insert it. Otherwise, we store x at a leaf child of the last node, p, encountered during the search for x. Whether the new node is the left or right child of p depends on the result of comparing x and p:x.

BinarySearchTreebool add(T x) {Node *p = findLast(x);Node *u = new Node;u->x = x;return addChild(p, u);}BinarySearchTreeNode* findLast(T x) {Node *w = r, *prev = nil;while (w != nil) {prev = w;int comp = compare(x, w->x);if (comp < 0) {w = w->left;} else if (comp > 0) {w = w->right;} else {return w;}}return prev;}BinarySearchTreebool addChild(Node *p, Node *u) {

57

Page 58: lecturer notes

if (p == nil) {r = u; // inserting into empty tree} else {int comp = compare(u->x, p->x);if (comp < 0) {p->left = u;} else if (comp > 0) {p->right = u;} else {return false; // u.x is already in the tree}u->parent = p;}n++;return true;}

Figure : Inserting the value 8:5 into a binary search treeAn example is shown in Figure . The most time-consuming part of this process is theinitial search for x, which takes time proportional to the height of the newly added nodeu. In the worst case, this is equal to the height of the BinarySearchTree.

Removal

Deleting a value stored in a node, u, of a BinarySearchTree is a little more difficult. If u is

a leaf, then we can just detach u from its parent. Even better: If u has only one child, then

we can splice u from the tree by having u:parent adopt u’s child (see Figure ):

BinarySearchTree

void splice(Node *u) {Node *s, *p;if (u->left != nil) {s = u->left;} else {s = u->right;

58

Page 59: lecturer notes

}if (u == r) {r = s;p = nil;} else {p = u->parent;if (p->left == u) {p->left = s;} else {p->right = s;}}if (s != nil) {s->parent = p;}n--;}

Things get tricky, though, when u has two children. In this case, the simplest thing to do is to find a node, w, that has less than two children such that we can replace u:x with w:x. To maintain the binary search tree property, the value w:x should be close to the value of u:x. For example, picking w such that w:x is the smallest value greater than u:x will do. Finding the node w is easy; it is the smallest value in the subtree rooted at u:right. This node can be easily removed because it has no left child. (See Figure )

BinarySearchTreevoid remove(Node *u) {if (u->left == nil || u->right == nil) {splice(u);delete u;} else {Node *w = u->right;while (w->left != nil)w = w->left;u->x = w->x;splice(w);delete w;}}

Summary

The find(x), add(x), and remove(x) operations in a BinarySearchTree each involve following a path from the root of the tree to some node in the tree. Without knowing more about the shape of the tree it is difficult to say much about the length of this path, except that

59

Page 60: lecturer notes

Figure : Deleting a value (11) from a node, u, with two children is done by replacing u’s

value with the smallest value in the right subtree of u.

it is less than n, the number of nodes in the tree. The following (unimpressive) theorem summarizes the performance of the BinarySearchTree data structure:

Theorem . A BinarySearchTree implements the SSet interface. A BinarySearchTree

supports the operations add(x), remove(x), and find(x) in O(n) time per operation.

Theorem compares poorly with Theorem 4.1, which shows that the SkiplistSSet

structure can implement the SSet interface with O(logn) expected time per operation. The

problem with the BinarySearchTree structure is that it can become unbalanced. Instead

of looking like the tree in Figure it can look like a long chain of n nodes, all but the last

having exactly one child.

There are a number of ways of avoiding unbalanced binary search trees, all of

which lead to data structures that have O(logn) time operations.

AVL trees

We now define the special balance property we maintain for an AVL tree.

Definition .

(1) A vertex of a tree is balanced if the heights of its children differ by one at most.

(2) An AVL tree is a binary search tree in which all vertices are balanced. The binary search tree in Figure is not an AVL tree, because the vertex with key 16 is not balanced. The tree in Figure is an AVL tree.

60

Page 61: lecturer notes

Figure . An AVL tree.

We now come to our key result.

Theorem. The height of an AVL tree storing n items is O(lg(n)).

PROOF For h ∈ N, let n(h) be the minimum number of items stored in an AVL-tree

of height h. Observe that n(1) = 1, n(2) = 2, and n(3) = 4.

Our first step is to prove, by induction on h, that for all h ≥ 1 we have

n(h) > 2h/2 − 1.

As the induction bases, we note that (5.1) holds for h = 1 and h = 2 because

n(1) = 1 > √2 − 1 and n(2) = 2 > 21 − 1.

For the induction step, we suppose that h ≥ 3 and that (5.1) holds for h − 1

and h − 2. We observe that by Defn 5.4, we have

n(h) ≥ 1 + n(h − 1) + n(h − 2),

since one of the two subtrees must have height at least h−1 and the other height

at least h − 2. (We are also using the fact that if h1 ≥ h2 then n(h1) ≥ n(h2), this

is very easy to prove and you should do this.) By the inductive hypothesis, this

yields

The last line follows since

2−1/2+ 2−1 > 1. This can be seen without explicit calculations

as it is equivalent to 2-1/2 > 1/2 which is equivalent to 2 1/2< 2 and this now

follows by squaring. This completes the proof of .

Therefore for every tree of height h storing n items we have n ≥ n(h) > 2h/2 − 1.

Thus h/2 < lg(n + 1) and so h < 2 lg(n + 1) = O(lg(n)). This completes the proof of the Theorem.

It follows from our discussion of findElement that we can find a key in an AVL tree storing n items in time O(lg(n)) in the worst case. However, we cannot just insert items into (or remove items from) an AVL tree in a naive fashion, as the resulting tree may not be an AVL tree. So we need to devise appropriateinsertion and removal methods that will maintain the balance properties set out

in Definition.

Insertions61

Page 62: lecturer notes

Suppose we want to insert an item (k, e) (a key-element pair) into an AVL tree. We start with the usual insertion method for binary search trees. This method is to search for the key k in the existing tree using the procedure in lines 1–7 of findElement(k), and use the result of this search to find the “right” leaf location for an item with key k. If we find k in an internal vertex, we must walk down to

the largest near-leaf in key value which is no greater than k, and then use the appropriate neighbour leaf. In effect we ignore the fact that an occurrence of k has been found and carry on with the search till we get to a vertex v after which the search takes us to a leaf l (see Algorithm ). The vertex v is the “largest near-leaf.” We make a new internal vertex u to replace the leaf l and store the

item there (setting u.key = k, and u.elt = e).

The updating of u from a leaf vertex to an internal vertex (which will have two empty child vertices, as usual) will sometimes cause the tree to become unbalanced (no longer satisfying Definition ), and in this case we will have to repair it.

Clearly, any newly unbalanced vertex that has arisen as a result of inserting

into u must be on the path from the new vertex to the root of the tree. Let z be the unbalanced vertex of minimum height. Then the heights of the two children of z must differ by 2. Let y be the child of z of greater height and let x be the child of y of greater height. If both children of y had the same height, then z would already have been unbalanced before the insertion, which is impossible, because before the insertion the tree was an AVL tree. Note that the newly added vertex might be x itself, or it might be located in the subtree rooted at x. Let V and W be the two subtrees rooted at the children of x. Let X be the subtree rooted at the sibling of x and let Y be the subtree rooted at the sibling of y. Thus

we are in one of the situations displayed in Figures . Now we apply the operation that leads to part (b) of the respective figure. These operations are called rotations; consideration of the figures shows why. By applying the rotation we balance vertex z. Its descendants (i.e., the vertices below z in the tree before rotation) remain balanced. To see this, we make the following observations about the heights of the subtrees V,W,X, Y :

• height(V ) − 1 ≤ height(W) ≤ height(V ) + 1 (because x is balanced). In the Figures, we have always assumed that W is the higher tree, but it does not make a difference.

• max{height(V ), height(W)} = height(X) (as y is balanced and height(x) >height(X)).

62

Page 63: lecturer notes

Figure . A clockwise single rotation

Figure . An anti-clockwise single rotation

• max{height(V ), height(W)} = height(Y ) (because height(Y ) = height(y) − 2).

The rotation reduces the height of z by 1, which means that it is the same as it was before the insertion. Thus all other vertices of the tree are also balanced. Algorithm overleaf summarises insertItem(k, e) for an AVL-tree. In order to implement this algorithm efficiently, each vertex must not only store references to its children, but also to its parent. In addition, each vertex stores the

height of its left subtree minus the height of its right subtree (this may be −1, 0, or 1).

We now discuss TinsertItem(n), the worst-case running time of insertItem on a AVL tree of size n. Let h denote the height of the tree. Line 1 of Algorithm requires time O(h), and line 2 just O(1) time. Line 3 also requires time O(h), because in the worst case one has to traverse a path from a leaf to the root. Lines 4.-6. only require constant time, because all that needs to be done is redirect a few references to subtrees. By Theorem , we have h ∈ O(lg(n)). Thus the overall asymptotic running time of Algorithm is O(lg(n)).

63

Page 64: lecturer notes

Figure. An anti-clockwise clockwise double rotation

Figure . A clockwise anti-clockwise double rotation

Removals

Removals are handled similarly to insertions. Suppose we want to remove an item with key k from an AVL-tree. We start by using the basic removeItem method for binary search trees. This means that we start by performing steps 1– 7 of findElement(k) hoping to arrive at some vertex t such that t.key = k. If we achieve this, then we will delete t (and return t.elt), and replace t with the closest (in key-value) vertex u (u is a “near-leaf” vertex as before—note that this does not imply both u’s children are leaves, but one will be). We can find u by “walking down” from t to the near-leaf with largest key value no greater than k. Then t gets u’s item and u’s height drops by 1.

After this deletion and re-arrangement, any newly unbalanced vertex must be on the path from u to the root of the tree. In fact, there can be at most one unbalanced vertex (why?). Let z be this unbalanced vertex. Then the heights of the two children of z must differ by 2. Let y be the child of z of greater height and x the child of y of greater height. If both children of y have the same height, let x be the left child of y if y is the left child of z, and let x be the right child of y if y is the right child of z.

64

Page 65: lecturer notes

Now we apply the appropriate rotation and obtain a tree where z is balanced and where x, y remain balanced. However, we may have

Algorithm insertItem(k, e)

1. Perform lines 1.-7. of findElement()(k, e) on the tree to find the “right”

place for an item with key k (if it finds k high in the tree, walk down to

the “near-leaf” with largest key no greater than k).

2. Neighbouring leaf vertex u becomes internal vertex, u.key ← k, u.elt ← e.

3. Find the lowest unbalanced vertex z on the path from u to the root.

4. if no such vertex exists, return (tree is still balanced).

5. else

6. Let y and x be child and grandchild of z on z → u path.

7. Apply the appropriate rotation to x, y, z.

reduced the height of the subtree originally located at z by one, and this may cause the parent of z to become unbalanced. If this happens, we have to apply a rotation to the parent and, if necessary, rotate our way up to the root. Algorithm on the following page gives pseudocode for removeItem(k) operating on an AVL-tree.

Algorithm removeItem(k)

1. Perform lines 1–7 of findElement(k) on the tree to get to vertex t.

2. if we find t with t.key = k,

3. then remove the item at t, set e = t.elt.

4. Let u be “near-leaf” closest to k. Move u’s item to t.

5. while u is not the root do

6. let z be the parent of u

7. if z is unbalanced then

8. do the appropriate rotation at z

9. Reset u to be the (possibly new) parent of u

10. return e

11. else return NO SUCH KEY

We now discuss the worst-case running-time TremoveItem(n) of our AVL implementation

of removeItem. Again letting h denote the height of the tree, we recall that line 1 requires time O(h) = O(lg(n)). Lines 2. and 3. will take O(1) time, while line 4. will take O(h) time again. The loop in lines 5–9 is iterated at most h times. Each iteration requires time O(1). Thus the execution of the whole loop requires time O(h). Altogether, the asymptotic worst-case running time of removeItem is

O(h) = O(lg(n)).

2-3 Trees

65

Page 66: lecturer notes

Balanced Search Trees

Many data structures use binary search trees or generalizations thereof. Operations on such

search trees are often proportional to the height of the tree. To guarantee that such operations are

e±cient, it is necessary to ensure that the height of the tree is logarithmic in the number of nodes.

This is usually achieved by some sort of balancing mechanism that guarantees that subtrees of a

node never differ “too much" in their heights (by either an additive or multiplicative factor).

There are many kinds of balanced search trees. Here we study a particularly elegant form of

balanced search tree known as a 2-3 tree. There are many other kinds of balanced search trees

(e.g., red-black trees, AVL trees, 2-3-4 trees, and B-trees), some of which you will encounter in

CS231.

2-3 Trees

A 2-3 tree has three di®erent kinds of nodes:

A leaf, written as .

2. A 2-node, written as

X is called the value of the 2-node; l is its left subtree; and r is its right subtree. Every

2-node must satisfy the following invariants:

(a) Every value v appearing in subtree l must be · X.

(b) Every value v appearing in subtree r must be ¸ X.

(c) The length of the path from the 2-node to every leaf in its subtrees must be the same.

3. A 3-node, written as

X is called the left value of the 3-node; Y is called the right value of the 3-node; l is its

left subtree; m is its middle subtree; and r is its right subtree.

Every 3-node must satisfy the following invariants:

(a) Every value v appearing in subtree l must be · X.

(b) Every value v appearing in subtree m must be ¸ X and · Y .

66

Page 67: lecturer notes

(c) Every value v appearing in subtree r must be ¸ Y .

(d) The length of a path from the 3-node to every leaf in its subtrees must be the same.

The last invariant for 2-nodes and 3-nodes is the path-length invariant. The balance of

2-3 trees is a consequence of this invariant. The height of a 2-3 tree with n nodes cannot exceed

log2(n + 1). Together, the tree balance and the ordered nature of the nodes means that testing

membership in, inserting an element into, and deleting an element from a 2-3 tree takes logarithmic

time.

2-3 Tree Examples

Given a collection of three or more values, there are several 2-3 trees containing those values.

For instance, below are all four distinct 2-3 trees containing first 7 positive integers.

We shall use the term terminal node to refer to a node that has leaves as its subtrees. To save space, we often will not explicitly show the leaves that are the childred of a terminal node. For instance, here is another depiction of the tree t2 above without the explicit leaves:

67

Page 68: lecturer notes

2-3 Tree Insertion: Downward Phase

When inserting an element v into a 2-3 tree, care is required to maintain the invariants of 2-nodes and 3-nodes. As shown in the rules below, the order invariants are maintained much as in a binary search tree by comparing v to the node values encountered when descending the tree and moving in a direction that satisfies the order invariants. In the following rules, the result of inserting an element v into a 2-3 tree is depicted as a circled v with an arrow pointing down toward the tree in which it is to be inserted. X and Y are variables that stand for any elements, while triangles labeled l, m, and r stand for whole subtrees.

The rules state that elements equal to a node value are always inserted to the left of the node. This is completely arbitrary; they could be inserted to the right as well. Note that the tree that results from

68

Page 69: lecturer notes

inserting v into a tree T had better not have a different height from T. Otherwise, the path-length invariant would be violated. We will see how this plays out below.

69

Page 70: lecturer notes

2-3 Tree Insertion: Upward Phase

If there is a 2-node upstairs, the kicked-up value w can be absorbed by the 2-node:

By our assumptions about height, the resulting tree is a valid 2-3 tree.

If there is a 3-node upstairs, w cannot simply be absorbed. Instead, the 3-node is split into two

2-nodes that become the subtrees of a new kicked-up node one level higher. The value w and the

two 3-node values X and Y are appropriately redistributed so that the middle of the three values

70

Page 71: lecturer notes

is kicked upstairs at the higher level:

The kicking-up process continues until either the kicked-up value is absorbed or the root of the tree is reached. In the latter case, the kicked-up value becomes the value of a new 2-node that increases the height of the tree by one. This is the only way that the height of a 2-3 tree can increase.Convince yourself that heights and element order are unchanged by the downward or upward phases of the insertion algorithm. This means that the tree result from insertion is a valid 2-3 tree.

2-3 Tree Insertion: Special Cases for Terminal Nodes

The aforementioned rules are all the rules needed for insertion. However, insertion into terminal nodes is tedious because the inserted value will be pushed down to a leaf and then reflected up right away. To reduce the number of steps performed in examples, we can pretend that insertion into terminal nodes is handled by the following rules:

71

Page 72: lecturer notes

72

Page 73: lecturer notes

2-3 Tree Deletion: Upward Phase

The goal of the upward phase of 2-3 tree deletion is to propagate the hole up the tree until it can be eliminated. It is eliminated either (1) by being \absorbed" into the tree (as in the cases 2, 3, and 4 below) or (2) by being propagated all the way to the root of the 2-3 tree by repeated applications of the case 1. If a hole node propagates all the way to the top of a tree, it is simply removed, decreasing the height of the 2-3 tree by one. This is the only way that the height of a 2-3 node can decrease.

There are four cases for hole propagation/removal, which are detailed below.

You should convince yourself that each rule preserves both the element-order and path-length invariants.

1. The hole has a 2-node as a parent and a 2-node as a sibling.

73

Page 74: lecturer notes

In this case, the heights of the subtrees l, m, and r are the same.

2. The hole has a 2-node as a parent and a 3-node as a sibling.

In this case, the heights of the subtrees a, b, c, and d are the same.

3. The hole has a 3-node as a parent and a 2-node as a sibling. There are two subcases:

(a) The first subcase involves subtrees a, b, and c whose heights are one less than that of subtree d.

(b) The second subcase involves subtrees b, c, and d whose heights are one less than that of subtree a. When the hole is in the middle, there may be ambiguity in terms of whether to apply the right-hand rule of the ¯rst subcase or the left-hand rule of the second subcase. Either application is OK.

74

Page 75: lecturer notes

2-3-4 Trees

2-3-4 trees are a kind of prefectly balanced search tree. They are so named because every node has 2, 3, or 4 children, except leaf nodes, which are all at the bottom level of the tree. Each node stores 1, 2, or 3 entries, which determine how other entries are distributed among its children’s subtrees. Each non-leaf node has one more child than keys. For example, a node with keys [20, 40, 50] has four children. Eack key k in the subtree rooted at the first child satisfies k<=µ 20; at the second child, 20 <=µ k<= µ 40; at the third child, 40<= µ k<= µ 50; and at the fourth child, k>=§ 50.

B-trees: the general case of a 2-3-4 tree

2-3-4 trees are a type of B-tree. A B-tree is a generalized version of this kind of tree where the number of children that each node can have varies. Because a range of child nodes is permitted, B-trees do not need re-balancing as frequently as other self-balancing binary search trees, but may waste some space, since nodes are not entirely full. The lower and upper bounds on the number of child nodes are typically fixed for a particular implementation. For example, in a 2-3-4 tree, each non-leaf node may have only 2,3, or 4 child nodes. The number of elements in a node is one less than the number of children.

A B-tree is kept balanced by requiring that all leaf nodes are at the same depth. This depth will increase slowly as elements are added to the tree, but an increase in the overall depth is

75

Page 76: lecturer notes

infrequent, and results in all leaf nodes being one more hop further removed from the root. B-trees have advantages over alternative implementations when node access times far exceed access times within nodes. This usually occurs when most nodes are in secondary storage, such as on hard drives. By maximizing the number of child nodes within each internal node, the height of the tree

decreases, balancing occurs less often, and efficiency increases. Usually this value is set such that each node takes up a full disk block or some other size convenient to the storage unit being used. So in practice, B-trees with larger internal node sizes are more commonly used, but we will be discussing 2-3-4 trees since it is useful to be able to work out examples with a managable node size.

2-3-4 tree operations

• find(Key k)

Finding an entry is straightforward. Start at the root. At each node, check for the key k; if it’s not present, move down to the appropriate child chosen by comparing k against the keys. Continue until k is found, or k is not found at a leaf node. For example, find(74) traverses the double-lined boxes in the diagram below.

• Insert(KeyValPair p)

insert(), like find(), walks down the tree in search of the key k. If it finds an entry with key k, it proceeds to that entry’s ”left child” and continues. Unlike find(), insert() sometimes modifies nodes of the tree as it walks down. Specifically, whenever insert() encounters a 3-key node, the middle key is ejected, and is placed in the parent node instead. Since the parent was previously treated the same way, the parent has at most two keys, and always has room for a third. The other two keys in the 3-key node are split into two separate 1-key nodes, which are divided underneath the old middle key (as the figure illustrates).

76

Page 77: lecturer notes

For example, suppose we insert 60 into the tree depicted earlier. The first node traversed is the root, which has three children; so we kick the middle child (40) upstairs. Since the root node has no parent,

a new node is created to hold 40 and becomes the root. Similarly, 62 is kicked upstairs when insert() finds the node containing it. This ensures us that when we arrive at the leaf node (labeled 57 in this

case), there’s room to add the new key 60.

Observe that along the way, we created a new 3-key node “62 70 79”. We do not kick its middle key upstairs until the next time it is traversed. Again, the reasons why we split every 3-key node we encounter (and move its middle key up one level) are (1) to make sure there’s room for the new key in the leaf node, and (2) to make sure that above the leaf nodes, there’s room for any key that gets kicked upstairs. Sometimes, an insertion operation increases the depth of the tree by one by creating a new root.

77

Page 78: lecturer notes

• Remove(Key k)

2-3-4 tree remove() is similar to remove() on binary trees: you find the entry you want to remove (having key k). If it’s in a leaf node, you remove it. If it’s in an interior node, you replace it with the entry with the next higher key. That entry must be in a leaf node. In either case, you remove an entry from a leaf node in the end.

Like insert(), remove() changes nodes of the tree as it walks down. Whereas insert() eliminates 3-key nodes (moving nodes up the tree) to make room for new keys, remove() eliminates 1-key nodes (sometimes pulling keys down the tree) so that a key can be removed from a leaf without leaving it empty. There are three ways 1-key nodes (except the root) are eliminated.

1. When remove() encounters a 1-key node (except the root), it tries to steal a key from an adjacent sibling. But we can’t just steal the sibling’s key without violating the search tree invariant. This figure shows remove’s “rotation” action, when it reaches “30”.

We move a key from the sibling to the parent, and we move a key from the parent to the 1-key node. We also move a subtree S from the sibling to the 1-key node (now a 2-key node). Note that we can’t steal a key from a non-adjacent sibling.

2. If no adjacent sibling has more than one key, a rotation can’t be used. In this case, the 1-key node steals a key from its parent. Since the parent was previously treated the same way (unless it’s the root), it has at least two keys, and can spare one. The sibling is also absorbed, and the 1-key node becomes a 3-key node. The figure illustrates remove’s action when it reaches “10”. This is called a “fusion” operation.

78

Page 79: lecturer notes

3. If the parent is the root and contains only one key, and the sibling contains only one key, then the current 1-key node, its 1-key sibling, and the 1-key root are merged into one 3-key node that serves as the new root. The depth of the tree decreases by one. Eventually we reach a leaf. After processing the leaf, it has at least two keys (if there are at least two keys in the tree), so we can delete the key and still have one key in the leaf.

For example, suppose we remove 40 from the large tree depicted earlier. The root node contains 40, which we mark ”xx” here to remind us that we plan to replace it with the smallest key in the root node’s right subtree. To find that key, we move on to the 1-key node labeled “50”. Following our rules for 1-key nodes, we merge 50 with its sibling and parent to create a new 3-key root labeled ”20 xx 50”.

Next, we visit the node labeled 43. Again following our rules for 1-key nodes, we move 62 from a sibling to the root, and move 50 from the root to the node containing 43.

79

Page 80: lecturer notes

Finally, we move down to the node labeled 42. A different rule for 1-key nodes requires us to merge the nodes labeled 42 and 47 into a 3-key node, stealing 43 from the parent node.

The last step is to remove 42 from the leaf node and replace ”xx” with

42.

Running times

A 2-3-4 tree with depth d has between 2d and 4d leaf nodes. If n is the total number of nodes in the tree, then n § >=2(d+1)−1. By taking the logarithm of both sides, we find that d is in O(log n).

80

Page 81: lecturer notes

The time spent visiting a 2-3-4 node is typically longer than in a binary search tree (because the nodes and the rotation and fusion operations are complicated), but the time per node is still in O(1).

The number of nodes visited is proportional to the depth of the tree. Hence, the running times of the find(), insert(), and remove() operations are in O(d) and hence in O(log n), even in the worst case.

Red-black Trees

Properties

A binary search tree in which

The root is colored black

All the paths from the root to the leaves agree on the number of black nodes

No path from the root to a leaf may contain two consecutive nodes colored red

Empty subtrees of a node are treated as subtrees with roots of black color.

The relation n > 2h/2 - 1 implies the bound h < 2 log 2(n + 1).

Insertions

Insert the new node the way it is done in binary search trees

Color the node red

If a discrepancy arises for the red-black tree, fix the tree according to the type of discrepancy.

A discrepancy can result from a parent and a child both having a red color. The type of discrepancy is determined by the location of the node with respect to its grand parent, and the color of the sibling of the parent. Discrepancies in which the sibling is red, are fixed by changes in color. Discrepancies in which the siblings are black, are fixed through AVL-like rotations. Changes in color may propagate the problem up toward the root. On the other hand, at most one rotation is sufficient for fixing a discrepancy.

LLr

if ‘A’ is the root, then it should be repainted to black

81

Page 82: lecturer notes

LRr

if ‘A’ is the root, then it should be repainted to black

LLb

LRb

Discrepancies of type RRr, RLr, RRb, and RLb are handled in a similar manner.

82

Page 83: lecturer notes

insert 1

insert 2

insert 3

RRb discrepancy

insert 4

RRr discrepancy

insert 5

RRb discrepancy

Deletions

Delete a key, and a node, the way it is done in binary search trees.

A node to be deleted will have at most one child. If the deleted node is red, the tree is still a red-black tree. If the deleted node has a red child, repaint the child to black.

If a discrepancy arises for the red-black tree, fix the tree according to the type of discrepancy. A discrepancy can result only from a loss of a black node.

Let A denote the lowest node with unbalanced subtrees. The type of discrepancy is determined by the location of the deleted node (Right or Left), the color of the sibling (black or red), the number of red children in the case of the black siblings, and and the number of grand-children in the case of red siblings.

In the case of discrepancies which result from the addition of nodes, the correction mechanism may propagate the color problem (i.e., parent and child painted red) up toward the root, and stopped on the way by a single rotation. Here, in the case of discrepancies which result from the deletion of nodes,

83

Page 84: lecturer notes

the discrepancy of a missing black node may propagate toward the root, and stopped on the way by an application of an appropriate rotation.

Rb0

change of color, sending the deficiency up to the root of the subtree

Rb1

84

Page 85: lecturer notes

Rb2

Rr0

might result in LLb discrepancy of parent and child having both the red color

Rr1

85

Page 86: lecturer notes

Rr2

Similar transformations apply to Lb0, Lb1, Lb2, Lr0, Lr1, and Lr2.

86

Page 87: lecturer notes

B-trees

87

Page 88: lecturer notes

88

Page 89: lecturer notes

89

Page 90: lecturer notes

90

Page 91: lecturer notes

91

Page 92: lecturer notes

92

Page 93: lecturer notes

93

Page 94: lecturer notes

94

Page 95: lecturer notes

95

Page 96: lecturer notes

96

Page 97: lecturer notes

97

Page 98: lecturer notes

98

Page 99: lecturer notes

99

Page 100: lecturer notes

100

Page 101: lecturer notes

101

Page 102: lecturer notes

102

Page 103: lecturer notes

103

Page 104: lecturer notes

104

Page 105: lecturer notes

Preceding-Child(x) Returns the left child of key x.

105

Page 106: lecturer notes

• Move-Key(k, n1, n2) Moves key k from node n1 to node n2.

• Merge-Nodes(n1, n2) Merges the keys of nodes n1 and n2 into a new node.

• Find-Predecessor-Key(n, k) Returns the key preceding key k in the child of node n.

• Remove-Key(k, n) Deletes key k from node n. n must be a leaf node.

Splay trees

A splay tree is another type of balanced binary search tree. All splay tree operations run in O(log n) time on average, where n is the number of entries in the tree, assuming you start with an empty tree. Any single operation can take Θ(n) time in the worst case, but operations slower than

O(log n) time happen rarely enough that they don’t affect the average. Splay trees really excel in applications where a small fraction of the entries are the targets of most of the find operations, because they’re designed to give especially fast access to entries that have been accessed recently.

Splay trees have become the most widely used data structure invented in the last 20 years, because they’re the fastest type of balanced search tree for many applications, since it is quite common to want to access a small number of entries very frequently, which is where splay trees excel.

Splay trees, like AVL trees, are kept balanced by means of rotations. Unlike AVL trees, splay trees are not kept perfectly balanced, but they tend to stay reasonably well-balanced most of the time, thereby averaging O(logn) time per operation in the worst case (and sometimes achieving O(1)

average running time in special cases). We’ll analyze this phenomenon more precisely when we discuss amortized analysis.

Splay tree operations

• find(Key k)

The find operation in a splay tree begins just like the find operation in an ordinary binary search tree: we walk down the tree until we find the entry with key k, or reach a dead end. However, a splay tree isn’t finished its job. Let X be the node where the search ended, whether it contains the key k or not. We splay X up the tree through a sequence of rotations, so that X becomes the root of the tree. Why? One reason is so that recently accessed entries are near the root of the tree, and if we access the same few entries repeatedly, accesses will be quite fast. Another reason is because if X lies deeply down an unbalanced branch of the tree, the splay operation will improve the balance along that branch.

When we splay a node to the root of the tree, there are three cases

that determine the rotations we use.

1. X is the right child of a left child (or the left child of a right child): let P be the parent of X, and let G be the grandparent of X. We first rotate X and P left, and then rotate X and G right, as illustrated below.

106

Page 107: lecturer notes

The mirror image of this case– where X is a left child and P is a right child–uses the same rotations in mirror image: rotate X and P right, then X and G left. Both the case illustrated above and its mirror image are called the ”zig-zag” case.

2. X is the left child of a left child (or the right child of a right child): the ORDER of the rotations is REVERSED from case 1. We start with the grandparent, and rotate G and P right. Then,

we rotate P and X right.

The mirror image of this case– where X and P are both right children–uses the same rotations in mirror image: rotate G and P left, then P and X left. Both the case illustrated below and its mirror image are called the ”zig-zig” case.

We repeatedly apply zig-zag and zig-zig rotations to X; each pair of rotations raises X two levels higher in the tree. Eventually, either X will reach the root (and we’re done), or X will become the

child of the root. One more case handles the latter circumstance.

3. X’s parent P is the root: we rotate X and P so that X becomes the root. This is called the ”zig” case.

107

Page 108: lecturer notes

Here’s an example of find(7). Note how the tree’s balance improves.

By inspecting each of the three cases (zig-zig, zig-zag, and zig), you can observe a few interesting facts. First, in none of these three cases does the depth of a subtree increase by more than two. Second, every time X takes two steps toward the root (zig-zig or zig-zag), every node

in the subtree rooted at X moves at least one step closer to the root. As more and more nodes enter X’s subtree, more of them get pulled closer to the root. A node that initially lies at depth d on the access path from the root to X moves to a final depth no greater than 3 + d/2. In other words,

all the nodes deep down the search path have their depths roughly halved. This tendency of nodes on the access path to move toward the root prevents a splay tree from staying unbalanced for long (as the

example below illustrates).

108

Page 109: lecturer notes

• first(), last()

These methods begin by finding the entry with minimum or maximum key, just like in an ordinary binary search tree. Then, the node containing the minimum or maximum key is splayed to the root.

• insert(KeyValPair p)

insert begins by inserting the new entry p, just like in an ordinary binary search tree. Then, it splays the new node to the root.

• remove(Key k)

An entry having key k is removed from the tree, just as with ordinary binary search trees. Let X be the node removed from the tree. After X is removed, splay its parent to the root. Here’s a sequence illustrating the operation remove(2).

109

Page 110: lecturer notes

In this example, the key 4 moved up to replace the key 2 at the root. After the node containing 4 was removed, its parent (containing 5) splayed to the root. If the key k is not in the tree, splay the node where the search ended to the root, just like in a find operation.

TriesWhat Is A Trie?

Let us, for a moment, step back and reflect on the many sort methods developed in the text. We see that the majority of these methods (e.g., insertion sort, bubble sort, selection sort, heap sort, merge sort, and quick sort) accomplish the sort by comparing pairs of elements. Radix sort (and bin sort, which is a special case of radix sort), on the other hand, does not perform a single comparison between elements. Rather, in a radix sort, we decompose keys into digits using some radix; and the elements are sorted digit by digit using a bin sort.

Now, let us reflect on the dictionary methods developed in the text. The hashing methods use a hash function to determine a home bucket, and then use element (or key) comparisons to search either the home bucket chain (in the case of a chained hash table) or a contiguous collection of full buckets beginning with the home bucket (in the case of linear open addressing). The search tree data structures direct the search based on the result of comparisons performed between the search key and the element(s) in the root of the current subtree. We have not, as yet, seen a dictionary data structure that is based on the digits of the keys!

The trie (pronounced ``try'' and derived from the word retrieval) is a data structure that uses the digits in the keys to organize and search the dictionary. Although, in practice, we can use any radix to decompose the keys into digits, in our examples, we shall choose our radixes so that the digits are natural entities such as decimal digits (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) and letters of the English alphabet (a-z, A-Z).

Suppose that the elements in our dictionary are student records that contain fields such as student name, major, date of birth, and social security number (SS#). The key field is the social security number, which is a nine digit decimal number. To keep the example manageable, assume that the dictionary has only five elements. The name and SS# fields for each of the five elements in our dictionary are shown below.

Name | Social Security Number (SS#)

Jack | 951-94-1654

Jill | 562-44-2169

110

Page 111: lecturer notes

Bill | 271-16-3624

Kathy | 278-49-1515

April | 951-23-7625

Figure 1 Five elements (student records) in a dictionary

To obtain a trie representation for these five elements, we first select a radix that will be used to decompose each key into digits. If we use the radix 10, the decomposed digits are just the decimal digits shown in Figure 1. We shall examine the digits of the key field (i.e., SS#) from left to right. Using the first digit of the SS#, we partition the elements into three groups--elements whose SS# begins with 2 (i.e., Bill and Kathy), those that begin with 5 (i.e., Jill), and those that begin with 9 (i.e., April and Jack). Groups with more than one element are partitioned using the next digit in the key. This partitioning process is continued until every group has exactly one element in it.

The partitioning process described above naturally results in a tree structure that has 10-way branching as is shown in Figure 2. The tree employs two types of nodes--branch nodes and element nodes. Each branch node has 10 children (or pointer/reference) fields. These fields, child[0:9], have been labeled 0, 1, ..., 9 for the root node of Figure 2. root.child[i] points to the root of a subtrie that contains all elements whose first digit is i. In Figure 2, nodes A, B, D, E, F, and I are branch nodes. The remaining nodes, nodes C, G, H, J, and K are element nodes. Each element node contains exactly one element of the dictionary. In Figure 2, only the key field of each element is shown in the element nodes.

Figure 2 Trie for the elements of Figure 1

Searching a TrieTo search a trie for an element with a given key, we start at the root and follow a path down

the trie until we either fall off the trie (i.e., we follow a null pointer in a branch node) or we reach an element node. The path we follow is determined by the digits of the search key. Consider the trie of Figure 2. Suppose we are to search for an element with key 951-23-7625. We use the first digit, 9, in the key to move from the root node A to the node A.child[9] = D. Since D is a branch node, we use the next digit, 5, of the key to move further down the trie. The node we reach is D.child[5] = F. To move to the next level of the trie, we use the next digit, 1, of the key. This move gets us to the node

111

Page 112: lecturer notes

F.child[1] = I. Once again, we are at a branch node and must move further down the trie. For this move, we use the next digit, 2, of the key, and we reach the element node I.child[2] = J. When an element node is reached, we compare the search key and the key of the element in the reached element node. Performing this comparison at node J, we get a match. The element in node J, is to be returned as the result of the search.

When searching the trie of Figure 2 for an element with key 951-23-1669, we follow the same path as followed for the key 951-23-7625. The key comparison made at node J tells us that the trie has no element with key 951-23-1669, and the search returns the value null. To search for the element with key 562-44-2169, we begin at the root A and use the first digit, 5, of the search key to reach the element node A.child[5] = C. The key of the element in node C is compared with the search key. Since the two keys agree, the element in node C is returned. When searching for an element with key 273-11-1341, we follow the path A, A.child[2] = B, B.child[7] = E, E.child[3] = null. Since we fall off the trie, we know that the trie contains no element whose key is 273-11-1341.

When analyzing the complexity of trie operations, we make the assumption that we can obtain the next digit of a key in O(1) time. Under this assumption, we can search a trie for an element with a d digit key in O(d) time. Keys With Different Length

In the example of Figure 2, all keys have the same number of digits (i.e., 9). In applications in which different keys may have a different number of digits, we normally add a special digit (say #) at the end of each key so that no key is a prefix of another. To see why this is done, consider the example of Figure 2. Suppose we are to search for an element with the key 27. Using the search strategy just described, we reach the branch node E. What do we do now? There is no next digit in the search key that can be used to reach the terminating condition (i.e., you either fall off the trie or reach an element node) for downward moves. To resolve this problem, we add the special digit # at the end of each key and also increase the number of children fields in an element node by one. The additional child field is used when the next digit equals #. Height of a Trie

In the worst case, a root-node to element-node path has a branch node for every digit in a key. Therefore, the height of a trie is at most number of digits + 1.

A trie for social security numbers has a height that is at most 10. If we assume that it takes the same time to move down one level of a trie as it does to move down one level of a binary search tree, then with at most 10 moves we can search a social-security trie. With this many moves, we can search a binary search tree that has at most 210 - 1 = 1023 elements. This means that, we expect searches in the social security trie to be faster than searches in a binary search tree (for student records) whenever the number of student records is more than 1023. The breakeven point will actually be less than 1023 because we will normally not be able to construct full or complete binary search trees for our element collection.

Since a SS# is nine digits, a social security trie can have up to 109 elements in it. An AVL tree with 109 elements can have a height that is as much as (approximately) 1.44 log2(109+2) = 44. Therefore, it could take us four times as much time to search for elements when we organize our student record dictionary as an AVL tree than when this dictionary is organized as a trie! Space Required and Alternative Node Structures

The use of branch nodes that have as many child fields as the radix of the digits (or one more than this radix when different keys may have different length) results in a fast search algorithm. However, this node structure is often wasteful of space because many of the child fields are null. A

112

Page 113: lecturer notes

radix r trie for d digit keys requires O(rdn) child fields, where n is the number of elements in the trie. To see this, notice that in a d digit trie with n information nodes, each information node may have at most d ancestors, each of which is a branch node. Therefore, the number of branch nodes is at most dn. (Actually, we cannot have this many branch nodes, because the information nodes have common ancestors like the root node.)

We can reduce the space requirements, at the expense of increased search time, by changing the node structure. For example, each branch node of a trie could be replaced by any of the following:

A chain of nodes, each node having the three fields digitValue, child, next. Node A of Figure 2, for example, would be replaced by the chain shown in Figure 3.

Figure 3 Chain for node A of Figure 2

The space required by a branch node changes from that required for r children/pointer/reference fields to that required for 2p pointer fields and p digit value fields, where p is the number of children fields in the branch node that are not null. Under the assumption that pointer fields and digit value fields are of the same size, a reduction in space is realized when more than two-thirds of the children fields in branch nodes are null. In the worst case, almost all the branch nodes have only 1 field that is not null and the space savings become almost (1 - 3/r) * 100%.

A (balanced) binary search tree in which each node has a digit value and a pointer to the subtrie for that digit value. Figure 4 shows the binary search tree for node A of Figure 2.

Figure 4 Binary search tree for node A of Figure 2 Under the assumption that digit values and pointers take the same amount of space, the binary search

tree representation requires space for 4p fields per branch node, because each search tree node has fields for a digit value, a subtrie pointer, a left child pointer, and a right child pointer. The binary search tree representation of a branch node saves us space when more than three-fourths of the

children fields in branch nodes are null. Note that for large r, the binary serach tree is faster to search than the chain described above.

A binary trie (i.e., a trie with radix 2). Figure 5 shows the binary trie for node A of Figure 2. The space required by a branch node represented as a binary trie is at most (2 * ceil(log2r) + 1)p.

113

Page 114: lecturer notes

Figure 5 Binary trie for node A of Figure 2

A hash table. When a hash table with a sufficiently small loading density is used, the expected time performance is about the same as when the node structure of Figure 1 is used. Since we expect the fraction of null child fields in a branch node to vary from node to node and also to increase as we go down the trie, maximum space efficiency is obtained by consolidating all of the branch nodes into a single hash table. To accomplish this, each node in the trie is assigned a number, and each parent to child pointer is replaced by a triple of the form (currentNode, digitValue, childNode). The numbering scheme for nodes is chosen so as to easily distinguish between branch and information nodes. For example, if we expect to have at most 100 elements in the trie at any time, the numbers 0 through 99 are reserved for information nodes and the numbers 100 on up are used for branch nodes. The information nodes are themselves represented as an array information[100]. (An alternative scheme is to represent pointers as tuples of the form (currentNode, digitValue, childNode, childNodeIsBranchNode), where childNodeIsBranchNode = true iff the child node is a branch node.)

Suppose that the nodes of the trie of Figure 2 are assigned numbers as given below. This number assignment assumes that the trie will have no more than 10 elements.

Node A B C D E F G H I J K

Number 10 11 0 12 13 14 1 2 15 3 4

The pointers in node A are represented by the tuples (10,2,11), (10,5,0), and (10,9,12). The pointers in node E are represented by the tuples (13,1,1) and (13,8,2). The pointer triples are stored in a hash table using the first two fields (i.e., the currentNode and digitValue) as the key. For this purpose, we may transform the two field key into an integer using the formula currentNode * r + digitValue, where r is the trie radix, and use the division method to hash the transformed key into a home bucket. The data presently in information node i is stored in information[i]. To see how all this works, suppose we have set up the trie of Figure 2 using the hash table scheme just described. Consider searching for an element with key 278-49-1515. We begin with the knowledge that the root node is assigned the number 10. Since the first digit of the search key is 2, we query our hash table for a pointer triple with key (10,2). The hash table search is successful and the triple (10,2,11) is retrieved. The childNode component of this triple is 11, and since all information nodes have a number 9 or less, the child node is determined to be a branch node. We make a move to the branch node 11. To move to the next level of the trie, we use the second digit 7 of the search key. For the move, we query the hash table for a pointer with key (11,7). Once again, the search is successful and the triple (11,7,13) is retrieved. The next query to the hash table is for a triple with key (13,8). This time, we obtain the triple (13,8,2). Since, childNode = 2 < 10, we know that the pointer gets us to an information node. So, we compare the search key with the key of the element information[2].

114

Page 115: lecturer notes

The keys match, and we have found the element we were looking for. When searching for an element with key 322-167-8976, the first query is for a triple with key (10,3). The hash table has no triple with this key, and we conclude that the trie has no element whose key equals the search key. The space needed for each pointer triple is about the same as that needed for each node in the chain of nodes representation of a trie node. Therefore, if we use a linear open addressed hash table with a loading density of alpha, the hash table scheme will take approximately (1/alpha - 1) * 100% more space than required by the chain of nodes scheme. However, when the hash table scheme is used, we can retrieve a pointer in O(1) expected time, whereas the time to retrieve a pointer using the chain of nodes scheme is O(r). When the (balanced) binary search tree or binary trie schemes are used, it takes O(log r) time to retrieve a pointer. For large radixes, the hash table scheme provides significant space saving over the scheme of Figure 2 and results in a small constant factor degradation in the expected time required to perform a search. The hash table scheme actually reduces the expected time to insert elements into a trie, because when the node structure of Figure 2 is used, we must spend O(r) time to initialize each new branch node (see the description of the insert operation below). However, when a hash table is used, the insertion time is independent of the trie radix. To support the removal of elements from a trie represented as a hash table, we must be able to reuse information nodes. This reuse is accomplished by setting up an available space list of information nodes that are currently not in use (see Section 3.5 (Simulating Pointers) of the text).

Inserting into a TrieTo insert an element theElement whose key is theKey, we first search the trie for an existing element with this key. If the trie contains such an element, then we replace the existing element with theElement. When the trie contains no element whose key equals theKey, theElement is inserted into the trie using the following procedure.

Case 1 For Insert ProcedureIf the search for theKey ended at an element node X, then the key of the element in X and theKey are used to construct a subtrie to replace X.

Suppose we are to insert an element with key 271-10-2529 into the trie of Figure 2. The search for the key 271-10-2529 terminates at node G and we determine that the key, 271-16-3624, of the element in node G is not equal to the key of the element to be inserted. Since the first three digits of the keys are used to get as far as node E of the trie, we set up branch nodes for the fourth digit (from the left) onwards until we reach the first digit at which the two keys differ. This results in branch nodes for the fourth and fifth digits followed by element nodes for each of the two elements. Figure 6 shows the resulting trie.

115

Page 116: lecturer notes

Figure 6 Trie of Figure 2 with 271-10-2529 inserted

Case 2 For Insert ProcedureIf the search for theKey ends by falling off the trie from the branch node X, then we simply

add a child (which is an element node) to the node X. The added element node contains theElement. Suppose we are to insert an element with key 987-33-1122 to the trie of Figure 2. The search

for an element with key equal to 987-33-1122 ends when we fall off the trie while following the pointer D.child[8]. We replace the null pointer D.child[8] with a pointer to a new element node that contains theElement, as is shown in Figure 7.

Figure 7 Trie of Figure 2 with 987-33-1122 inserted

The time required to insert an element with a d digit key into a radix r trie is O(dr) because the insertion may require us to create O(d) branch nodes and it takes O(r) time to intilize the children pointers in a branch node.

116

Page 117: lecturer notes

Removing an ElementTo remove the element whose key is theKey, we first search for the element with this key. If

there is no matching element in the trie, nothing is to be done. So, assume that the trie contains an element theElement whose key is theKey. The element node X that contains theElement is discarded, and we retrace the path from X to the root discarding branch nodes that are roots of subtries that have only 1 element in them. This path retracing stops when we either reach a branch node that is not discarded or we discard the root.

Consider the trie of Figure 7. When the element with key 951-23-7625 is removed, the element node J is discarded and we follow the path from node J to the root node A. The branch node I is discarded because the subtrie with root I contains the single element node K. We next reach the branch node F. This node is also discarded, and we proceed to the branch node D. Since the subtrie rooted at D has 2 element nodes (K and L), this branch node is not discarded. Instead, node K is made a child of this branch node, as is shown in Figure 8.

Figure 8 Trie of Figure 7 with 951-23-7635 removed To remove the element with key 562-44-2169 from the trie of Figure 8, we discard the element node C. Since its parent node remains the root of a subtrie that has more than one element, the parent node is not discarded and the removal operation is complete. Figure 9 show the resulting trie.

Figure 9 Trie of Figure 8 with 562-44-2169 removed The time required to remove an element with a d digit key from a radix r trie is O(dr) because the removal may require us to discard O(d) branch nodes and it takes O(r) time to determine whether a

117

Page 118: lecturer notes

branch node is to be discarded. The complexity of the remove operation can be reduced to O(r+d) by adding a numberOfElementsInSubtrie field to each branch node.

118

Page 119: lecturer notes

UNIT IV GREEDY , DIVIDE AND CONQUER

Quicksort

Quicksort is a fast sorting algorithm, which is used not only for educational purposes, but widely applied in practice. On the average, it has O(n log n) complexity, making quicksort suitable for sorting big data volumes. The idea of the algorithm is quite simple and once you realize it, you can write quicksort as fast as bubble sort.

Algorithm

The divide-and-conquer strategy is used in quicksort. Below the recursion step is described:

Choose a pivot value. We take the value of the middle element as pivot value, but it can be any value, which is in range of sorted values, even if it doesn't present in the array.

Partition. Rearrange elements in such a way, that all elements which are lesser than the pivot go to the left part of the array and all elements greater than the pivot, go to the right part of the array. Values equal to the pivot can stay in any part of the array. Notice, that array may be divided in non-equal parts.

Sort both parts. Apply quicksort algorithm recursively to the left and the right parts.

Partition algorithm in detail

There are two indices i and j and at the very beginning of the partition algorithm i points to the first element in the array and j points to the last one. Then algorithm moves i forward, until an element with value greater or equal to the pivot is found. Index j is moved backward, until an element with value lesser or equal to the pivot is found. If i ≤ jthen they are swapped and i steps to the next position (i + 1), j steps to the previous one (j - 1). Algorithm stops, when i becomes greater than j.

After partition, all values before i-th element are less or equal than the pivot and all values after j-th element are greater or equal to the pivot.

Why does it work?

On the partition step algorithm divides the array into two parts and every element a from the left part is less or equal than every element b from the right part. Also a and b satisfy a ≤ pivot ≤ b inequality. After completion of the recursion calls both of the parts become sorted and, taking into account arguments stated above, the whole array is sorted.

Complexity analysis

On the average quicksort has O(n log n) complexity, but strong proof of this fact is not trivial

and not presented here. Still, you can find the proof in [1]. In worst case, quicksort runs O(n2) time, but on the most "practical" data it works just fine and outperforms other O(n log n) sorting algorithms.

Implementation

void quickSort(int numbers[], int array_size)

{

119

Page 120: lecturer notes

q_sort(numbers, 0, array_size - 1);

}

void q_sort(int numbers[], int left, int right)

{

int pivot, l_hold, r_hold;

l_hold = left;

r_hold = right;

pivot = numbers[left];

while (left < right)

{

while ((numbers[right] >= pivot) && (left < right))

right--;

if (left != right)

{

numbers[left] = numbers[right];

left++;

}

while ((numbers[left] <= pivot) && (left < right))

left++;

if (left != right)

{

numbers[right] = numbers[left];

right--;

}

}

numbers[left] = pivot;

pivot = left;

left = l_hold;

right = r_hold;

if (left < pivot)

q_sort(numbers, left, pivot-1);

if (right > pivot)

120

Page 121: lecturer notes

q_sort(numbers, pivot+1, right);

}

Strassen's Matrix Multiplication Algorithm

Matrix multiplication

Given two matrices AM*P and BP*N, the product of the two is a matrix CM*N which is computed as follows:

void seqMatMult(int m, int n, int p, double** A, double** B, double** C)

{

for (int i = 0; i < m; i++)

for (int j = 0; j < n; j++) {

C[i][j] = 0.0;

for (int k = 0; k < p; k++)

C[i][j] += A[i][k] * B[k][j];

}

}

Strassen's algorithm

To calculate the matrix product C = AB, Strassen's algorithm partitions the data to reduce the

number of multiplications performed. This algorithm requires M, N and P to be powers of 2.

The algorithm is described below.

1. Partition A, B and and C into 4 equal parts:

A = A11 A12

A21 A22

B = B11 B12

B21 B22

C = C11 C12

C21 C22

2. Evaluate the intermediate matrices:

M1 = (A11 + A22) (B11 + B22)

M2 = (A21 + A22) B11

M3 = A11 (B12 – B22)

M4 = A22 (B21 – B11)

M5 = (A11 + A12) B22

M6 = (A21 – A11) (B11 + B12)

121

Page 122: lecturer notes

M7 = (A12 – A22) (B21 + B22)

3. Construct C using the intermediate matrices:

C11 = M1 + M4 – M5 + M7

C12 = M3 + M5

C21 = M2 + M4

C22 = M1 – M2 + M3 + M6

Serial Algorithm

1. Partition A and B into quarter matrices as described above.

2. Compute the intermediate matrices:

1. If the sizes of the matrices are greater than a threshold value, multiply them

recursively using Strassen's algorithm.

2. Else use the traditional matrix multiplication algorithm.

3. Construct C using the intermediate matrices.

Parallelization

The evaluations of intermediate matrices M1, M2 ... M7 are independent and hence, can be

computed in parallel. On a machine with Q processors, Q jobs can be run at a time.

Initial Approach

The initial approach to a parallel solution used a task pool model to compute M1, M2 ... M7. As

shown in the diagram below, the second level of the algorithm creates 49 (7 * 7) independent

multiplication tasks which can all be executed in parallel, Q jobs at a time.

A Realization

Since the number of jobs is 49, ideally, on a machine with Q cores (where Q = 2q, Q <= 16),

122

Page 123: lecturer notes

48 of these would run concurrently while 1 would end up being executed later, thus, giving

poor processor utilization. It would be a better idea to split the last task further.

Final Parallel Algorithm

The final solution uses thread pooling with a pool of Q threads (where Q is the number of

processors), in conjunction with the Strategy pattern to implement Strassen's algorithm. The

algorithm is described below:

1 If the sizes of A and B are less than the threshold

1.1 Compute C = AB using the traditional matrix multiplication algorithm.

2 Else use Strassen's algorithm

2.1 Split matrices A and B

2.2 For each of Mi i = 1 to 7

2.2.1 Create a new thread to compute Mi = A'i B'i

2.2.2 If the sizes of the matrices are less than the threshold

2.2.2.1 Compute C using the traditional matrix multiplication algorithm.

2.2.3 Else use Strassen's algorithm

2.2.3.1 Split matrices A'i and B'i

2.2.3.2 For each of Mij j = 1 to 7

2.2.3.2.1 If i=7 and j=7 go to step 1 with A = A'77 and B = B'77

2.2.3.2.2 Get a thread from the thread pool to compute Mij = A'ij B'ij

2.2.3.2.3 Execute the recursive version of Strassen's algorithm in this thread

2.2.3.3 Wait for the Mij threads to complete execution

2.2.3.4 Compute Mi

2.3 Wait for the Mi threads to complete execution

2.4Compute C

123

Page 124: lecturer notes

The above algorithm defines 3 distinct strategies to be used with Strassen's algorithm:

1. Execute each child multiplication operation in a new thread (M1, M2, ..., M7)

2. Execute each child multiplication operation in a thread (Mij) from the thread pool

3. Execute each child multiplication operation using recursion

Conclusion

Strassen's algorithm definitely performs better than the traditional matrix multiplication

algorithm due to the reduced number of multiplications and better memory separation.

However, it requires a large amount of memory to run. The performance gain is sub-linear

which could be due to the fact that there are threads waiting for other threads to complete

execution.

Convex Hulls

A polygon is convex if any line segment joining two points on the boundary stays within the polygon. Equivalently, if you walk around the boundary of the polygon in counterclockwise direction you always take left turns. The convex hull of a set of points in the plane is the smallest convex polygon for which each point is either on the boundary or in the interior of the polygon. One might think of the points as being nails sticking out of a wooden board: then the convex hull is the shape formed by a tight rubber band that surrounds all the nails. A vertex is a corner of a polygon. For example, the highest, lowest, leftmost and rightmost points are all vertices of the convex hull. Some other characterizations are given in the exercises.

124

Page 125: lecturer notes

We discuss three algorithms: Graham Scan, Jarvis March and Divide & Conquer. We present the algorithms under the assumption that:

¤ no 3 points are collinear (on a straight line)

Graham Scan

The idea is to identify one vertex of the convex hull and sort the other points as viewed

from that vertex. Then the points are scanned in order. Let x0 be the leftmost point (which is guaranteed to be in the convex hull) and number the remaining points by angle from x0 going counterclockwise: x1; x2; : : : ; xn-1. Let xn = x0, the chosen point. Assume that no two points have the same angle from x0. The algorithm is simple to state with a single stack:

To prove that the algorithm works, it sucess to argue that:

¤ A discarded point is not in the convex hull. If xj is discarded, then for some i < j < k

the points xi --> xj ---> xk form a right turn. So, xj is inside the triangle x0, xi, xk

and hence is not on the convex hull.

125

Page 126: lecturer notes

What remains is convex. This is immediate as every turn is a left turn. The running time: Each time the while loop is executed, a point is either stacked or discarded. Since a point is looked at only once, the loop is executed at most 2n times. There is a constant-time subroutine for checking, given three points in order, whether the angle is a left or a right turn (Exercise). This gives an O(n) time algorithm, apart from the initial sort which takes time O(n log n). (Recall that the notation O(f(n)), pronounced “order f(n)", means “asymptotically at most a constant times f(n)".)

Jarvis March

This is also called the wrapping algorithm. This algorithm finds the points on the convex hull in the order in which they appear. It is quick if there are only a few points on the convex hull, but slow if there are many. Let x0 be the leftmost point. Let x1 be the first point counterclockwise when viewed from x0. Then x2 is the first point counterclockwise when viewed from x1, and so on.

126

Page 127: lecturer notes

Finding xi+1 takes linear time. The while loop is executed at most n times. More specifically, the while loop is executed h times where h is the number of vertices on the convex

hull. So Jarvis March takes time O(nh). The best case is h = 3. The worst case is h = n, when the points are, for example, arranged on the circumference of a circle.

Divide and Conquer

Divide and Conquer is a popular technique for algorithm design. We use it here to find the convex hull. The first step is a Divide step, the second step is a Conquer step, and the third step is a Combine step.

The idea is to:

Part 2 is simply two recursive calls. The first point to notice is that, if a point is in the overall convex hull, then it is in the convex hull of any subset of points that contain it. (Use characterization in exercise.) So the task is: given two convex hulls find the convex hull of their union.

Combining two hulls

It helps to work with convex hulls that do not overlap. To ensure this, all the points are presorted from left to right. So we have a left and right half and hence a left and right convex hull.

Define a bridge as any line segment joining a vertex on the left and a vertex on the right that does not cross the side of either polygon. What we need are the upper and lower bridges. The following produces the upper bridge.

1. Start with any bridge. For example, a bridge is guaranteed if you join the rightmost vertex on the left to the leftmost vertex on the right.

127

Page 128: lecturer notes

2. Keeping the left end of the bridge fixed, see if the right end can be raised. That is, look at the next vertex on the right polygon going clockwise, and see whether that would be a (better) bridge. Otherwise, see if the left end can be raised while the right end remains fixed.

3. If made no progress in (2) (cannot raise either side), then stop else repeat (2).

We need to be sure that one will eventually stop. Is this obvious?

Now, we need to determine the running time of the algorithm. The key is to perform step (2) in constant time. For this it is sucffiient that each vertex has a pointer to the next vertex going clockwise and going counterclockwise. Hence the choice of data structure: we store each hull using a doubly linked circular linked list .

It follows that the total work done in a merge is proportional to the number of vertices.

And as we shall see in the next chapter, this means that the overall algorithm takes time

O(n log n).

Tree vertex splitting

• Directed and weighted binary tree

• Consider a network of power line transmission

• The transmission of power from one node to the other results in some loss, such as drop in voltage

• Each edge is labeled with the loss that occurs (edge weight)

• Network may not be able to tolerate losses beyond a certain level

• You can place boosters in the nodes to account for the losses

Definition 1 Given a network and a loss tolerance level, the tree vertex splitting problem is to determine the optimal

placement of boosters.

You can place boosters only in the vertices and nowhere else

128

Page 129: lecturer notes

• More definitions

129

Page 130: lecturer notes

The above algorithm computes the delay by visiting each node using post-order traversal

int tvs ( tree T, int delta )

{130

Page 131: lecturer notes

if ( T == NULL ) return ( 0 ); // Leaf node

d_l = tvs ( T.left(), delta ); // Delay in left subtree

d_r = tvs ( T.right(), delta ); // Delay in right subtree

current_delay = max ( w_l + d_l,// Weight of left edge

w_r + d_r ); // Weight of right edge

if ( current_delay > delta )

{

if ( w_l + d_l > delta )

{

write ( T.left().info() );

d_l = 0;

}

if ( r_l + d_r > delta )

{

write ( T.right().info() );

d_r = 0;

}

}

current_delay = max ( w_l + d_l, w_r + d_r );

return ( current_delay );

}

Algorithm tvs runs in Θ(n) time

tvs is called only once on each node in the tree

On each node, only a constant number of operations are performed, excluding the time for recursive calls

Theorem 2 Algorithm tvs outputs a minimum cardinality set U such that d(T/U)<= µ δ on any tree T, provided no edge

of T has weight > δ.

Proof by induction:

Base case. If the tree has only one node, the theorem is true.

Induction hypothesis. Assume that the theorem is true for all trees of size <= µ n.

131

Page 132: lecturer notes

JOB SEQUENCING WITH DEADLINES ALGORITHM An SIMULATION

Consider a scheduling problem where the 6 jobs have a profit of (10,34,67,45,23,99) and corresponding deadlines (2,3,1,4,5,3). Obtain the optimum schedule. What is the time complexity of your algorithm? Can you improve it?

Ordering the jobs be nonincreasing order of profit:

Jobs = (99, 67, 45, 34, 23, 10)

Job No. =(6, 3, 4, 2, 5, 1)

Deadlines =(2, 3, 1, 4, 5, 3)

New job no. =(I, II, III. IV, V, VI)

Job I is allotted slot [0,1]

132

Page 133: lecturer notes

133

Page 134: lecturer notes

Job VI has a deadline of 3 but we cannot shift the array to the left, so we reject job VI.

The above is a the schedule.

FAST Job Sequencing with deadlines.

Consider a scheduling problem where the 6 jobs have a profit of (10,34,67,45,23,99) and corresponding deadlines (2,3,1,4,5,3). Obtain the optimum schedule. What is the time complexity of your algorithm? Can you improve it?

Sort jobs in nondecreasing order of profit:

Profits = (99, 67, 45, 34, 23, 10)

Job no. = ( 6, 3, 4, 2, 5, 1)

New no = ( I, II, III, IV, V, VI)

Deadline = (2, 3, 1, 4, 5, 3)

Start with all slots free.

134

Page 135: lecturer notes

135

Page 136: lecturer notes

136

Page 137: lecturer notes

137

Page 138: lecturer notes

138

Page 139: lecturer notes

139

Page 140: lecturer notes

140

Page 141: lecturer notes

141

Page 142: lecturer notes

142

Page 143: lecturer notes

143

Page 144: lecturer notes

UNIT V DYNAMIC PROGRAMMING AND BACKTRACKING

MULTISTAGE GRAPHS –

ALL PAIR SHORTESET PATH - FLOYD’S ALGORITHM

Given a weighted connected graph (undirected or directed), the all-pair shortest paths problem asks to find the distances (the lengths of the shortest paths) from each vertex to all other vertices. It is convenient to record the lengths of shortest paths in an n-by-n matrix D called the distance matrix: the element dij in the ith row and the jth column of this matrix indicates the length of the shortest path from the ith vertex to the jth vertex (1≤ i,j ≤ n). We can generate the distance matrix with an algorithm called Floyd’s algorithm. It is applicable to both undirected and directed weighted graphs provided that they do not contain a cycle of a negative length.

column of matrix D (k=0,1,. . . ,n) is equal to the length of the shortest path among all paths from the ith vertex to the jth vertex with each intermediate vertex, if any, numbered not higher than k. In particular, the series starts with D(0), which does not allow any intermediate vertices

144

Page 145: lecturer notes

in its paths; hence, D(0) is nothing but the weight matrix of the graph. The last matrix in the series, D(n) contains the lengths of the shortest paths among all pathsthat can use all n vertices as intermediate and hence is nothing but the distance matrix being sought.

145

Page 146: lecturer notes

0/1 KNAPSACK PROBLEM (dynamic programming)

Let i be the highest-numbered item in an optimal solution S for W pounds. Then S` = S - {i} is an optimal solution for W - wi pounds and the value to the solution S is Vi plus the value of the subproblem.

We can express this fact in the following formula: define c[i, w] to be the solution for items 1,2, . . . , i and maximum weight w. Then

146

Page 147: lecturer notes

This says that the value of the solution to i items either include ith item, in which case it is vi plus a subproblem solution for (i - 1) items and the weight excluding wi, or does not include ith item, in which case it is a subproblem's solution for (i - 1) items and the same weight.

That is, if the thief picks item i, thief takes vi value, and thief can choose from items w - wi, and get c[i - 1, w - wi] additional value. On other hand, if thief decides not to take item i, thief can choose from item 1,2, . . . , i- 1 upto the weight limit w, and get c[i - 1, w] value. The better of these two choices should be made.

Although the 0-1 knapsack problem, the above formula for c is similar to LCS formula: boundary values are 0, and other values are computed from the input and "earlier" values of c. So the 0-1 knapsack algorithm is like the LCS-length algorithm given in CLR for finding a longest common subsequence of two sequences.

The algorithm takes as input the maximum weight W, the number of items n, and the two sequences v = <v1, v2, . . . , vn> and w = <w1, w2, . . . , wn>. It stores the c[i, j] values in the table, that is, a two dimensional array, c[0 . . n, 0 . . w] whose entries are computed in a row-major order. That is, the first row of c is filled in from left to right, then the second row, and so on. At the end of the computation, c[n, w] contains the maximum value that can be picked into the knapsack.

147

Page 148: lecturer notes

The set of items to take can be deduced from the table, starting at c[n. w] and tracing backwards where the optimal values came from. If c[i, w] = c[i-1, w] item i is not part of the solution, and we are continue tracing with c[i-1, w]. Otherwise item i is part of the solution, and we continue tracing with c[i-1, w-W].

Analysis

This dynamic-0-1-kanpsack algorithm takes θ(nw) times, broken up as follows: θ(nw) times to fill the c-table, which has (n +1).(w +1) entries, each requiring θ(1) time to compute. O(n) time to trace the solution, because the tracing process starts in row n of the table and moves up 1 row at each step.

N-QUEENS PROBLEM

The problem is to place it queens on an n-by-n chessboard so that no two queens attack each other by being in the same row or in the same column or on the same diagonal. For n = 1, the problem has a trivial solution, and it is easy to see that there is no solution for n = 2 and n =3. So let us consider the four-queens problem and solve it by the backtracking technique. Since each of the four queens has to be placed in its own row, all we need to do is to assign a column for each queen on the board presented in the following figure.

Steps to be followed

We start with the empty board and then place queen 1 in the first possible position of its row, which is in column 1 of row 1.

Then we place queen 2, after trying unsuccessfully columns 1 and 2, in the first acceptable position for it, which is square (2,3), the square in row 2 and column 3. This proves to be a dead end because there i no acceptable position for queen 3. So, the algorithm backtracks and puts queen 2 in the next possible position at (2,4).

Then queen 3 is placed at (3,2), which proves to be another dead end.

148

Page 149: lecturer notes

The algorithm then backtracks all the way to queen 1 and moves it to (1,2). Queen 2 then goes to (2,4), queen 3 to (3,1), and queen 4 to (4,3), which is a solution to the problem.

The state-space tree of this search is given in the following figure.

Fig: State-space tree of solving the four-queens problem by back tracking.

(x denotes an unsuccessful attempt to place a queen in the indicated column.

The numbers above the nodes indicate the order in which the nodes are generated)

If other solutions need to be found, the algorithm can simply resume its operations at the leaf at which it stopped. Alternatively, we can use the board‘s symmetry for this purpose.

GRAPH COLORING

A coloring of a graph is an assignment of a color to each vertex of the graph so that no two vertices connected by an edge have the same color. It is not hard to see that our problem is one of coloring the graph of incompatible turns using as few colors as possible.

The problem of coloring graphs has been studied for many decades, and the theory of algorithms tells us a lot about this problem. Unfortunately, coloring an arbitrary graph with as few colors as possible is one of a large class of problems called "NP-complete problems," for which all known solutions are essentially of the type "try all possibilities."

149

Page 150: lecturer notes

A k-coloring of an undirected graph G = (V, E) is a function c : V → {1, 2,..., k} such that c(u) ≠ c(v) for every edge (u, v) E. In other words, the numbers 1, 2,..., k represent the k colors, and adjacent vertices must have different colors. The graph-coloring problem is to determine the minimum number of colors needed to color a given graph.

a. Give an efficient algorithm to determine a 2-coloring of a graph if one exists.

b. Cast the graph-coloring problem as a decision problem. Show that your decision problem is solvable in polynomial time if and only if the graph-coloring problem is solvable in polynomial time.

c. Let the language 3-COLOR be the set of graphs that can be 3-colored. Show that if 3-COLOR is NP-complete, then your decision problem from part (b) is NP-complete.

To prove that 3-COLOR is NP-complete, we use a reduction from 3-CNF-SAT. Given a formula φ of m clauses on n variables x, x,..., x, we construct a graph G = (V, E) as follows.

The set V consists of a vertex for each variable, a vertex for the negation of each variable, 5 vertices for each clause, and 3 special vertices: TRUE, FALSE, and RED. The edges of the graph are of two types: "literal" edges that are independent of the clauses and "clause" edges that depend on the clauses. The literal edges form a triangle on the special vertices and also form a triangle on x, ¬x, and RED for i = 1, 2,..., n.

d. Argue that in any 3-coloring c of a graph containing the literal edges, exactly one of a variable and its negation is colored c(TRUE) and the other is colored c(FALSE).

Argue that for any truth assignment for φ, there is a 3-coloring of the graph containing just the literal edges.

The widget is used to enforce the condition corresponding to a clause (x y z). Each clause requires a unique copy of the 5 vertices that are heavily shaded; they connect as shown to the literals of the clause and the special vertex TRUE.

e. Argue that if each of x, y, and z is colored c(TRUE) or c(FALSE), then the widget is 3-colorable if and only if at least one of x, y, or z is colored c(TRUE).

f. Complete the proof that 3-COLOR is NP-complete.

150

Page 151: lecturer notes

Fig: The widget corresponding to a clause (x y z), used in Problem

KNAPSACK PROBLEM ( backtracking)

The knapsack problem or rucksack problem is a problem in combinatorial optimization: Given a set of items, each with a weight and a value, determine the number of each item to include in a collection so that the total weight is less than a given limit and the total value is as large as possible.

It derives its name from the problem faced by someone who is constrained by a fixed-size knapsack and must fill it with the most useful items.

The problem often arises in resource allocation with financial constraints. A similar problem also appears in combinatorics, complexity theory, cryptography and applied mathematics.

The decision problem form of the knapsack problem is the question "can a value of at least V be achieved without exceeding the weight W?"

E.g. A thief enters a store and sees the following items:

His Knapsack holds 4 pounds. What should he steal to maximize profit?

Fractional Knapsack Problem

Thief can take a fraction of an item.

151

Page 152: lecturer notes

152

Page 153: lecturer notes

If knapsack holds k=5 pds, solution is:

1 pds A

3 pds B

1 pds C

General Algorithm-O(n):

Given:

weight w1 w2 … wn

cost c1 c2 … cn

Knapsack weight limit K

1. Calculate vi = ci / wi for i = 1, 2, …, n

2. Sort the item by decreasing vi

3. Find j, s.t.

w1 + w2 +…+ wj <= k < w1 + w2 +…+ wj+1

153

Page 154: lecturer notes

Answer is { wi pds item i, for i <= j

K-Σi<=j wi pds item j+1

Flow Shop Scheduling Problem:

1. INTRODUCTION

The purpose of this paper is twofold: (1) to provide a simulation program able to find the optimum /

near optimum sequence for general flow shop scheduling problem with make-span minimization as

main criteria; (2) to compare different dispatching rules on minimizing multiple criteria.

Numerous combinatorial optimization procedures have been proposed for solving the general

flowshop problem with the maximum flow time criterion. Many researches have been successful in

developing efficient solution algorithms for flowshop scheduling and sequencing [1, 2, 3, 4, 5, 6, 7

and 8] using up to 10 machines. Dannenbring [2] found that for small size shop problems his

heuristic outperformed others in minimizing the make-span for 1280 flowshop scheduling

problems. Ezat and El Baradie carried a simulation study for pure flowshop scheduling with makespan

minimization as a major criterion for n y90 on m y90 [9]. In This paper study general flow

shop scheduling problem with make-span minimization as main criteria for n y 250 and m y 250

with different ranges of random numbers generated (0-99) for processing times matrix.

2. THE FLOWSHOP SCHEDULING PROBLEM

The flowshop problem has interested researchers for nearly half a century. The flowshop problem

consists of two major elements: (1) a production system of ‘m’ machines; and (2) a set of ‘n’ jobs to

be processed on these machines. All ‘n’ jobs are so similar that they have essential the same order

of processing on the M machines, Fig. 1. The focus of this problem is to sequence or order the ‘n’

jobs through the ‘m’ machine(s) production system so that some measure of production cost is

minimized [10]. Indeed, flowshop scheduling problem has been shown to be NP-complete for nonpreemptive

schedules [11].

154

Page 155: lecturer notes

The assumptions of the flowshop problem are well documented in the production research literature

[3,4,5,18]. In summary:

1) All ‘n’ jobs are available for processing, beginning on machine1, at time zero.

2) Once started into the process, one job may not pass another, but must remain in the same

sequence position for its entire processing through the ‘m’ machines.

3) Each job may be processed on only a single machine at one time, so that job splitting is not

permitted.

4) There is only one of each type of machine available.

5) At most, only one job at a time can be processed on an individual machine.

6) The processing times of all ‘n’ jobs on each of the ‘m’ machines are predetermined.

7) The set-up times for the jobs are sequence independent so that set-up times can be considered

a part of the processing times.

8) In-process inventory is allowed between consecutive machines in the production system.

9) Non-preemption; whereas operations can not be interrupted and each machine can handle

only one job at a time.

10) Skipping is allowed in this model.

3. THE PERFORMANCE CRITERIA

The performance criteria are those most commonly used as proposed by Stafford [15], for

optimizing the general flowshop model.

155

Page 156: lecturer notes

1. Makespan

Throughout the half century of flowshop scheduling research, the predominant objective

function has been to minimize make-span. [10]

The expression used is as follows:

Minimize: Cmax

2. Mean Completion Time

Conway et al. (1967), Panwalker and Khan (1975), Bensal (1977), and Scwarc (1983) have

all discussed mean job completion time or mean flow time as an appropriate measure of the

quality of a flowshop scheduling problem solution. Mean job completion time may be

expressed as follows:

c = i=1n∑Job completion times / n

Job completion times / n

3. Total Waiting Time

Minimizing total job idle time, while the jobs wait for the next machine in the processing

sequence to be ready to process them, may be expressed as follows:

W( nxm ) = Mi=1∑ N

j=1∑ wij

4. Total Idle Time

Overall all machine idle time will be considered in this model (the time that machines 2,…. ,

M spend waiting for the first job in the sequence to arrive will be counted). Overall machine

idle time may be minimized according to the following expression:

Minimize:Mi=1∑ N

j=1∑ wij

4. DISPATCHING RULES

A dispatching rule is used to select the next job to be processed from a set of jobs awaiting service

at a facility that becomes free. The difficulty of the choice of a dispatching rule arises from the fact

that there are n! ways of sequencing ‘n’ jobs waiting in the queue at a particular facility and the

156

Page 157: lecturer notes

shop floor conditions elsewhere in the shop may influence the optimal sequence of jobs at the

present facility [12].

Five basic dispatching rules have been selected to be investigated in this research. A brief

description about each rule will be presented:

Rule (1) FCFS (First Come First Served): This rule dispatches jobs based on their arrival times

or release dates. The job that has been waiting in queue the longest is selected. The FCFS rule

is simple to implement and has a number of noteworthy properties. For example, if the

processing times of the jobs are random variables from the same distribution, then the FCFS

rule minimizes the variance of the average waiting time. This rule tends to construct schedules

that exhibit a low variance in the average total time spent by the jobs in this shop.

Rule (2) SPT (Shortest Processing Time): The SPT first rule is a widely used dispatching rule.

The SPT rule minimizes the sum of the completion times SCj (usually referred as the flow

time), the number of jobs in the system at any point in time, and the average number of jobs in

the system over time for the following machine environments: set of unique machines in series,

the bank of identical machines in parallel, and the proportionate flow shop.

Rule (3) LPT (Longest Processing Time): The LPT rule is particularly useful in the case of a

bank of parallel machines where the make-span has to be minimized. This rule selects the job

with the longest processing (from the queue of jobs) to go next when a machine becomes

available. Inherently, the LPT rule has a load balancing property, as it tends to avoid the

situation where one long job is in process while all other machines are free. Therefore, after

using the LPT rule to partition the jobs among the machines, it is possible to resequence the

jobs for the individual machines to optimize another objective besides make-span. This rule is

more effective when preemption is allowed.

Rule (4) SRPT (Shortest Remaining Processing Time): The SRPT is a variation of SPT that is

applicable when the jobs have different release dates. SRPT rule selects operations that belong

to the job with the smallest total processing time remaining. It can be effective in minimizing

the make-span when preemption is allowed.

Rule (5) LRPT (longest Remaining Processing Time): The LRPT is a variation of LPT that

selects the operations that belong to the job with the largest total processing time remaining.

LRPT rule is of importance when preemption is allowed and especially in parallel identical

machines. LRPT rule always minimizes the idle time of machines.

157

Page 158: lecturer notes

QUESTION BANK

UNIT –IPART A

1. What is a program? 2. What is an algorithm?3. Define the term data structure?4. What is a data?5. What are the applications of data structure?6. What is linear data structure? 7. What is non linear data structure?8. What is space complexity? 9. What is time complexity? 10. What is asymptotic notation? 11. What is Big –oh notation (O)? 12. What is BIG THETA Notation? 13. What is big omega notation ()? 14. What is little oh notation? 15. What is worst case efficiency?16. What is best case efficiency? 17. What is average case efficiency? 18. Define Amortized Cost.19. What is Np complete?20. What is Np hard?21. Define recurrence relation.22. Define Linked list ADT.23. Define doubly linked list24. Define circular linked list

PART B

1. Explain in detail about asymptotic notations with examples.2. List the properties of big oh notation and prove it.3. Explain conditional asymptotic notation with example.4. Explain in detail about amortized analysis.5. Write notes on NP completeness and NP hard.6. Explain recurrence equation with examples.7. How linked list can be implemented using pointers and arrays8. Explain implementation of doubly linked list using pointers.9. How circular linked list can be implemented using pointers.10. Explain tree traversals with an application.11. How will you implement binary trees.

158

Page 159: lecturer notes

UNIT –IIPART A

1. Define heap.2. What is heap property?

3. What is min heap property?

4. What is max heap property?

5. Define Lefitist tree.

6. Define binomial heap.

7. What is fibonocci heap?

8. What is skew heap?

PART B

1. What is heap data structure? What are the operations performed on heap?2. Explain min-max heap operations with example.

3. Explain Deaps with example.

4. Write the driving and actual routine for merging leftlist heaps.5. Write Insertion and delete routine for leftlist heaps.6. Explain skew heap with an example.7. Explain the structure for Binomial heap.8. Explain the operations on for Binomial heap with an example.9. Explain fibanocci heap with example.

10. Explain Lazy binomial heap.

UNIT –IIIPART A

1. Define binary search tree.2. Define AVL tree . Give example.

3. Define balance factor of AVL tree.

159

Page 160: lecturer notes

4. What is meant by rotation? When it is performed?

5. Define 2-3 tree. Give example.

6. Define 2-3-4 tree.

7. What is red black tree.

8. Define B-tree.

9. Define splay tree.

10. What trie.

PART B

1. Explain binary search tree in detail.2. Write short notes on AVL trees.3. Discuss AVL trees and its rotations with example.4. Construct AVL tree for the given list of numbers (5,4,3,2,1,6,8).5. How splaying is performed in trees. Explain with an example.6. Write short notes on B-trees.7. Explain 2-3 trees with example.8. Explain 2-3-4 trees with example.9. Explain Red black tree with example.10. Explain tries with example.

UNIT –IVPART A

1. Define greedy algorithm.2. What is divide and conquer method.3. Differentiate greedy technique with dynamic programming.4. Define quick sort.5. What is pivot element?6. State strassen’s algorithm.7. Define convex hull problem.8. What is tree vertex splitting.9. List the basic concepts of greedy method.

160

Page 161: lecturer notes

PART B

1. Sort the elements using quick sort algorithm10,2,7,23,34,8,1,9,11,5

2. How will you implement strassen’s algorithm for matrix multiplication.3. Explain Convex hull problem with example.4. Explain Tree-vertex splitting with example5. Explain Job sequencing with deadlines with example6. Explain Optimal storage on tapes with example

161

Page 162: lecturer notes

UNIT V

PART A

1. What is meant by backtracking.2. Give examples for backtracking techniques.

3. What is meant by dynamic programming?

4. Define multigraph.

5. Define knapsack problem.

6. What is flow shop scheduling.

7. Define 8-queens problem.

8. What is graph coloring?

PART B

1. Explain multistage graphs in detail.2. Discuss the solution of 0/1 knapsack problem using dynamic programming.

3. Explain Flow shop scheduling in detail.

4. What is back tracking? Explain in detail with 8-queens problem.

5. Explain graph coloring with example.

6. Discuss the solution of knapsack problem using backtracking.

162