makes x be the root of T. If T is not empty, then the first phase inserts x as a child of an existing node. Because we assume that the value of x. f

depends only on information in the other attributes of x itself and the

information in x’s children, and because x’s children are both the sentinel T. nil, it takes only O(1) time to compute the value of x. f.

Having computed x. f, the change propagates up the tree. Thus, the total time for the first phase of insertion is O(lg n). During the second phase, the only structural changes to the tree come from rotations. Since only

two nodes change in a rotation, but a change to an attribute might need

to propagate up to the root, the total time for updating the f attributes is

O(lg n) per rotation. Since the number of rotations during insertion is at most two, the total time for insertion is O(lg n).

Like insertion, deletion has two phases, as Section 13.4 discusses. In the first phase, changes to the tree occur when a node is deleted, and at

most two other nodes could move within the tree. Propagating the

updates to f caused by these changes costs at most O(lg n), since the changes modify the tree locally along a simple path from the lowest

changed node to the root. Fixing up the red-black tree during the

second phase requires at most three rotations, and each rotation

requires at most O(lg n) time to propagate the updates to f. Thus, like insertion, the total time for deletion is O(lg n).

In many cases, such as maintaining the size attributes in order-

statistic trees, the cost of updating after a rotation is O(1), rather than

the O(lg n) derived in the proof of Theorem 17.1. Exercise 17.2-3 gives an example.

On the other hand, when an update after a rotation requires a traversal all the way up to the root, it is important that insertion into

and deletion from a red-black tree require a constant number of

rotations. The chapter notes for Chapter 13 list other schemes for balancing search trees that do not bound the number of rotations per

insertion or deletion by a constant. If each operation might require Θ(lg

n) rotations and each rotation traverses a path up to the root, then a single operation could require Θ(lg2 n) time, rather than the O(lg n) time bound given by Theorem 17.1.

Exercises

17.2-1

Show, by adding pointers to the nodes, how to support each of the

dynamic-set queries MINIMUM, MAXIMUM, SUCCESSOR, and

PREDECESSOR in O(1) worst-case time on an augmented order-

statistic tree. The asymptotic performance of other operations on order-

statistic trees should not be affected.

17.2-2

Can you maintain the black-heights of nodes in a red-black tree as

attributes in the nodes of the tree without affecting the asymptotic

performance of any of the red-black tree operations? Show how, or

argue why not. How about maintaining the depths of nodes?

17.2-3

Let ⊗ be an associative binary operator, and let a be an attribute

maintained in each node of a red-black tree. Suppose that you want to

include in each node x an additional attribute f such that x. f = x 1. a

x 2. a ⊗ … ⊗ xm. a, where x 1, x 2, … , xm is the inorder listing of nodes in the subtree rooted at x. Show how to update the f attributes in O(1) time after a rotation. Modify your argument slightly to apply it to the

size attributes in order-statistic trees.

17.3 Interval trees

Image 574

This section shows how to augment red-black trees to support

operations on dynamic sets of intervals. In this section, we’ll assume

that intervals are closed. Extending the results to open and half-open

intervals is conceptually straightforward. (See page 1157 for definitions

of closed, open, and half-open intervals.)

Intervals are convenient for representing events that each occupy a

continuous period of time. For example, you could query a database of

time intervals to find out which events occurred during a given interval.

The data structure in this section provides an efficient means for

maintaining such an interval database.

A simple way to represent an interval [ t 1, t 2] is as an object i with attributes i. low = t 1 (the low endpoint) and i. high = t 2 (the high endpoint). We say that intervals i and ioverlap if ii′ ≠ ∅ , that is, if i. lowi′. high and i′. lowi. high.

Figure 17.3 The interval trichotomy for two closed intervals i and i′. (a) If i and i′ overlap, there are four situations, and in each, i. lowi′. high and i′. lowi. high. (b) The intervals do not overlap, and i. high < i′. low. (c) The intervals do not overlap, and i′. high < i. low.

As Figure 17.3 shows, any two intervals i and i′ satisfy the interval trichotomy, that is, exactly one of the following three properties holds:

a. i and i′ overlap,

b. i is to the left of i′ (i.e., i. high < i′. low), c. i is to the right of i′ (i.e., i′. high < i. low).

An interval tree is a red-black tree that maintains a dynamic set of

elements, with each element x containing an interval x. int. Interval trees

support the following operations:

INTERVAL-INSERT( T, x) adds the element x, whose int attribute is assumed to contain an interval, to the interval tree T.

INTERVAL-DELETE( T, x) removes the element x from the interval tree T.

INTERVAL-SEARCH( T, i) returns a pointer to an element x in the interval tree T such that x. int overlaps interval i, or a pointer to the sentinel T. nil if no such element belongs to the set.

Figure 17.4 shows how an interval tree represents a set of intervals. The four-step method from Section 17.2 will guide our design of an interval tree and the operations that run on it.

Step 1: Underlying data structure

A red-black tree serves as the underlying data structure. Each node x

contains an interval x. int. The key of x is the low endpoint, x. int. low, of the interval. Thus, an inorder tree walk of the data structure lists the

intervals in sorted order by low endpoint.

Image 575

Figure 17.4 An interval tree. (a) A set of 10 intervals, shown sorted bottom to top by left endpoint. (b) The interval tree that represents them. Each node x contains an interval, shown above the dashed line, and the maximum value of any interval endpoint in the subtree rooted at x, shown below the dashed line. An inorder tree walk of the tree lists the nodes in sorted order by left endpoint.

Step 2: Additional information

In addition to the intervals themselves, each node x contains a value x. max, which is the maximum value of any interval endpoint stored in

the subtree rooted at x.

Step 3: Maintaining the information

We must verify that insertion and deletion take O(lg n) time on an interval tree of n nodes. It is simple enough to determine x. max in O(1)

time, given interval x. int and the max values of node x’s children: x. max = max { x. int. high, x. left. max, x. right. max}.

Thus, by Theorem 17.1, insertion and deletion run in O(lg n) time. In fact, you can use either Exercise 17.2-3 or 17.3-1 to show how to update

all the max attributes that change after a rotation in just O(1) time.

Step 4: Developing new operations

The only new operation is INTERVAL-SEARCH( T, i), which finds a

node in tree T whose interval overlaps interval i. If there is no interval in the tree that overlaps i, the procedure returns a pointer to the sentinel

T. nil.

INTERVAL-SEARCH( T, i)

1 x = T. root

2 while xT. nil and i does not overlap x. int 3

if x. leftT. nil and x. left. maxi. low 4

x = x. left // overlap in left subtree or no overlap in right subtree

5

else x

=// no overlap in left subtree

x. right

6 return x

The search for an interval that overlaps i starts at the root of the tree

and proceeds downward. It terminates when either it finds an

overlapping interval or it reaches the sentinel T. nil. Since each iteration of the basic loop takes O(1) time, and since the height of an n-node red-black tree is O(lg n), the INTERVAL-SEARCH procedure takes O(lg n) time.

Before we see why INTERVAL-SEARCH is correct, let’s examine

how it works on the interval tree in Figure 17.4. Let’s look for an interval that overlaps the interval i = [22, 25]. Begin with x as the root, which contains [16, 21] and does not overlap i. Since x. left. max = 23 is greater than i. low = 22, the loop continues with x as the left child of the root—the node containing [8, 9], which also does not overlap i. This

time, x. left. max = 10 is less than i. low = 22, and so the loop continues with the right child of x as the new x. Because the interval [15, 23]

stored in this node overlaps i, the procedure returns this node.

Now let’s try an unsuccessful search, for an interval that overlaps i =

[11, 14] in the interval tree of Figure 17.4. Again, begin with x as the root. Since the root’s interval [16, 21] does not overlap i, and since x. left. max = 23 is greater than i. low = 11, go left to the node containing

[8, 9]. Interval [8, 9] does not overlap i, and x. left. max = 10 is less than i. low = 11, and so the search goes right. (No interval in the left subtree overlaps i.) Interval [15, 23] does not overlap i, and its left child is T. nil, so again the search goes right, the loop terminates, and INTERVAL-SEARCH returns the sentinel T. nil.

To see why INTERVAL-SEARCH is correct, we must understand

why it suffices to examine a single path from the root. The basic idea is

that at any node x, if x. int does not overlap i, the search always proceeds in a safe direction: the search will definitely find an overlapping interval

if the tree contains one. The following theorem states this property more

precisely.

Theorem 17.2

Any execution of INTERVAL-SEARCH( T, i) either returns a node

whose interval overlaps i, or it returns T. nil and the tree T contains no node whose interval overlaps i.

Proof The while loop of lines 2–5 terminates when either x = T. nil or i overlaps x. int. In the latter case, it is certainly correct to return x.

Therefore, we focus on the former case, in which the while loop

terminates because x = T. nil, which is the node that INTERVAL-SEARCH returns.

We’ll prove that if the procedure returns T. nil, then it did not miss

any intervals in T that overlap i. The idea is to show that whether the search goes left in line 4 or right in line 5, it always heads toward a node

containing an interval overlapping i, if any such interval exists. In particular, we’ll prove that

1. If the search goes left in line 4, then the left subtree of node x

contains an interval that overlaps i or the right subtree of x

Image 576

contains no interval that overlaps i. Therefore, even if x’s left subtree contains no interval that overlaps i but the search goes

left, it does not make a mistake, because x’s right subtree does

not contain an interval overlapping i, either.

2. If the search goes right in line 5, then the left subtree of x

contains no interval that overlaps i. Thus, if the search goes

right, it does not make a mistake.

For both cases, we rely on the interval trichotomy. Let’s start with

the case where the search goes right, whose proof is simpler. By the tests

