This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License
In this lesson, you will create a binary search tree in C. This is a tree data structure with the property that the left subtree of any node contains only values less than the parent and the right subtree contains only values greater than the parent. You should already be familiar with implementing trees using Java.
You should also be familar with the “object-oriented” idioms described in the “Employee” lesson. They will be used but not re-explained in this this lesson. And that lesson expected you to be familiar with the companion C for Java Programmers document.
Each step of the lesson asks you to answer a question or write some code. Do the steps yourself before checking your work by revealing the answer.
Let’s assume that we will store integers in our binary search tree. You could also redo this lesson storing strings or other “comparable” elements.
Node
contains references to other Node
s.
struct
to define a “structured type” (a type composed of other types):
struct Node {
int value;
struct Node* leftChild;
struct Node* rightChild;
};
As in Java, this is recursively defined. Kernighan & Ritchie call this a “self-referential structure” (The C Programming Language, 2nd Ed., p. 139–143).
Node
class:
public Node(int value) {
this.value = value;
}
You create a new instance by calling the constructor:
Node n = new Node(123);
struct Node
type, initializes it, and returns the pointer to it:
struct Node* new_Node(int value) {
struct Node* this = (struct Node*)malloc(sizeof(struct Node));
if (this == NULL) {
return NULL; // Out of memory...
}
this->value = value;
this->leftChild = NULL;
this->rightChild = NULL;
return this;
}
Then just like Java, you create a new instance by calling the “constructor”: struct Node* n = new_Node(123); These idioms were described in the “Employee” lesson.
toString
method in Java).
main
function to what we have already. Something like:
int main(int argc, char* argv[]) {
struct Node* node1 = new_Node(1);
Node_print(node1);
struct Node* node2 = new_Node(22);
Node_print(node2);
struct Node* node3 = new_Node(333);
Node_print(node3);
}
When you run this program, the nodes will all print one after each other on the same line with no spacing or newlines. You could add calls to printf
in between if you wanted.
Download full program: tree1.c
NULL
for this, just like null
in Java (None
in Python, nil
in Lisp and Swift, but I digress). You should be comfortable with null
references in Java. They’re the same in C except that the keyword is in uppercase.
NULL
pointer, write a function to add a new integer to a tree maintaining the binary search tree property. Hint: You will need to handle four cases.
Note that this function always returns a tree (a pointer to a Node
) containing the given element (and all previously-added elements, of course).
Node_print
function to print the entire tree rooted at the given Node
. Don’t forget the definition of what an empty tree is. Hint: This is going to be a special kind of function that you should be used to already in Computer Science.
main
function, for example:
int main(int argc, char* argv[]) {
struct Node* root = NULL;
root = Node_add(root, 20);
root = Node_add(root, 10);
root = Node_add(root, 5);
root = Node_add(root, 15);
root = Node_add(root, 30);
root = Node_add(root, 40);
root = Node_add(root, 20);
Node_print(root);
}
The final call to Node_add
has no effect since the value 20 is already in the tree.
Note that we have to always assign the result of Node_add
to our variable root
, even though in fact the only time the root changes is when the first element is added to the tree. But remember: it's a recursive function. Every time a new element is added, the root of some subtree is created (and hence returned from some recursive call, namely the last one). However in this example, you could, if you wanted to, only assign to root
the first time. Try it! But then if you could also remove elments from the tree... Try that also!
Download full program: tree2.c
In Java, you would define a class Tree
with an instance variable representing the Node
at the root of the tree. How would you do that in C?
NULL
pointer to a tree node. A Tree
is initially empty, so its root is NULL
.
Tree_add
and Tree_print
that connect trees (pointers to Tree
s) to tree nodes (pointers to Node
s).
Tree_add
encapsulates the management of the root of the tree, which we had to do ourselves when trees were just pointers to tree nodes.
Tree
rather than a Node
.
Download full program: tree3.c
0
for false and non-zero (in this case 1
) for true. Here's a main
function to test it:
int main(int argc, char* argv[]) {
struct Node* root = NULL;
root = Tree_add(root, 20);
root = Tree_add(root, 10);
root = Tree_add(root, 5);
root = Tree_add(root, 15);
root = Tree_add(root, 30);
root = Tree_add(root, 40);
root = Tree_add(root, 20);
Tree_print(root);
printf("%d\n", Tree_lookup(root, 15)); // 1
printf("%d\n", Tree_lookup(root, 99)); // 0
}
You could also use the result of Tree_lookup
in a conditional statement or loop. Try it!
Download full program: tree4.c
int
as its arguments. (See the C for Java Programmers document if you need a reminder about function pointers.) Your function should call the given “callback” function with each value in the tree, one after the other in sorted order.
You will also need a function Tree_walk
that forwards to Node_walk
on its root.
And then here's an example of a callback function that just prints the given value: void myCallback(int value) { printf("%d\n", value); } If you need a refresher on passing function pointers as arguments to other functions, check out Section 8.8 of the C for Java Programmers document.
With this callback function, what will a call to Tree_walk
do? Think about it... It will print the elements in the tree in sorted order. Way cool.
typedef
to get rid of all the explicit pointers in the program.
Node
” is the type “pointer to Node
structure” and “Tree
” is the type “pointer to Tree
structure”. If that sounds confusing, remember that the name following the keyword struct
in a structure definition is not a type but rather what C calls a “tag.”
Now replace all occurences of struct Node*
with simply Node
and similarly for Tree
. The result looks pretty much like Java, except for your “new_
” functions (which are pure boilerplate), the lack of namespaces for “method” (function) names, and the need to pass the “instance” as the first argument to every “method.” Still, pretty cool!
Download full program: tree6.c
How would you evaluate the cost of adding an element to a binary search tree like this one?
Node_add
calls itself recursively (since the other cases take constant time).
Node_add
?
NULL
) or the value is already stored at the root of the tree.
Node_add
moves from a node to one of its children.
As implemented, nothing guarantees that our tree is balanced in any way. The worst case is that all the nodes (except the last) have exactly one child, meaning that they form a chain of nodes from the root to the only leaf. In that case, the cost to add a new element is linear in the size of the collection.
There are other orderings that create chains that mix left and right children, although still with only one child per node (except the last). In fact, there are 2n such orderings over n elements.
It’s not really enough to say simply that the order must be random. Assuming that “random” means that all sequences are equally likely, then the probability of the worst-case occurring is 2n / n! since there are n! total orderings of n elements. This drops below 1% for n=8 and gets small very quickly after that. But under what circumstances can you be confident that all sequences are equally likely (see next question)?
That’s why we do Computer Science.