A tree is an abstract data type that stores elements hierarchically. With the
exception of the top element, each element in a tree has a parent element and zero
or more children elements. A tree is usually visualized by placing elements inside
ovals or rectangles, and by drawing the connections between parents and children
with straight lines. (See Figure 7.2.) We typically call the top element the root of
the tree, but it is drawn as the highest element, with the other elements being
connected below (just the opposite of a botanical tree).
Figure 7.2: A tree with 17 nodes representing the
organization of a fictitious corporation. The root stores
Electronics R'Us. The children of the root store R&D,
Sales, Purchasing, and Manufacturing. The internal
nodes store Sales, International, Overseas, Electronics
R'Us, and Manufacturing
92 trang |
Chia sẻ: tlsuongmuoi | Lượt xem: 2839 | Lượt tải: 0
Bạn đang xem trước 20 trang tài liệu Tree Definitions and Properties, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
methods of the array list ADT on such a representation. Is it better to store the
entries in L by increasing indices or not?
C-6.17
There is a simple, but inefficient, algorithm, called bubble-sort, for sorting a
sequence S of n comparable elements. This algorithm scans the sequence n−1
times, where, in each scan, the algorithm compares the current element with the
next one and swaps them if they are out of order. Give a pseudo-code
description of bubble-sort that is as efficient as possible assuming S is
implemented with a doubly linked list. What is the running time of this
algorithm?
C-6.18
Answer Exercise C-6.17 assuming S is implemented with an array list.
C-6.19
A useful operation in databases is the natural join. If we view a database as a
list of ordered pairs of objects, then the natural join of databases A and B is the
list of all ordered triples (x,y,z) such that the pair (x,y) is in A and the pair (y,z) is
in B. Describe and analyze an efficient algorithm for computing the natural join
of a list A of n pairs and a list B of m pairs.
C-6.20
When Bob wants to send Alice a message M on the Internet, he breaks M into n
data packets, numbers the packets consecutively, and injects them into the
network. When the packets arrive at Alice's computer, they may be out of order,
so Alice must assemble the sequence of n packets in order before she can be
sure she has the entire message. Describe an efficient scheme for Alice to do
this. What is the running time of this algorithm?
C-6.21
Given a list L of n positive integers, each represented with k = logn + 1 bits,
describe an O(n)-time method for finding a k-bit integer not in L.
C-6.22
Argue why any solution to the previous problem must run in Ω(n) time.
C-6.23
Given a list L of n arbitrary integers, design an O(n)-time method for finding an
integer that cannot be formed as the sum of two integers in L.
370
C-6.24
Isabel has an interesting way of summing up the values in an array A of n
integers, where n is a power of two. She creates an array B of half the size of A
and sets B[i] = A[2i] +A[2i+ 1], for i = 0,1,..., (n/2) − 1. If B has size 1, then she
outputs B[0]. Otherwise, she replaces A with B, and repeats the process. What is
the running time of her algorithm?
Projects
P-6.1
Implement the array list ADT by means of an extendable array used in a circular
fashion, so that insertions and deletions at the beginning and end of the array list
run in constant time.
P-6.2
Implement the array list ADT using a doubly linked list. Show experimentally
that this implementation is worse than the array-based approach.
P-6.3
Write a simple text editor, which stores and displays a string of characters using
the list ADT, together with a cursor object that highlights a position in this
string. Your editor should support the following operations:
•
left: Move cursor left one character (do nothing if at text end).
•
right: Move cursor right one character (do nothing if at text end).
•
cut: Delete the character right of the cursor (do nothing at text end).
•
paste c: Insert the character c just after the cursor.
P-6.4
Implement a phased favorites list. A phase consists of N accesses in the list, for
a given parameter N. During a phase, the list should maintain itself so that
elements are ordered by decreasing access counts during that phase. At the end
371
of a phase, it should clear all the access counts and start the next phase.
Experimentally, determine what are the best values of N for various list sizes.
P-6.5
Write a complete adapter class that implements the sequence ADT using a
java.util.ArrayList object.
P-6.6
Implement the favorites list application using an array list instead of a list.
Compare it experimentally to the list-based implementation.
Chapter Notes
The concept of viewing data structures as collections (and other principles of object-
oriented design) can be found in object-oriented design books by Booch [14], Budd
[17], Golberg and Robson [40], and Liskov and Guttag [69]. Lists and iterators are
pervasive concepts in the Java Collections Framework. Our node list ADT is derived
from the "position" abstraction introduced by Aho, Hopcroft, and Ullman [5], and the
list ADT of Wood [100]. Implementations of lists via arrays and linked lists are
discussed by Knuth [62].
372
Chapter 7 Trees
Contents
7.1
General Trees ......................
266
7.1.1
Tree Definitions and Properties............
267
7.1.2
The Tree Abstract Data Type ............
373
270
7.1.3
Implementing a Tree .................
271
7.2
Tree Traversal Algorithms................
273
7.2.1
Depth and Height...................
273
7.2.2
Preorder Traversal...................
276
7.2.3
Postorder Traversal..................
279
7.3
Binary Trees.......................
282
7.3.1
The Binary Tree ADT.................
284
7.3.2
A Binary Tree Interface in Java............
284
374
7.3.3
Properties of Binary Trees ..............
285
7.3.4
A Linked Structure for Binary Trees.........
287
7.3.5
An Array-List Representation of a Binary Tree....
296
7.3.6
Traversals of Binary Trees...............
298
7.3.7
The Template Method Pattern............
305
7.4
Exercises.........................
309
java.datastructures.net
7.1 General Trees
Productivity experts say that breakthroughs come by thinking "nonlinearly." In this
chapter, we discuss one of the most important nonlinear data structures in
computing—trees. Tree structures are indeed a breakthrough in data organization, for
they allow us to implement a host of algorithms much faster than when using linear
data structures, such as list. Trees also provide a natural organization for data, and
consequently have become ubiquitous structures in file systems, graphical user
interfaces, databases, Web sites, and other computer systems.
375
It is not always clear what productivity experts mean by "nonlinear" thinking, but
when we say that trees are "nonlinear," we are referring to an organizational
relationship that is richer than the simple "before" and "after" relationships between
objects in sequences. The relationships in a tree are hierarchical, with some objects
being "above" and some "below" others. Actually, the main terminology for tree data
structures comes from family trees, with the terms "parent," "child," "ancestor," and
"descendent" being the most common words used to describe relationships. We show
an example of a family tree in Figure 7.1.
Figure 7.1: A family tree showing some
descendents of Abraham, as recorded in Genesis,
chapters 25–36.
7.1.1 Tree Definitions and Properties
A tree is an abstract data type that stores elements hierarchically. With the
exception of the top element, each element in a tree has a parent element and zero
or more children elements. A tree is usually visualized by placing elements inside
ovals or rectangles, and by drawing the connections between parents and children
with straight lines. (See Figure 7.2.) We typically call the top element the root of
the tree, but it is drawn as the highest element, with the other elements being
connected below (just the opposite of a botanical tree).
Figure 7.2: A tree with 17 nodes representing the
organization of a fictitious corporation. The root stores
376
Electronics R'Us. The children of the root store R&D,
Sales, Purchasing, and Manufacturing. The internal
nodes store Sales, International, Overseas, Electronics
R'Us, and Manufacturing.
Formal Tree Definition
Formally, we define a tree T as a set of nodes storing elements such that the nodes
have a parent-child relationship, that satisfies the following properties:
• If T is nonempty, it has a special node, called the root of T, that has no
parent.
• Each node v of T different from the root has a unique parent node w;
every node with parent w is a child of w.
Note that according to our definition, a tree can be empty, meaning that it doesn't
have any nodes. This convention also allows us to define a tree recursively, such
that a tree T is either empty or consists of a node r, called the root of T, and a
(possibly empty) set of trees whose roots are the children of r.
Other Node Relationships
377
Two nodes that are children of the same parent are siblings. A node v is external
if v has no children. A node v is internal if it has one or more children. External
nodes are also known as leaves.
Example 7.1: In most operating systems, files are organized hierarchically
into nested directories (also called folders), which are presented to the user in the
form of a tree. (See Figure 7.3.) More specifically, the internal nodes of the tree
are associated with directories and the external nodes are associated with regular
files. In the UNIX and Linux operating systems, the root of the tree is
appropriately called the "root directory," and is represented by the symbol "/."
Figure 7.3: Tree representing a portion of a file
system.
A node u is an ancestor of a node v if u = v or u is an ancestor of the parent of v.
Conversely, we say that a node v is a descendent of a node u if u is an ancestor of
v. For example, in Figure 7.3, cs252/ is an ancestor of papers/, and pr3 is a
descendent of cs016/. The subtree of T rooted at a node v is the tree consisting
of all the descendents of v in T (including v itself). In Figure 7.3, the subtree
rooted at cs016/ consists of the nodes cs016/, grades, homeworks/,
programs/, hw1, hw2, hw3, pr1, pr2, and pr3.
378
Edges and Paths in Trees
An edge of tree T is a pair of nodes (u, v) such that u is the parent of v, or vice
versa. A path of T is a sequence of nodes such that any two consecutive nodes in
the sequence form an edge. For example, the tree in Figure 7.3 contains the path
(cs252/, projects/, demos/, market).
Example 7.2: The inheritance relation between classes in a Java program
forms a tree. The root, java. lang. Object, is an ancestor of all other
classes. Each class, C, is a descendent of this root and is the root of a subtree of
the classes that extend C. Thus, there is a path from C to the root,
java.lang.Object, in this inheritance tree.
Ordered Trees
A tree is ordered if there is a linear ordering defined for the children of each node;
that is, we can identify the children of a node as being the first, second, third, and
so on. Such an ordering is usually visualized by arranging siblings left to right,
according to their ordering. Ordered trees typically indicate the linear order
among siblings by listing them in the correct order.
Example 7.3: The components of a structured document, such as a book, are
hierarchically organized as a tree whose internal nodes are parts, chapters, and
sections, and whose external nodes are paragraphs, tables, figures, and so on.
(See Figure 7.4.) The root of the tree corresponds to the book itself. We could, in
fact, consider expanding the tree further to show paragraphs consisting of
sentences, sentences consisting of words, and words consisting of characters.
Such a tree is an example of an ordered tree, because there is a well-defined
ordering among the children of each node.
Figure 7.4: An ordered tree associated with a book.
379
7.1.2 The Tree Abstract Data Type
The tree ADT stores elements at positions, which, as with positions in a list, are
defined relative to neighboring positions. The positions in a tree are its nodes, and
neighboring positions satisfy the parent-child relationships that define a valid tree.
Therefore, we use the terms "position" and "node" interchangeably for trees. As
with a list position, a position object for a tree supports the method:
element(): return the object stored at this position.
The real power of node positions in a tree, however, comes from the accessor
methods of the tree ADT that return and accept positions, such as the following:
root():
return the tree's root; an error occurs if the tree is empty.
parent (v):
return the parent of v; an error occurs if v is the root.
children(v):
return an iterable collection containing the children of node v.
If a tree T is ordered, then the iterable collection, children(v), stores the children of
v in order. If v is an external node, then children(v) is empty.
In addition to the above fundamental accessor methods, we also include the
following query methods:
isInternal(v):
Test whether node v is internal.
isExternal(v):
Test whether node v is external.
isRoot(v):
Test whether node v is the root.
These methods make programming with trees easier and more readable, since we
can use them in the conditionals of if statements and while loops, rather than
using a nonintuitive conditional.
380
There are also a number of generic methods a tree should probably support that are
not necessarily related to its tree structure, including the following:
size():
return the number of nodes in the tree.
isEmpty():
Test whether the tree has any nodes or not.
iterator():
return an iterator of all the elements stored at nodes of the tree.
positions():
return an iterable collection of all the nodes of the tree.
replace(v,e):
Replace with e and return the element stored at node v.
Any method that takes a position as an argument should generate an error condition
if that position is invalid. We do not define any specialized update methods for trees
here. Instead, we prefer to describe different tree update methods in conjunction
with specific applications of trees in subsequent chapters. In fact, we can imagine
several kinds of tree update operations beyond those given in this book.
7.1.3 Implementing a Tree
The Java interface shown in Code Fragment 7.1 represents the tree ADT. Error
conditions are handled as follows: Each method that can take a position as an
argument, may throw an InvalidPositionException, to indicate that the
position is invalid. Method parent throws a BoundaryViolationException
if it is called on the root. Method root throws an EmptyTreeException if it is
called on an empty tree.
Code Fragment 7.1: Java interface Tree
representing the tree ADT. Additional update methods
may be added, depending on the application. We do
not include such methods in the interface, however.
381
A Linked Structure for General Trees
A natural way to realize a tree T is to use a linked structure, where we represent
each node v of T by a position object (see Figure 7.5a) with the following fields:
A reference to the element stored at v, a link to the parent of v, and a some kind of
collection (for example, a list or array) to store links to the children of v. If v is the
root of T, then the parent field of v is null. Also, we store a reference to the root of
T and the number of nodes of T in internal variables. This structure is
schematically illustrated in Figure 7.5b.
382
Figure 7.5: The linked structure for a general tree:
(a) the position object associated with a node; (b) the
portion of the data structure associated with a node
and its children.
Table 7.1 summarizes the performance of the implementation of a general tree
using a linked structure. The analysis is left as an exercise (C-7.25), but we note
that, by using a collection to store the children of each node v, we can implement
children(v) simply by returning a reference to this collection.
Table 7.1: Running times of the methods of an n-
node general tree implemented with a linked
structure. We let cv denote the number of children of a
node v. The space usage is O(n).
Operation
Time
size, isEmpty
O(1)
iterator, positions
O(n)
383
replace
O(1)
root, parent
O(1)
children(v)
O(cv)
isInternal, isExternal, isRoot
O(1)
7.2 Tree Traversal Algorithms
In this section, we present algorithms for performing traversal computations on a tree
by accessing it through the tree ADT methods.
7.2.1 Depth and Height
Let v be a node of a tree T. The depth of v is the number of ancestors of v,
excluding v itself. For example, in the tree of Figure 7.2, the node storing
International has depth 2. Note that this definition implies that the depth of the root
of T is 0.
The depth of a node v can also be recursively defined as follows:
• If v is the root, then the depth of v is 0
• Otherwise, the depth of v is one plus the depth of the parent of v.
Based on this definition, we present a simple, recursive algorithm, depth, in Code
Fragment 7.2, for computing the depth of a node v in T. This method calls itself
recursively on the parent of v, and adds 1 to the value returned. A simple Java
implementation of this algorithm is shown in Code Fragment 7.3.
Code Fragment 7.2: Algorithm for computing the
depth of a node v in a tree T.
384
Code Fragment 7.3: Method depth written in Java.
The running time of algorithm depth(T, v) is O(dv), where dv denotes the depth of
the node v in the tree T, because the algorithm performs a constant-time recursive
step for each ancestor of v. Thus, algorithm depth (T, v) runs in O(n) worst-case
time, where n is the total number of nodes of T, since a node of T may have depth n
− 1 in the worst case. Although such a running time is a function of the input size, it
is more accurate to characterize the running time in terms of the parameter dv, since
this parameter can be much smaller than n.
Height
The height of a node v in a tree T is also defined recursively:
• If v is an external node, then the height of v is 0
• Otherwise, the height of v is one plus the maximum height of a child of v.
The height of a nonempty tree T is the height of the root of T. For example, the
tree of Figure 7.2 has height 4. In addition, height can also be viewed as follows.
Proposition 7.4: The height of a nonempty tree T is equal to the maximum
depth of an external node of T.
We leave the justification of this fact to an exercise (R-7.6). We present here an
algorithm, height1, shown in Code Fragment 7.4 and implemented in Java in
Code Fragment 7.5, for computing the height of a nonempty tree T based on the
proposition above and the algorithm depth from Code Fragment 7.2.
385
Code Fragment 7.4: Algorithm height1 for
computing the height of a nonempty tree T. Note that
this algorithm calls algorithm depth (Code Fragment
7.2).
Code Fragment 7.5: Method height1 written in
Java. Note the use of the max method of class
java.lang. Math.
Unfortunately, algorithm height1 is not very efficient. Since height1 calls
algorithm depth (v) on each external node v of T, the running time of height1 is
given by O(n + Σv(1 + dv)), where n is the number of nodes of T, dv is the depth
of node v, and E is the set of external nodes of T. In the worst case, the sumΣv(1
d
+
v) is proportional to n2. (See Exercise C-7.6.) Thus, algorithm height1 runs in
O(n2) time.
Algorithm height2, shown in Code Fragment 7.6 and implemented in Java in
Code Fragment 7.7, computes the height of tree T in a more efficient manner by
using the recursive definition of height.
386
Code Fragment 7.6: Algorithm height2 for
computing the height of the subtree of tree T rooted
at a node v.
Code Fragment 7.7: Method height2 written in
Java.
Algorithm height2 is more efficient than height1 (from Code Fragment 7.4).
The algorithm is recursive, and, if it is initially called on the root of T, it will
eventually be called on each node of T. Thus, we can determine the running time
of this method by summing, over all the nodes, the amount of time spent at each
node (on the nonrecursive part). Processing each node in children(v) takes O(cv)
time, where cv denotes the number of children of node v. Also, the while loop
has cv iterations and each iteration of the loop takes O(1) time plus the time for
the recursive call on a child of v. Thus, algorithm height2 spends O(1 + cv)
time at each node v, and its running time is O(Σv(1 + cv)). In order to complete the
analysis, we make use of the following property.
Proposition 7.5: Let T be a tree with n nodes, and let cv denote the
number of children of a node v of T. Then, summing over the vertices in T, Σvcv=
n − 1.
387
Justification: Each node of T, with the exception of the root, is a child of
another node, and thus contributes one unit to the above sum.
By Proposition 7.5, the running time of algorithm height2, when called on the
root of T, is O(n), where n is the number of nodes of T.
7.2.2 Preorder Traversal
A traversal of a tree T is a systematic way of accessing, or "visiting," all the nodes
of T. In this section, we present a basic traversal scheme for trees, called preorder
traversal. In the next section, we will study another basic traversal scheme, called
postorder traversal.
In a preorder traversal of a tree T, the root of T is visited first and then the subtrees
rooted at its children are traversed recursively. If the tree is ordered, then the
subtrees are traversed according to the order of the children. The specific action
associated with the "visit" of a node v depends on the application of this traversal,
and could involve anything from incrementing a counter to performing some
complex computation for v. The pseudo-code for the preorder traversal of the
subtree rooted at a node v is shown in Code Fragment 7.8. We initially call this
algorithm with preorder(T,T.root()).
Code Fragment 7.8: Algorithm preorder for
performing the preorder traversal of the subtree of a
tree T rooted at a node v.
The preorder traversal algorithm is useful for producing a linear ordering of the
nodes of a tree where parents must always come before their children in the
ordering. Such orderings have several different applications. We explore a simple
instance of such an application in the next example.
Figure 7.6: Preorder traversal of an ordered tree,
where the children of each node are ordered from left
to right.
388
Example 7.6: The preorder traversal of the tree associated with a document, as
in Example 7.3, examines an entire document sequentially, from beginning to end.
If the external nodes are removed before the traversal, then the traversal examines
the table of contents of the document. (See Figure 7.6.)
The preorder traversal is also an efficient way to access all the nodes of a tree. To
justify this, let us consider the running time of the preorder traversal of a tree T with
n nodes under the assumption that visiting a node takes O(1) time. The analysis of
the preorder traversal algorithm is actually similar to that of algorithm height2
(Code Fragment 7.7), given in Section 7.2.1. At each node v, the nonrecursive part
of the preorder traversal algorithm requires time O(1 + cv), where cv is the number
of children of v. Thus, by Proposition 7.5, the overall running time of the preorder
traversal of T is O(n).
Algorithm toStringPreorder(T, v), implemented in Java in Code
Fragment 7.9, performs a preorder printing of the subtree of a node v of T, that is, it
performs the preorder traversal of the subtree rooted at v and prints the element
stored at a node when the node is visited. Recall that, for an ordered tree T, method
T.children(v) returns an iterable collection that accesses the children of v in
order.
Code Fragment 7.9: Method toStringPreorder(T, v)
that performs a preorder printing of the elements in the
subtree of node v of T.
389
There is an interesting application of the preorder traversal algorithm that produces
a string representation of an entire tree. Let us assume again that for each element e
stored in tree T, calling e.toString() returns a string associated with e. The
parenthetic string representation P(T) of tree T is recursively defined as follows. If
T consists of a single node v, then
P(T) = v.element().toString().
Otherwise,
P(T) = v.element().toString() + " (" + P(T1) + "," + ··· + ", "+ P(Tk) +")",
where v is the root of T and T1, T2,..., Tk are the subtrees rooted at the children of v,
which are given in order if T is an ordered tree.
Note that the above definition of P(T) is recursive. Also, we are using "+" here to
denote string concatenation. The parenthetic representation of the tree of Figure 7.2
is shown in Figure 7.7.
Figure 7.7: Parenthetic representation of the tree of
Figure 7.2. Indentation, line breaks and spaces have
been added for clarity.
Note that, technically speaking, there are some computations that occur between
and after the recursive calls at a node's children in the above algorithm. We still
consider this algorithm to be a preorder traversal, however, since the primary action
of printing a node's contents occurs prior to the recursive calls.
The Java method parentheticRepresentation, shown in Code Fragment
7.10, is a variation of method toStringPreorder (Code Fragment 7.9). It
implements the definition given above to output a parenthetic string representation
of a tree T. As with the method toStringPreorder, the method
parentheticRepresentation makes use of the toString method that
is defined for every Java object. In fact, we can view this method as a kind of
toString() method for tree objects.
390
Code Fragment 7.10: Algorithm
parentheticRepresentation. Note the use of the +
operator to concatenate two strings.
We explore a modification to Code Fragment 7.10 in Exercise R-7.9, to display a
tree in a fashion more closely matching that given in Figure 7.7.
7.2.3 Postorder Traversal
Another important tree traversal algorithm is the postorder traversal. This
algorithm can be viewed as the opposite of the preorder traversal, because it
recursively traverses the subtrees rooted at the children of the root first, and then
visits the root. It is similar to the preorder traversal, however, in that we use it to
solve a particular problem by specializing an action associated with the "visit" of a
node v. Still, as with the preorder traversal, if the tree is ordered, we make recursive
calls for the children of a node v according to their specified order. Pseudo-code for
the postorder traversal is given in Code Fragment 7.11.
Code Fragment 7.11: Algorithm postorder for
performing the postorder traversal of the subtree of a
tree T rooted at a node v.
391
The name of the postorder traversal comes from the fact that this traversal method
will visit a node v after it has visited all the other nodes in the subtree rooted at v.
(See Figure 7.8.)
Figure 7.8: Postorder traversal of the ordered tree
of Figure 7.6.
The analysis of the running time of a postorder traversal is analogous to that of a
preorder traversal. (See Section 7.2.2.) The total time spent in the nonrecursive
portions of the algorithm is proportional to the time spent visiting the children of
each node in the tree. Thus, a postorder traversal of a tree T with n nodes takes O(n)
time, assuming that visiting each node takes O(1) time. That is, the postorder
traversal runs in linear time.
As an example of postorder traversal, we show a Java method
toStringPostorder in Code Fragment 7.12, which performs a postorder
traversal of a tree T. This method prints the element stored at a node when it is
visited.
Code Fragment 7.12: Method
toStringPostorder(T, v) that performs a
postorder printing of the elements in the subtree of
node v of T. The method implicitly calls toString on
elements, when they are involved in a string
concatenation operation.
392
The postorder traversal method is useful for solving problems where we wish to
compute some property for each node v in a tree, but computing that property for v
requires that we have already computed that same property for v's children. Such an
application is illustrated in the following example.
Example 7.7: Consider a file system tree T, where external nodes represent
files and internal nodes represent directories (Example 7.1). Suppose we want to
compute the disk space used by a directory, which is recursively given by the sum
of:
• The size of the directory itself
• The sizes of the files in the directory
• The space used by the children directories.
(See Figure 7.9.) This computation can be done with apostorder traversal of tree T.
After the subtrees of an internal node v have been traversed, we compute the space
used by v by adding the sizes of the directory v itself and of the files contained in v
to the space used by each internal child of v, which was computed by the recursive
postorder traversals of the children of v.
A Recursive Java Method for Computing Disk Space
Motivated by Example 7.7, Java method diskSpace, shown in Code Fragment 7.13,
performs a postorder traversal of a file-system tree T, printing the name and disk
space used by the directory associated with each internal node of T. When called on
the root of tree T, diskSpace runs in time O(n), where n is the number of nodes of T,
provided the auxiliary methods name and size take O(1) time.
Figure 7.9: The tree of Figure 7.3 representing a file
system, showing the name and size of the associated
file/directory inside each node, and the disk space used
by the associated directory above each internal node.
393
Code Fragment 7.13: Method diskSpace prints
the name and disk space used by the directory
associated with each internal node of a file-system tree.
This method calls the auxiliary methods name and size,
which should be defined to return the name and size of
the file/directory associated with a node.
Other Kinds of Traversals
394
Although the preorder and postorder traversals are common ways of visiting the
nodes of a tree, we can also imagine other traversals. For example, we could
traverse a tree so that we visit all the nodes at depth d before we visit the nodes at
depth d + 1. Consecutively numbering the nodes of a tree T as we visit them in
this traversal is called the level numbering of the nodes of T (see Section 7.3.5).
7.3 Binary Trees
A binary tree is an ordered tree with the following properties:
1. Every node has at most two children.
2. Each child node is labeled as being either a left child or a right child.
3. A left child precedes a right child in the ordering of children of a node.
The subtree rooted at a left or right child of an internal node v is called a left subtree
or right subtree, respectively, of v. A binary tree is proper if each node has either
zero or two children. Some people also refer to such trees as being full binary trees.
Thus, in a proper binary tree, every internal node has exactly two children. A binary
tree that is not proper is improper.
Example 7.8: An important class of binary trees arises in contexts where we wish
to represent a number of different outcomes that can result from answering a series of
yes-or-no questions. Each internal node is associated with a question. Starting at the
root, we go to the left or right child of the current node, depending on whether the
answer to the question is "Yes" or "No." With each decision, we follow an edge from
a parent to a child, eventually tracing a path in the tree from the root to an external
node. Such binary trees are known as decision trees, because each external node v in
such a tree represents a decision of what to do if the questions associated with v's
ancestors are answered in a way that leads to v. A decision tree is a proper binary
tree. Figure 7.10 illustrates a decision tree that provides recommendations to a
prospective investor.
Figure 7.10: A decision tree providing investment
advice.
395
Example 7.9: An arithmetic expression can be represented by a binary tree
whose external nodes are associated with variables or constants, and whose internal
nodes are associated with one of the operators +, −, ×, and /. (See Figure 7.11.)
Each node in such a tree has a value associated with it.
• If a node is external, then its value is that of its variable or constant.
• If a node is internal, then its value is defined by applying its operation to the
values of its children.
An arithmetic expression tree is a proper binary tree, since each operator +, −, ×, and /
takes exactly two operands. Of course, if we were to allow for unary operators, like
negation (−), as in "−x," then we could have an improper binary tree.
Figure 7.11: A binary tree representing an arithmetic
expression. This tree represents the expression ((((3 + 1)
× 3)/((9 −5) +2)) − ((3 × (7 −4)) + 6)). The value
associated with the internal node labeled "/" is 2.
396
A Recursive Binary Tree Definition
Incidentally, we can also define a binary tree in a recursive way such that a binary
tree is either empty or consists of:
• A node r, called the root of T and storing an element
• A binary tree, called the left subtree of T
• A binary tree, called the right subtree of T.
We discuss some of the specialized topics for binary trees below.
7.3.1 The Binary Tree ADT
As an abstract data type, a binary tree is a specialization of a tree that supports three
additional accessor methods:
left(v):
Return the left child of v; an error condition occurs if v has no left child.
right(v):
Return the right child of v; an error condition occurs if v has no right child.
hasLeft(v):
Test whether v has a left child.
397
hasRight(v):
Test whether v has a right child.
Just as in Section 7.1.2 for the tree ADT, we do not define specialized update
methods for binary trees here. Instead, we will consider some possible update
methods when we describe specific implementations and applications of binary
trees.
7.3.2 A Binary Tree Interface in Java
We model a binary tree as an abstract data type that extends the tree ADT and adds
the three specialized methods for a binary tree. In Code Fragment 7.14, we show the
simple Java interface we can define using this approach. By the way, since binary
trees are ordered trees, the iterable collection returned by method children(v)
(inherited from the Tree interface) stores the left child of v before the right child of
v.
Code Fragment 7.14: Java interface Binary Tree
for the binary tree ADT. Interface Binary Tree
extends interface Tree (Code Fragment 7.1).
7.3.3 Properties of Binary Trees
Binary trees have several interesting properties dealing with relationships between
their heights and number of nodes. We denote the set of all nodes of a tree T at the
398
same depth d as the level dof T. In a binary tree, level 0 has at most one node (the
root), level 1 has at most two nodes (the children of the root), level 2 has at most
four nodes, and so on. (See Figure 7.12.) In general, level d has at most 2d nodes.
Figure 7.12: Maximum number of nodes in the levels
of a binary tree.
We can see that the maximum number of nodes on the levels of a binary tree grows
exponentially as we go down the tree. From this simple observation, we can derive
the following properties relating the height of a binary T with its number of nodes.
A detailed justification of these properties is left as an exercise (R-7.15).
Proposition 7.10: Let T be a nonempty binary tree, and let n, nE, nI and h
denote the number of nodes, number of external nodes, number of internal nodes,
and height of T, respectively. Then T has the following properties:
1. h+1 ≤ n ≤ 2 h+1 −1
2. 1≤nE≤2h
3. h≤nI≤2h−1
4. log(n+1)−1 ≤h≤n−1.
Also, if T is proper, then T has the following properties:
399
1. 2h+1 ≤ n≤2h+1−1
2. h+1≤nE≤2h
3. h≤nI≤2h−1
4. log(n + 1) − 1 ≤ h ≤ (n − 1)/2.
Relating Internal Nodes to External Nodes in a Proper
Binary Tree
In addition to the binary tree properties above, we also have the following
relationship between the number of internal nodes and external nodes in a proper
binary tree.
Proposition 7.11: In a nonempty proper binary tree T, with nE external
nodes and nI internal nodes, we have ne = nI + 1.
Justification: We justify this proposition by removing nodes from T and
dividing them up into two "piles", an internal-node pile and an external-node pile,
until T becomes empty. The piles are initially empty. At the end, the external-
node pile will have one more node than the internal-node pile. We consider two
cases:
Case 1: If T has only one node v, we remove v and place it on the external-node
pile. Thus, the external-node pile has one node and the internal-node pile is
empty.
Case 2: Otherwise (T has more than one node), we remove from T an (arbitrary)
external node w and its parent v, which is an internal node. We place w on the
external-node pile and v on the internal-node pile. If v has a parent u, then we
reconnect u with the former sibling z of w, as shown in Figure 7.13. This
operation, removes one internal node and one external node, and leaves the tree
being a proper binary tree.
Repeating this operation, we eventually are left with a final tree consisting of a
single node. Note that the same number of external and internal nodes have been
removed and placed on their respective piles by the sequence of operations
leading to this final tree. Now, we remove the node of the final tree and we place
it on the external-node pile. Thus, the the external-node pile has one more node
than the internal-node pile.
400
Figure 7.13: Operation that removes an external
node and its parent node, used in the justification of
Proposition 7.11.
Note that the above relationship does not hold, in general, for improper binary
trees and nonbinary trees, although there are other interesting relationships that
can hold, as we explore in an exercise (C-7.7).
7.3.4 A Linked Structure for Binary
Trees
As with a general tree, a natural way to realize a binary tree T is to use a linked
structure, where we represent each node v of T by a position object (see Figure
7.14a) with fields providing references to the element stored at v and to the position
objects associated with the children and parent of v. If v is the root of T, then the
parent field of v is null. If v has no left child, then the left field of v is null. If v has
no right child, then the right field of v is null. Also, we store the number of nodes of
T in a variable, called size. We show the linked structure representation of a binary
tree in Figure 7.14b.
Figure 7.14: A node (a) and a linked structure (b) for
representing a binary tree.
401
Java Implementation of a Binary Tree Node
402
We use a Java interface BTPosition (not shown) to represent a node of a
binary tree. This interfaces extends Position, thus inheriting method element,
and has additional methods for setting the element stored at the node
(setElement) and for setting and returning the left child (setLeft and
getLeft), right child (setRight and getRight), and parent (setParent
and getParent) of the node. Class BTNode (Code Fragment 7.15) implements
interface BTPosition by an object with fields element, left, right, and parent,
which, for a node v, reference the element at v, the left child of v, the right child of
v, and the parent of v, respectively.
Code Fragment 7.15: Auxiliary class BTNode for
implementing binary tree nodes.
403
Java Implementation of the Linked Binary Tree Structure
In Code Fragments 7.16–7.18, we show portions of class Linked Binary
Tree that implements the Binary Tree interface (Code Fragment 7.14)
using a linked data structure. This class stores the size of the tree and a reference
to the BTNode object associated with the root of the tree in internal variables. In
404
addition to the Binary Tree interface methods, LinkedBinaryTree has
various other methods, including accessor method sibling(v), which returns
the sibling of a node v, and the following update methods:
addRoot(e):
Create and return a new node r storing element e and make r the root of
the tree; an error occurs if the tree is not empty.
insertLeft(v,e):
Create and return a new node w storing element e, add w as the the left
child of v and return w; an error occurs if v already has a left child.
insertRight(v,e):
Create and return a new node z storing element e, add z as the the right
child of v and return z; an error occurs if v already has a right child.
remove(v):
Remove node v, replace it with its child, if any, and return the element
stored at v; an error occurs if v has two children.
attach(v,T1,T2):
Attach T1 and T2, respectively, as the left and right subtrees of the
external node v; an error condition occurs if v is not external.
Class LinkedBinaryTree has a constructor with no arguments that returns an
empty binary tree. Starting from this empty tree, we can build any binary tree by
creating the first node with method addRoot and repeatedly applying the
insertLeft and insertRight methods and/or the attach method. Likewise,
we can dismantle any binary tree T using the remove operation, ultimately
reducing such a tree T to an empty binary tree.
When a position v is passed as an argument to one of the methods of class
LinkedBinaryTree, its validity is checked by calling an auxiliary helper
method, checkPosition(v). A list of the nodes visited in a preorder traversal
of the tree is constructed by recursive method preorderPositions. Error
conditions are indicated by throwing exceptions Invalid Position
Exception, BoundaryViolation Exception,
EmptyTreeException, and NonEmptyTreeException.
Code Fragment 7.16: Portions of the Linked
Binary Tree class, which implements the Binary
Tree interface. (Continues in Code Fragment 7.17.)
405
406
Code Fragment 7.17: Portions of the Linked
Binary Tree class, which implements the Binary
Tree interface. (Continues in Code Fragment 7.18.)
407
408
Code Fragment 7.18: Portions of the Linked
Binary Tree class, which implements the Binary
Tree interface. (Continues in Code Fragment 7.19.)
409
Code Fragment 7.19: Portions of the
LinkedBinaryTree class, which implements the
410
Binary Tree interface. (Continues in Code Fragment
7.20.)
Code Fragment 7.20: Portions of the Linked
Binary Tree class, which implements the Binary
Tree interface. (Continued from Code Fragment 7.19.)
411
Performance of the Linked Binary Tree Implementation
Let us now analyze the running times of the methods of class Linked Binary
Tree, which uses a linked structure representation:
412
• Methods size() and isEmpty() use an instance variable storing the
number of nodes of T, and each take O(1) time.
• The accessor methods root, left, right, sibling and parent take O(1) time.
• Method replace(v,e) takes O(1) time.
• Methods iterator() and positions() are implemented by performing a pre-
order traversal of the tree (using the auxiliary method preorderPositions). The
nodes visited by the traversal are stored in a position list implemented by class
NodePositionList (Section 6.2.4) and the output iterator is generated with
method iterator() of class NodePositionList. Methods
iterator() and positions() take O(n) time and methods hasNext() and
next() of the returned iterators run in O(1) time.
• Method children uses a similar approach to construct the returned iterable
collection, but it runs in O(1) time, since there are at most two children for any
node in a binary tree.
• The update methods insertLeft, insertRight, attach, and
remove all run in O(1) time, as they involve constant-time manipulation of a
constant number of nodes.
Considering the space required by this data structure for a tree with n nodes, note
that there is an object of class BTNode (Code Fragment 7.15) for every node of
tree T. Thus, the overall space requirement is O(n). Table 7.2 summarizes the
performance of the linked structure implementation of a binary tree.
Table 7.2: Running times for the methods of an n-
node binary tree implemented with a linked structure.
Methods hasNext() and next() of the iterators returned
by iterator(), positions().iterator(), and
children(v).iterator() run in O(1) time. The
space usage is O(n).
Operation
Time
size, isEmpty
O(1)
iterator, positions
413
O(n)
replace
O(1)
root, parent, children, left, right, sibling
O(1)
hasLeft, hasRight, isInternal, isExternal,
isRoot
O(1)
insertLeft, insertRight, attach, remove
O(1)
7.3.5 An Array-List Representation of a
Binary Tree
An alternative representation of a binary tree T is based on a way of numbering the
nodes of T. For every node v of T, let p(v) be the integer defined as follows.
• If v is the root of T, then p(v) = 1.
• If v is the left child of node u, then p(v) = 2p(u).
• If v is the right child of node u, then p(v) = 2p(u) + 1.
The numbering function p is known as a level numbering of the nodes in a binary
tree T, for it numbers the nodes on each level of T in increasing order from left to
right, although it may skip some numbers. (See Figure 7.15.)
Figure 7.15: Binary tree level numbering: (a) general
scheme; (b) an example.
414
The level numbering function p suggests a representation of a binary tree T by
means of an array list S such that node v of T is the element of S at index p(v). As
mentioned in the previous chapter, we realize the array list S by means of an
extendable array. (See Section 6.1.4.) Such an implementation is simple and
efficient, for we can use it to easily perform the methods root, parent,
left, right, hasLeft, hasRight, isInternal, isExternal,
and isRoot by using simple arithmetic operations on the numbers p(v) associated
with each node v involved in the operation. We leave the details of this
implementation as an exercise (R-7.26).
We show an example array-list representation of a binary tree in Figure 7.16.
Figure 7.16: Representation of a binary tree T by
means of an array list S.
415
Let n be the number of nodes of T, and let pM be the maximum value of p(v) over
all the nodes of T. The array list S has size N = pM + 1 since the element of S at
index 0 is not associated with any node of T. Also, S will have, in general, a number
of empty elements that do not refer to existing nodes of T. In fact, in the worst case,
N = 2n, the justification of which is left as an exercise (R-7.23). In Section 8.3, we
will see a class of binary trees, called "heaps" for which N = n + 1. Thus, in spite of
the worst-case space usage, there are applications for which the array-list
representation of a binary tree is space efficient. Still, for general binary trees, the
exponential worst-case space requirement of this representation is prohibitive.
Table 7.3 summarizes running times of the methods of a binary tree implemented
with an array list. We do not include any tree update methods here.
Table 7.3: Running times for a binary tree T
implemented with an array list S. We denote the
number of nodes of T with n, and N denotes the size of
S. The space usage is O(N), which is O(2n) in the worst
case.
Operation
416
Time
size, isEmpty
O(1)
iterator, positions
O(n)
replace
O(1)
root, parent, children, left, right
O(1)
hasLeft, hasRight, isInternal, isExternal, isRoot
O(1)
7.3.6 Traversals of Binary Trees
As with general trees, binary tree computations often involve traversals.
Building an Expression Tree
Consider the problem of constructing an expression tree from a fully
parenthesized arithmetic expression of size n. (Recall Example 7.9 and Code
Fragment 7.24.) In Code Fragment 7.21, we give algorithm buildExpression
for building such an expression tree, assuming all arithmetic operations are binary
and variables are not parenthesized. Thus, every parenthesized subexpression
contains an operator in the middle. The algorithm uses a stack S while scanning
the input expression E looking for variables, operators, and right parentheses.
• When we see a variable or operator x, we create a single-node binary tree
T, whose root stores x and we push T on the stack.
• When we see a right parenthesis, ")", we pop the top three trees from the
stack S, which represent a subexpression (E1 o E2). We then attach the trees for
E1 and E2 to the one for o, and push the resulting tree back on S.
We repeat this until the expression E has been processed, at which time the top
element on the stack is the expression tree for E. The total running time is O(n).
417
Code Fragment 7.21: Algorithm
buildExpression.
Preorder Traversal of a Binary Tree
Since any binary tree can also be viewed as a general tree, the preorder traversal
for general trees (Code Fragment 7.8) can be applied to any binary tree. We can
simplify the algorithm in the case of a binary tree traversal, however, as we show
in Code Fragment 7.22.
Code Fragment 7.22: Algorithm binaryPreorder
for performing the preorder traversal of the subtree of
a binary tree T rooted at a node v.
418
As is the case for general trees, there are many applications of the preorder
traversal for binary trees.
Postorder Traversal of a Binary Tree
Analogously, the postorder traversal for general trees (Code Fragment 7.11) can
be specialized for binary trees, as shown in Code Fragment 7.23.
Code Fragment 7.
Các file đính kèm theo tài liệu này:
- Tree Definitions and Properties.pdf