in line 3, we know that x. left = T. nil or x. left. max < i. low. If x. left =

T. nil, then x’s left subtree contains no interval that overlaps i, since it contains no intervals at all. Now suppose that x. leftT. nil, so that we must have x. left. max < i. low. Consider any interval i′ in x’s left subtree.

Because x. left. max is the maximum endpoint in x’s left subtree, we have i′. highx. left. max. Thus, as Figure 17.5(a) shows, i′. high x. left. max

< i. low.

By the interval trichotomy, therefore, intervals i and i′ do not overlap, and so x’s left subtree contains no interval that overlaps i.

Figure 17.5 Intervals in the proof of Theorem 17.2. The value of x. left. max is shown in each case as a dashed line. (a) The search goes right. No interval i′ in x’s left subtree can overlap i. (b) The search goes left. The left subtree of x contains an interval that overlaps i (situation not shown), or x’s left subtree contains an interval i′ such that i′. high = x. left. max. Since i does not overlap i′, neither does it overlap any interval i″ in x’s right subtree, since i′. lowi″. low.

Now we examine the case in which the search goes left. If the left

subtree of node x contains an interval that overlaps i, we’re done, so let’s

assume that no node in x’s left subtree overlaps i. We need to show that in this case, no node in x’s right subtree overlaps i, so that going left will not miss any overlaps in x’s right subtree. By the tests in line 3, the left

subtree of x is not empty and x. left. maxi. low. By the definition of the max attribute, x’s left subtree contains some interval i′ such that i′. high = x. left. max

i. low,

as illustrated in Figure 17.5(b). Since i′ is in x’s left subtree, it does not overlap i, and since i′. highi. low, the interval trichotomy tells us that i. high < i′. low. Now we bring in the property that interval trees are keyed on the low endpoints of intervals. Because i′ is in x’s left subtree, we have i′. lowx. int. low. Now consider any interval i″ in x’s right subtree, so that x. int. lowi″. low. Putting inequalities together, we get i. high < i′. low

x. int. low

i″. low.

Because i. high < i″. low, the interval trichotomy tells us that i and i″ do not overlap. Since we chose i″ as any interval in x’s right subtree, no node in x’s right subtree overlaps i.

Thus, the INTERVAL-SEARCH procedure works correctly.

Exercises

17.3-1

Write pseudocode for LEFT-ROTATE that operates on nodes in an

interval tree and updates all the max attributes that change in O(1) time.

17.3-2

Describe an efficient algorithm that, given an interval i, returns an interval overlapping i that has the minimum low endpoint, or T. nil if no such interval exists.

17.3-3

Given an interval tree T and an interval i, describe how to list all intervals in T that overlap i in O(min { n, k lg n}) time, where k is the number of intervals in the output list. ( Hint: One simple method makes

several queries, modifying the tree between queries. A slightly more

complicated method does not modify the tree.)

17.3-4

Suggest modifications to the interval-tree procedures to support the new

operation INTERVAL-SEARCH-EXACTLY( T, i), where T is an

interval tree and i is an interval. The operation should return a pointer

to a node x in T such that x. int. low = i. low and x. int. high = i. high, or T. nil if T contains no such node. All operations, including INTERVAL-SEARCH-EXACTLY, should run in O(lg n) time on an n-node interval tree.

17.3-5

Show how to maintain a dynamic set Q of numbers that supports the

operation MIN-GAP, which gives the absolute value of the difference of

the two closest numbers in Q. For example, if we have Q = {1, 5, 9, 15, 18, 22}, then MIN-GAP( Q) returns 3, since 15 and 18 are the two

closest numbers in Q. Make the operations INSERT, DELETE,

SEARCH, and MIN-GAP as efficient as possible, and analyze their

running times.

17.3-6

VLSI databases commonly represent an integrated circuit as a list of

rectangles. Assume that each rectangle is rectilinearly oriented (sides

parallel to the x- and y-axes), so that each rectangle is represented by four values: its minimum and maximum x- and y-coordinates. Give an

O( n lg n)-time algorithm to decide whether a set of n rectangles so represented contains two rectangles that overlap. Your algorithm need

not report all intersecting pairs, but it must report that an overlap exists

if one rectangle entirely covers another, even if the boundary lines do

not intersect. ( Hint: Move a “sweep” line across the set of rectangles.)

Problems

17-1 Point of maximum overlap

You wish to keep track of a point of maximum overlap in a set of

intervals—a point with the largest number of intervals in the set that

overlap it.

a. Show that there is always a point of maximum overlap that is an

endpoint of one of the intervals.

b. Design a data structure that efficiently supports the operations

INTERVAL-INSERT, INTERVAL-DELETE, and FIND-POM,

which returns a point of maximum overlap. ( Hint: Keep a red-black

tree of all the endpoints. Associate a value of +1 with each left

endpoint, and associate a value of −1 with each right endpoint.

Augment each node of the tree with some extra information to

maintain the point of maximum overlap.)

17-2 Josephus permutation

We define the Josephus problem as follows. A group of n people form a

circle, and we are given a positive integer mn. Beginning with a designated first person, proceed around the circle, removing every m th

person. After each person is removed, counting continues around the

circle that remains. This process continues until nobody remains in the

circle. The order in which the people are removed from the circle defines

the ( n, m)- Josephus permutation of the integers 1, 2, … , n. For example, the (7, 3)-Josephus permutation is 〈3, 6, 2, 7, 5, 1, 4〉.

a. Suppose that m is a constant. Describe an O( n)-time algorithm that, given an integer n, outputs the ( n, m)-Josephus permutation.

b. Suppose that m is not necessarily a constant. Describe an O( n lg n)-

time algorithm that, given integers n and m, outputs the ( n, m)-

Josephus permutation.

Chapter notes

In their book, Preparata and Shamos [364] describe several of the interval trees that appear in the literature, citing work by H.

Edelsbrunner (1980) and E. M. McCreight (1981). The book details an

interval tree that, given a static database of n intervals, allows us to enumerate all k intervals that overlap a given query interval in O( k + lg n) time.

18 B-Trees

B-trees are balanced search trees designed to work well on disk drives or

other direct-access secondary storage devices. B-trees are similar to red-

black trees (Chapter 13), but they are better at minimizing the number of operations that access disks. (We often say just “disk” instead of

“disk drive.”) Many database systems use B-trees, or variants of B-trees,

to store information.

B-trees differ from red-black trees in that B-tree nodes may have

many children, from a few to thousands. That is, the “branching factor”

of a B-tree can be quite large, although it usually depends on

characteristics of the disk drive used. B-trees are similar to red-black

trees in that every n-node B-tree has height O(lg n), so that B-trees can implement many dynamic-set operations in O(lg n) time. But a B-tree has a larger branching factor than a red-black tree, so the base of the

logarithm that expresses its height is larger, and hence its height can be

considerably lower.

B-trees generalize binary search trees in a natural manner. Figure

18.1 shows a simple B-tree. If an internal B-tree node x contains x. n

keys, then x has x. n + 1 children. The keys in node x serve as dividing points separating the range of keys handled by x into x. n + 1 subranges, each handled by one child of x. A search for a key in a B-tree makes an

( x. n + 1)-way decision based on comparisons with the x. n keys stored at node x. An internal node contains pointers to its children, but a leaf node does not.

Section 18.1 gives a precise definition of B-trees and proves that the height of a B-tree grows only logarithmically with the number of nodes

Image 577

it contains. Section 18.2 describes how to search for a key and insert a key into a B-tree, and Section 18.3 discusses deletion. Before proceeding, however, we need to ask why we evaluate data structures

designed to work on a disk drive differently from data structures

designed to work in main random-access memory.

Figure 18.1 A B-tree whose keys are the consonants of English. An internal node x containing x. n keys has x. n + 1 children. All leaves are at the same depth in the tree. The blue nodes are examined in a search for the letter R.

Data structures on secondary storage

Computer systems take advantage of various technologies that provide

memory capacity. The main memory of a computer system normally

consists of silicon memory chips. This technology is typically more than

an order of magnitude more expensive per bit stored than magnetic

storage technology, such as tapes or disk drives. Most computer systems

also have secondary storage based on solid-state drives (SSDs) or

magnetic disk drives. The amount of such secondary storage often

exceeds the amount of primary memory by one to two orders of

magnitude. SSDs have faster access times than magnetic disk drives,

which are mechanical devices. In recent years, SSD capacities have

increased while their prices have decreased. Magnetic disk drives

typically have much higher capacities than SSDs, and they remain a

more cost-effective means for storing massive amounts of information.

Disk drives that store several terabytes1 can be found for under $100.

Figure 18.2 shows a typical disk drive. The drive consists of one or more platters, which rotate at a constant speed around a common

spindle. A magnetizable material covers the surface of each platter. The

drive reads and writes each platter by a head at the end of an arm. The arms can move their heads toward or away from the spindle. The

surface that passes underneath a given head when it is stationary is

called a track.

Although disk drives are cheaper and have higher capacity than

main memory, they are much, much slower because they have moving

mechanical parts. The mechanical motion has two components: platter

rotation and arm movement. As of this writing, commodity disk drives

rotate at speeds of 5400–15,000 revolutions per minute (RPM). Typical

speeds are 15,000 RPM in server-grade drives, 7200 RPM in drives for

desktops, and 5400 RPM in drives for laptops. Although 7200 RPM

may seem fast, one rotation takes 8.33 milliseconds, which is over 5

orders of magnitude longer than the 50 nanosecond access times (more

or less) commonly found for main memory. In other words, if a

computer waits a full rotation for a particular item to come under the

read/write head, it could access main memory more than 100,000 times

during that span. The average wait is only half a rotation, but still, the

