Lets take a look at one of my favorite implementations of the tree data structure and identify some terminology along the way:
Figure 1: A tree data structure modeling one of Great Houses of Westeros
Trees are organized into nodes that hold data along various branches. Nodes are related through parent
, child
, and sibling
relationships.
Every tree has a root
node at the highest level (ie: Rickard Stark). Every node is also the root
of a subtree
. We can see that Ned Stark is the root
node of the subtree
containing him, Robb, Sansa, Arya, Bran, and Rickon.
Depth refers to the level at which a node sits relative to the root
. When referring to the entire tree, a root
node, like Rickard Stark has a depth of 0, while Jon Snow and Arya Stark have a depth of 2. However, depth is also relative to a subtree
. If we were only considering Ned Stark's subtree, Arya would have a depth of 1.
The height of any node is measured through the deepest child
node it has. So the height of Benjen Stark is 0, while the height of Rickard Stark is 2.
To represent a tree, it would be useful to abstract away the "spider web" feel of the actual tree into something more manageable. Imagine that each node has 2 reference pointers firstChild
and nextSibling
. Then, we can represent the tree in a chained hierarchy structure focussed on node depth. It would look something like this:
Figure 2: Efficiently representing House Stark with our TreeNode
class
In practice, we would actually need more than just those 2 pointers to work with the tree efficiently. Here is an outline of our TreeNode
class:
data <String>
- Stark family member's name (ie "Ned Stark")firstChild <TreeNode>
- A pointer to a child ofthis
, if anynextSibling <TreeNode>
- A pointer to a sibling ofthis
, if anypreviousNode <TreeNode>
- A pointer to previous Node along the chain (see Figure 2)myRoot <TreeNode>
- A pointer to the root node associated withthis
public class TreeNode {
protected TreeNode firstChild, nextSibling, previousNode;
protected String data;
protected TreeNode myRoot;
public TreeNode() {
// Constructor Logic...
}
public E getData() {
return data; // Accessor
}
}
<TreeNode> this |
this.firstChild |
this.nextSibling |
this.previousNode |
---|---|---|---|
rickardStark |
nedStark |
null |
null |
nedStark |
robbStark |
brandonStark |
rickardStark |
brandonStark |
null |
benjenStark |
nedStark |
benjenStark |
null |
lyannaStark |
brandonStark |
lyannaStark |
jonSnow |
null |
benjenStark |
robbStark |
null |
sansaStark |
nedStark |
sansaStark |
null |
aryaStark |
robbStark |
aryaStark |
null |
branStark |
sansaStark |
branStark |
null |
rickonStark |
aryaStark |
rickonStark |
null |
null |
branStark |
jonSnow |
null |
null |
lyannaStark |
Let's take a mini tour of a barebones Tree
class linking together TreeNodes
. We'll focus on the find()
method for now and assume an object of this class called starkFamilyTree
is already setup.
public class Tree {
private int mSize;
TreeNode mRoot;
public Tree() {
// Constrcutor Logic...
}
// Client side find() function
public TreeNode find(String data) {
return find(mRoot, data, 0); // Runs private recursive version of find()
}
// Recursive logic - overloaded find()
private TreeNode find(TreeNode root, String data, int level) {
TreeNode stackResponse;
if (mSize == 0 || root == null) // Base case 1 (nothing to search)
return null;
if (root.data.equals(data)) // Base case 2 (found!)
return root;
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive Call
if (stackResponse != null)
return stackResponse; // Base case 3 (forward result it along the recusrive chain!)
}
return find(root.firstChild, data, ++level); // Recursive Call
}
}
To traverse a tree, and check out different nodes on the way we usually use recursion. I really like Mattias Petter Johansson's definition of recursion (check out his awesome YouTube channel Fun Fun Function)
Recursion is when a function calls itself until it doesn't. That is seriously all recursion is. It's really simple.
With that in mind, we see that private TreeNode find(TreeNode root, String data, int level)
calls itself mutltiple times. Each call, generates a Stack Frame (a snapshot of the call's local variables), and recursion aims to pile and unpile these Stack Frames. Luckily base cases help us break out of this cycle and eventually end up with one answer.
Let's assume we have a Tree
object named starkFamilyTree
already setup, so we can try to find() aryaStark
. Since a girl has many faces lol, we have to be very careful with our recursive process - it can get very complicated. Follow along if you like, this is the whole procedure...
This frame runs the following global code statement:
TreeNode result = starkFamilyTree.find("Arya Stark"); // Call into Function
Stack Frame | Global | |
---|---|---|
global variable | value | |
result | starkFamilyTree.find("Arya Stark") | awaiting return from Function |
This frame runs the following code statement from
public TreeNode find(String data)
return find(mRoot, data, 0); // Recursive Call to Node 0 (rickardStark)
Stack Frame | Function | |
---|---|---|
local variable | value | |
mRoot | rickardStark | |
data | "Arya Stark" | |
return value | find(rickardStark, "Arya Stark", 0) | awaiting return from Node 0 & will eventually return to Global |
Because local variable level == 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to Node 1 (nedStark)
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to Node 2 (brandonStark)
if (stackResponse != null)
return stackResponse;
}
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to Node 3 (benjenStark)
if (stackResponse != null)
return stackResponse;
}
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to Node 4 (lyannaStark)
if (stackResponse != null)
return stackResponse;
}
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to lyannaStark.nextSibling
if (stackResponse != null)
return stackResponse;
}
Because local variable root == null
, this frame hits the following base case from
private TreeNode find(TreeNode root, String data, int level)
if (mSize == 0 || root == null)
return null; // Recursive return to Node 4 (lyannaStark)
Stack Frame | lyannaStark.nextSibling | |
---|---|---|
local variable | value | |
root | null | |
data | "Arya Stark" | |
level | 1 | |
return value | null | returning to Node 4 (lyannaStark) |
Node 4 now has received stackResponse == null
from lyannaStark.nextSibling, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to Node 5 (jonSnow)
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to jonSnow.nextSibling
if (stackResponse != null)
return stackResponse;
}
Because local variable root == null
, this frame hits the following base case from
private TreeNode find(TreeNode root, String data, int level)
if (mSize == 0 || root == null)
return null; // Recursive return to Node 5 (jonSnow)
Stack Frame | jonSnow.nextSibling | |
---|---|---|
local variable | value | |
root | null | |
data | "Arya Stark" | |
level | 2 | |
return value | null | returning to Node 5 (jonSnow) |
Frame 8 now has received stackResponse == null
from jonSnow.nextSibling, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to jonSnow.firstChild
Because local variable root == null
, this frame hits the following base case from
private TreeNode find(TreeNode root, String data, int level)
if (mSize == 0 || root == null)
return null; // Recursive return to Node 5 (jonSnow)
Stack Frame | jonSnow.firstChild | |
---|---|---|
local variable | value | |
root | null | |
data | "Arya Stark" | |
level | 3 | |
return value | null | returning to Node 5 (jonSnow) |
Node 5 now has received null
from jonSnow.firstChild, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Node 4 (lyannaStark)
Node 4 now has received null
from 8, so it finishes the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Node 3 (benjenStark)
Node 3 now has received stackResponse == null
from Node 4, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to benjenStark.firstChild
Because local variable root == null
, this frame hits the following base case from
private TreeNode find(TreeNode root, String data, int level)
if (mSize == 0 || root == null)
return null; // Recursive return to Node 3 (benjenStark)
Stack Frame | benjenStark.firstChild | |
---|---|---|
local variable | value | |
root | null | |
data | "Arya Stark" | |
level | 2 | |
return value | null | returning value to Node 3 (benjenStark) |
Node 3 now has received null
from benjenStark.firstChild, so it finishes the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Node 2 (brandonStark)
Frame 4 now has received stackResponse == null
from 5, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to brandonStark.firstChild
Because local variable root == null
, this frame hits the following base case from
private TreeNode find(TreeNode root, String data, int level)
if (mSize == 0 || root == null)
return null; // Recursive return to Node 2 (brandonStark)
Stack Frame | brandonStark.firstChild | |
---|---|---|
local variable | value | |
root | null | |
data | "Arya Stark" | |
level | 2 | |
return value | null | returning value to Node 2 (brandonStark) |
Node 2 now has received null
from brandonStark.firstChild, so it finishes the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Node 1 (nedStark)
Node 1 now has received stackResponse == null
from Node 2, so it runs the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive call to Node 6 (robbStark)
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to Node 7 (sansaStark)
if (stackResponse != null)
return stackResponse;
}
Because local variable level > 0
, This frame hits the following code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level); // Recursive call to Node 8 (aryaStark)
if (stackResponse != null)
return stackResponse;
}
Because local variable root.data == "Arya Stark"
, This frame hits the base case and finds Arya!
private TreeNode find(TreeNode root, String data, int level)
if (root.data.equals(data)) // base case
return root; // recursive return to Node 7 (sansaStark)
Stack Frame | Node 7 (aryaStark) | |
---|---|---|
local variable | value | |
root | ||
data | "Arya Stark" | |
level | 2 | |
return value | aryaStark | returning to Node 7 (sansaStark) |
Node 7 (sansaStark) receives stackReponse == aryaStark
from Node 8 (aryaStark), so it finishes the stack by returning stackResponse to Node 6 in this code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level);
if (stackResponse != null)
return stackResponse; // Recursive return to Node 6 (robbStark)
}
Stack Frame | Node 7 (sansaStark) | |
---|---|---|
local variable | value | |
root | ||
data | "Arya Stark" | |
level | 2 | |
stackResonse | aryaStark | received from Node 8 (aryaStark) |
return value | aryaStark | returning to Node 6 (robbStark) |
Node 6 (robbStark) receives stackReponse == aryaStark
from Node 7 (sansaStark), so it finishes the stack by returning stackResponse to Node 1 in this code block from
private TreeNode find(TreeNode root, String data, int level)
if (level > 0) {
stackResponse = find(root.nextSibling, data, level);
if (stackResponse != null)
return stackResponse; // Recursive return to Node 1 (nedStark)
}
Stack Frame | Node 6 (robbStark) | |
---|---|---|
local variable | value | |
root | ||
data | "Arya Stark" | |
level | 2 | |
stackResonse | aryaStark | received from Node 7 (sansaStark) |
return value | aryaStark | returning to Node 1 (nedStark) |
Node 1(nedStark) now has received aryaStark
from Node 6 (robbStark), so it finishes the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Node 0 (rickardStark)
Node 0 (rickardStark) receives aryaStark
from Node 1 (nedStark) Because local variable level == 0
, This frame finishes the following code block from
private TreeNode find(TreeNode root, String data, int level)
return find(root.firstChild, data, ++level); // Recursive return to Function
Stack Frame | Node 0 (rickardStark) | |
---|---|---|
local variable | value | |
root | ||
data | "Arya Stark" | |
level | 0 | |
return value | aryaStark | received from Node 1 (nedStark) & returning to Function |
This frame receives aryaStark
from Node 0 (rickardStark) and finishes the following code block from
public TreeNode find(String data)
return find(mRoot, data, 0); // Recursive return to Global
Stack Frame | Function | |
---|---|---|
local variable | value | |
mRoot | rickardStark | |
data | "Arya Stark" | |
return value | aryaStark | received from Node 0 & returning to Global |
This frame receives aryaStark
from Function and finishes the following global code statement:
TreeNode result = starkFamilyTree.find("Arya Stark"); // end of recusrsion
Stack Frame | Global | |
---|---|---|
global variable | value | |
result | aryaStark | received from Function |