>> Okay now we want to
talk about tree iterators. According to your course
organization, that's Chapter 14. So let's look at the problem. When you have a binary search
tree it can represent the data, for example in the numeric order
10, 20, 30, 40, 50, 60, 70. So here's a perfectly
balanced tree of that data and here's one that's unbalanced
toward the right of the root. It has the same data. And here's one that's unbalanced
to the left, has the same data. And then many other configurations, but on our search tree it would
represent exactly that same data. And when you iterate through the
set represented by that tree, you of course want to
encounter that data in order. And so the external syntax of the
iteration is the usual forward structure of the iterator, but what happens
underneath the hood in this tree varies from structure to structure
inside you know that mutation. So it's a challenging and
fun task to get this done. And there's really kind of 3
approaches that we will talk about. And there's a 4th approach you could
use which is considered a tree, it's just a graph and use one of the
graph search algorithms to do the, [inaudible] iterator, but we really
aren't going to get into that because we aren't going
to study graphs yet. But the 3 approaches are
what I call navigation, and threading, and ADT control. So navigation is the
one I'll discuss first. That is the gable you pretend
you can climb up, or bound left or bound right in the tree. You can always use algorithms
and by climbing up and down you can always
find the next mode. Now the disadvantage of that
approach is simply that to climb up you need a pointer, you need
a third pointer in the tree node, which is a pointer to the parent. So if you've got a pointer
to the parent, you can move up to the
parent without any trouble. If you have, you would of course
always have a pointer to the left child and a pointer to the right child. So that means we got 3
directions if you're a parent, or 3 directions in which you can move. And that's to kind of think
about kind of a tree monkey. You can use that tree monkey moving around in the tree to
implement iterators. And we'll talk about
that, that method first. So we'd like the tree iterators to be
bidirectional iterators when possible. There's 1 special case where that's not
going to be possible and we're going to have to use forward iterators. And of course, as you know you want
to restrict access by client programs so they can get to data,
but not pointers. So the client program perspective has
no, is a totally opaque wall behind which all this floating around
in the tree is taking place, but in front of that wall everything
is just like, there's a set and you're going through the
set one element at a time. So, of course we want to
make everything object-based. Iterators of course allow, they
go just one-step at a time, so they're much better for many
purposes than the traditional and recursive implementation
of traversal. Which in our light versions of these
things we use 3 cursive traversal because there was no other choice,
if you don't have iterators. They as a classic C approach
to true traversal. But iterators are much better
at traversing for many reasons. The 2 biggest ones are
an iterator can stop, where recursive, traversal cannot stop. So you got to make up your mind in
advance what you want the traversal to do at every node and in a
function, or a function object and press a go button and it's
going to go through the entire tree and do whatever that function is. With an iterator you can go through the
tree and search for different things. You can stop whenever you want to. You can continue, or
you can stop for good. And the other major advantage of
iterators is you can have more than 1. So you can have like 5 iterators
pointing into the same set. And each of them stopping and
indicating different places in the set. So clearly iterators are
a desirable thing to make. So what we've normally done so far
is talk about node two pointers left and right child and a value so
you can down left and down right. A 3-D alternative as a parent pointer. Everything else keeps as before so you
can go up, down left and down right. And that actually, so there's
a picture here on the side. You know you're going to
add a third pointer there. All the other stuff that we've
been using for nodes you can keep. You know the colors, alive and
dead and that sort of thing. So you can keep all of that. Just add that third pointer
to the parent. And notice that we also are going to
invent a new toy called a navigator. So a tree navigator is
philosophically similar to an iterator, except it's going to be able
to climb around in the tree, where an iterator goes through the
set, it's the abstraction of the tree. So the tree navigator,
and these exist for trees with 3 pointers, nodes with 3 pointers. You can initialize them to a tree,
plus, plus n is going to move down to the left, n plus, plus is going
to move down to the right one click and decrement [inaudible] will move
up one click and will go to parent. And just emphasize this
is not an iterator. It has the same, we're for using the
operations that are familiar to you from iterators, but it's not
an iterator, it's a navigator. But you can find the navigator concept for 3-way binary trees [inaudible] I'm
going to close this [inaudible] sorry. [ Inaudible ] All right so when you have a
section parent pointer you have to maintain the parent pointers
involved in all the various functions. So for example in an insert
you put, you would do a test if the current value is less than the
value of the node you would go left. If it's right, if it's bigger
than you'd go right and so on. And instead of just going right
when you do that you have to set, so here you'd set the left child to whatever your current value
[inaudible] insert function. What you have to do is add a line
of code that finds the parent of the left child to be your cell. And that left child then
knows who its parent is after he or she gets [inaudible]. So it's not very difficult, but you do
have to maintain those parent pointers. It's a little more difficult
in the rotations, to do the rotation without parents. This is a rotation with the parent. So all of this code right here
is adding in order to make sure that the parent pointer gets
correctly set during the left rotation and there's a similar
thing for right rotation. Just to take a look at
the binary tree navigator, it's sort of like an
abstraction of the node class. So we have the value, you know you
have you actually have value type and navigator type, it's proper type so you got all your you know [inaudible]
constructor copying structure. You can initialize one and
have them kind of [inaudible]. That's basically an automatic
type converter that text. I'm sorry that's the [inaudible]
there's automatic type converter as well with a node to a navigator. And you got clearly the questions
you can ask is does it have a parent, does it have a left child,
does it have a right child. Is it a left child, or
is it a right child? And these are answerable questions. But it makes the code very messy to answer those questions
directly in the code. Just like when we were talking about
the other day, it's messy to always have to go to the actual code for
whether or not a node is dead, or whether or not a node is read. It's much cleaner to have a
function, a brilliant vibrant function that tells you that sort of thing and
makes much, much more readable code. And typically doesn't add any
complexity to the executable data. These are all things
like end line functions. So anyway that's the [inaudible]
and well this is how you implement. So let's just talk about HasParent. So a current node, so binary, I'm sorry
a navigator has a thing called current node, it's a pointer to node. That navigator will have a parent
if the current node is not zero and the current node's
parent is not zero. Similarly, the left child
and the right child. So, then what you can do is define a
class inorder iterator that's based on navigators and then you simply by
using that [inaudible] find interface for navigators to, to
find the iterators. So here is kind of where the rubber
meets the road, the notion of increment where binary tree inorder iterators. So how would that go? Okay you're going to
[inaudible] courses. Its own little navigator called nav. So the first question
is the navigator valid. If it's not then you can't do anything. But if it is valid then if the navigator
has a right child then what you're going to do is go into that right child
and then slide left all the way down to the bottom with the, let's
draw a quick picture of that. So you've got a tree. Let's say we are, let's say we are here. If as a right child and to find the
next node you go to the right child and then you go as far to the left as
you can and that would be the next node. That's where you are right here at
node K and here is where you've been. Okay [inaudible] you'd go to the right
child, then down as far as you can. Then you create back up until you get
to here and then when you get here if you have a right child
you, go down in it. Same thing. Now what happens if you don't have a
right child that means that you got to go up until you find a right
child, until you find a place where you weren't a left child and
then start the search over again. So, so this is where you are, this
is where you go to the right child and then slide down the left. The other case is if there is
no right child then you've got to remember whether or not you
were a right child and keep going up to the parent as long
as you were a right child. And as soon as you get to a point where you are not the right
child then you can stop. But if you go up as a left child, then your parent is the next
place in the relationship. You know [inaudible]. And then the increment, I'm sorry the
operator plus plus is defined by doing, you do increment of the
node that's dead. So in other words do wild means do
1 at least and you keep doing more until you get to a right node. So that's the way you skip over the
dead nodes with operator plus, plus. So increments it's a structural,
it uses the tree structure to get to the next node in an
inorder traversal. The plus, plus operator increments
to the next live note in the tree. So you can have an increment as operator
plus, plus as structural in nature and you can think of operator plus, plus
as increment from the public perspective of going to the next item in the set. Now you can do, the one really neat
thing is you can do postorder iterators, you can do preorder iterators,
and you can do leveliterators. The truth is while they're
fun to do the pre and the postorder iterators
don't have a lot of applications, it's just fun to do. And so what we, so what you can do, you
can do inorder, preorder, postorder, and levelorder iterators
using tree navigators. You can do all of those. So anyway. So that's, but just think of
that as being the, in our case. That is the one that would
become the official iterator for the tree and it words. Now the good side of
that is you can do it. The not so good side is that you have to
put an extra pointer on every tree node. So you know from big sets, that's a
big extra load of memory and we'll talk about a way to use consistent
memory in a little while to accomplish the same thing. But let's now go to the
next big class of iterators, which is the ADT based iterators. And these are probably the most
universally palatable type of iterator. And when we get to your assignment, you're going to be doing an ADT based
iterator for both binary search trees and for read, black, left-leaning trees. And you'll find that they
work essentially the same way. And these work for this, what I
call the 2 dimensional alternative. So these are the nodes we've been using that have only 2 pointers
in the downward direction. So because you can't go up in the tree
you have to come up with some other way to accomplish keeping track
of parental information. Because clearly you can't get to
the next level by always going down in the tree sometimes
you have to go up in the tree. So the idea here is to use some sort
of controlled structure just based on an abstract data type a
stack or a queue in these cases. So we'll start out talking
about stack control. Remember stack controllers
essentially do a depth of search of whatever you're doing. And if you think about, when
we talk about tree iterator, tree searches before, the inorder,
preorder, and postorder traversals on a tree are really depth of
searches of the tree from the root with preorder visiting the
first time you get to the node, the inorder visiting between
the 2 times you get to the node, and postorder visiting just
as you're leaving the node. But the algorithm to wander around
the tree is essentially the same, it's just a question, where do
you stop, it's the same as just between the pre, in, and post orders. And they're all depth
for searches of the tree and can be controlled with stack. We also will at time level our iterator
that will be controlled by a queue. So I'm going to illustrate
this with a preorder iterator. What you're doing in your assignment
is just an inorder iterator, but they work in essentially
the same manner. So this will be, we will
have a stack here. I call it conStack, control stack. And it's going to be a stack of nodes
of your tree and it's a [inaudible]. It's got one assignment operator. It's got a for some reason
I left out [inaudible]. It's got 2 increment operators,
the pre and post phase, and 2 increment operator
pre and post phase. The prefix increment operator got
left out of the slide somehow. And so for the preorder iterator, you
initialize by just pushing the root onto the tree, but remember
in preorder you visit, the first time you encounter the node, the reference will be it's
returning the top of the stack, or the value at the top of the stack, value of the node that's
at the top of the stack. And an increment will
go something like this. Remember this is tree order. So what you're going to do is push the
left child if it has one and that's it. And if there is no left
child, then you need to go up until you find a right child. And when you find a right
child go there. So that is copying the stack,
remembering the top of the old stack and when you get to that,
when you get to that top, check to see if it has a right child
and if it does, push that right child onto the stack, quit and
return, otherwise pop again. Of course we pop all the way to the
root and then it becomes to the top, it becomes null and the
iteration is concluded. So how would you level an iterator. Well in order to level iterator
you have to control queue. This is going to be very similar to
the [inaudible] it's a proper type. And it. Need to see what I've done
in the slides [inaudible]. It would probably would be
[inaudible] these slides, I seem to have left out
some of the operators. But here's how you would initialize the
levelorder is you just push the root onto the queue and the d reference
is returning the front of the queue. And the increment is to,
if the queue is not empty. If the front has a left child, you
push that left child up the queue. If the front has a right child,
you push that right child up the queue, then you pop the queue. If you think of that from quality
of depth of search, the left and right child if they exist
are 2 as yet unvisited locations, so they go onto the back of the
queue and both are adjacent to, both are unvisited locations that
are accessible from the front. So you pop front. So this is essentially
the same algorithm that you'd use to search [inaudible]. So the third way to find
iterators is really a clever thing. If you notice that a binary tree
has a lot of null pointers in it. All the leaves down at the bottom
of the tree have null pointers. In fact you can actually
prove that a binary tree, no matter what its balance structure is, a binary tree has n plus
1 null pointers. So maybe you can make use of those. So the idea behind threading is this. We know how to go down in tree because
we got left and right child pointers. So the idea behind threading is suppose
you're at a given node and you want to find the next node, well if it's a
downward search, we know how to do that, we just search down like we did before. If it's an upward search we can't do that because we don't
have parent pointers, so what you do instead is let's
say it's a forward operator plus, plus then you store in that,
so if you could go down then that right child would
be not null right. But if you can't go down
if that right child is no so you'd have to go up and you're stuck. What you do is you store in that
right child the place you go to in the tree instead of
having to search for it. So that brings you the problem,
well how can you tell the difference between a right child
that's a true child and a right child that's
actually a pointer to the next place in the iteration? And that's where you have the
child flags come into play. So we have has left and right
child isleftthread, isrightthread, setleftthread, setrightthread,
and setrightchild, setleftchild. So let's just look at
hasleftchild for example. Remember we had this node in
iteration with zero dead red and we got things you've seen before, we
have to explain it's a left thread flag and an 0x04 on the 4 bit and
one [inaudible] thread flag. And we make convenience
definition of the threads plural, being both the left thread
and the right thread flags. So how do you tell if
it has a left child, well you see if the left child is
not null, and it's not left threaded. You see if it's left threaded you
have, the left thread flag is set, in other words, it's not zero. It's right threaded if the
right thread flag is not zero. Set, these are funny looking at first. So we got a set left
thread and a set left child. Takes a node, in both cases that node, that node pointer gets
set to the left child. So the right child equals n
in both cases, here and here. But the difference is, is that
a left child or a left thread. And that's, so if you do this, flags
and equals the compliment of left thread that unsets the left thread
flag, so that makes it a child. On the other hand, if you or equals the
left threaded flag, that sets that flag and makes it a left thread so remember
we still have just that one extra byte in our node and we're using
those flags now 2 of those flags to do left thread and right thread. So maintaining of threads
has to be done. So for example, which one is this? Oh we're in the middle of
let's say our insert of code. So I guess this is what we're doing in
maintaining the parent [inaudible] now. Remember we maintain the parent primary
in some of our search algorithms and that's not a, you know that's just
maintaining a value of this parent of this current node and it's
where the current node used to be before you went down the tree. So if that parent's left child
is not zero then you say it's, then you know with default
value being thread and threads and return otherwise continue. But the threading code, you have
to make sure the thread is right. So if the parent is left
threaded, then what you've got to do is make a new node,
but make the left child of that node be the same
as parent's left child. Remember that's a thread going to the
place where you would decrement to from that node and then you would set
the right child to be a parent. That's another thread. Remember we put in a node in
here at the bottom of the tree, so neither one of its 2
children are going to be, in a normal base they would both
be null, which they in fact have, [inaudible] they would both be threads. So when you put a left child in where
is its right thread pointing to? Well to its parent. When you put that left child in where
should its left child pointer point to? Well the same place the parent's
left child used to point to. So that's the decrement. So from looking at the parent, the new left child you have the
decrementation of that left child. And then decrementing from that
left child takes you to the place where you would have gone
form the parent before you put that child in there. And so that's what that's all about. And it's threaded notations as well. And there's threaded iterator support. So you can have net function
in your iterator. That's a node and what it does
is slide down the left child. It's truly a left child or in other
words, [inaudible] not left threaded So just a sec here let's get settled in. So a thread iterator class
would look something like this. It's got an increment and a decrement
and notice that's private and you've got to also have and [inaudible]
to both private. The [inaudible] get called
by the public begin functions in the binary searching class. Increment and decrement get
called by operator plus, plus in the iterator class
like we talked about before. So I think I've just about said
most of what I want to say here. Except I want to talked about
runtime complexity in these things. This is a serious question to ask
are these iterators sufficient. Because what we've seen is we
have loops defined in things like our increment function. And that gives you, that raises a
red flag because you want iterators. You want going to the next thing in
the set to be fast, it can't be slow. And on the other hand with the binary
tree, the next item in the set might be, you might have to go in the
worst case from the bottom of the tree all the way to the top. So you can imagine you're at
the bottom leaf of the tree and the very next thing could be the
root of a tree in that iteration. And so sometimes you have to traverse a
path with that, with the loop structure if it's a path the total length
of the vertical path in the tree. And so the way to think of that is this. Suppose you did a complete
traversal using this iterator. Well that iterator will go down and up
each edge in the tree exactly one time. So it will go down an edge, search
around awhile for the bottom end of that edge and when it gets
back it will go up that edge. It will never go back
down that edge again. So even through there are some
strings of fairly lengthy loops, if you look at the total number
of times you traverse an edge, it's going to be 2 times
the number of edges. And then you've got also, an operation
that, the initializing operation which you can think of as jumping
under the tree at the root, and the terminating operation which is
jumping off of a tree from the root. So if you think of those initializing
and terminating operations. So tree monkey operations. That's 2. And then it's
2 times the number of edges in the tree added to that. So that's 2 plus 2E, 2 plus 2E,
which you might call edge moves, 2 plus 2E edge moves in a
complete traversal tree. Well if you remember from discrete math
a tree with N nodes has N minus 1 edges. So you've got a B equals N minus 1. So. So we have, we get 1 to get on the tree,
plus 1 edge move to get off the tree, plus 2E edge moves moving up and down
the tree, that adds up to 2 plus 2E. If N is the number of nodes then
N is 1 plus the number of edges. So [inaudible] there we see that
the total for a complete traversal of tree is going to be 2N edge
moves, where N is the size of a tree. So the average number of edge
moves for any given invocation of operator plus plus
is 2N over N which is 2. And so while it is the case that sometimes there's a fairly lengthy
search up and down the tree to get to the next node, the average, the amortized time is
just the constant of 2. And so when you do an entire
traversal, you get to call the average, so you get to look at the average and
the average is favorable, it's constant. So it's not quite as good as being
able to search every implication of operator plus plus it's a cost
of time, but it's almost as good because it says that if you invoke
operator plus, plus N times, then the cost will be approximately M. And so the average cost
would be approximately 1. And this is true for navigator-based
iterators, thread based iterators, and [inaudible] based iterators. And these are expanded upon in
the narrative a little bit more than what I just did here in class. So I want you to study these lecture
notes carefully on tree iterators and our next talk will be
about, a little bit more about implementing some
of these iterator classes.