difference in access times for main memory compared with disk drives is

enormous. Moving the arms also takes some time. As of this writing,

average access times for commodity disk drives are around 4

milliseconds.

Image 578

Figure 18.2 A typical magnetic disk drive. It consists of one or more platters covered with a magnetizable material (two platters are shown here) that rotate around a spindle. Each platter is read and written with a head, shown in red, at the end of an arm. Arms rotate around a common pivot axis. A track, drawn in blue, is the surface that passes beneath the read/write head when the head is stationary.

In order to amortize the time spent waiting for mechanical

movements, also known as latency, disk drives access not just one item

but several at a time. Information is divided into a number of equal-

sized blocks of bits that appear consecutively within tracks, and each disk read or write is of one or more entire blocks. 2 Typical disk drives have block sizes running from 512 to 4096 bytes. Once the read/write

head is positioned correctly and the platter has rotated to the beginning

of the desired block, reading or writing a magnetic disk drive is entirely

electronic (aside from the rotation of the platter), and the disk drive can

quickly read or write large amounts of data.

Often, accessing a block of information and reading it from a disk

drive takes longer than processing all the information read. For this

reason, in this chapter we’ll look separately at the two principal

components of the running time:

the number of disk accesses, and

the CPU (computing) time.

We measure the number of disk accesses in terms of the number of

blocks of information that need to be read from or written to the disk

drive. Although disk-access time is not constant—it depends on the distance between the current track and the desired track and also on the

initial rotational position of the platters—the number of blocks read or

written provides a good first-order approximation of the total time

spent accessing the disk drive.

In a typical B-tree application, the amount of data handled is so

large that all the data do not fit into main memory at once. The B-tree

algorithms copy selected blocks from disk into main memory as needed

and write back onto disk the blocks that have changed. B-tree

algorithms keep only a constant number of blocks in main memory at

any time, and thus the size of main memory does not limit the size of B-

trees that can be handled.

B-tree procedures need to be able to read information from disk into

main memory and write information from main memory to disk.

Consider some object x. If x is currently in the computer’s main memory, then the code can refer to the attributes of x as usual: x. key, for example. If x resides on disk, however, then the procedure must

perform the operation DISK-READ( x) to read the block containing

object x into main memory before it can refer to x’s attributes. (Assume that if x is already in main memory, then DISK-READ( x) requires no

disk accesses: it is a “no-op.”) Similarly, procedures call DISK-

WRITE( x) to save any changes that have been made to the attributes of

object x by writing to disk the block containing x. Thus, the typical pattern for working with an object is as follows:

x = a pointer to some object

DISK-READ( x)

operations that access and/or modify the attributes of x

DISK-WRITE( x) // omitted if no attributes of x were changed

other operations that access but do not modify attributes of x

The system can keep only a limited number of blocks in main memory

at any one time. Our B-tree algorithms assume that the system

automatically flushes from main memory blocks that are no longer in

use.

Image 579

Since in most systems the running time of a B-tree algorithm

depends primarily on the number of DISK-READ and DISK-WRITE

operations it performs, we typically want each of these operations to

read or write as much information as possible. Thus, a B-tree node is

usually as large as a whole disk block, and this size limits the number of

children a B-tree node can have.

Figure 18.3 A B-tree of height 2 containing over one billion keys. Shown inside each node x is x. n, the number of keys in x. Each internal node and leaf contains 1000 keys. This B-tree has 1001 nodes at depth 1 and over one million leaves at depth 2.

Large B-trees stored on disk drives often have branching factors

between 50 and 2000, depending on the size of a key relative to the size

of a block. A large branching factor dramatically reduces both the

height of the tree and the number of disk accesses required to find any

key. Figure 18.3 shows a B-tree with a branching factor of 1001 and height 2 that can store over one billion keys. Nevertheless, if the root

node is kept permanently in main memory, at most two disk accesses

suffice to find any key in this tree.

18.1 Definition of B-trees

To keep things simple, let’s assume, as we have for binary search trees

and red-black trees, that any satellite information associated with a key

resides in the same node as the key. In practice, you might actually store

with each key just a pointer to another disk block containing the

satellite information for that key. The pseudocode in this chapter

implicitly assumes that the satellite information associated with a key, or

the pointer to such satellite information, travels with the key whenever the key is moved from node to node. A common variant on a B-tree,

known as a B+- tree, stores all the satellite information in the leaves and stores only keys and child pointers in the internal nodes, thus

maximizing the branching factor of the internal nodes.

A B-tree T is a rooted tree with root T. root having the following properties:

1. Every node x has the following attributes:

a. x. n, the number of keys currently stored in node x,

b. the x. n keys themselves, x. key 1, x. key 2, … , x. keyx. n, stored in monotonically increasing order, so that x. key 1 ≤ x. key 2 ≤ ⋯ ≤

x. keyx. n,

c. x. leaf, a boolean value that is TRUE if x is a leaf and FALSE

if x is an internal node.

2. Each internal node x also contains x. n + 1 pointers x. c 1, x. c 2, …

, x. cx. n+1 to its children. Leaf nodes have no children, and so their ci attributes are undefined.

3. The keys x. keyi separate the ranges of keys stored in each subtree: if ki is any key stored in the subtree with root x. ci, then k 1 ≤ x. key 1 ≤ k 2 ≤ x. key 2 ≤ ⋯ ≤ x. keyx. nkx. n+1.

4. All leaves have the same depth, which is the tree’s height h.

5. Nodes have lower and upper bounds on the number of keys they

can contain, expressed in terms of a fixed integer t ≥ 2 called the

minimum degree of the B-tree:

a. Every node other than the root must have at least t − 1 keys.

Every internal node other than the root thus has at least t

children. If the tree is nonempty, the root must have at least

one key.

Image 580

Image 581

b. Every node may contain at most 2 t − 1 keys. Therefore, an

internal node may have at most 2 t children. We say that a node

is full if it contains exactly 2 t − 1 keys. 3

The simplest B-tree occurs when t = 2. Every internal node then has

either 2, 3, or 4 children, and it is a 2-3-4 tree. In practice, however, much larger values of t yield B-trees with smaller height.

The height of a B-tree

The number of disk accesses required for most operations on a B-tree is

proportional to the height of the B-tree. The following theorem bounds

the worst-case height of a B-tree.

Figure 18.4 A B-tree of height 3 containing a minimum possible number of keys. Shown inside each node x is x. n.

Theorem 18.1

If n ≥ 1, then for any n-key B-tree T of height h and minimum degree t

2,

Proof By definition, the root of a nonempty B-tree T contains at least one key, and all other nodes contain at least t − 1 keys. Let h be the height of T. Then T contains at least 2 nodes at depth 1, at least 2 t

Image 582

nodes at depth 2, at least 2 t 2 nodes at depth 3, and so on, until at depth

h, it has at least 2 th−1 nodes. Figure 18.4 illustrates such a tree for h =

3. The number n of keys therefore satisfies the inequality

so that th ≤ ( n + 1)/2. Taking base- t logarithms of both sides proves the theorem.

You can see the power of B-trees as compared with red-black trees.

Although the height of the tree grows as O(log n) in both cases (recall that t is a constant), for B-trees the base of the logarithm can be many

times larger. Thus, B-trees save a factor of about lg t over red-black trees

in the number of nodes examined for most tree operations. Because

examining an arbitrary node in a tree usually entails accessing the disk,

B-trees avoid a substantial number of disk accesses.

Exercises

18.1-1

Why isn’t a minimum degree of t = 1 allowed?

18.1-2

For what values of t is the tree of Figure 18.1 a legal B-tree?

18.1-3

Show all legal B-trees of minimum degree 2 that store the keys 1, 2, 3, 4,

5.

18.1-4

As a function of the minimum degree t, what is the maximum number

of keys that can be stored in a B-tree of height h?

18.1-5

Describe the data structure that results if each black node in a red-black

tree absorbs its red children, incorporating their children with its own.

18.2 Basic operations on B-trees

This section presents the details of the operations B-TREE-SEARCH,

B-TREE-CREATE, and B-TREE-INSERT. These procedures observe

two conventions:

The root of the B-tree is always in main memory, so that no

procedure ever needs to perform a DISK-READ on the root. If

any changes to the root node occur, however, then DISK-WRITE

must be called on the root.

Any nodes that are passed as parameters must already have had a

DISK-READ operation performed on them.

The procedures are all “one-pass” algorithms that proceed downward

from the root of the tree, without having to back up.

Searching a B-tree

Searching a B-tree is much like searching a binary search tree, except

that instead of making a binary, or “two-way,” branching decision at

each node, the search makes a multiway branching decision according

to the number of the node’s children. More precisely, at each internal

node x, the search makes an ( x. n + 1)-way branching decision.

The procedure B-TREE-SEARCH generalizes the TREE-SEARCH

procedure defined for binary search trees on page 316. It takes as input

a pointer to the root node x of a subtree and a key k to be searched for in that subtree. The top-level call is thus of the form B-TREE-SEARCH( T. root, k). If k is in the B-tree, then B-TREE-SEARCH

returns the ordered pair ( y, i) consisting of a node y and an index i such that y. keyi = k. Otherwise, the procedure returns NIL.

B-TREE-SEARCH( x, k)

1 i = 1

2 while ix. n and k > x. keyi

3

i = i + 1

4 if ix. n and k == x. keyi

5

return ( x, i)

6 elseif x. leaf

7

returnNIL

8 else DISK-READ( x. ci)

9

return B-TREE-SEARCH( x. ci, k)

Using a linear-search procedure, lines 1–3 of B-TREE-SEARCH find

the smallest index i such that kx. keyi, or else they set i to x. n + 1.

Lines 4–5 check to see whether the search has discovered the key,

returning if it has. Otherwise, if x is a leaf, then line 7 terminates the search unsuccessfully, and if x is an internal node, lines 8–9 recurse to

search the appropriate subtree of x, after performing the necessary

DISK-READ on that child. Figure 18.1 illustrates the operation of BTREE-SEARCH. The blue nodes are those examined during a search

for the key R.

As in the TREE-SEARCH procedure for binary search trees, the

nodes encountered during the recursion form a simple path downward

from the root of the tree. The B-TREE-SEARCH procedure therefore

accesses O( h) = O(log t n) disk blocks, where h is the height of the B-tree and n is the number of keys in the B-tree. Since x. n < 2 t, the while loop of lines 2–3 takes O( t) time within each node, and the total CPU time is O( th) = O( t log tn).

Creating an empty B-tree

To build a B-tree T, first use the B-TREE-CREATE procedure on the

next page to create an empty root node and then call the B-TREE-

INSERT procedure on page 508 to add new keys. Both of these

procedures use an auxiliary procedure ALLOCATE-NODE, whose

pseudocode we omit and which allocates one disk block to be used as a

new node in O(1) time. A node created by ALLOCATE-NODE requires

no DISK-READ, since there is as yet no useful information stored on

the disk for that node. B-TREE-CREATE requires O(1) disk operations

and O(1) CPU time.

B-TREE-CREATE( T)

1 x = ALLOCATE-NODE()

2 x. leaf = TRUE

3 x. n = 0

4 DISK-WRITE( x)

5 T. root = x

Inserting a key into a B-tree

Inserting a key into a B-tree is significantly more complicated than

inserting a key into a binary search tree. As with binary search trees,

you search for the leaf position at which to insert the new key. With a B-

tree, however, you cannot simply create a new leaf node and insert it, as

the resulting tree would fail to be a valid B-tree. Instead, you insert the

new key into an existing leaf node. Since you cannot insert a key into a

leaf node that is full, you need an operation that splits a full node y (having 2 t − 1 keys) around its median key y. keyt into two nodes having only t − 1 keys each. The median key moves up into y’s parent to identify the dividing point between the two new trees. But if y’s parent is

also full, you must split it before you can insert the new key, and thus

you could end up splitting full nodes all the way up the tree.

To avoid having to go back up the tree, just split every full node you

encounter as you go down the tree. In this way, whenever you need to

split a full node, you are assured that its parent is not full. Inserting a

key into a B-tree then requires only a single pass down the tree from the

root to a leaf.

Splitting a node in a B-tree

The procedure B-TREE-SPLIT-CHILD on the facing page takes as

input a nonfull internal node x (assumed to reside in main memory) and

an index i such that x. ci (also assumed to reside in main memory) is a full child of x. The procedure splits this child in two and adjusts x so that it has an additional child. To split a full root, you first need to make

the root a child of a new empty root node, so that you can use B-TREE-

SPLIT-CHILD. The tree thus grows in height by 1: splitting is the only

means by which the tree grows taller.

B-TREE-SPLIT-CHILD( x, i)

1 y = x. ci

// full node to split

2 z = ALLOCATE-NODE()

// z will take half of y

3 z. leaf = y. leaf

4 z. n = t − 1

5 for j = 1 to t − 1

// z gets y’s greatest keys …

6

z. keyj = y. keyj+ t

7 if not y. leaf

8

for j = 1 to t

// … and its corresponding children

9

z. cj = y. cj+ t

10 y. n = t − 1

// y keeps t − 1 keys

11 for j = x. n + 1 downto i + 1

// shift x’s children to the right …

12

x. cj+1 = x. cj

13 x. ci+1 = z

// … to make room for z as a child

14 for j = x. n downto i

// shift the corresponding keys in x

15

x. keyj+1 = x. keyj

16 x. keyi = y. keyt

// insert y’s median key

17 x. n = x. n + 1

// x has gained a child

18 DISK-WRITE( y)

19 DISK-WRITE( z)

20 DISK-WRITE( x)

Figure 18.5 illustrates how a node splits. B-TREE-SPLIT-CHILD

splits the full node y = x. ci about its median key ( S in the figure), which moves up into y’s parent node x. Those keys in y that are greater than

Image 583

the median key move into a new node z, which becomes a new child of

x. B-TREE-SPLIT-CHILD works by straightforward cutting and

pasting. Node x is the parent of the node y being split, which is x’s i th child (set in line 1). Node y originally has 2 t children and 2 t − 1 keys, but splitting reduces y to t children and t − 1 keys. The t largest children and t − 1 keys of node y move over to node z, which becomes a new child of x, positioned just after y in x’s table of children. The median key of y moves up to become the key in node x that separates the pointers to nodes y and z.

Lines 2–9 create node z and give it the largest t − 1 keys and, if y and z are internal nodes, the corresponding t children of y. Line 10 adjusts the key count for y. Then, lines 11–17 shift keys and child pointers in x

to the right in order to make room for x’s new child, insert z as a new child of x, move the median key from y up to x in order to separate y from z, and adjust x’s key count. Lines 18–20 write out all modified disk blocks. The CPU time used by B-TREE-SPLIT-CHILD is Θ( t), due to

the for loops in lines 5–6 and 8–9. (The for loops in lines 11–12 and 14–

15 also run for O( t) iterations.) The procedure performs O(1) disk operations.

Figure 18.5 Splitting a node with t = 4. Node y = x. ci splits into two nodes, y and z, and the median key S of y moves up into y’s parent.

Inserting a key into a B-tree in a single pass down the tree

Inserting a key k into a B-tree T of height h requires just a single pass down the tree and O( h) disk accesses. The CPU time required is O( th) =

O( t log t n). The B-TREE-INSERT procedure uses B-TREE-SPLIT-CHILD to guarantee that the recursion never descends to a full node. If

the root is full, B-TREE-INSERT splits it by calling the procedure B-

TREE-SPLIT-ROOT on the facing page.

B-TREE-INSERT( T, k)

1 r = T. root

2 if r. n == 2 t − 1

3

s = B-TREE-SPLIT-ROOT( T)

4

B-TREE-INSERT-NONFULL( s, k)

5 else B-TREE-INSERT-NONFULL( r, k)

B-TREE-INSERT works as follows. If the root is full, then line 3

calls B-TREE-SPLIT-ROOT in line 3 to split it. A new node s (with

two children) becomes the root and is returned by B-TREE-SPLIT-

ROOT. Splitting the root, illustrated in Figure 18.6, is the only way to increase the height of a B-tree. Unlike a binary search tree, a B-tree

increases in height at the top instead of at the bottom. Regardless of

whether the root split, B-TREE-INSERT finishes by calling B-TREE-

INSERT-NONFULL to insert key k into the tree rooted at the nonfull

root node, which is either the new root (the call in line 4) or the original

root (the call in line 5).

Image 584

Figure 18.6 Splitting the root with t = 4. Root node r splits in two, and a new root node s is created. The new root contains the median key of r and has the two halves of r as children. The B-tree grows in height by one when the root is split. A B-tree’s height increases only when the root splits.

B-TREE-SPLIT-ROOT( T)

1 s = ALLOCATE-NODE()

2 s. leaf = FALSE

3 s. n = 0

4 s. c 1 = T. root

5 T. root = s

6 B-TREE-SPLIT-CHILD( s, 1)

7 return s

The auxiliary procedure B-TREE-INSERT-NONFULL on page 511

inserts key k into node x, which is assumed to be nonfull when the procedure is called. B-TREEINSERT-NONFULL recurses as

necessary down the tree, at all times guaranteeing that the node to

which it recurses is not full by calling B-TREE-SPLIT-CHILD as

necessary. The operation of B-TREE-INSERT and the recursive

operation of B-TREE-INSERT-NONFULL guarantee that this

assumption is true.

Figure 18.7 illustrates the various cases of how B-TREE-INSERT-NONFULL inserts a key into a B-tree. Lines 3–8 handle the case in

which x is a leaf node by inserting key k into x, shifting to the right all

keys in x that are greater than k. If x is not a leaf node, then k should go into the appropriate leaf node in the subtree rooted at internal node x.

Lines 9–11 determine the child x. ci to which the recursion descends.

Line 13 detects whether the recursion would descend to a full child, in

which case line 14 calls B-TREE-SPLIT-CHILD to split that child into

two nonfull children, and lines 15–16 determine which of the two

children is the correct one to descend to. (Note that DISK-READ( x. ci)

is not needed after line 16 increments i, since the recursion descends in

this case to a child that was just created by B-TREE-SPLIT-CHILD.)

The net effect of lines 13–16 is thus to guarantee that the procedure

never recurses to a full node. Line 17 then recurses to insert k into the

appropriate subtree.

Image 585

Figure 18.7 Inserting keys into a B-tree. The minimum degree t for this B-tree is 3, so that a node can hold at most 5 keys. Blue nodes are modified by the insertion process. (a) The initial tree for this example. (b) The result of inserting B into the initial tree. This case is a simple insertion into a leaf node. (c) The result of inserting Q into the previous tree. The node RST U V splits into two nodes containing RS and U V, the key T moves up to the root, and Q is inserted in the leftmost of the two halves (the RS node). (d) The result of inserting L into the previous tree. The root splits right away, since it is full, and the B-tree grows in height by one. Then L is inserted into the leaf containing JK. (e) The result of inserting F into the previous tree. The node ABCDE splits before F is inserted into the rightmost of the two halves (the DE node).

B-TREE-INSERT-NONFULL( x, k)

1 i = x. n

2 if x. leaf

// inserting into a leaf?

3

while i ≥ 1 and k < x. keyi // shift keys in x to make room for k 4

x. keyi+1 = x. keyi

5

i = i − 1

6

x. keyi+1 = k

// insert key k in x

7

x. n = x. n + 1

// now x has 1 more key

8

DISK-WRITE( x)

9 else while i ≥ 1 and k < x. keyi // find the child where k belongs 10

i = i − 1

11

i = i + 1

12

DISK-READ( x. ci)

13

if x. ci. n == 2 t − 1

// split the child if it’s full

14

B-TREE-SPLIT-CHILD( x, i)

15

if k > x. keyi

// does k go into x. ci or x. ci+1?

16

i = i + 1

17

B-TREE-INSERT-NONFULL( x. ci, k)

For a B-tree of height h, B-TREE-INSERT performs O( h) disk accesses, since only O(1) DISK-READ and DISK-WRITE operations

occur at each level of the tree. The total CPU time used is O( t) in each level of the tree, or O( th) = O( t log t n) overall. Since B-TREE-INSERT-NONFULL is tail-recursive, you can instead implement it with a while

loop, thereby demonstrating that the number of blocks that need to be

in main memory at any time is O(1).

Exercises

18.2-1

Show the results of inserting the keys

F, S, Q, K, C, L, H, T, V, W, M, R, N, P, A, B, X, Y, D, Z, E

in order into an empty B-tree with minimum degree 2. Draw only the

configurations of the tree just before some node must split, and also

draw the final configuration.

18.2-2

Explain under what circumstances, if any, redundant DISK-READ or

DISK-WRITE operations occur during the course of executing a call to

B-TREE-INSERT. (A redundant DISK-READ is a DISK-READ for a

block that is already in memory. A redundant DISK-WRITE writes to

disk a block of information that is identical to what is already stored

there.)

18.2-3

Professor Bunyan asserts that the B-TREE-INSERT procedure always

results in a B-tree with the minimum possible height. Show that the

professor is mistaken by proving that with t = 2 and the set of keys {1,

2, … , 15}, there is no insertion sequence that results in a B-tree with the

minimum possible height.

18.2-4

If you insert the keys {1, 2, … , n} into an empty B-tree with minimum

degree 2, how many nodes does the final B-tree have?

18.2-5

Since leaf nodes require no pointers to children, they could conceivably

use a different (larger) t value than internal nodes for the same disk block size. Show how to modify the procedures for creating and

inserting into a B-tree to handle this variation.

18.2-6

Suppose that you implement B-TREE-SEARCH to use binary search

rather than linear search within each node. Show that this change makes

the required CPU time O(lg n), independent of how t might be chosen as a function of n.

18.2-7

Suppose that disk hardware allows you to choose the size of a disk

block arbitrarily, but that the time it takes to read the disk block is

a+ bt, where a and b are specified constants and t is the minimum degree

for a B-tree using blocks of the selected size. Describe how to choose t so as to minimize (approximately) the B-tree search time. Suggest an

optimal value of t for the case in which a = 5 milliseconds and b = 10

microseconds.

18.3 Deleting a key from a B-tree

Deletion from a B-tree is analogous to insertion but a little more

complicated, because you can delete a key from any node—not just a

leaf—and when you delete a key from an internal node, you must

rearrange the node’s children. As in insertion, you must guard against

deletion producing a tree whose structure violates the B-tree properties.

Just as a node should not get too big due to insertion, a node must not

get too small during deletion (except that the root is allowed to have

fewer than the minimum number t − 1 of keys). And just as a simple

insertion algorithm might have to back up if a node on the path to

where the key is to be inserted is full, a simple approach to deletion

might have to back up if a node (other than the root) along the path to

where the key is to be deleted has the minimum number of keys.

The procedure B-TREE-DELETE deletes the key k from the subtree

rooted at x. Unlike the procedures TREE-DELETE on page 325 and

RB-DELETE on page 348, which are given the node to delete—

presumably as the result of a prior search—B-TREE-DELETE

combines the search for key k with the deletion process. Why do we

combine search and deletion in B-TREE-DELETE? Just as B-TREE-

INSERT prevents any node from becoming overfull (having more than

2 t − 1 keys) while making a single pass down the tree, B-TREE-

DELETE prevents any node from becoming underfull (having fewer

than t − 1 keys) while also making a single pass down the tree, searching

for and ultimately deleting the key.

To prevent any node from becoming underfull, the design of B-

TREE-DELETE guarantees that whenever it calls itself recursively on a

node x, the number of keys in x is at least the minimum degree t at the time of the call. (Although the root may have fewer than t keys and a

recursive call may be made from the root, no recursive call is made on

Image 586

Image 587

the root.) This condition requires one more key than the minimum

required by the usual B-tree conditions, and so a key might have to be

moved from x into one of its child nodes (still leaving x with at least the minimum t − 1 keys) before a recursive call is made on that child, thus

allowing deletion to occur in one downward pass without having to

traverse back up the tree.

We describe how the procedure B-TREE-DELETE( T, k) deletes a

key k from a B-tree T instead of presenting detailed pseudocode. We examine three cases, illustrated in Figure 18.8. The cases are for when the search arrives at a leaf, at an internal node containing key k, and at

an internal node not containing key k. As mentioned above, in all three

cases node x has at least t keys (with the possible exception of when x is the root). Cases 2 and 3—when x is an internal node—guarantee this

property as the recursion descends through the B-tree.

Image 588

Figure 18.8 Deleting keys from a B-tree. The minimum degree for this B-tree is t = 3, so that, other than the root, every node must have at least 2 keys. Blue nodes are those that are modified by the deletion process. (a) The B-tree of Figure 18.7(e). (b) Deletion of F, which is case 1: simple deletion from a leaf when all nodes visited during the search (other than the root) have at least t = 3 keys. (c) Deletion of M, which is case 2a: the predecessor L of M moves up to take M’s position. (d) Deletion of G, which is case 2c: push G down to make node DEGJK and then delete G from this leaf (case 1). (e) Deletion of D, which is case 3b: since the recursion cannot descend to node CL because it has only 2 keys, push P down and merge it with CL and TX to form CLP TX. Then delete D from a leaf (case 1). (e0) After (e), delete the empty root. The tree shrinks in height by 1. (f) Deletion of B, which is case 3a: C moves to fill B’s position and E

moves to fill C’s position.

Case 1: The search arrives at a leaf node x. If x contains key k, then delete k from x. If x does not contain key k, then k was not in the Btree and nothing else needs to be done.

Case 2: The search arrives at an internal node x that contains key k. Let k

= x. keyi. One of the following three cases applies, depending on the

number of keys in x. ci (the child of x that precedes k) and x. ci+1 (the child of x that follows k).

Case 2a: x. ci has at least t keys. Find the predecessor k′ of k in the subtree rooted at x. ci. Recursively delete k′ from x. ci, and replace k by k′ in x. (Key k′ can be found and deleted in a single downward pass.)

Case 2b: x. ci has t − 1 keys and x. ci+1 has at least t keys. This case is symmetric to case 2a. Find the successor k′ of k in the subtree rooted

at x. ci+1. Recursively delete k′ from x. ci+1, and replace k by k′ in x.

(Again, finding and deleting k′ can be done in a single downward

pass.)

Case 2c: Both x. ci and x. ci+1 have t − 1 keys. Merge k and all of x. ci+1

into x. ci, so that x loses both k and the pointer to x. ci+1, and x. ci now contains 2 t − 1 keys. Then free x. ci+1 and recursively delete k from x. ci.

Case 3: The search arrives at an internal node x that does not contain key

k. Continue searching down the tree while ensuring that each node

visited has at least t keys. To do so, determine the root x. ci of the appropriate subtree that must contain k, if k is in the tree at all. If x. ci has only t − 1 keys, execute case 3a or 3b as necessary to guarantee

descending to a node containing at least t keys. Then finish by

recursing on the appropriate child of x.

Case 3a: x. ci has only t − 1 keys but has an immediate sibling with at least t keys. Give x. ci an extra key by moving a key from x down into x. ci, moving a key from x. ci’s immediate left or right sibling up into x, and moving the appropriate child pointer from the sibling into x. ci.

Case 3b: x. ci and each of x. ci’s immediate siblings have t − 1 keys. (It is possible for x. ci to have either one or two siblings.) Merge x. ci with one sibling, which involves moving a key from x down into the new

merged node to become the median key for that node.

In cases 2c and 3b, if node x is the root, it could end up having no

keys. When this situation occurs, then x is deleted, and x’s only child x. c 1 becomes the new root of the tree. This action decreases the height of the tree by one and preserves the property that the root of the tree

contains at least one key (unless the tree is empty).

Since most of the keys in a B-tree are in the leaves, deletion

operations often end up deleting keys from leaves. The B-TREE-

DELETE procedure then acts in one downward pass through the tree,

without having to back up. When deleting a key in an internal node x,

however, the procedure might make a downward pass through the tree

to find the key’s predecessor or successor and then return to node x to

replace the key with its predecessor or successor (cases 2a and 2b).

Returning to node x does not require a traversal through all the levels

between x and the node containing the predecessor or successor,

however, since the procedure can just keep a pointer to x and the key

position within x and put the predecessor or successor key directly

there.

Although this procedure seems complicated, it involves only O( h) disk operations for a B-tree of height h, since only O(1) calls to DISK-READ and DISK-WRITE are made between recursive invocations of

the procedure. The CPU time required is O( th) = O( t log tn).

Exercises

18.3-1

Show the results of deleting C, P, and V, in order, from the tree of

Figure 18.8(f).

18.3-2

Write pseudocode for B-TREE-DELETE.

Problems

18-1 Stacks on secondary storage

Consider implementing a stack in a computer that has a relatively small

amount of fast primary memory and a relatively large amount of slower

disk storage. The operations PUSH and POP work on single-word

values. The stack can grow to be much larger than can fit in memory,

and thus most of it must be stored on disk.

A simple, but inefficient, stack implementation keeps the entire stack

on disk. Maintain in memory a stack pointer, which is the disk address

of the top element on the stack. Indexing block numbers and word

offsets within blocks from 0, if the pointer has value p, the top element is the ( p mod m)th word on block ⌊ p/ m⌊ of the disk, where m is the number of words per block.

To implement the PUSH operation, increment the stack pointer,

read the appropriate block into memory from disk, copy the element to

be pushed to the appropriate word on the block, and write the block

back to disk. A POP operation is similar. Read in the appropriate block

from disk, save the top of the stack, decrement the stack pointer, and

return the saved value. You need not write back the block, since it was

not modified, and the word in the block that contained the popped

value is ignored.

As in the analyses of B-tree operations, two costs matter: the total

number of disk accesses and the total CPU time. A disk access also

incurs a cost in CPU time. In particular, any disk access to a block of m

words incurs charges of one disk access and Θ( m) CPU time.

a. Asymptotically, what is the worst-case number of disk accesses for n

stack operations using this simple implementation? What is the CPU

time for n stack operations? Express your answer in terms of m and n for this and subsequent parts.

Now consider a stack implementation in which you keep one block of

the stack in memory. (You also maintain a small amount of memory to

record which block is currently in memory.) You can perform a stack

operation only if the relevant disk block resides in memory. If necessary,

you can write the block currently in memory to the disk and read the

new block from the disk into memory. If the relevant disk block is

already in memory, then no disk accesses are required.

b. What is the worst-case number of disk accesses required for n PUSH

operations? What is the CPU time?

c. What is the worst-case number of disk accesses required for n stack operations? What is the CPU time?

Suppose that you now implement the stack by keeping two blocks in

memory (in addition to a small number of words for bookkeeping).

Image 589

Image 590

Image 591

Image 592

Image 593

Image 594

Image 595

d. Describe how to manage the stack blocks so that the amortized

number of disk accesses for any stack operation is O(1/ m) and the

amortized CPU time for any stack operation is O(1).

18-2 Joining and splitting 2-3-4 trees

The join operation takes two dynamic sets S′ and S″ and an element x such that x′. key < x. key < x″. key for any x′ ∈ S′ and x″ ∈ S″. It returns a set S = S′ ∪ { x} ∪ S″. The split operation is like an “inverse” join: given a dynamic set S and an element xS, it creates a set S′ that consists of all elements in S − { x} whose keys are less than x. key and another set S″ that consists of all elements in S − { x} whose keys are greater than x. key. This problem investigates how to implement these operations on 2-3-4 trees (B-trees with t = 2). Assume for convenience

that elements consist only of keys and that all key values are distinct.

a. Show how to maintain, for every node x of a 2-3-4 tree, the height of the subtree rooted at x as an attribute x. height. Make sure that your implementation does not affect the asymptotic running times of

searching, insertion, and deletion.

b. Show how to implement the join operation. Given two 2-3-4 trees T

and T″ and a key k, the join operation should run in O(1 + | h′ − h″|) time, where h′ and h″ are the heights of T′ and T″, respectively.

c. Consider the simple path p from the root of a 2-3-4 tree T to a given key k, the set S′ of keys in T that are less than k, and the set S″ of keys in T that are greater than k. Show that p breaks S′ into a set of trees and a set of keys

such that

for i

= 1, 2, … , m and any keys

and

. What is the relationship

between the heights of

and ? Describe how p breaks S″ into sets

of trees and keys.

d. Show how to implement the split operation on T. Use the join

operation to assemble the keys in S′ into a single 2-3-4 tree T′ and the keys in S″ into a single 2-3-4 tree T″. The running time of the split

operation should be O(lg n), where n is the number of keys in T. ( Hint: The costs for joining should telescope.)

Chapter notes

Knuth [261], Aho, Hopcroft, and Ullman [5], and Sedgewick and Wayne [402] give further discussions of balanced-tree schemes and Btrees. Comer [99] provides a comprehensive survey of B-trees. Guibas and Sedgewick [202] discuss the relationships among various kinds of balanced-tree schemes, including red-black trees and 2-3-4 trees.

In 1970, J. E. Hopcroft invented 2-3 trees, a precursor to B-trees and

2-3-4 trees, in which every internal node has either two or three children.

Bayer and McCreight [39] introduced B-trees in 1972 with no explanation of their choice of name.

Bender, Demaine, and Farach-Colton [47] studied how to make Btrees perform well in the presence of memory-hierarchy effects. Their

cache-oblivious algorithms work efficiently without explicitly knowing

the data transfer sizes within the memory hierarchy.

1 When specifying disk capacities, one terabyte is one trillion bytes, rather than 240 bytes.

2 SSDs also exhibit greater latency than main memory and access data in blocks.

3 Another common variant on a B-tree, known as a B*- tree, requires each internal node to be at least 2/3 full, rather than at least half full, as a B-tree requires.

19 Data Structures for Disjoint Sets

Some applications involve grouping n distinct elements into a collection

of disjoint sets—sets with no elements in common. These applications

often need to perform two operations in particular: finding the unique

set that contains a given element and uniting two sets. This chapter

explores methods for maintaining a data structure that supports these

operations.

Section 19.1 describes the operations supported by a disjoint-set data structure and presents a simple application. Section 19.2 looks at a simple linked-list implementation for disjoint sets. Section 19.3 presents a more efficient representation using rooted trees. The running time

using the tree representation is theoretically superlinear, but for all

practical purposes it is linear. Section 19.4 defines and discusses a very quickly growing function and its very slowly growing inverse, which

appears in the running time of operations on the tree-based

implementation, and then, by a complex amortized analysis, proves an

upper bound on the running time that is just barely superlinear.

19.1 Disjoint-set operations

A disjoint-set data structure maintains a collection S = { S 1, S 2, … , Sk}

of disjoint dynamic sets. To identify each set, choose a representative, which is some member of the set. In some applications, it doesn’t matter

which member is used as the representative; it matters only that if you

ask for the representative of a dynamic set twice without modifying the

set between the requests, you get the same answer both times. Other applications may require a prespecified rule for choosing the

representative, such as choosing the smallest member in the set (for a set

whose elements can be ordered).

As in the other dynamic-set implementations we have studied, each

element of a set is represented by an object. Letting x denote an object,

we’ll see how to support the following operations:

MAKE-SET( x), where x does not already belong to some other set, creates a new set whose only member (and thus representative) is x.

UNION( x, y) unites two disjoint, dynamic sets that contain x and y, say Sx and Sy, into a new set that is the union of these two sets. The representative of the resulting set is any member of SxSy, although many implementations of UNION specifically choose the

representative of either Sx or Sy as the new representative. Since the

sets in the collection must at all times be disjoint, the UNION

operation destroys sets Sx and Sy, removing them from the collection

S. In practice, implementations often absorb the elements of one of

the sets into the other set.

FIND-SET( x) returns a pointer to the representative of the unique set

containing x.

Throughout this chapter, we’ll analyze the running times of disjoint-

set data structures in terms of two parameters: n, the number of MAKE-

SET operations, and m, the total number of MAKE-SET, UNION, and

FIND-SET operations. Because the total number of operations m

includes the n MAKE-SET operations, mn. The first n operations are always MAKE-SET operations, so that after the first n operations, the

collection consists of n singleton sets. Since the sets are disjoint at all times, each UNION operation reduces the number of sets by 1. After n

1 UNION operations, therefore, only one set remains, and so at most n

1 UNION operations can occur.

An application of disjoint-set data structures

Image 596

One of the many applications of disjoint-set data structures arises in

determining the connected components of an undirected graph (see

Section B.4). Figure 19.1(a), for example, shows a graph with four connected components.

The procedure CONNECTED-COMPONENTS on the following

page uses the disjoint-set operations to compute the connected

components of a graph. Once the CONNECTED-COMPONENTS

procedure has preprocessed the graph, the procedure SAME-

COMPONENT answers queries about whether two vertices belong to

the same connected component. In pseudocode, we denote the set of

vertices of a graph G by G. V and the set of edges by G. E.

The procedure CONNECTED-COMPONENTS initially places each

vertex v in its own set. Then, for each edge ( u, v), it unites the sets containing u and v. By Exercise 19.1-2, after all the edges are processed, two vertices belong to the same connected component if and only if the

objects corresponding to the vertices belong to the same set. Thus

CONNECTED-COMPONENTS computes sets in such a way that the

procedure SAME-COMPONENT can determine whether two vertices

are in the same connected component. Figure 19.1(b) illustrates how CONNECTED-COMPONENTS computes the disjoint sets.

Figure 19.1 (a) A graph with four connected components: { a, b, c, d}, { e, f, g}, { h, i}, and { j }. (b) The collection of disjoint sets after processing each edge.

CONNECTED-COMPONENTS( G)

1 for each vertex vG. V

2

MAKE-SET( v)

3 for each edge ( u, v) ∈ G. E

4

if FIND-SET( u) ≠ FIND-SET( v)

5

UNION( u, v)

SAME-COMPONENT( u, v)

1 if FIND-SET( u) == FIND-SET( v)

2

return TRUE

3 else returnFALSE

In an actual implementation of this connected-components

algorithm, the representations of the graph and the disjoint-set data

structure would need to reference each other. That is, an object

representing a vertex would contain a pointer to the corresponding

disjoint-set object, and vice versa. Since these programming details

depend on the implementation language, we do not address them further

here.

When the edges of the graph are static—not changing over time—

depth-first search can compute the connected components faster (see

Exercise 20.3-12 on page 572). Sometimes, however, the edges are added

dynamically, with the connected components updated as each edge is

added. In this case, the implementation given here can be more efficient

than running a new depth-first search for each new edge.

Exercises

19.1-1

The CONNECTED-COMPONENTS procedure is run on the

undirected graph G = ( V, E), where V = { a, b, c, d, e, f, g, h, i, j, k}, and the edges of E are processed in the order ( d, i), ( f, k), ( g, i), ( b, g), ( a, h), ( i, j), ( d, k), ( b, j), ( d, f), ( g, j), ( a, e). List the vertices in each connected component after each iteration of lines 3–5.

19.1-2

Show that after all edges are processed by CONNECTED-

COMPONENTS, two vertices belong to the same connected component

if and only if they belong to the same set.

19.1-3

During the execution of CONNECTED-COMPONENTS on an

undirected graph G = ( V, E) with k connected components, how many times is FIND-SET called? How many times is UNION called? Express

your answers in terms of | V |, | E|, and k.

19.2 Linked-list representation of disjoint sets

Figure 19.2(a) shows a simple way to implement a disjoint-set data structure: each set is represented by its own linked list. The object for

each set has attributes head, pointing to the first object in the list, and

tail, pointing to the last object. Each object in the list contains a set member, a pointer to the next object in the list, and a pointer back to the

set object. Within each linked list, the objects may appear in any order.

The representative is the set member in the first object in the list.

With this linked-list representation, both MAKE-SET and FIND-

SET require only O(1) time. To carry out MAKE-SET( x), create a new

linked list whose only object is x. For FIND-SET( x), just follow the pointer from x back to its set object and then return the member in the

object that head points to. For example, in Figure 19.2(a), the call FIND-SET( g) returns f.

Image 597

Figure 19.2 (a) Linked-list representations of two sets. Set S 1 contains members d, f, and g, with representative f, and set S 2 contains members b, c, e, and h, with representative c. Each object in the list contains a set member, a pointer to the next object in the list, and a pointer back to the set object. Each set object has pointers head and tail to the first and last objects, respectively. (b) The result of UNION( g, e), which appends the linked list containing e to the linked list containing g.

The representative of the resulting set is f. The set object for e’s list, S 2, is destroyed.

A simple implementation of union

The simplest implementation of the UNION operation using the linked-

list set representation takes significantly more time than MAKE-SET or

FIND-SET. As Figure 19.2(b) shows, the operation UNION( x, y) appends y’s list onto the end of x’s list. The representative of x’s list becomes the representative of the resulting set. To quickly find where to

append y’s list, use the tail pointer for x’s list. Because all members of y’s list join x’s list, the UNION operation destroys the set object for y’s list.

The UNION operation is where this implementation pays the price for

FIND-SET taking constant time: UNION must also update the pointer

to the set object for each object originally on y’s list, which takes time

linear in the length of y’s list. In Figure 19.2, for example, the operation UNION( g, e) causes pointers to be updated in the objects for b, c, e, and h.

Image 598

Image 599

In fact, we can construct a sequence of m operations on n objects that

requires Θ( n 2) time. Starting with objects x 1, x 2, … , xn, execute the sequence of n MAKE-SET operations followed by n − 1 UNION

operations shown in Figure 19.3, so that m = 2 n−1. The n MAKE-SET

operations take Θ( n) time. Because the i th UNION operation updates i objects, the total number of objects updated by all n−1 UNION

operations forms an arithmetic series:

Figure 19.3 A sequence of 2 n − 1 operations on n objects that takes Θ( n 2) time, or Θ( n) time per operation on average, using the linked-list set representation and the simple implementation of UNION.

The total number of operations is 2 n−1, and so each operation on

average requires Θ( n) time. That is, the amortized time of an operation is

Θ( n).

A weighted-union heuristic

In the worst case, the above implementation of UNION requires an

average of Θ( n) time per call, because it might be appending a longer list

onto a shorter list, and the procedure must update the pointer to the set

object for each member of the longer list. Suppose instead that each list

also includes the length of the list (which can be maintained

straightforwardly with constant overhead) and that the UNION

procedure always appends the shorter list onto the longer, breaking ties arbitrarily. With this simple weighted-union heuristic, a single UNION

operation can still take Ω( n) time if both sets have Ω( n) members. As the following theorem shows, however, a sequence of m MAKE-SET,

UNION, and FIND-SET operations, n of which are MAKE-SET

operations, takes O( m + n lg n) time.

Theorem 19.1

Using the linked-list representation of disjoint sets and the weighted-

union heuristic, a sequence of m MAKE-SET, UNION, and FIND-SET

operations, n of which are MAKE-SET operations, takes O( m + n lg n) time.

Proof Because each UNION operation unites two disjoint sets, at most

n − 1 UNION operations occur over all. We now bound the total time

taken by these UNION operations. We start by determining, for each

object, an upper bound on the number of times the object’s pointer back

to its set object is updated. Consider a particular object x. Each time x’s pointer is updated, x must have started in the smaller set. The first time

x’s pointer is updated, therefore, the resulting set must have at least 2

members. Similarly, the next time x’s pointer is updated, the resulting set

must have had at least 4 members. Continuing on, for any kn, after x’s pointer has been updated ⌈lg k⌉ times, the resulting set must have at least

k members. Since the largest set has at most n members, each object’s pointer is updated at most ⌈lg n⌉ times over all the UNION operations.

Thus the total time spent updating object pointers over all UNION

operations is O( n lg n). We must also account for updating the tail pointers and the list lengths, which take only Θ(1) time per UNION

operation. The total time spent in all UNION operations is thus O( n lg

n). The time for the entire sequence of m operations follows. Each MAKE-SET and FIND-SET operation takes O(1) time, and there are

O( m) of them. The total time for the entire sequence is thus O( m + n lg n).

Exercises

19.2-1

Write pseudocode for MAKE-SET, FIND-SET, and UNION using the

linked-list representation and the weighted-union heuristic. Make sure to

specify the attributes that you assume for set objects and list objects.

19.2-2

Show the data structure that results and the answers returned by the

FIND-SET operations in the following program. Use the linked-list

representation with the weighted-union heuristic. Assume that if the sets

containing xi and xj have the same size, then the operation UNION( xi, xj) appends xj’s list onto xi’s list.

1for i = 1 to 16

2

MAKE-SET( xi)

3for i = 1 to 15 by 2

4

UNION( xi, xi+1)

5for i = 1 to 13 by 4

6

UNION( xi, xi+2)

7UNION( x 1, x 5)

8UNION( x 11, x 13)

9UNION( x 1, x 10)

10FIND-SET( x 2)

11FIND-SET( x 9)

19.2-3

Adapt the aggregate proof of Theorem 19.1 to obtain amortized time

bounds of O(1) for MAKE-SET and FIND-SET and O(lg n) for UNION using the linked-list representation and the weighted-union

heuristic.

19.2-4

Give a tight asymptotic bound on the running time of the sequence of

operations in Figure 19.3 assuming the linked-list representation and the weighted-union heuristic.

19.2-5

Professor Gompers suspects that it might be possible to keep just one

pointer in each set object, rather than two ( head and tail), while keeping the number of pointers in each list element at two. Show that the

professor’s suspicion is well founded by describing how to represent each

set by a linked list such that each operation has the same running time as

the operations described in this section. Describe also how the

operations work. Your scheme should allow for the weighted-union

heuristic, with the same effect as described in this section. ( Hint: Use the

tail of a linked list as its set’s representative.)

19.2-6

Suggest a simple change to the UNION procedure for the linked-list

representation that removes the need to keep the tail pointer to the last

object in each list. Regardless of whether the weighted-union heuristic is

used, your change should not change the asymptotic running time of the

UNION procedure. ( Hint: Rather than appending one list to another,

splice them together.)

19.3 Disjoint-set forests

A faster implementation of disjoint sets represents sets by rooted trees,

with each node containing one member and each tree representing one

set. In a disjoint-set forest, illustrated in Figure 19.4(a), each member points only to its parent. The root of each tree contains the

representative and is its own parent. As we’ll see, although the

straightforward algorithms that use this representation are no faster than

ones that use the linked-list representation, two heuristics—“union by

rank” and “path compression”—yield an asymptotically optimal

disjoint-set data structure.

The three disjoint-set operations have simple implementations. A

MAKE-SET operation simply creates a tree with just one node. A

FIND-SET operation follows parent pointers until it reaches the root of

the tree. The nodes visited on this simple path toward the root constitute

the find path. A UNION operation, shown in Figure 19.4(b), simply causes the root of one tree to point to the root of the other.

Image 600

Figure 19.4 A disjoint-set forest. (a) Trees representing the two sets of Figure 19.2. The tree on the left represents the set { b, c, e, h}, with c as the representative, and the tree on the right represents the set { d, f, g}, with f as the representative. (b) The result of UNION ( e, g).

Heuristics to improve the running time

So far, disjoint-set forests have not improved on the linked-list

implementation. A sequence of n − 1 UNION operations could create a

tree that is just a linear chain of n nodes. By using two heuristics, however, we can achieve a running time that is almost linear in the total

number m of operations.

The first heuristic, union by rank, is similar to the weighted-union

heuristic we used with the linked-list representation. The common-sense

approach is to make the root of the tree with fewer nodes point to the

root of the tree with more nodes. Rather than explicitly keeping track of

the size of the subtree rooted at each node, however, we’ll adopt an

approach that eases the analysis. For each node, maintain a rank, which

is an upper bound on the height of the node. Union by rank makes the

root with smaller rank point to the root with larger rank during a

UNION operation.

The second heuristic, path compression, is also quite simple and

highly effective. As shown in Figure 19.5, FIND-SET operations use it to make each node on the find path point directly to the root. Path

compression does not change any ranks.

Pseudocode for disjoint-set forests

Image 601

The union-by-rank heuristic requires its implementation to keep track of

ranks. With each node x, maintain the integer value x. rank, which is an upper bound on the height of x (the number of edges in the longest simple path from a descendant leaf to x). When MAKE-SET creates a

singleton set, the single node in the corresponding tree has an initial rank

of 0. Each FIND-SET operation leaves all ranks unchanged. The

UNION operation has two cases, depending on whether the roots of the

trees have equal rank. If the roots have unequal ranks, make the root

with higher rank the parent of the root with lower rank, but don’t

change the ranks themselves. If the roots have equal ranks, arbitrarily

choose one of the roots as the parent and increment its rank.

Figure 19.5 Path compression during the operation FIND-SET. Arrows and self-loops at roots are omitted. (a) A tree representing a set prior to executing FIND-SET( a). Triangles represent subtrees whose roots are the nodes shown. Each node has a pointer to its parent. (b) The same set after executing FIND-SET( a). Each node on the find path now points directly to the root.

Let’s put this method into pseudocode, appearing on the next page.

The parent of node x is denoted by x. p. The LINK procedure, a subroutine called by UNION, takes pointers to two roots as inputs. The

FIND-SET procedure with path compression, implemented recursively,

turns out to be quite simple.

The FIND-SET procedure is a two-pass method: as it recurses, it makes one pass up the find path to find the root, and as the recursion

unwinds, it makes a second pass back down the find path to update each

node to point directly to the root. Each call of FIND-SET( x) returns x. p in line 3. If x is the root, then FIND-SET skips line 2 and just returns

x. p, which is x. In this case the recursion bottoms out. Otherwise, line 2

executes, and the recursive call with parameter x. p returns a pointer to the root. Line 2 updates node x to point directly to the root, and line 3

returns this pointer.

MAKE-SET( x)

1 x. p = x

2 x. rank = 0

UNION( x, y)

1 LINK(FIND-SET( x), FIND-SET( y))

LINK( x, y)

1 if x. rank > y. rank

2

y. p = x

3 else x. p = y

4

if x. rank == y. rank

5

y. rank = y. rank + 1

FIND-SET( x)

1 if xx. p

// not the root?

2

x. p = FIND-SET( x. p)

// the root becomes the parent

3 return x. p

// return the root

Effect of the heuristics on the running time

Separately, either union by rank or path compression improves the

running time of the operations on disjoint-set forests, and combining the

two heuristics yields an even greater improvement. Alone, union by rank

yields a running time of O( m lg n) for a sequence of m operations, n of which are MAKE-SET (see Exercise 19.4-4), and this bound is tight (see

Exercise 19.3-3). Although we won’t prove it here, for a sequence of n MAKE-SET operations (and hence at most n − 1 UNION operations)

and f FIND-SET operations, the worst-case running time using only the

path-compression heuristic is Θ( n + f · (1 + log2+ f/ nn)).

Combining union by rank and path compression gives a worst-case

running time of O( m α( n)), where α( n) is a very slowly growing function, defined in Section 19.4. In any conceivable application of a disjoint-set data structure, α( n) ≤ 4, and thus, its running time is as good as linear in m for all practical purposes. Mathematically speaking, however, it is

superlinear. Section 19.4 proves this O( ( n)) upper bound.

Exercises

19.3-1

Redo Exercise 19.2-2 using a disjoint-set forest with union by rank and

path compression. Show the resulting forest with each node including its

xi and rank.

19.3-2

Write a nonrecursive version of FIND-SET with path compression.

19.3-3

Give a sequence of m MAKE-SET, UNION, and FIND-SET

operations, n of which are MAKE-SET operations, that takes Ω( m lg n) time when using only union by rank and not path compression.

19.3-4

Consider the operation PRINT-SET( x), which is given a node x and prints all the members of x’s set, in any order. Show how to add just a

single attribute to each node in a disjoint-set forest so that PRINT-

SET( x) takes time linear in the number of members of x’s set and the asymptotic running times of the other operations are unchanged.

Assume that you can print each member of the set in O(1) time.

19.3-5

Show that any sequence of m MAKE-SET, FIND-SET, and LINK

operations, where all the LINK operations appear before any of the

Image 602

Image 603

Image 604

Image 605

Image 606

Image 607

FIND-SET operations, takes only O( m) time when using both path compression and union by rank. You may assume that the arguments to

LINK are roots within the disjoint-set forest. What happens in the same

situation when using only path compression and not union by rank?

★ 19.4 Analysis of union by rank with path compression

As noted in Section 19.3, the combined union-by-rank and path-compression heuristic runs in O( m α( n)) time for m disjoint-set operations on n elements. In this section, we’ll explore the function α to see just how slowly it grows. Then we’ll analyze the running time using

the potential method of amortized analysis.

A very quickly growing function and its very slowly growing inverse

For integers j, k ≥ 0, we define the function Ak( j) as

where the expression

uses the functional-iteration notation

defined in equation (3.30) on page 68. Specifically, equation (3.30) gives

and

for i ≥ 1. We call the parameter

k the level of the function A.

The function Ak( j) strictly increases with both j and k. To see just how quickly this function grows, we first obtain closed-form expressions

for A 1( j) and A 2( j).

Lemma 19.2

For any integer j ≥ 1, we have A 1( j) = 2 j + 1.

Proof We first use induction on i to show that

. For the

base case,

. For the inductive step, assume that

Image 608

Image 609

Image 610

Image 611

Image 612

Image 613

Image 614

Image 615

Image 616

Image 617

Image 618

. Then

.

Finally, we note that

.

Lemma 19.3

For any integer j ≥ 1, we have A 2 ( j) = 2 j+1( j + 1) − 1.

Proof We first use induction on i to show that

.

For the base case, we have

. For the inductive

step,

assume

that

.

Then

. Finally, we note that

.

Now we can see how quickly Ak( j) grows by simply examining Ak(1) for levels k = 0, 1, 2, 3, 4. From the definition of A 0( j) and the above lemmas, we have A 0(1) = 1 + 1 = 2, A 1(1) = 2 · 1 + 1 = 3, and A 2(1) =

21+1 · (1 + 1) − 1 = 7. We also have

A 3(1) =

= A 2( A 2(1))

= A 2(7)

= 28 · 8 − 1

= 211 − 1

= 2047

and

A 4(1) =

= A 3( A 3(1))

= A 3(2047)

=

Image 619

Image 620

A 2(2047)

= 22048 · 2048 − 1

= 22059 − 1

> 22056

= (24)514

= 16514

≫ 1080,

which is the estimated number of atoms in the observable universe. (The

symbol “≫” denotes the “much-greater-than” relation.)

We define the inverse of the function Ak( n), for integer n ≥ 0, by In words, α( n) is the lowest level k for which Ak(1) is at least n. From the above values of Ak(1), we see that

It is only for values of n so large that the term “astronomical”

understates them (greater than A 4(1), a huge number) that α( n) > 4, and so α( n) ≤ 4 for all practical purposes.

Properties of ranks

In the remainder of this section, we prove an O( ( n)) bound on the running time of the disjoint-set operations with union by rank and path

compression. In order to prove this bound, we first prove some simple

properties of ranks.

Lemma 19.4

For all nodes x, we have x. rankx. p. rank, with strict inequality if xx. p ( x is not a root). The value of x. rank is initially 0, increases through time until xx. p, and from then on, x. rank does not change. The value of x. p. rank monotonically increases over time.

Proof The proof is a straightforward induction on the number of

operations, using the implementations of MAKE-SET, UNION, and

FIND-SET that appear on page 530, and is left as Exercise 19.4-1.

Corollary 19.5

On the simple path from any node going up toward a root, node ranks

strictly increase.

Lemma 19.6

Every node has rank at most n − 1.

Proof Each node’s rank starts at 0, and it increases only upon LINK

operations. Because there are at most n − 1 UNION operations, there

are also at most n − 1 LINK operations. Because each LINK operation

either leaves all ranks alone or increases some node’s rank by 1, all ranks

are at most n − 1.

Lemma 19.6 provides a weak bound on ranks. In fact, every node has

rank at most ⌊lg n⌊ (see Exercise 19.4-2). The looser bound of Lemma

19.6 suffices for our purposes, however.

Proving the time bound

In order to prove the O( ( n)) time bound, we’ll use the potential method of amortized analysis from Section 16.3. In performing the amortized analysis, it will be convenient to assume that we invoke the

LINK operation rather than the UNION operation. That is, since the

parameters of the LINK procedure are pointers to two roots, we act as

though we perform the appropriate FIND-SET operations separately.

The following lemma shows that even if we count the extra FIND-SET

Image 621

operations induced by UNION calls, the asymptotic running time

remains unchanged.

Lemma 19.7

Suppose that we convert a sequence S′ of m′ MAKE-SET, UNION, and

FIND-SET operations into a sequence S of m MAKE-SET, LINK, and

FIND-SET operations by turning each UNION into two FIND-SET

operations followed by one LINK. Then, if sequence S runs in O( ( n)) time, sequence S′ runs in O( mα( n)) time.

Proof Since each UNION operation in sequence S′ is converted into three operations in S, we have m′ ≤ m ≤ 3 m′, so that m = Θ( m′), Thus, an O( m α( n)) time bound for the converted sequence S implies an O( mα( n)) time bound for the original sequence S′.

From now on, we assume that the initial sequence of m′ MAKE-SET,

UNION, and FIND-SET operations has been converted to a sequence