Data Structures
Data Structures
Binary Trees
by Nick Parlante
This article introduces the basic concepts of binary trees, and then works through a series of practice problems with
solution code in C/C++ and Java. Binary trees have an elegant recursive pointer structure, so they are a good way to
learn recursive pointer algorithms.
Contents
Section 1. Binary Tree Structure -- a quick introduction to binary trees and the code that operates on them
Section 2. Binary Tree Problems -- practice problems in increasing order of difficulty
Section 3. C Solutions -- solution code to the problems for C and C++ programmers
Section 4. Java versions -- how binary trees work in Java, with solution code
This is article #110 in the Stanford CS Education Library. This and other free CS materials are available at the
library (http://cslibrary.stanford.edu/). That people seeking education should have the opportunity to find it.
This article may be used, reproduced, excerpted, or sold so long as this paragraph is clearly reproduced. Copyright
2000-2001, Nick Parlante, [email protected].
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 2
A "binary search tree" (BST) or "ordered binary tree" is a type of binary tree where the nodes are arranged in order:
for each node, all elements in its left subtree are less-or-equal to the node (<=), and all the elements in its right
subtree are greater than the node (>). The tree shown above is a binary search tree -- the "root" node is a 5, and its
left subtree nodes (1, 3, 4) are <= 5, and its right subtree nodes (6, 9) are > 5. Recursively, each of the subtrees must
also obey the binary search tree constraint: in the (1, 3, 4) subtree, the 3 is the root, the 1 <= 3 and 4 > 3. Watch out
for the exact wording in the problems -- a "binary search tree" is different from a "binary tree".
The nodes at the bottom edge of the tree have empty subtrees and are called "leaf" nodes (1, 4, 6) while the others
are "internal" nodes (3, 5, 9).
Basically, binary search trees are fast at insert and lookup. The next section presents the code for these two
algorithms. On average, a binary search tree algorithm can locate a node in an N node tree in order lg(N) time (log
base 2). Therefore, binary search trees are good for "dictionary" problems where the code inserts and looks up
information indexed by some key. The lg(N) behavior is the average case -- it's possible for a particular tree to be
much slower depending on its shape.
Strategy
Some of the problems in this article use plain binary trees, and some use binary search trees. In any case, the
problems concentrate on the combination of pointers and recursion. (See the articles linked above for pointer articles
that do not emphasize recursion.)
The node/pointer structure that makes up the tree and the code that manipulates it
The algorithm, typically recursive, that iterates over the tree
When thinking about a binary tree problem, it's often a good idea to draw a few little trees to think about the
various cases.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 3
As an introduction, we'll look at the code for the two most basic binary search tree operations -- lookup() and
insert(). The code here works for C or C++. Java programers can read the discussion here, and then look at the Java
versions in Section 4.
In C or C++, the binary tree is built with a node type like this...
struct node {
int data;
struct node* left;
struct node* right;
}
Lookup()
Given a binary search tree and a "target" value, search the tree to see if it contains the target. The basic pattern of
the lookup() code occurs in many recursive tree algorithms: deal with the base case where the tree is empty, deal
with the current node, and then use recursion to deal with the subtrees. If the tree is a binary search tree, there is
often some sort of less-than test on the node to decide if the recursion should go left or right.
/*
Given a binary tree, return true if a node
with the target data is found in the tree. Recurs
down the tree, chooses the left or right
branch by comparing the target to each node.
*/
static int lookup(struct node* node, int target) {
// 1. Base case == empty tree
// in that case, the target is not found so return false
if (node == NULL) {
return(false);
}
else {
// 2. see if found here
if (target == node->data) return(true);
else {
// 3. otherwise recur down the correct subtree
if (target < node->data) return(lookup(node->left, target));
else return(lookup(node->right, target));
}
}
}
The lookup() algorithm could be written as a while-loop that iterates down the tree. Our version uses recursion to
help prepare you for the problems below that require recursion.
There is a common problem with pointer intensive code: what if a function needs to change one of the pointer
parameters passed to it? For example, the insert() function below may want to change the root pointer. In C and
C++, one solution uses pointers-to-pointers (aka "reference parameters"). That's a fine technique, but here we will
use the simpler technique that a function that wishes to change a pointer passed to it will return the new value of
the pointer to the caller. The caller is responsible for using the new value. Suppose we have a change() function
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 4
that may change the the root, then a call to change() will look like this...
We take the value returned by change(), and use it as the new value for root. This construct is a little awkward, but
it avoids using reference parameters which confuse some C and C++ programmers, and Java does not have reference
parameters at all. This allows us to focus on the recursion instead of the pointer mechanics. (For lots of problems
that use reference parameters, see CSLibrary #105, Linked List Problems, http://cslibrary.stanford.edu/105/).
Insert()
Insert() -- given a binary search tree and a number, insert a new node with the given number into the tree in the
correct place. The insert() code is similar to lookup(), but with the complication that it modifies the tree structure.
As described above, insert() returns the new tree pointer to use to its caller. Calling insert() with the number 5 on
this tree...
2
/ \
1 10
2
/ \
1 10
/
5
The solution shown here introduces a newNode() helper function that builds a single node. The base-case/recursion
structure is similar to the structure in lookup() -- each call checks for the NULL case, looks at the node at hand, and
then recurs down the left or right subtree if needed.
/*
Helper function that allocates a new node
with the given data and NULL left and right
pointers.
*/
struct node* NewNode(int data) {
struct node* node = new(struct node); // "new" is like "malloc"
node->data = data;
node->left = NULL;
node->right = NULL;
return(node);
}
/*
Give a binary search tree and a number, inserts a new node
with the given number in the correct place in the tree.
Returns the new root pointer which the caller should
then use (the standard trick to avoid using reference
parameters).
*/
struct node* insert(struct node* node, int data) {
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 5
The shape of a binary tree depends very much on the order that the nodes are inserted. In particular, if the nodes
are inserted in increasing order (1, 2, 3, 4), the tree nodes just grow to the right leading to a linked list shape where
all the left pointers are NULL. A similar thing happens if the nodes are inserted in decreasing order (4, 3, 2, 1). The
linked list shape defeats the lg(N) performance. We will not address that issue here, instead focusing on pointers
and recursion.
Reading about a data structure is a fine introduction, but at some point the only way to learn is to actually try to
solve some problems starting with a blank sheet of paper. To get the most out of these problems, you should at least
attempt to solve them before looking at the solution. Even if your solution is not quite right, you will be building up
the right skills. With any pointer-based code, it's a good idea to make memory drawings of a a few simple cases to
see how the algorithm should work.
1. build123()
This is a very basic problem with a little pointer manipulation. (You can skip this problem if you are already
comfortable with pointers.) Write code that builds the following little 1-2-3 binary search tree...
2
/ \
1 3
(In Java, write a build123() method that operates on the receiver to change it to be the 1-2-3 tree with the given
coding constraints. See Section 4.)
2. size()
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 6
This problem demonstrates simple binary tree traversal. Given a binary tree, count the number of nodes in the tree.
3. maxDepth()
Given a binary tree, compute its "maxDepth" -- the number of nodes along the longest path from the root node down
to the farthest leaf node. The maxDepth of the empty tree is 0, the maxDepth of the tree on the first page is 3.
4. minValue()
Given a non-empty binary search tree (an ordered binary tree), return the minimum data value found in that tree.
Note that it is not necessary to search the entire tree. A maxValue() function is structurally very similar to this
function. This can be solved with recursion or with a simple while loop.
5. printTree()
Given a binary search tree (aka an "ordered binary tree"), iterate over the nodes to print them out in increasing
order. So the tree...
4
/ \
2 5
/ \
1 3
Produces the output "1 2 3 4 5". This is known as an "inorder" traversal of the tree.
Hint: For each node, the strategy is: recur left, print the node data, recur right.
6. printPostorder()
Given a binary tree, print out the nodes of the tree according to a bottom-up "postorder" traversal -- both subtrees of
a node are printed out completely before the node itself is printed, and each left subtree is printed before the right
subtree. So the tree...
4
/ \
2 5
/ \
1 3
Produces the output "1 3 2 5 4". The description is complex, but the code is simple. This is the sort of bottom-up
traversal that would be used, for example, to evaluate an expression tree where a node is an operation like '+' and
its subtrees are, recursively, the two subexpressions for the '+'.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 7
7. hasPathSum()
We'll define a "root-to-leaf path" to be a sequence of nodes in a tree starting with the root node and proceeding
downward to a leaf (a node with no children). We'll say that an empty tree contains no root-to-leaf paths. So for
example, the following tree has exactly four root-to-leaf paths:
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
Root-to-leaf paths:
path 1: 5 4 11 7
path 2: 5 4 11 2
path 3: 5 8 13
path 4: 5 8 4 1
For this problem, we will be concerned with the sum of the values of such a path -- for example, the sum of the
values on the 5-4-11-7 path is 5 + 4 + 11 + 7 = 27.
Given a binary tree and a sum, return true if the tree has a root-to-leaf path such that adding up all the values
along the path equals the given sum. Return false if no such path can be found. (Thanks to Owen Astrachan for
suggesting this problem.)
8. printPaths()
Given a binary tree, print out all of its root-to-leaf paths as defined above. This problem is a little harder than it
looks, since the "path so far" needs to be communicated between the recursive calls. Hint: In C, C++, and Java,
probably the best solution is to create a recursive helper function printPathsRecur(node, int path[], int pathLen),
where the path array communicates the sequence of nodes that led up to the current call. Alternately, the problem
may be solved bottom-up, with each node returning its list of paths. This strategy works quite nicely in Lisp, since
it can exploit the built in list and mapping primitives. (Thanks to Matthias Felleisen for suggesting this problem.)
Given a binary tree, print out all of its root-to-leaf paths, one per line.
9. mirror()
Change a tree so that the roles of the left and right pointers are swapped at every node.
So the tree...
4
/ \
2 5
/ \
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 8
1 3
is changed to...
4
/ \
5 2
/ \
3 1
The solution is short, but very recursive. As it happens, this can be accomplished without changing the root node
pointer, so the return-the-new-root construct is not necessary. Alternately, if you do not want to change the tree
nodes, you may construct and return a new mirror tree based on the original tree.
10. doubleTree()
For each node in a binary search tree, create a new duplicate node, and insert the duplicate as the left child of the
original node. The resulting tree should still be a binary search tree.
So the tree...
2
/ \
1 3
is changed to...
2
/ \
2 3
/ /
1 3
/
1
As with the previous problem, this can be accomplished without changing the root node pointer.
11. sameTree()
Given two binary trees, return true if they are structurally identical -- they are made of nodes with the same values
arranged in the same way. (Thanks to Julie Zelenski for suggesting this problem.)
12. countTrees()
This is not a binary tree programming problem in the ordinary sense -- it's more of a math/combinatorics recursion
problem that happens to use binary trees. (Thanks to Jerry Cain for suggesting this problem.)
Suppose you are building an N node binary search tree with the values 1..N. How many structurally different
binary search trees are there that store those values? Write a recursive function that, given the number of distinct
values, computes the number of structurally unique binary search trees that store those values. For example,
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 9
countTrees(4) should return 14, since there are 14 structurally unique binary search trees that store 1, 2, 3, and 4. The
base case is easy, and the recursion is short but dense. Your code should not construct any actual trees; it's just a
counting problem.
This background is used by the next two problems: Given a plain binary tree, examine the tree to determine if it
meets the requirement to be a binary search tree. To be a binary search tree, for every node, all of the nodes in its
left tree must be <= the node, and all of the nodes in its right subtree must be > the node. Consider the following four
examples...
a. 5 -> TRUE
/ \
2 7
c. 5 -> TRUE
/ \
2 7
/
1
d. 5 -> FALSE, the 6 is ok with the 2, but the 6 is not ok with the 5
/ \
2 7
/ \
1 6
For the first two cases, the right answer can be seen just by comparing each node to the two nodes immediately
below it. However, the fourth case shows how checking the BST quality may depend on nodes which are several
layers apart -- the 5 and the 6 in that case.
13 isBST() -- version 1
Suppose you have helper functions minValue() and maxValue() that return the min or max int value from a
non-empty tree (see problem 3 above). Write an isBST() function that returns true if a tree is a binary search tree
and false otherwise. Use the helper functions, and don't forget to check every node in the tree. It's ok if your
solution is not very efficient. (Thanks to Owen Astrachan for the idea of having this problem, and comparing it to
problem 14)
Version 1 above runs slowly since it traverses over some parts of the tree many times. A better solution looks at each
node only once. The trick is to write a utility helper function isBSTRecur(struct node* node, int min, int max) that
traverses down the tree keeping track of the narrowing min and max allowed values as it goes, looking at each node
only once. The initial values for min and max should be INT_MIN and INT_MAX -- they narrow from there.
/*
Returns true if the given tree is a binary search tree
(efficient version).
*/
int isBST2(struct node* node) {
return(isBSTRecur(node, INT_MIN, INT_MAX));
}
/*
Returns true if the given tree is a BST and its
values are >= min and <= max.
*/
int isBSTRecur(struct node* node, int min, int max) {
15. Tree-List
The Tree-List problem is one of the greatest recursive pointer problems ever devised, and it happens to use binary
trees as well. CLibarary #109 http://cslibrary.stanford.edu/109/ works through the Tree-List problem in detail
and includes solution code in C and Java. The problem requires an understanding of binary trees, linked lists,
recursion, and pointers. It's a great problem, but it's complex.
root->left = lChild;
root->right= rChild;
return(root);
}
// call newNode() three times, and use only one local variable
struct node* build123b() {
struct node* root = newNode(2);
root->left = newNode(1);
root->right = newNode(3);
return(root);
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 11
/*
Build 123 by calling insert() three times.
Note that the '2' must be inserted first.
*/
struct node* build123c() {
struct node* root = NULL;
root = insert(root, 2);
root = insert(root, 1);
root = insert(root, 3);
return(root);
}
/*
Compute the number of nodes in a tree.
*/
int size(struct node* node) {
if (node==NULL) {
return(0);
} else {
return(size(node->left) + 1 + size(node->right));
}
}
/*
Compute the "maxDepth" of a tree -- the number of nodes along
the longest path from the root node down to the farthest leaf node.
*/
int maxDepth(struct node* node) {
if (node==NULL) {
return(0);
}
else {
// compute the depth of each subtree
int lDepth = maxDepth(node->left);
int rDepth = maxDepth(node->right);
/*
Given a non-empty binary search tree,
return the minimum data value found in that tree.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 12
return(current->data);
}
/*
Given a binary search tree, print out
its data elements in increasing
sorted order.
*/
void printTree(struct node* node) {
if (node == NULL) return;
printTree(node->left);
printf("%d ", node->data);
printTree(node->right);
}
/*
Given a binary tree, print its
nodes according to the "bottom-up"
postorder traversal.
*/
void printPostorder(struct node* node) {
if (node == NULL) return;
/*
Given a tree and a sum, return true if there is a path from the root
down to a leaf, such that adding up all the values along the path
equals the given sum.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 13
Strategy: subtract the node value from the sum when recurring down,
and check to see if the sum is 0 when you run out of tree.
*/
int hasPathSum(struct node* node, int sum) {
// return true if we run out of tree and sum==0
if (node == NULL) {
return(sum == 0);
}
else {
// otherwise check both subtrees
int subSum = sum - node->data;
return(hasPathSum(node->left, subSum) ||
hasPathSum(node->right, subSum));
}
}
/*
Given a binary tree, print out all of its root-to-leaf
paths, one per line. Uses a recursive helper to do the work.
*/
void printPaths(struct node* node) {
int path[1000];
/*
Recursive helper function -- given a node, and an array containing
the path from the root node up to but not including this node,
print out all the root-leaf paths.
*/
void printPathsRecur(struct node* node, int path[], int pathLen) {
if (node==NULL) return;
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 14
/*
Change a tree so that the roles of the
left and right pointers are swapped at every node.
So the tree...
4
/ \
2 5
/ \
1 3
is changed to...
4
/ \
5 2
/ \
3 1
*/
void mirror(struct node* node) {
if (node==NULL) {
return;
}
else {
struct node* temp;
// do the subtrees
mirror(node->left);
mirror(node->right);
/*
For each node in a binary search tree,
create a new duplicate node, and insert
the duplicate as the left child of the original node.
The resulting tree should still be a binary search tree.
So the tree...
2
/ \
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 15
1 3
Is changed to...
2
/ \
2 3
/ /
1 3
/
1
*/
void doubleTree(struct node* node) {
struct node* oldLeft;
if (node==NULL) return;
// do the subtrees
doubleTree(node->left);
doubleTree(node->right);
/*
Given two trees, return true if they are
structurally identical.
*/
int sameTree(struct node* a, struct node* b) {
// 1. both empty -> true
if (a==NULL && b==NULL) return(true);
/*
For the key values 1...numKeys, how many structurally unique
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 16
if (numKeys <=1) {
return(1);
}
else {
// there will be one value at the root, with whatever remains
// on the left and right each forming their own subtrees.
// Iterate through all the values that could be the root...
int sum = 0;
int left, right, root;
return(sum);
}
}
/*
Returns true if a binary tree is a binary search tree.
*/
int isBST(struct node* node) {
if (node==NULL) return(true);
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 17
/*
Returns true if the given tree is a binary search tree
(efficient version).
*/
int isBST2(struct node* node) {
return(isBSTUtil(node, INT_MIN, INT_MAX));
}
/*
Returns true if the given tree is a BST and its
values are >= min and <= max.
*/
int isBSTUtil(struct node* node, int min, int max) {
if (node==NULL) return(true);
The solution code in C and Java to the great Tree-List recursion problem is in CSLibrary #109
http://cslibrary.stanford.edu/109/
In Java, we will have a BinaryTree object that contains a single root pointer. The root pointer points to an internal
Node class that behaves just like the node struct in the C/C++ version. The Node class is private -- it is used only
for internal storage inside the BinaryTree and is not exposed to clients. With this OOP structure, almost every
operation has two methods: a one-line method on the BinaryTree that starts the computation, and a recursive
method that works on the Node objects. For the lookup() operation, there is a BinaryTree.lookup() method that
the client uses to start a lookup operation. Internal to the BinaryTree class, there is a private recursive
lookup(Node) method that implements the recursion down the Node structure. This second, private recursive
method is basically the same as the recursive C/C++ functions above -- it takes a Node argument and uses recursion
to iterate over the pointer structure.
To get started, here are the basic definitions for the Java BinaryTree class, and the lookup() and insert() methods
as examples...
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 18
// BinaryTree.java
public class BinaryTree {
// Root node pointer. Will be null for an empty tree.
private Node root;
/*
--Node--
The binary tree is built using this nested node class.
Each node stores one data element, and has left and right
sub-tree pointer which may be null.
The node is a "dumb" nested class -- we just use it for
storage; it does not have any methods.
*/
private static class Node {
Node left;
Node right;
int data;
Node(int newData) {
left = null;
right = null;
data = newData;
}
}
/**
Creates an empty binary tree -- a null root pointer.
*/
public void BinaryTree() {
root = null;
}
/**
Returns true if the given target is in the binary tree.
Uses a recursive helper.
*/
public boolean lookup(int data) {
return(lookup(root, data));
}
/**
Recursive lookup -- given a node, recur
down searching for the given data.
*/
private boolean lookup(Node node, int data) {
if (node==null) {
return(false);
}
if (data==node.data) {
return(true);
}
else if (data<node.data) {
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 19
return(lookup(node.left, data));
}
else {
return(lookup(node.right, data));
}
}
/**
Inserts the given data into the binary tree.
Uses a recursive helper.
*/
public void insert(int data) {
root = insert(root, data);
}
/**
Recursive insert -- given a node pointer, recur down and
insert the given data into the tree. Returns the new
node pointer (the standard way to communicate
a changed pointer back to the caller).
*/
private Node insert(Node node, int data) {
if (node==null) {
node = new Node(data);
}
else {
if (data <= node.data) {
node.left = insert(node.left, data);
}
else {
node.right = insert(node.right, data);
}
}
From the client point of view, the BinaryTree class demonstrates good OOP style -- it encapsulates the binary tree
state, and the client sends messages like lookup() and insert() to operate on that state. Internally, the Node class
and the recursive methods do not demonstrate OOP style. The recursive methods like insert(Node) and lookup
(Node, int) basically look like recursive functions in any language. In particular, they do not operate against a
"receiver" in any special way. Instead, the recursive methods operate on the arguments that are passed in which is
the classical way to write recursion. My sense is that the OOP style and the recursive style do not be combined
nicely for binary trees, so I have left them separate. Merging the two styles would be especially awkward for the
"empty" tree (null) case, since you can't send a message to the null pointer. It's possible to get around that by having
a special object to represent the null tree, but that seems like a distraction to me. I prefer to keep the recursive
methods simple, and use different examples to teach OOP.
Java Solutions
Here are the Java solutions to the 14 binary tree problems. Most of the solutions use two methods:a one-line OOP
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 20
method that starts the computation, and a recursive method that does the real operation. Make an attempt to
solve each problem before looking at the solution -- it's the best way to learn.
/**
Build 123 using three pointer variables.
*/
public void build123a() {
root = new Node(2);
Node lChild = new Node(1);
Node rChild = new Node(3);
root.left = lChild;
root.right= rChild;
}
/**
Build 123 using only one pointer variable.
*/
public void build123b() {
root = new Node(2);
root.left = new Node(1);
root.right = new Node(3);
}
/**
Build 123 by calling insert() three times.
Note that the '2' must be inserted first.
*/
public void build123c() {
root = null;
root = insert(root, 2);
root = insert(root, 1);
root = insert(root, 3);
}
/**
Returns the number of nodes in the tree.
Uses a recursive helper that recurs
down the tree and counts the nodes.
*/
public int size() {
return(size(root));
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 21
/**
Returns the max root-to-leaf depth of the tree.
Uses a recursive helper that recurs down to find
the max depth.
*/
public int maxDepth() {
return(maxDepth(root));
}
/**
Returns the min value in a non-empty binary search tree.
Uses a helper method that iterates to the left to find
the min value.
*/
public int minValue() {
return( minValue(root) );
}
/**
Finds the min value in a non-empty binary search tree.
*/
private int minValue(Node node) {
Node current = node;
while (current.left != null) {
current = current.left;
}
return(current.data);
}
/**
Prints the node values in the "inorder" order.
Uses a recursive helper to do the traversal.
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 22
*/
public void printTree() {
printTree(root);
System.out.println();
}
/**
Prints the node values in the "postorder" order.
Uses a recursive helper to do the traversal.
*/
public void printPostorder() {
printPostorder(root);
System.out.println();
}
/**
Given a tree and a sum, returns true if there is a path from the root
down to a leaf, such that adding up all the values along the path
equals the given sum.
Strategy: subtract the node value from the sum when recurring down,
and check to see if the sum is 0 when you run out of tree.
*/
public boolean hasPathSum(int sum) {
return( hasPathSum(root, sum) );
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 23
return(sum == 0);
}
else {
// otherwise check both subtrees
int subSum = sum - node.data;
return(hasPathSum(node.left, subSum) || hasPathSum(node.right, subSum));
}
}
/**
Given a binary tree, prints out all of its root-to-leaf
paths, one per line. Uses a recursive helper to do the work.
*/
public void printPaths() {
int[] path = new int[1000];
printPaths(root, path, 0);
}
/**
Recursive printPaths helper -- given a node, and an array containing
the path from the root node up to but not including this node,
prints out all the root-leaf paths.
*/
private void printPaths(Node node, int[] path, int pathLen) {
if (node==null) return;
/**
Utility that prints ints from an array on one line.
*/
private void printArray(int[] ints, int len) {
int i;
for (i=0; i<len; i++) {
System.out.print(ints[i] + " ");
}
System.out.println();
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 24
/**
Changes the tree into its mirror image.
So the tree...
4
/ \
2 5
/ \
1 3
is changed to...
4
/ \
5 2
/ \
3 1
/**
Changes the tree by inserting a duplicate node
on each nodes's .left.
So the tree...
2
/ \
1 3
Is changed to...
2
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 25
/ \
2 3
/ /
1 3
/
1
// do the subtrees
doubleTree(node.left);
doubleTree(node.right);
/*
Compares the receiver to another tree to
see if they are structurally identical.
*/
public boolean sameTree(BinaryTree other) {
return( sameTree(root, other.root) );
}
/**
Recursive helper -- recurs down two trees in parallel,
checking to see if they are identical.
*/
boolean sameTree(Node a, Node b) {
// 1. both empty -> true
if (a==null && b==null) return(true);
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 26
/**
For the key values 1...numKeys, how many structurally unique
binary search trees are possible that store those keys?
return(sum);
}
}
/**
Tests if a tree meets the conditions to be a
binary search tree (BST).
*/
public boolean isBST() {
return(isBST(root));
}
/**
Recursive helper -- checks if a tree is a BST
using minValue() and maxValue() (not efficient).
*/
private boolean isBST(Node node) {
if (node==null) return(true);
http://cslibrary.stanford.edu/110/
BinaryTrees.html
Binary Trees Page: 27
/**
Tests if a tree meets the conditions to be a
binary search tree (BST). Uses the efficient
recursive helper.
*/
public boolean isBST2() {
return( isBST2(root, Integer.MIN_VALUE, Integer.MAX_VALUE) );
}
/**
Efficient BST helper -- Given a node, and min and max values,
recurs down the tree to verify that it is a BST, and that all
its nodes are within the min..max range. Works in O(n) time --
visits each node only once.
*/
private boolean isBST2(Node node, int min, int max) {
if (node==null) {
return(true);
}
else {
// left should be in range min...node.data
boolean leftOk = isBST2(node.left, min, node.data);
return(rightOk);
}
}
http://cslibrary.stanford.edu/110/
BinaryTrees.html
By Nick Parlante
Essential C Copyright 1996-2003, Nick Parlante
This Stanford CS Education document tries to summarize all the basic features of the C
language. The coverage is pretty quick, so it is most appropriate as review or for someone
with some programming background in another language. Topics include variables, int
types, floating point types, promotion, truncation, operators, control structures (if, while,
for), functions, value parameters, reference parameters, structs, pointers, arrays, the pre-
processor, and the standard C library functions.
The most recent version is always maintained at its Stanford CS Education Library URL
http://cslibrary.stanford.edu/101/. Please send your comments to
[email protected].
I hope you can share and enjoy this document in the spirit of goodwill in which it is given
away -- Nick Parlante, 4/2003, Stanford California.
Stanford CS Education Library This is document #101, Essential C, in the Stanford
CS Education Library. This and other educational materials are available for free at
http://cslibrary.stanford.edu/. This article is free to be used, reproduced, excerpted,
retransmitted, or sold so long as this notice is clearly reproduced at its beginning.
Table of Contents
Introduction .........................................................................................pg. 2
Where C came from, what is it like, what other resources might you look at.
Section 1 Basic Types and Operators ..........................................pg. 3
Integer types, floating point types, assignment operator, comparison operators,
arithmetic operators, truncation, promotion.
Section 2 Control Structures ........................................................pg. 11
If statement, conditional operator, switch, while, for, do-while, break, continue.
Section 3 Complex Data Types .....................................................pg. 15
Structs, arrays, pointers, ampersand operator (&), NULL, C strings, typedef.
Section 4 Functions ........................................................................pg. 24
Functions, void, value and reference parameters, const.
Section 5 Odds and Ends ..............................................................pg. 29
Main(), the .h/.c file convention, pre-processor, assert.
Section 6 Advanced Arrays and Pointers ....................................pg. 33
How arrays and pointers interact. The [ ] and + operators with pointers, base
address/offset arithmetic, heap memory management, heap arrays.
Section 7 Operators and Standard Library Reference ..............pg. 41
A summary reference of the most common operators and library functions.
The C Language
C is a professional programmer's language. It was designed to get in one's way as little as
possible. Kernighan and Ritchie wrote the original language definition in their book, The
C Programming Language (below), as part of their research at AT&T. Unix and C++
emerged from the same labs. For several years I used AT&T as my long distance carrier
in appreciation of all that CS research, but hearing "thank you for using AT&T" for the
millionth time has used up that good will.
2
Some languages are forgiving. The programmer needs only a basic sense of how things
work. Errors in the code are flagged by the compile-time or run-time system, and the
programmer can muddle through and eventually fix things up to work correctly. The C
language is not like that.
The C programming model is that the programmer knows exactly what they want to do
and how to use the language constructs to achieve that goal. The language lets the expert
programmer express what they want in the minimum time by staying out of their way.
C is "simple" in that the number of components in the language is small-- If two language
features accomplish more-or-less the same thing, C will include only one. C's syntax is
terse and the language does not restrict what is "allowed" -- the programmer can pretty
much do whatever they want.
C's type system and error checks exist only at compile-time. The compiled code runs in a
stripped down run-time model with no safety checks for bad type casts, bad array indices,
or bad pointers. There is no garbage collector to manage memory. Instead the
programmer mangages heap memory manually. All this makes C fast but fragile.
Other Resources
• The C Programming Language, 2nd ed., by Kernighan and Ritchie. The thin book
which for years was the bible for all C programmers. Written by the original
designers of the language. The explanations are pretty short, so this book is better as a
reference than for beginners.
• http://cslibrary.stanford.edu/102/ Pointers and Memory -- Much more detail
about local memory, pointers, reference parameters, and heap memory than in this
article, and memory is really the hardest part of C and C++.
• http://cslibrary.stanford.edu//103/ Linked List Basics -- Once you understand the
basics of pointers and C, these problems are a good way to get more practice.
3
Section 1
Basic Types and Operators
C provides a standard, minimal set of basic data types. Sometimes these are called
"primitive" types. More complex data structures can be built up from these basic types.
Integer Types
The "integral" types in C form a family of integer types. They all behave like integers and
can be mixed together and used in similar ways. The differences are due to the different
number of bits ("widths") used to implement each type -- the wider types can store a
greater ranges of values.
char ASCII character -- at least 8 bits. Pronounced "car". As a practical matter
char is basically always a byte which is 8 bits which is enough to store a single
ASCII character. 8 bits provides a signed range of -128..127 or an unsigned range is
0..255. char is also required to be the "smallest addressable unit" for the machine --
each byte in memory has its own address.
int Default integer -- at least 16 bits, with 32 bits being typical. Defined to be
the "most comfortable" size for the computer. If you do not really care about the
range for an integer variable, declare it int since that is likely to be an appropriate
size (16 or 32 bit) which works well for that machine.
long Large integer -- at least 32 bits. Typical size is 32 bits which gives a signed
range of about -2 billion ..+2 billion. Some compilers support "long long" for 64 bit
ints.
The integer types can be preceded by the qualifier unsigned which disallows
representing negative numbers, but doubles the largest positive number representable. For
example, a 16 bit implementation of short can store numbers in the range
-32768..32767, while unsigned short can store 0..65535. You can think of pointers
as being a form of unsigned long on a machine with 4 byte pointers. In my opinion,
it's best to avoid using unsigned unless you really need to. It tends to cause more
misunderstandings and problems than it is worth.
char Constants
A char constant is written with single quotes (') like 'A' or 'z'. The char constant 'A' is
really just a synonym for the ordinary integer value 65 which is the ASCII value for
4
uppercase 'A'. There are special case char constants, such as '\t' for tab, for characters
which are not convenient to type on a keyboard.
'A' uppercase 'A' character
'\0' the "null" character -- integer value 0 (different from the char digit '0')
int Constants
Numbers in the source code such as 234 default to type int. They may be followed by
an 'L' (upper or lower case) to designate that the constant should be a long such as 42L.
An integer constant can be written with a leading 0x to indicate that it is expressed in
hexadecimal -- 0x10 is way of expressing the number 16. Similarly, a constant may be
written in octal by preceding it with "0" -- 012 is a way of expressing the number 10.
precision and double is about 15 digits of precision. Most C programs use double for
their computations. The main reason to use float is to save memory if many numbers
need to be stored. The main thing to remember about floating point numbers is that they
are inexact. For example, what is the value of the following double expression?
(1.0/3.0 + 1.0/3.0 + 1.0/3.0) // is this equal to 1.0 exactly?
The sum may or may not be 1.0 exactly, and it may vary from one type of machine to
another. For this reason, you should never compare floating numbers to eachother for
equality (==) -- use inequality (<) comparisons instead. Realize that a correct C program
run on different computers may produce slightly different outputs in the rightmost digits
of its floating point computations.
Comments
Comments in C are enclosed by slash/star pairs: /* .. comments .. */ which
may cross multiple lines. C++ introduced a form of comment started by two slashes and
extending to the end of the line: // comment until the line end
The // comment form is so handy that many C compilers now also support it, although it
is not technically part of the C language.
Along with well-chosen function names, comments are an important part of well written
code. Comments should not just repeat what the code says. Comments should describe
what the code accomplishes which is much more interesting than a translation of what
each statement does. Comments should also narrate what is tricky or non-obvious about a
section of code.
Variables
As in most languages, a variable declaration reserves and names an area in memory at run
time to hold a value of particular type. Syntactically, C puts the type first followed by the
name of the variable. The following declares an int variable named "num" and the 2nd
line stores the value 42 into num.
int num;
num = 42;
num 42
A variable corresponds to an area of memory which can store a value of the given type.
Making a drawing is an excellent way to think about the variables in a program. Draw
each variable as box with the current value inside the box. This may seem like a
"beginner" technique, but when I'm buried in some horribly complex programming
problem, I invariably resort to making a drawing to help think the problem through.
Variables, such as num, do not have their memory cleared or set in any way when they
are allocated at run time. Variables start with random values, and it is up to the program
to set them to something sensible before depending on their values.
Names in C are case sensitive so "x" and "X" refer to different variables. Names can
contain digits and underscores (_), but may not begin with a digit. Multiple variables can
be declared after the type by separating them with commas. C is a classical "compile
time" language -- the names of the variables, their types, and their implementations are all
flushed out by the compiler at compile time (as opposed to figuring such details out at run
time like an interpreter).
6
float x, y, z, X;
Assignment Operator =
The assignment operator is the single equals sign (=).
i = 6;
i = i + 1;
The assignment operator copies the value from its right hand side to the variable on its
left hand side. The assignment also acts as an expression which returns the newly
assigned value. Some programmers will use that feature to write things like the following.
y = (x = 2 * x); // double x, and also put x's new value in y
Truncation
The opposite of promotion, truncation moves a value from a type to a smaller type. In
that case, the compiler just drops the extra bits. It may or may not generate a compile
time warning of the loss of information. Assigning from an integer to a smaller integer
(e.g.. long to int, or int to char) drops the most significant bits. Assigning from a
floating point type to an integer drops the fractional part of the number.
char ch;
int i;
i = 321;
ch = i; // truncation of an int value to fit in a char
// ch is now 65
The assignment will drop the upper bits of the int 321. The lower 8 bits of the number
321 represents the number 65 (321 - 256). So the value of ch will be (char)65 which
happens to be 'A'.
The assignment of a floating point type to an integer type will drop the fractional part of
the number. The following code will set i to the value 3. This happens when assigning a
floating point number to an integer or passing a floating point number to a function which
takes an integer.
double pi;
int i;
pi = 3.14159;
i = pi; // truncation of a double to fit in an int
// i is now 3
Unfortunately, score will almost always be set to 0 for this code because the integer
division in the expression (score/20) will be 0 for every value of score less than 20.
The fix is to force the quotient to be computed as a floating point number...
score = ((double)score / 20) * 100; // OK -- floating point division from cast
will execute until the variable i takes on the value 10 at which time the expression (i -
10) will become false (i.e. 0). (we'll see the while() statement a bit later)
Mathematical Operators
C includes the usual binary and unary arithmetic operators. See the appendix for the table
of precedence. Personally, I just use parenthesis liberally to avoid any bugs due to a
misunderstanding of precedence. The operators are sensitive to the type of the operands.
So division (/) with two integer arguments will do integer division. If either argument is
a float, it does floating point division. So (6/4) evaluates to 1 while (6/4.0)
evaluates to 1.5 -- the 6 is promoted to 6.0 before the division.
+ Addition
- Subtraction
/ Division
* Multiplication
% Remainder (mod)
int i = 42;
i++; // increment on i
// i is now 43
i--; // decrement on i
// i is now 42
j = (i++ + 10);
// i is now 43
// j is now 52 (NOT 53)
j = (++i + 10)
// i is now 44
// j is now 54
Now then, isn't that nicer? (editorial) Build programs that do something cool rather than
programs which flex the language's syntax. Syntax -- who cares?
Relational Operators
These operate on integer or floating point values and return a 0 or 1 boolean value.
== Equal
9
!= Not Equal
Pitfall = ==
An absolutely classic pitfall is to write assignment (=) when you mean comparison (==).
This would not be such a problem, except the incorrect assignment version compiles fine
because the compiler assumes you mean to use the value returned by the assignment. This
is rarely what you want
if (x = 3) ...
This does not test if x is 3. This sets x to the value 3, and then returns the 3 to the if for
testing. 3 is not 0, so it counts as "true" every time. This is probably the single most
common error made by beginning C programmers. The problem is that the compiler is no
help -- it thinks both forms are fine, so the only defense is extreme vigilance when
coding. Or write "= ≠ ==" in big letters on the back of your hand before coding. This
mistake is an absolute classic and it's a bear to debug. Watch Out! And need I say:
"Professional Programmer's Language."
Logical Operators
The value 0 is false, anything else is true. The operators evaluate left to right and stop as
soon as the truth or falsity of the expression can be deduced. (Such operators are called
"short circuiting") In ANSI C, these are furthermore guaranteed to use 1 to represent true,
and not just some random non-zero bit pattern. However, there are many C programs out
there which use values other than 1 for true (non-zero pointers for example), so when
programming, do not assume that a true boolean is necessarily 1 exactly.
! Boolean not (unary)
|| Boolean or
Bitwise Operators
C includes operators to manipulate memory at the bit level. This is useful for writing low-
level hardware or operating system code where the ordinary abstractions of numbers,
characters, pointers, etc... are insufficient -- an increasingly rare need. Bit manipulation
code tends to be less "portable". Code is "portable" if with no programmer intervention it
compiles and runs correctly on different types of computers. The bitwise operations are
10
typically used with unsigned types. In particular, the shift operations are guaranteed to
shift 0 bits into the newly vacated positions when used on unsigned values.
~ Bitwise Negation (unary) – flip 0 to 1 and 1 to 0 throughout
| Bitwise Or
^ Bitwise Exclusive Or
Do not confuse the Bitwise operators with the logical operators. The bitwise connectives
are one character wide (&, |) while the boolean connectives are two characters wide (&&,
||). The bitwise operators have higher precedence than the boolean operators. The
compiler will never help you out with a type error if you use & when you meant &&. As
far as the type checker is concerned, they are identical-- they both take and produce
integers since there is no distinct boolean type.
%= Mod by RHS
Section 2
Control Structures
Curly Braces {}
C uses curly braces ({}) to group multiple statements together. The statements execute in
order. Some languages let you declare variables on any line (C++). Other languages insist
that variables are declared only at the beginning of functions (Pascal). C takes the middle
road -- variables may be declared within the body of a function, but they must follow a
'{'. More modern languages like Java and C++ allow you to declare variables on any line,
which is handy.
If Statement
Both an if and an if-else are available in C. The <expression> can be any valid
expression. The parentheses around the expression are required, even if it is just a single
variable.
if (<expression>) <statement> // simple form with no {}'s or else clause
Switch Statement
The switch statement is a sort of specialized form of if used to efficiently separate
different blocks of code based on the value of an integer. The switch expression is
evaluated, and then the flow of control jumps to the matching const-expression case. The
case expressions are typically int or char constants. The switch statement is probably
the single most syntactically awkward and error-prone features of the C language.
switch (<expression>) {
case <const-expression-1>:
<statement>
break;
case <const-expression-2>:
<statement>
break;
default: // optional
<statement>
}
Each constant needs its own case keyword and a trailing colon (:). Once execution has
jumped to a particular case, the program will keep running through all the cases from that
point down -- this so called "fall through" operation is used in the above example so that
expression-3 and expression-4 run the same statements. The explicit break statements
are necessary to exit the switch. Omitting the break statements is a common error -- it
compiles, but leads to inadvertent fall-through behavior.
Why does the switch statement fall-through behavior work the way it does? The best
explanation I can think of is that originally C was developed for an audience of assembly
language programmers. The assembly language programmers were used to the idea of a
jump table with fall-through behavior, so that's the way C does it (it's also relatively easy
to implement it this way.) Unfortunately, the audience for C is now quite different, and
the fall-through behavior is widely regarded as a terrible part of the language.
While Loop
The while loop evaluates the test expression before every loop, so it can execute zero
times if the condition is initially false. It requires the parenthesis like the if.
while (<expression>) {
<statement>
}
13
Do-While Loop
Like a while, but with the test condition at the bottom of the loop. The loop body will
always execute at least once. The do-while is an unpopular area of the language, most
everyone tries to use the straight while if at all possible.
do {
<statement>
} while (<expression>)
For Loop
The for loop in C is the most general looping construct. The loop header contains three
parts: an initialization, a continuation condition, and an action.
for (<initialization>; <continuation>; <action>) {
<statement>
}
The initialization is executed once before the body of the loop is entered. The loop
continues to run as long as the continuation condition remains true (like a while). After
every execution of the loop, the action is executed. The following example executes 10
times by counting 0..9. Many loops look very much like the following...
for (i = 0; i < 10; i++) {
<statement>
}
C programs often have series of the form 0..(some_number-1). It's idiomatic in C for the
above type loop to start at 0 and use < in the test so the series runs up to but not equal to
the upper bound. In other languages you might start at 1 and use <= in the test.
Each of the three parts of the for loop can be made up of multiple expressions separated
by commas. Expressions separated by commas are executed in order, left to right, and
represent the value of the last expression. (See the string-reverse example below for a
demonstration of a complex for loop.)
Break
The break statement will move control outside a loop or switch statement. Stylistically
speaking, break has the potential to be a bit vulgar. It's preferable to use a straight
while with a single test at the top if possible. Sometimes you are forced to use a break
because the test can occur only somewhere in the midst of the statements in the loop
body. To keep the code readable, be sure to make the break obvious -- forgetting to
account for the action of a break is a traditional source of bugs in loop behavior.
while (<expression>) {
<statement>
<statement>
<statement>
<statement>
}
// control jumps down here on the break
14
The break does not work with if. It only works in loops and switches. Thinking that a
break refers to an if when it really refers to the enclosing while has created some high
quality bugs. When using a break, it's nice to write the enclosing loop to iterate in the
most straightforward, obvious, normal way, and then use the break to explicitly catch
the exceptional, weird cases.
Continue
The continue statement causes control to jump to the bottom of the loop, effectively
skipping over any code below the continue. As with break, this has a reputation as
being vulgar, so use it sparingly. You can almost always get the effect more clearly using
an if inside your loop.
while (<expression>) {
...
if (<condition>)
continue;
...
...
// control jumps here on the continue
}
15
Section 3
Complex Data Types
C has the usual facilities for grouping things together to form composite types-- arrays
and records (which are called "structures"). The following definition declares a type
called "struct fraction" that has two integer sub fields named "numerator" and
"denominator". If you forget the semicolon it tends to produce a syntax error in whatever
thing follows the struct declaration.
struct fraction {
int numerator;
int denominator;
}; // Don't forget the semicolon!
This declaration introduces the type struct fraction (both words are required) as a
new type. C uses the period (.) to access the fields in a record. You can copy two records
of the same type using a single assignment statement, however == does not work on
structs.
struct fraction f1, f2; // declare two fractions
f1.numerator = 22;
f1.denominator = 7;
Arrays
The simplest type of array in C is one which is declared and used in one place. There are
more complex uses of arrays which I will address later along with pointers. The following
declares an array called scores to hold 100 integers and sets the first and last elements.
C arrays are always indexed from 0. So the first int in scores array is scores[0]
and the last is scores[99].
int scores[100];
13 -5673 22541 42
Index 0 1 2 99
Multidimensional Arrays
The following declares a two-dimensional 10 by 10 array of integers and sets the first and
last elements to be 13.
int board [10][10];
board[0][0] = 13;
board[9][9] = 13;
The implementation of the array stores all the elements in a single contiguous block of
memory. The other possible implementation would be a combination of several distinct
one dimensional arrays -- that's not how C does it. In memory, the array is arranged with
the elements of the rightmost index next to each other. In other words, board[1][8]
comes right before board[1][9] in memory.
(highly optional efficiency point) It's typically efficient to access memory which is near
other recently accessed memory. This means that the most efficient way to read through a
chunk of the array is to vary the rightmost index the most frequently since that will access
elements that are near each other in memory.
17
Array of Structs
The following declares an array named "numbers" which holds 1000 struct
fraction's.
struct fraction numbers[1000];
Here's a general trick for unraveling C variable declarations: look at the right hand side
and imagine that it is an expression. The type of that expression is the left hand side. For
the above declarations, an expression which looks like the right hand side
(numbers[1000], or really anything of the form numbers[...]) will be the type
on the left hand side (struct fraction).
Pointers
A pointer is a value which represents a reference to another value sometimes known as
the pointer's "pointee". Hopefully you have learned about pointers somewhere else, since
the preceding sentence is probably inadequate explanation. This discussion will
concentrate on the syntax of pointers in C -- for a much more complete discussion of
pointers and their use see http://cslibrary.stanford.edu/102/, Pointers and Memory.
Syntax
Syntactically C uses the asterisk or "star" (*) to indicate a pointer. C defines pointer types
based on the type pointee. A char* is type of pointer which refers to a single char. a
struct fraction* is type of pointer which refers to a struct fraction.
int* intPtr; // declare an integer pointer variable intPtr
Pointer Dereferencing
We'll see shortly how a pointer is set to point to something -- for now just assume the
pointer points to memory of the appropriate type. In an expression, the unary * to the left
of a pointer dereferences it to retrieve the value it points to. The following drawing shows
the types involved with a single pointer pointing to a struct fraction.
18
7 denominator
f1
22 numerator
struct fraction*
struct fraction int
(the whole (within
block of block of
memory) memory)
Expression Type
f1 struct fraction*
*f1 struct fraction
(*f1).numerator int
One nice thing about the C type syntax is that it avoids the circular definition problems
which come up when a pointer structure needs to refer to itself. The following definition
defines a node in a linked list. Note that no preparatory declaration of the node pointer
type is necessary.
struct node {
int data;
struct node* next;
};
void foo() {
int* p; // p is a pointer to an integer
int i; // i is an integer
i 13
When using a pointer to an object created with &, it is important to only use the pointer so
long as the object exists. A local variable exists only as long as the function where it is
declared is still executing (we'll see functions shortly). In the above example, i exists
only as long as foo() is executing. Therefore any pointers which were initialized with
&i are valid only as long as foo() is executing. This "lifetime" constraint of local
memory is standard in many languages, and is something you need to take into account
when using the & operator.
NULL
A pointer can be assigned the value 0 to explicitly represent that it does not currently
have a pointee. Having a standard representation for "no current pointee" turns out to be
very handy when using pointers. The constant NULL is defined to be 0 and is typically
used when setting a pointer to NULL. Since it is just 0, a NULL pointer will behave like
a boolean false when used in a boolean context. Dereferencing a NULL pointer is an error
which, if you are lucky, the computer will detect at runtime -- whether the computer
detects this depends on the operating system.
(3) The pointer (1) must be initialized so that it points to the pointee (2)
The most common pointer related error of all time is the following: Declare and allocate
the pointer (step 1). Forget step 2 and/or 3. Start using the pointer as if it has been setup
to point to something. Code with this error frequently compiles fine, but the runtime
results are disastrous. Unfortunately the pointer does not point anywhere good unless (2)
and (3) are done, so the run time dereference operations on the pointer with * will misuse
and trample memory leading to a random crash at some point.
20
{
int* p;
i -14346
Of course your code won't be so trivial, but the bug has the same basic form: declare a
pointer, but forget to set it up to point to a particular pointee.
Using Pointers
Declaring a pointer allocates space for the pointer itself, but it does not allocate space
for the pointee. The pointer must be set to point to something before you can dereference
it.
Here's some code which doesn't do anything useful, but which does demonstrate (1) (2)
(3) for pointer use correctly...
int* p; // (1) allocate the pointer
int i; // (2) allocate pointee
struct fraction f1; // (2) allocate pointee
p = &(f1.denominator); // (3)
*p = 7;
So far we have just used the & operator to create pointers to simple variables such as i.
Later, we'll see other ways of getting pointers with arrays and other techniques.
C Strings
C has minimal support of character strings. For the most part, strings operate as ordinary
arrays of characters. Their maintenance is up to the programmer using the standard
facilities available for arrays and pointers. C does include a standard library of functions
which perform common string operations, but the programmer is responsible for the
managing the string memory and calling the right functions. Unfortunately computations
involving strings are very common, so becoming a good C programmer often requires
becoming adept at writing code which manages strings which means managing pointers
and arrays.
21
A C string is just an array of char with the one additional convention that a "null"
character ('\0') is stored after the last real character in the array to mark the end of the
string. The compiler represents string constants in the source code such as "binky" as
arrays which follow this convention. The string library functions (see the appendix for a
partial list) operate on strings stored in this way. The most useful library function is
strcpy(char dest[], const char source[]); which copies the bytes of
one string over to another. The order of the arguments to strcpy() mimics the arguments
in of '=' -- the right is assigned to the left. Another useful string function is
strlen(const char string[]); which returns the number of characters in C
string not counting the trailing '\0'.
Note that the regular assignment operator (=) does not do string copying which is why
strcpy() is necessary. See Section 6, Advanced Pointers and Arrays, for more detail on
how arrays and pointers work.
The following code allocates a 10 char array and uses strcpy() to copy the bytes of the
string constant "binky" into that local array.
{
char localString[10];
strcpy(localString, "binky");
}
localString
b i n k y 0 x x x x
0 1 2 ...
The memory drawing shows the local variable localString with the string "binky"
copied into it. The letters take up the first 5 characters and the '\0' char marks the end of
the string after the 'y'. The x's represent characters which have not been set to any
particular value.
If the code instead tried to store the string "I enjoy languages which have good string
support" into localString, the code would just crash at run time since the 10 character
array can contain at most a 9 character string. The large string will be written passed the
right hand side of localString, overwriting whatever was stored there.
strcpy(string, "binky");
len = strlen(string);
/*
Reverse the chars in the string:
i starts at the beginning and goes up
j starts at the end and goes down
i/j exchange their chars as they go until they meet
*/
int i, j;
char temp;
for (i = 0, j = len - 1; i < j; i++, j--) {
temp = string[i];
string[i] = string[j];
string[j] = temp;
}
The program works fine so long as the strings stored are 999 characters or shorter.
Someday when the program needs to store a string which is 1000 characters or longer,
then it crashes. Such array-not-quite-big-enough problems are a common source of bugs,
and are also the source of so called "buffer overflow" security problems. This scheme has
the additional disadvantage that most of the time when the array is storing short strings,
95% of the memory reserved is actually being wasted. A better solution allocates the
string dynamically in the heap, so it has just the right size.
To avoid buffer overflow attacks, production code should check the size of the data first,
to make sure it fits in the destination string. See the strlcpy() function in Appendix A.
char*
Because of the way C handles the types of arrays, the type of the variable
localString above is essentially char*. C programs very often manipulate strings
using variables of type char* which point to arrays of characters. Manipulating the
actual chars in a string requires code which manipulates the underlying array, or the use
23
of library functions such as strcpy() which manipulate the array for you. See Section 6 for
more detail on pointers and arrays.
TypeDef
A typedef statement introduces a shorthand name for a type. The syntax is...
typedef <type> <name>;
The following defines Fraction type to be the type (struct fraction). C is case
sensitive, so fraction is different from Fraction. It's convenient to use typedef to
create types with upper case names and use the lower-case version of the same word as a
variable.
typedef struct fraction Fraction;
The following typedef defines the name Tree as a standard pointer to a binary tree node
where each node contains some data and "smaller" and "larger" subtree pointers.
typedef struct treenode* Tree;
struct treenode {
int data;
Tree smaller, larger; // equivalently, this line could say
}; // "struct treenode *smaller, *larger"
24
Section 4
Functions
All languages have a construct to separate and package blocks of code. C uses the
"function" to package blocks of code. This article concentrates on the syntax and
peculiarities of C functions. The motivation and design for dividing a computation into
separate blocks is an entire discipline in its own.
A function has a name, a list of arguments which it takes when called, and the block of
code it executes when called. C functions are defined in a text file and the names of all
the functions in a C program are lumped together in a single, flat namespace. The special
function called "main" is where program execution begins. Some programmers like to
begin their function names with Upper case, using lower case for variables and
parameters, Here is a simple C function declaration. This declares a function named
Twice which takes a single int argument named num. The body of the function
computes the value which is twice the num argument and returns that value to the caller.
/*
Computes double of a number.
Works by tripling the number, and then subtracting to get back to double.
*/
static int Twice(int num) {
int result = num * 3;
result = result - num;
return(result);
}
Syntax
The keyword "static" defines that the function will only be available to callers in the
file where it is declared. If a function needs to be called from another file, the function
cannot be static and will require a prototype -- see prototypes below. The static form
is convenient for utility functions which will only be used in the file where they are
declared. Next , the "int" in the function above is the type of its return value. Next
comes name of the function and its list of parameters. When referring to a function by
name in documentation or other prose, it's a convention to keep the parenthesis () suffix,
so in this case I refer to the function as "Twice()". The parameters are listed with their
types and names, just like variables.
Inside the function, the parameter num and the local variable result are "local" to the
function -- they get their own memory and exist only so long as the function is executing.
This independence of "local" memory is a standard feature of most languages (See
CSLibrary/102 for the detailed discussion of local memory).
The "caller" code which calls Twice() looks like...
int num = 13;
int a = 1;
int b = 2;
a = Twice(a); // call Twice() passing the value of a
b = Twice(b + num); // call Twice() passing the value b+num
// a == 2
// b == 30
// num == 13 (this num is totally independent of the "num" local to Twice()
25
Things to notice...
(vocabulary) The expression passed to a function by its caller is called the "actual
parameter" -- such as "a" and "b + num" above. The parameter storage local to the
function is called the "formal parameter" such as the "num" in "static int Twice(int
num)".
Parameters are passed "by value" that means there is a single copying assignment
operation (=) from each actual parameter to set each formal parameter. The actual
parameter is evaluated in the caller's context, and then the value is copied into the
function's formal parameter just before the function begins executing. The alternative
parameter mechanism is "by reference" which C does not implement directly, but
which the programmer can implement manually when needed (see below). When a
parameter is a struct, it is copied.
The variables local to Twice(), num and result, only exist temporarily while
Twice() is executing. This is the standard definition for "local" storage for
functions.
The return at the end of Twice() computes the return value and exits the function.
Execution resumes with the caller. There can be multiple return statements within a
function, but it's good style to at least have one at the end if a return value needs to be
specified. Forgetting to account of a return somewhere in the middle of a function
is a traditional source of bugs.
int TakesNothingAndReturnsAnInt();
int TakesNothingAndReturnsAnInt(void); // equivalent syntax for above
2) Sometimes it is undesirable to copy the value from the caller to the callee because the
value is large and so copying it is expensive, or because at a conceptual level copying
the value is undesirable.
The alternative is to pass the arguments "by reference". Instead of passing a copy of a
value from the caller to the callee, pass a pointer to the value. In this way there is only
one copy of the value at any time, and the caller and callee both access that one value
through pointers.
Some languages support reference parameters automatically. C does not do this -- the
programmer must implement reference parameters manually using the existing pointer
constructs in the language.
Swap Example
The classic example of wanting to modify the caller's memory is a swap() function
which exchanges two values. Because C uses call by value, the following version of
Swap will not work...
void Swap(int x, int y) { // NO does not work
int temp;
temp = x;
x = y; // these operations just change the local x,y,temp
y = temp; // -- nothing connects them back to the caller's a,b
}
Swap() does not affect the arguments a and b in the caller. The function above only
operates on the copies of a and b local to Swap() itself. This is a good example of how
"local" memory such as ( x, y, temp) behaves -- it exists independent of everything else
only while its owning function is running. When the owning function exits, its local
memory disappears.
temp = *x; // use * to follow the pointer back to the caller's memory
*x = *y;
*y = temp;
}
27
Swap(&a, &b);
Things to notice...
• The formal parameters are int* instead of int.
• The caller uses & to compute pointers to its local memory (a,b).
• The callee uses * to dereference the formal parameter pointers back to get the caller's
memory.
Since the operator & produces the address of a variable -- &a is a pointer to a. In
Swap() itself, the formal parameters are declared to be pointers, and the values of
interest (a,b) are accessed through them. There is no special relationship between the
names used for the actual and formal parameters. The function call matches up the actual
and formal parameters by their order -- the first actual parameter is assigned to the first
formal parameter, and so on. I deliberately used different names (a,b vs x,y) to emphasize
that the names do not matter.
const
The qualifier const can be added to the left of a variable or parameter type to declare that
the code using the variable will not change the variable. As a practical matter, use of
const is very sporadic in the C programming community. It does have one very handy
use, which is to clarify the role of a parameter in a function prototype...
void foo(const struct fraction* fract);
In the foo() prototype, the const declares that foo() does not intend to change the struct
fraction pointee which is passed to it. Since the fraction is passed by pointer, we could
not know otherwise if foo() intended to change our memory or not. Using the const,
foo() makes its intentions clear. Declaring this extra bit of information helps to clarify the
role of the function to its implementor and caller.
28
int main()
{
int alice = 10;
int bob = 20;
Swap(&alice, &bob);
// at this point alice==20 and bob==10
IncrementAndSwap(&alice, &bob);
// at this point alice==11 and bob==21
return 0;
}
29
Section 5
Odds and Ends
main()
The execution of a C program begins with function named main(). All of the files and
libraries for the C program are compiled together to build a single program file. That file
must contain exactly one main() function which the operating system uses as the starting
point for the program. Main() returns an int which, by convention, is 0 if the program
completed successfully and non-zero if the program exited due to some error condition.
This is just a convention which makes sense in shell oriented environments such as Unix
or DOS.
Multiple Files
For a program of any size, it's convenient to separate the functions into several separate
files. To allow the functions in separate files to cooperate, and yet allow the compiler to
work on the files independently, C programs typically depend on two features...
Prototypes
A "prototype" for a function gives its name and arguments but not its body. In order for a
caller, in any file, to use a function, the caller must have seen the prototype for that
function. For example, here's what the prototypes would look like for Twice() and
Swap(). The function body is absent and there's a semicolon (;) to terminate the
prototype...
int Twice(int num);
void Swap(int* a, int* b);
In pre-ANSI C, the rules for prototypes where very sloppy -- callers were not required to
see prototypes before calling functions, and as a result it was possible to get in situations
where the compiler generated code which would crash horribly.
In ANSI C, I'll oversimplify a little to say that...
1) a function may be declared static in which case it can only be used in the same file
where it is used below the point of its declaration. Static functions do not require a
separate prototype so long as they are defined before or above where they are called
which saves some work.
Preprocessor
The preprocessing step happens to the C source before it is fed to the compiler. The two
most common preprocessor directives are #define and #include...
30
#define
The #define directive can be used to set up symbolic replacements in the source. As with
all preprocessor operations, #define is extremely unintelligent -- it just does textual
replacement without understanding. #define statements are used as a crude way of
establishing symbolic constants.
#define MAX 100
#define SEVEN_WORDS that_symbol_expands_to_all_these_words
Later code can use the symbols MAX or SEVEN_WORDS which will be replaced by the
text to the right of each symbol in its #define.
#include
The "#include" directive brings in text from different files during compilation. #include is
a very unintelligent and unstructured -- it just pastes in the text from the given file and
continues compiling. The #include directive is used in the .h/.c file convention below
which is used to satisfy the various constraints necessary to get prototypes correct.
#include "foo.h" // refers to a "user" foo.h file --
// in the originating directory for the compile
foo.h vs foo.c
The universally followed convention for C is that for a file named "foo.c" containing a
bunch of functions...
• A separate file named foo.h will contain the prototypes for the functions in foo.c
which clients may want to call. Functions in foo.c which are for "internal use
only" and should never be called by clients should be declared static.
• Near the top of foo.c will be the following line which ensures that the function
definitions in foo.c see the prototypes in foo.h which ensures the "prototype
before definition" rule above.
#include "foo.h" // show the contents of "foo.h"
// to the compiler at this point
• Any xxx.c file which wishes to call a function defined in foo.c must include the
following line to see the prototypes, ensuring the "clients must see prototypes" rule
above.
#include "foo.h"
31
#if
At compile time, there is some space of names defined by the #defines. The #if test can
be used at compile-time to look at those symbols and turn on and off which lines the
compiler uses. The following example depends on the value of the FOO #define symbol.
If it is true, then the "aaa" lines (whatever they are) are compiled, and the "bbb" lines are
ignored. If FOO were 0, then the reverse would be true.
#define FOO 1
...
#if FOO
aaa
aaa
#else
bbb
bbb
#endif
You can use #if 0 ...#endif to effectively comment out areas of code you don't
want to compile, but which you want to keeep in the source file.
Assert
Array out of bounds references are an extremely common form of C run-time error. You
can use the assert() function to sprinkle your code with your own bounds checks. A few
seconds putting in assert statements can save you hours of debugging.
Getting out all the bugs is the hardest and scariest part of writing a large piece of
software. Assert statements are one of the easiest and most effective helpers for that
difficult phase.
#include <assert.h>
#define MAX_INTS 100
{
int ints[MAX_INTS];
i = foo(<something complicated>); // i should be in bounds,
// but is it really?
assert(i>=0); // safety assertions
assert(i<MAX_INTS);
ints[i] = 0;
32
Depending on the options specified at compile time, the assert() expressions will be left
in the code for testing, or may be ignored. For that reason, it is important to only put
expressions in assert() tests which do not need to be evaluated for the proper functioning
of the program...
int errCode = foo(); // yes
assert(errCode == 0);
Section 6
Advanced Arrays and Pointers
Advanced C Arrays
In C, an array is formed by laying out all the elements contiguously in memory. The
square bracket syntax can be used to refer to the elements in the array. The array as a
whole is referred to by the address of the first element which is also known as the "base
address" of the whole array.
{
int array[6];
int sum = 0;
sum += array[0] + array[1]; // refer to elements using []
}
array
The array name acts like a pointer to the
first element- in this case an (int*).
The programmer can refer to elements in the array with the simple [ ] syntax such as
array[1]. This scheme works by combining the base address of the whole array with
the index to compute the base address of the desired element in the array. It just requires
a little arithmetic. Each element takes up a fixed number of bytes which is known at
compile-time. So the address of element n in the array using 0 based indexing will be at
an offset of (n * element_size) bytes from the base address of the whole array.
address of nth element = address_of_0th_element + (n * element_size_in_bytes)
The square bracket syntax [ ] deals with this address arithmetic for you, but it's useful to
know what it's doing. The [ ] takes the integer index, multiplies by the element size, adds
the resulting offset to the array base address, and finally dereferences the resulting pointer
to get to the desired element.
{
int intArray[6];
intArray[3] = 13;
}
34
intArray (intArray+3)
12 bytes of offset
13
Index 0 1 2 3 4 5
Offset 0 4 8 12 16 20
in bytes =
n * elem_size
Assume sizeof(int) = 4i.e. Each array
element takes up 4 bytes.
'+' Syntax
In a closely related piece of syntax, a + between a pointer and an integer does the same
offset computation, but leaves the result as a pointer. The square bracket syntax gives the
nth element while the + syntax gives a pointer to the nth element.
So the expression (intArray + 3) is a pointer to the integer intArray[3].
(intArray + 3) is of type (int*) while intArray[3] is of type int. The two
expressions only differ by whether the pointer is dereferenced or not. So the expression
(intArray + 3) is exactly equivalent to the expression (&(intArray[3])). In
fact those two probably compile to exactly the same code. They both represent a pointer
to the element at index 3.
Any [] expression can be written with the + syntax instead. We just need to add in the
pointer dereference. So intArray[3] is exactly equivalent to *(intArray + 3).
For most purposes, it's easiest and most readable to use the [] syntax. Every once in a
while the + is convenient if you needed a pointer to the element instead of the element
itself.
while (1) {
dest[i] = source[i];
if (dest[i] == '\0') break; // we're done
i++;
}
}
The above code does not add the number 12 to the address in p-- that would increment p
by 12 bytes. The code above increments p by 12 ints. Each int probably takes 4 bytes, so
at run time the code will effectively increment the address in p by 48. The compiler
figures all this out based on the type of the pointer.
Using casts, the following code really does just add 12 to the address in the pointer p. It
works by telling the compiler that the pointer points to char instead of int. The size of
char is defined to be exactly 1 byte (or whatever the smallest addressable unit is on the
computer). In other words, sizeof(char) is always 1. We then cast the resulting
36
(char*) back to an (int*). The programmer is allowed to cast any pointer type to
any other pointer type like this to change the code the compiler generates.
p = (int*) ( ((char*)p) + 12);
intPtr = &i;
intArray[3] = 13; // ok
intPtr[0] = 12; // odd, but ok. Changes i.
intPtr[3] = 13; // BAD! There is no integer reserved here!
}
37
intArray (intArray+3)
13
Index 0 1 2 3 4 5
intPtr (intPtr+3)
12 13
These bytes exist, but they have not been explicitly reserved.
They are the bytes which happen to be adjacent to the
memory for i. They are probably being used to store
something already, such as a smashed looking smiley face.
The 13 just gets blindly written over the smiley face. This
error will only be apparent later when the program tries to
read the smiley face data.
Array parameters are passed as pointers. The following two definitions of foo look
different, but to the compiler they mean exactly the same thing. It's preferable to use
whichever syntax is more accurate for readability. If the pointer coming in really is the
base address of a whole array, then use [ ].
void foo(int arrayParam[]) {
arrayParam = NULL; // Silly but valid. Just changes the local pointer
}
Heap Memory
C gives programmers the standard sort of facilities to allocate and deallocate dynamic
heap memory. A word of warning: writing programs which manage their heap memory is
notoriously difficult. This partly explains the great popularity of languages such as Java
and Perl which handle heap management automatically. These languages take over a task
which has proven to be extremely difficult for the programmer. As a result Perl and Java
programs run a little more slowly, but they contain far fewer bugs. (For a detailed
discussion of heap memory see http://cslibrary.stanford.edu/102/, Pointers and Memory.)
C provides access to the heap features through library functions which any C code can
call. The prototypes for these functions are in the file <stdlib.h>, so any code which
wants to call these must #include that header file. The three functions of interest are...
void* malloc(size_t size) Request a contiguous block of memory
of the given size in the heap. malloc() returns a pointer to the heap block or NULL if
the request could not be satisfied. The type size_t is essentially an unsigned
long which indicates how large a block the caller would like measured in bytes.
Because the block pointer returned by malloc() is a void* (i.e. it makes no claim
about the type of its pointee), a cast will probably be required when storing the void*
pointer into a regular typed pointer.
Memory Management
All of a program's memory is deallocated automatically when the it exits, so a program
only needs to use free() during execution if it is important for the program to recycle its
memory while it runs -- typically because it uses a lot of memory or because it runs for a
39
long time. The pointer passed to free() must be exactly the pointer which was originally
returned by malloc() or realloc(), not just a pointer into somewhere within the heap block.
Dynamic Arrays
Since arrays are just contiguous areas of bytes, you can allocate your own arrays in the
heap using malloc(). The following code allocates two arrays of 1000 ints-- one in the
stack the usual "local" way, and one in the heap using malloc(). Other than the different
allocations, the two are syntactically similar in use.
{
int a[1000];
int *b;
b = (int*) malloc( sizeof(int) * 1000);
assert(b != NULL); // check that the allocation succeeded
free(b);
}
Although both arrays can be accessed with [ ], the rules for their maintenance are very
different....
• The array will exist until it is explicitly deallocated with a call to free().
• You can change the size of the array at will at run time using realloc(). The following
changes the size of the array to 2000. Realloc() takes care of copying over the old
elements.
...
b = realloc(b, sizeof(int) * 2000);
assert(b != NULL);
• You have to remember to deallocate it exactly once when you are done with it, and you
have to get that right.
• The above two disadvantages have the same basic profile: if you get them wrong, your
code still looks right. It compiles fine. It even runs for small cases, but for some input
cases it just crashes unexpectedly because random memory is getting overwritten
somewhere like the smiley face. This sort of "random memory smasher" bug can be a
real ordeal to track down.
40
Dynamic Strings
The dynamic allocation of arrays works very well for allocating strings in the heap. The
advantage of heap allocating a string is that the heap block can be just big enough to store
the actual number of characters in the string. The common local variable technique such
as char string[1000]; allocates way too much space most of the time, wasting
the unused bytes, and yet fails if the string ever gets bigger than the variable's fixed size.
#include <string.h>
/*
Takes a c string as input, and makes a copy of that string
in the heap. The caller takes over ownership of the new string
and is responsible for freeing it.
*/
char* MakeStringInHeap(const char* source) {
char* newString;
Section 7
Details and Library Functions
Precedence and Associativity
function-call() [] -> . L to R
* / % L to R
(the top tier arithmetic binary ops)
+ - L to R
(second tier arithmetic binary ops)
== != L to R
, (comma) . L to R
limits.h, float.h constants which define type range values such as INT_MAX
stdio.h
Stdio.h is a very common file to #include -- it includes functions to print and read strings
from files and to open and close files in the file system.
FILE* fopen(const char* fname, const char* mode);
Open a file named in the filesystem and return a FILE* for it. Mode = "r" read,"w"
write,"a"append, returns NULL on error. The standard files stdout, stdin,
stderr are automatically opened and closed for you by the system.
ctype.h
ctype.h includes macros for doing simple tests and operations on characters
isalpha(ch) // ch is an upper or lower case letter
string.h
None of these string routines allocate memory or check that the passed in memory is the
right size. The caller is responsible for making sure there is "enough" memory for the
operation. The type size_t is an unsigned integer wide enough for the computer's
address space -- most likely an unsigned long.
size_t strlen(const char* string);
Return the number of chars in a C string. EG strlen("abc")==3
stdlib.h
int rand();
Returns a pseudo random integer in the range 0..RAND_MAX (limits.h) which is at
least 32767.
Revision History
11/1998 -- original major version. Based on my old C handout for CS107. Thanks to Jon
Becker for proofreading and Mike Cleron for the original inspiration.
Revised 4/2003 with many helpful typo and other suggestions from Negar Shamma and
A. P. Garcia
Linked List
Basics
By Nick Parlante Copyright © 1998-2001, Nick Parlante
Abstract
This document introduces the basic structures and techniques for building linked lists
with a mixture of explanations, drawings, sample code, and exercises. The material is
useful if you want to understand linked lists or if you want to see a realistic, applied
example of pointer-intensive code. A separate document, Linked List Problems
(http://cslibrary.stanford.edu/105/), presents 18 practice problems covering a wide range
of difficulty.
Linked lists are useful to study for two reasons. Most obviously, linked lists are a data
structure which you may want to use in real programs. Seeing the strengths and
weaknesses of linked lists will give you an appreciation of the some of the time, space,
and code issues which are useful to thinking about any data structures in general.
Somewhat less obviously, linked lists are great way to learn about pointers. In fact, you
may never use a linked list in a real program, but you are certain to use lots of pointers.
Linked list problems are a nice combination of algorithms and pointer manipulation.
Traditionally, linked lists have been the domain where beginning programmers get the
practice to really understand pointers.
Audience
The article assumes a basic understanding of programming and pointers. The article uses
C syntax for its examples where necessary, but the explanations avoid C specifics as
much as possible — really the discussion is oriented towards the important concepts of
pointer manipulation and linked list algorithms.
Other Resources
• Link List Problems (http://cslibrary.stanford.edu/105/) Lots of linked
list problems, with explanations, answers, and drawings. The "problems"
article is a companion to this "explanation" article.
This is document #103, Linked List Basics, in the Stanford CS Education Library. This
and other free educational materials are available at http://cslibrary.stanford.edu/. This
document is free to be used, reproduced, or sold so long as this notice is clearly
reproduced at its beginning.
2
Contents
Section 1 — Basic List Structures and Code 2
Section 2 — Basic List Building 11
Section 3 — Linked List Code Techniques 17
Section 3 — Code Examples 22
Edition
Originally 1998 there was just one "Linked List" document that included a basic
explanation and practice problems. In 1999, it got split into two documents: #103 (this
document) focuses on the basic introduction, while #105 is mainly practice problems.
This 4-12-2001 edition represents minor edits on the 1999 edition.
Dedication
This document is distributed for free for the benefit and education of all. That a person
seeking knowledge should have the opportunity to find it. Thanks to Stanford and my
boss Eric Roberts for supporing me in this project. Best regards, Nick --
[email protected]
Section 1 —
Linked List Basics
Why Linked Lists?
Linked lists and arrays are similar since they both store collections of data. The
terminology is that arrays and linked lists store "elements" on behalf of "client" code. The
specific type of element is not important since essentially the same structure works to
store elements of any type. One way to think about linked lists is to look at how arrays
work and think about alternate approaches.
Array Review
Arrays are probably the most common data structure used to store collections of
elements. In most languages, arrays are convenient to declare and the provide the handy
[ ] syntax to access any element by its index number. The following example shows some
typical array code and a drawing of how the array might look in memory. The code
allocates an array int scores[100], sets the first three elements set to contain the
numbers 1, 2, 3 and leaves the rest of the array uninitialized...
void ArrayTest() {
int scores[100];
Here is a drawing of how the scores array might look like in memory. The key point is
that the entire array is allocated as one block of memory. Each element in the array gets
its own space in the array. Any element can be accessed directly using the [ ] syntax.
scores
1 2 3 -3451 23142
index 0 1 2 3 99
Once the array is set up, access to any element is convenient and fast with the [ ]
operator. (Extra for experts) Array access with expressions such as scores[i] is
almost always implemented using fast address arithmetic: the address of an element is
computed as an offset from the start of the array which only requires one multiplication
and one addition.
The disadvantages of arrays are...
1) The size of the array is fixed — 100 elements in this case. Most often this
size is specified at compile time with a simple declaration such as in the
example above . With a little extra effort, the size of the array can be
deferred until the array is created at runtime, but after that it remains fixed.
(extra for experts) You can go to the trouble of dynamically allocating an
array in the heap and then dynamically resizing it with realloc(), but that
requires some real programmer effort.
Linked lists have their own strengths and weaknesses, but they happen to be strong where
arrays are weak. The array's features all follow from its strategy of allocating the memory
for all its elements in one block of memory. Linked lists use an entirely different strategy.
As we will see, linked lists allocate memory for each element separately and only when
necessary.
Pointer Refresher
Here is a quick review of the terminology and rules for pointers. The linked list code to
follow will depend on these rules. (For much more detailed coverage of pointers and
memory, see Pointers and Memory, http://cslibrary.stanford.edu/102/).
4
pointer to the first node. Here is what a list containing the numbers 1, 2, and 3 might look
like...
Stack Heap
BuildOneTwoThree()
head The overall list is built by connecting the
nodes together by their next pointers. The
nodes are all allocated in the heap.
1 2 3
A “head” pointer local to Each node Each node stores The next field of
BuildOneTwoThree() keeps stores one one next pointer. the last node is
the whole list by storing a data element NULL.
pointer to the first node. (int in this
example).
This drawing shows the list built in memory by the function BuildOneTwoThree() (the
full source code for this function is below). The beginning of the linked list is stored in a
"head" pointer which points to the first node. The first node contains a pointer to the
second node. The second node contains a pointer to the third node, ... and so on. The last
node in the list has its .next field set to NULL to mark the end of the list. Code can access
any node in the list by starting at the head and following the .next pointers. Operations
towards the front of the list are fast while operations which access node farther down the
list take longer the further they are from the front. This "linear" cost to access a node is
fundamentally more costly then the constant time [ ] access provided by arrays. In this
respect, linked lists are definitely less efficient than arrays.
Drawings such as above are important for thinking about pointer code, so most of the
examples in this article will associate code with its memory drawing to emphasize the
habit. In this case the head pointer is an ordinary local pointer variable, so it is drawn
separately on the left to show that it is in the stack. The list nodes are drawn on the right
to show that they are allocated in the heap.
struct node {
int data;
struct node* next;
};
• Node Pointer The type for pointers to nodes. This will be the type of the
head pointer and the .next fields inside each node. In C and C++, no
separate type declaration is required since the pointer type is just the node
type followed by a '*'. Type: struct node*
BuildOneTwoThree() Function
Here is simple function which uses pointer operations to build the list {1, 2, 3}. The
memory drawing above corresponds to the state of memory at the end of this function.
This function demonstrates how calls to malloc() and pointer assignments (=) work to
build a pointer structure in the heap.
/*
Build the list {1, 2, 3} in the heap and store
its head pointer in a local stack variable.
Returns the head pointer to the caller.
*/
struct node* BuildOneTwoThree() {
struct node* head = NULL;
struct node* second = NULL;
struct node* third = NULL;
Exercise
Q: Write the code with the smallest number of assignments (=) which will build the
above memory structure. A: It requires 3 calls to malloc(). 3 int assignments (=) to setup
the ints. 4 pointer assignments to setup head and the 3 next fields. With a little cleverness
and knowledge of the C language, this can all be done with 7 assignment operations (=).
7
Length() Function
The Length() function takes a linked list and computes the number of elements in the list.
Length() is a simple list function, but it demonstrates several concepts which will be used
in later, more complex list functions...
/*
Given a linked list head pointer, compute
and return the number of nodes in the list.
*/
int Length(struct node* head) {
struct node* current = head;
int count = 0;
return count;
}
current = current->next;
}
2) The while loop tests for the end of the list with (current != NULL).
This test smoothly catches the empty list case — current will be NULL
on the first iteration and the while loop will just exit before the first
iteration.
code which goes into an infinite loop, often the problem is that step (3) has
been forgotten.
Calling Length()
Here's some typical code which calls Length(). It first calls BuildOneTwoThree() to make
a list and store the head pointer in a local variable. It then calls Length() on the list and
catches the int result in a local variable.
void LengthTest() {
struct node* myList = BuildOneTwoThree();
Memory Drawings
The best way to design and think about linked list code is to use a drawing to see how the
pointer operations are setting up memory. There are drawings below of the state of
memory before and during the call to Length() — take this opportunity to practice
looking at memory drawings and using them to think about pointer intensive code. You
will be able to understand many of the later, more complex functions only by making
memory drawings like this on your own.
Start with the Length() and LengthTest() code and a blank sheet of paper. Trace through
the execution of the code and update your drawing to show the state of memory at each
step. Memory drawings should distinguish heap memory from local stack memory.
Reminder: malloc() allocates memory in the heap which is only be deallocated by
deliberate calls to free(). In contrast, local stack variables for each function are
automatically allocated when the function starts and deallocated when it exits. Our
memory drawings show the caller local stack variables above the callee, but any
convention is fine so long as you realize that the caller and callee are separate. (See
cslibrary.stanford.edu/102/, Pointers and Memory, for an explanation of how local
memory works.)
9
Stack Heap
LengthTest()
myList
len -14231
1 2 3
The head
pointer for
the list is len has a
stored in the random
local variable value until Nodes allocated in the heap
myList. it is via calls to malloc() in
assigned. BuildOneTwoThree().
10
Stack Heap
LengthTest()
myList
len -14231
1 2 3
Length()
head
current
Notice how the local variables in Length() (head and current) are separate from the
local variables in LengthTest() (myList and len). The local variables head and
current will be deallocated (deleted) automatically when Length() exits. This is fine
— the heap allocated links will remain even though stack allocated pointers which were
pointing to them have been deleted.
Exercise
Q: What if we said head = NULL; at the end of Length() — would that mess up the
myList variable in the caller? A: No. head is a local which was initialized with a copy
of the actual parameter, but changes do not automatically trace back to the actual
parameter. Changes to the local variables in one function do not affect the locals of
another function.
Exercise
Q: What if the passed in list contains no elements, does Length() handle that case
properly? A: Yes. The representation of the empty list is a NULL head pointer. Trace
Length() on that case to see how it handles it.
11
Section 2 —
List Building
BuildOneTwoThree() is a fine as example of pointer manipulation code, but it's not a
general mechanism to build lists. The best solution will be an independent function which
adds a single new node to any list. We can then call that function as many times as we
want to build up any list. Before getting into the specific code, we can identify the classic
3-Step Link In operation which adds a single node to the front of a linked list. The 3 steps
are...
1) Allocate Allocate the new node in the heap and set its .data to
whatever needs to be stored.
struct node* newNode;
newNode = malloc(sizeof(struct node));
newNode->data = data_client_wants_stored;
2) Link Next Set the .next pointer of the new node to point to the current
first node of the list. This is actually just a pointer assignment —
remember: "assigning one pointer to another makes them point to the same
thing."
newNode->next = head;
3) Link Head Change the head pointer to point to the new node, so it is
now the first node in the list.
head = newNode;
Stack Heap
LinkTest()
head
newNode 2 3
1
Insert this node with the 3-Step Link In:
1) Allocate the new node
2) Set its .next to the old head
3) Set head to point to the new node
Before: list = {2, 3}
After: list = {1, 2, 3}
Push() Function
With the 3-Step Link In in mind, the problem is to write a general function which adds a
single node to head end of any list. Historically, this function is called "Push()" since
we're adding the link to the head end which makes the list look a bit like a stack.
Alternately it could be called InsertAtFront(), but we'll use the name Push().
WrongPush()
Unfortunately Push() written in C suffers from a basic problem: what should be the
parameters to Push()? This is, unfortunately, a sticky area in C. There's a nice, obvious
way to write Push() which looks right but is wrong. Seeing exactly how it doesn't work
will provide an excuse for more practice with memory drawings, motivate the correct
solution, and just generally make you a better programmer....
void WrongPush(struct node* head, int data) {
struct node* newNode = malloc(sizeof(struct node));
newNode->data = data;
newNode->next = head;
head = newNode; // NO this line does not work!
}
void WrongPushTest() {
List head = BuildTwoThree();
WrongPush() is very close to being correct. It takes the correct 3-Step Link In and puts it
an almost correct context. The problem is all in the very last line where the 3-Step Link
In dictates that we change the head pointer to refer to the new node. What does the line
head = newNode; do in WrongPush()? It sets a head pointer, but not the right one. It
sets the variable named head local to WrongPush(). It does not in any way change the
variable named head we really cared about which is back in the caller WrontPushTest().
Exercise
Make the memory drawing tracing WrongPushTest() to see how it does not work. The
key is that the line head = newElem; changes the head local to WrongPush() not
the head back in WrongPushTest(). Remember that the local variables for WrongPush()
and WrongPushTest() are separate (just like the locals for LengthTest() and Length() in
the Length() example above).
Reference Parameters In C
We are bumping into a basic "feature" of the C language that changes to local parameters
are never reflected back in the caller's memory. This is a traditional tricky area of C
programming. We will present the traditional "reference parameter" solution to this
problem, but you may want to consult another C resource for further information. (See
Pointers and Memory (http://cslibrary.stanford.edu/102/) for a detailed explanation of
reference parameters in C and C++.)
We need Push() to be able to change some of the caller's memory — namely the head
variable. The traditional method to allow a function to change its caller's memory is to
pass a pointer to the caller's memory instead of a copy. So in C, to change an int in the
caller, pass a int* instead. To change a struct fraction, pass a struct
fraction* intead. To change an X, pass an X*. So in this case, the value we want to
change is struct node*, so we pass a struct node** instead. The two stars
(**) are a little scary, but really it's just a straight application of the rule. It just happens
that the value we want to change already has one star (*), so the parameter to change it
has two (**). Or put another way: the type of the head pointer is "pointer to a struct
node." In order to change that pointer, we need to pass a pointer to it, which will be a
"pointer to a pointer to a struct node".
Instead of defining WrongPush(struct node* head, int data); we define
Push(struct node** headRef, int data);. The first form passes a copy of
the head pointer. The second, correct form passes a pointer to the head pointer. The rule
is: to modify caller memory, pass a pointer to that memory. The parameter has the word
"ref" in it as a reminder that this is a "reference" (struct node**) pointer to the
head pointer instead of an ordinary (struct node*) copy of the head pointer.
14
newNode->data = data;
newNode->next = *headRef; // The '*' to dereferences back to the real head
*headRef = newNode; // ditto
}
void PushTest() {
struct node* head = BuildTwoThree();// suppose this returns the list {2, 3}
Stack Heap
PushTest()
head
Push() 2 3
headRef
data 1
1
The key point: the headRef This node inserted by the call to
parameter to Push() is not the Push(). Push follows its headRef to
real head of the list. It is a modify the real head.
pointer to the real head of the
list back in the caller’s
memory space.
Exercise
The above drawing shows the state of memory at the end of the first call to Push() in
PushTest(). Extend the drawing to trace through the second call to Push(). The result
should be that the list is left with elements {13, 1, 2, 3}.
Exercise
The following function correctly builds a three element list using nothing but Push().
Make the memory drawing to trace its execution and show the final state of its list. This
will also demonstrate that Push() works correctly for the empty list case.
void PushTest2() {
struct node* head = NULL; // make a list with no elements
Push(&head, 1);
Push(&head, 2);
Push(&head, 3);
they appear in the source, which is the most convenient for the programmer. So In C++,
Push() and PushTest() look like...
/*
Push() in C++ -- we just add a '&' to the right hand
side of the head parameter type, and the compiler makes
that parameter work by reference. So this code changes
the caller's memory, but no extra uses of '*' are necessary --
we just access "head" directly, and the compiler makes that
change reference back to the caller.
*/
void Push(struct node*& head, int data) {
struct node* newNode = malloc(sizeof(struct node));
newNode->data = data;
newNode->next = head; // No extra use of * necessary on head -- the compiler
head = newNode; // just takes care of it behind the scenes.
}
void PushTest() {
struct node* head = BuildTwoThree();// suppose this returns the list {2, 3}
The memory drawing for the C++ case looks the same as for the C case. The difference is
that the C case, the *'s need to be taken care of in the code. In the C++ case, it's handled
invisibly in the code.
17
Section 3 —
Code Techniques
This section summarizes, in list form, the main techniques for linked list code. These
techniques are all demonstrated in the examples in the next section.
return(count);
}
Alternately, some people prefer to write the loop as a for which makes the initialization,
test, and pointer advance more centralized, and so harder to omit...
for (current = head; current != NULL; current = current->next) {
• Use '&' in the caller to compute and pass a pointer to the value of interest.
• Use '*' on the parameter in the callee function to access and change the
value of interest.
The following simple function sets a head pointer to NULL by using a reference
parameter....
// Change the passed in head pointer to be NULL
// Uses a reference pointer to access the caller's memory
void ChangeToNull(struct node** headRef) { // Takes a pointer to
// the value of interest
18
void ChangeCaller() {
struct node* head1;
struct node* head2;
Here is a drawing showing how the headRef pointer in ChangeToNull() points back to
the variable in the caller...
Stack
ChangeCaller()
head1
ChangToNull()
headRef
See the use of Push() above and its implementation for another example of reference
pointers.
from NULL to point to the new node, such as the tail variable in the following
example of adding a "3" node to the end of the list {1, 2}...
Stack Heap
head 1 2
tail 3
newNode
This is just a special case of the general rule: to insert or delete a node inside a list, you
need a pointer to the node just before that position, so you can change its .next field.
Many list problems include the sub-problem of advancing a pointer to the node before the
point of insertion or deletion. The one exception is if the node is the first in the list — in
that case the head pointer itself must be changed. The following examples show the
various ways code can handle the single head case and all the interior cases...
// Deal with the head node here, and set the tail pointer
Push(&head, 1);
tail = head;
dummy.next = NULL;
Some linked list implementations keep the dummy node as a permanent part of the list.
For this "permanent dummy" strategy, the empty list is not represented by a NULL
pointer. Instead, every list has a dummy node at its head. Algorithms skip over the
dummy node for all operations. That way the heap allocated dummy node is always
present to provide the above sort of convenience in the code.
Our dummy-in-the stack strategy is a little unusual, but it avoids making the dummy a
permanent part of the list. Some of the solutions presented in this document will use the
temporary dummy strategy. The code for the permanent dummy strategy is extremely
similar, but is not shown.
7) Build — Local References
Finally, here is a tricky way to unifying all the node cases without using a dummy node.
The trick is to use a local "reference pointer" which always points to the last pointer in
the list instead of to the last node. All additions to the list are made by following the
reference pointer. The reference pointer starts off pointing to the head pointer. Later, it
points to the .next field inside the last node in the list. (A detailed explanation follows.)
struct node* BuildWithLocalRef() {
struct node* head = NULL;
struct node** lastPtrRef= &head; // Start out pointing to the head pointer
int i;
This technique is short, but the inside of the loop is scary. This technique is rarely used.
(Actually, I'm the only person I've known to promote it. I think it has a sort of compact
charm.) Here's how it works...
1) At the top of the loop, lastPtrRef points to the last pointer in the list.
Initially it points to the head pointer itself. Later it points to the .next
field inside the last node in the list.
2) Push(lastPtrRef, i); adds a new node at the last pointer. The
new node becaomes the last node in the list.
3) lastPtrRef= &((*lastPtrRef)->next); Advance the
lastPtrRef to now point to the .next field inside the new last node
— that .next field is now the last pointer in the list.
Here is a drawing showing the state of memory for the above code just before the third
node is added. The previous values of lastPtrRef are shown in gray...
Stack Heap
LocalRef()
head 1 2
lastPtrRef
This technique is never required to solve a linked list problem, but it will be one of the
alternative solutions presented for some of the advanced problems.
Both the temporary-dummy strategy and the reference-pointer strategy are a little
unusual. They are good ways to make sure that you really understand pointers, since they
use pointers in unusual ways.
22
Section 4 — Examples
This section presents some complete list code to demonstrate all of the techniques above.
For many more sample problems with solutions, see CS Education Library #105, --
Linked List Problems (http://cslibrary.stanford.edu/105/).
AppendNode() Example
Consider a AppendNode() function which is like Push(), except it adds the new node at
the tail end of the list instead of the head. If the list is empty, it uses the reference pointer
to change the head pointer. Otherwise it uses a loop to locate the last node in the list. This
version does not use Push(). It builds the new node directly.
struct node* AppendNode(struct node** headRef, int num) {
struct node* current = *headRef;
struct node* newNode;
current->next = newNode;
}
}
CopyList() Example
Consider a CopyList() function that takes a list and returns a complete copy of that list.
One pointer can iterate over the original list in the usual way. Two other pointers can
keep track of the new list: one head pointer, and one tail pointer which always points to
the last node in the new list. The first node is done as a special case, and then the tail
pointer is used in the standard way for the others...
struct node* CopyList(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* newList = NULL; // head of the new list
struct node* tail = NULL; // kept pointing to the last node in the new list
return(newList);
}
Stack Heap
CopyList()
head 1 2
current
newList
1 2
tail
24
return(newList);
}
dummy.next = NULL;
tail = &dummy; // start the tail pointing at the dummy
return(dummy.next);
}
list. When the list is empty, it points to the head pointer itself. Later it points to the
.next pointer inside the last node in the list.
// Local reference variant
struct node* CopyList(struct node* head) {
struct node* current = head; // used to iterate over the original list
struct node* newList = NULL;
struct node** lastPtr;
return(newList);
}
CopyList() Recursive
Finally, for completeness, here is the recursive version of CopyList(). It has the pleasing
shortness that recursive code often has. However, it is probably not good for production
code since it uses stack space proportional to the length of its list.
// Recursive variant
struct node* CopyList(struct node* head) {
if (head == NULL) return NULL;
else {
struct node* newList = malloc(sizeof(struct node)); // make the one node
newList->data = current->data;
return(newList);
}
}
Appendix —
Other Implementations
There are a many variations on the basic linked list which have individual advantages
over the basic linked list. It is probably best to have a firm grasp of the basic linked list
and its code before worrying about the variations too much.
• Dummy Header Forbid the case where the head pointer is NULL.
Instead, choose as a representation of the empty list a single "dummy"
node whose .data field is unused. The advantage of this technique is that
the pointer-to-pointer (reference parameter) case does not come up for
operations such as Push(). Also, some of the iterations are now a little
simpler since they can always assume the existence of the dummy header
node. The disadvantage is that allocating an "empty" list now requires
26
• Circular Instead of setting the .next field of the last node to NULL,
set it to point back around to the first node. Instead of needing a fixed head
end, any pointer into the list will do.
• Tail Pointer The list is not represented by a single head pointer. Instead
the list is represented by a head pointer which points to the first node and a
tail pointer which points to the last node. The tail pointer allows operations
at the end of the list such as adding an end element or appending two lists
to work efficiently.
• Head struct A variant I like better than the dummy header is to have a
special "header" struct (a different type from the node type) which
contains a head pointer, a tail pointer, and possibly a length to make many
operations more efficient. Many of the reference parameter problems go
away since most functions can deal with pointers to the head struct
(whether it is heap allocated or not). This is probably the best approach to
use in a language without reference parameters, such as Java.
• Chunk List Instead of storing a single client element in each node, store
a little constant size array of client elements in each node. Tuning the
number of elements per node can provide different performance
characteristics: many elements per node has performance more like an
array, few elements per node has performance more like a linked list. The
Chunk List is a good way to build a linked list with good performance.
Abstract
This document reviews basic linked list code techniques and then works through 18
linked list problems covering a wide range of difficulty. Most obviously, these problems
are a way to learn about linked lists. More importantly, these problems are a way to
develop your ability with complex pointer algorithms. Even though modern languages
and tools have made linked lists pretty unimportant for day-to-day programming, the
skills for complex pointer algorithms are very important, and linked lists are an excellent
way to develop those skills.
The problems use the C language syntax, so they require a basic understanding of C and
its pointer syntax. The emphasis is on the important concepts of pointer manipulation and
linked list algorithms rather than the features of the C language.
For some of the problems we present multiple solutions, such as iteration vs. recursion,
dummy node vs. local reference. The specific problems are, in rough order of difficulty:
Count, GetNth, DeleteList, Pop, InsertNth, SortedInsert, InsertSort, Append,
FrontBackSplit, RemoveDuplicates, MoveNode, AlternatingSplit, ShuffleMerge,
SortedMerge, SortedIntersect, Reverse, and RecursiveReverse.
Contents
Section 1 — Review of basic linked list code techniques 3
Section 2 — 18 list problems in increasing order of difficulty 10
Section 3 — Solutions to all the problems 20
This is document #105, Linked List Problems, in the Stanford CS Education Library.
This and other free educational materials are available at http://cslibrary.stanford.edu/.
This document is free to be used, reproduced, or sold so long as this notice is clearly
reproduced at its beginning.
Related CS Education Library Documents
Related Stanford CS Education library documents...
• Linked List Basics (http://cslibrary.stanford.edu/103/)
Explains all the basic issues and techniques for building linked lists.
• Pointers and Memory (http://cslibrary.stanford.edu/102/)
Explains how pointers and memory work in C and other languages. Starts
with the very basics, and extends through advanced topics such as
reference parameters and heap management.
• Binary Trees (http://cslibrary.stanford.edu/110/)
Introduction to binary trees
• Essential C (http://cslibrary.stanford.edu/101/)
Explains the basic features of the C programming language.
2
• Complex Algorithm Even though linked lists are simple, the algorithms
that operate on them can be as complex and beautiful as you want (See
problem #18). It's easy to find linked list algorithms that are complex, and
pointer intensive.
• Pointer Intensive Linked list problems are really about pointers. The
linked list structure itself is obviously pointer intensive. Furthermore,
linked list algorithms often break and re-weave the pointers in a linked list
as they go. Linked lists really test your understanding of pointers.
Not to appeal to your mercenary side, but for all of the above reasons, linked list
problems are often used as interview and exam questions. They are short to state, and
have complex, pointer intensive solutions. No one really cares if you can build linked
lists, but they do want to see if you have programming agility for complex algorithms and
pointer manipulation. Linked lists are the perfect source of such problems.
Dedication
This Jan-2002 revision includes many small edits. The first major release was Jan 17,
1999. Thanks to Negar Shamma for her many corrections. This document is distributed
for the benefit and education of all. Thanks to the support of Eric Roberts and Stanford
University. That someone seeking education should have the opportunity to find it. May
you learn from it in the spirit of goodwill in which it is given.
Best Regards, Nick Parlante -- [email protected]
3
Section 1 —
Linked List Review
This section is a quick review of the concepts used in these linked list problems. For more
detailed coverage, see Link List Basics (http://cslibrary.stanford.edu/103/) where all of
this material is explained in much more detail.
To keep thing ssimple, we will not introduce any intermediate typedefs. All pointers to
nodes are declared simply as struct node*. Pointers to pointers to nodes are declared
as struct node**. Such pointers to pointers are often called "reference pointers".
If these basic functions do not make sense to you, you can (a) go see Linked List Basics
(http://cslibrary.stanford.edu/103/) which explains the basics of linked lists in detail, or
(b) do the first few problems, but avoid the intermediate and advanced ones.
return(count);
}
Alternately, some people prefer to write the loop as a for which makes the initialization,
test, and pointer advance more centralized, and so harder to omit...
for (current = head; current != NULL; current = current->next) {
5
• Use '&' in the caller to compute and pass a pointer to the value of interest.
• Use '*' on the parameter in the callee function to access and change the
value of interest.
The following simple function sets a head pointer to NULL by using a reference
parameter....
// Change the passed in head pointer to be NULL
// Uses a reference pointer to access the caller's memory
void ChangeToNull(struct node** headRef) { // Takes a pointer to
// the value of interest
void ChangeCaller() {
struct node* head1;
struct node* head2;
Here is a drawing showing how the headRef pointer in ChangeToNull() points back to
the variable in the caller...
Stack
ChangeCaller()
head1
ChangeToNull(&head1)
headRef
6
Many of the functions in this document use reference pointer parameters. See the use of
Push() above and its implementation in the appendix for another example of reference
pointers. See problem #8 and its solution for a complete example with drawings. For
more detailed explanations, see the resources listed on page 1.
Stack Heap
head 1 2
tail 3
newNode
This is just a special case of the general rule: to insert or delete a node inside a list, you
need a pointer to the node just before that position, so you can change its .next field.
Many list problems include the sub-problem of advancing a pointer to the node before the
point of insertion or deletion. The one exception is if the operation falls on the first node
in the list — in that case the head pointer itself must be changed. The following examples
show the various ways code can handle the single head case and all the interior cases...
7
// Deal with the head node here, and set the tail pointer
Push(&head, 1);
tail = head;
dummy.next = NULL;
Some linked list implementations keep the dummy node as a permanent part of the list.
For this "permanent dummy" strategy, the empty list is not represented by a NULL
pointer. Instead, every list has a heap allocated dummy node at its head. Algorithms skip
over the dummy node for all operations. That way the dummy node is always present to
provide the above sort of convenience in the code. I prefer the temporary strategy shown
here, but it is a little peculiar since the temporary dummy node is allocated in the stack,
while all the other nodes are allocated in the heap. For production code, I do not use
either type of dummy node. The code should just cope with the head node boundary
cases.
This technique is short, but the inside of the loop is scary. This technique is rarely used,
but it's a good way to see if you really understand pointers. Here's how it works...
1) At the top of the loop, lastPtrRef points to the last pointer in the list.
Initially it points to the head pointer itself. Later it points to the .next
field inside the last node in the list.
2) Push(lastPtrRef, i); adds a new node at the last pointer. The
new node becomes the last node in the list.
3) lastPtrRef= &((*lastPtrRef)->next); Advance the
lastPtrRef to now point to the .next field inside the new last node
— that .next field is now the last pointer in the list.
Here is a drawing showing the state of memory for the above code just before the third
node is added. The previous values of lastPtrRef are shown in gray...
9
Stack Heap
LocalRef()
head 1 2
lastPtrRef
This technique is never required to solve a linked list problem, but it will be one of the
alternative solutions presented for some of the advanced problems. The code is shorter
this way, but the performance is probably not any better.
Unusual Techniques
Both the temporary-stack-dummy and the local-reference-pointer techniques are a little
unusual. They are cute, and they let us play around with yet another variantion in pointer
intensive code. They use memory in unusual ways, so they are a nice way to see if you
really understand what's going on. However, I probably would not use them in production
code.
10
Section 2 —
Linked List Problems
Here are 18 linked list problems arranged in order of difficulty. The first few are quite
basic and the last few are quite advanced. Each problem starts with a basic definition of
what needs to be accomplished. Many of the problems also include hints or drawings to
get you started. The solutions to all the problems are in the next section.
It's easy to just passively sweep your eyes over the solution — verifying its existence
without lettings its details touch your brain. To get the most benefit from these problems,
you need to make an effort to think them through. Whether or not you solve the problem,
you will be thinking through the right issues, and the given solution will make more
sense.
Great programmers can visualize data structures to see how the code and memory will
interact. Linked lists are well suited to that sort of visual thinking. Use these problems to
develop your visualization skill. Make memory drawings to trace through the execution
of code. Use drawings of the pre- and post-conditions of a problem to start thinking about
a solution.
"The will to win means nothing without the will to prepare." - Juma Ikangaa, marathoner
(also attributed to Bobby Knight)
1 — Count()
Write a Count() function that counts the number of times a given int occurs in a list. The
code for this has the classic list traversal structure as demonstrated in Length().
void CountTest() {
List myList = BuildOneTwoThree(); // build {1, 2, 3}
int count = Count(myList, 2); // returns 1 since there's 1 '2' in the list
}
/*
Given a list and an int, return the number of times that int occurs
in the list.
*/
int Count(struct node* head, int searchFor) {
// Your code
11
2 — GetNth()
Write a GetNth() function that takes a linked list and an integer index and returns the data
value stored in the node at that index position. GetNth() uses the C numbering convention
that the first node is index 0, the second is index 1, ... and so on. So for the list {42, 13,
666} GetNth() with index 1 should return 13. The index should be in the range [0..length-
1]. If it is not, GetNth() should assert() fail (or you could implement some other error
case strategy).
void GetNthTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}
int lastNode = GetNth(myList, 2); // returns the value 3
}
Essentially, GetNth() is similar to an array[i] operation — the client can ask for
elements by index number. However, GetNth() no a list is much slower than [ ] on an
array. The advantage of the linked list is its much more flexible memory management —
we can Push() at any time to add more elements and the memory is allocated as needed.
// Given a list and an index, return the data
// in the nth node of the list. The nodes are numbered from 0.
// Assert fails if the index is invalid (outside 0..lengh-1).
int GetNth(struct node* head, int index) {
// Your code
3 — DeleteList()
Write a function DeleteList() that takes a list, deallocates all of its memory and sets its
head pointer to NULL (the empty list).
void DeleteListTest() {
struct node* myList = BuildOneTwoThree(); // build {1, 2, 3}
Stack Heap
DeleteListTest()
myList
myList is 1 2 3
overwritten
with the
value NULL.
DeleteList()
The DeleteList() implementation will need to use a reference parameter just like Push()
so that it can change the caller's memory (myList in the above sample). The
implementation also needs to be careful not to access the .next field in each node after
the node has been deallocated.
void DeleteList(struct node** headRef) {
// Your code
4 — Pop()
Write a Pop() function that is the inverse of Push(). Pop() takes a non-empty list, deletes
the head node, and returns the head node's data. If all you ever used were Push() and
Pop(), then our linked list would really look like a stack. However, we provide more
general functions like GetNth() which what make our linked list more than just a stack.
Pop() should assert() fail if there is not a node to pop. Here's some sample code which
calls Pop()....
void PopTest() {
struct node* head = BuildOneTwoThree(); // build {1, 2, 3}
int a = Pop(&head); // deletes "1" node and returns 1
int b = Pop(&head); // deletes "2" node and returns 2
int c = Pop(&head); // deletes "3" node and returns 3
int len = Length(head); // the list is now empty, so len == 0
}
Pop() Unlink
Pop() is a bit tricky. Pop() needs to unlink the front node from the list and deallocate it
with a call to free(). Pop() needs to use a reference parameter like Push() so that it can
change the caller's head pointer. A good first step to writing Pop() properly is making the
memory drawing for what Pop() should do. Below is a drawing showing a Pop() of the
first node of a list. The process is basically the reverse of the 3-Step-Link-In used by
Push() (would that be "Ni Knil Pets-3"?). The overwritten pointer value is shown in gray,
and the deallocated heap memory has a big 'X' drawn on it...
13
Stack Heap
PopTest()
head
Pop()
/*
The opposite of Push(). Takes a non-empty list
and removes the front node, and returns the data
which was in that node.
*/
int Pop(struct node** headRef) {
// your code...
5 — InsertNth()
A more difficult problem is to write a function InsertNth() which can insert a new node at
any index within a list. Push() is similar, but can only insert a node at the head end of the
list (index 0). The caller may specify any index in the range [0..length], and the new node
should be inserted so as to be at that index.
void InsertNthTest() {
struct node* head = NULL; // start with the empty list
InsertNth() is complex — you will want to make some drawings to think about your
solution and afterwards, to check its correctness.
/*
A more general version of Push().
Given a list, an index 'n' in the range 0..length,
and a data element, add a new node to the list so
that it has the given index.
*/
void InsertNth(struct node** headRef, int index, int data) {
// your code...
6 — SortedInsert()
Write a SortedInsert() function which given a list that is sorted in increasing order, and a
single node, inserts the node into the correct sorted position in the list. While Push()
allocates a new node to add to the list, SortedInsert() takes an existing node, and just
rearranges pointers to insert it into the list. There are many possible solutions to this
problem.
void SortedInsert(struct node** headRef, struct node* newNode) {
// Your code...
7 — InsertSort()
Write an InsertSort() function which given a list, rearranges its nodes so they are sorted in
increasing order. It should use SortedInsert().
// Given a list, change it to be in sorted order (using SortedInsert()).
void InsertSort(struct node** headRef) { // Your code
8 — Append()
Write an Append() function that takes two lists, 'a' and 'b', appends 'b' onto the end of 'a',
and then sets 'b' to NULL (since it is now trailing off the end of 'a'). Here is a drawing of
a sample call to Append(a, b) with the start state in gray and the end state in black. At the
end of the call, the 'a' list is {1, 2, 3, 4}, and 'b' list is empty.
Stack Heap
a 1 2
3 4
15
It turns out that both of the head pointers passed to Append(a, b) need to be reference
parameters since they both may need to be changed. The second 'b' parameter is always
set to NULL. When is 'a' changed? That case occurs when the 'a' list starts out empty. In
that case, the 'a' head must be changed from NULL to point to the 'b' list. Before the call
'b' is {3, 4}. After the call, 'a' is {3, 4}.
Stack Heap
3 4
// Append 'b' onto the end of 'a', and then set 'b' to NULL.
void Append(struct node** aRef, struct node** bRef) {
// Your code...
9 — FrontBackSplit()
Given a list, split it into two sublists — one for the front half, and one for the back half. If
the number of elements is odd, the extra element should go in the front list. So
FrontBackSplit() on the list {2, 3, 5, 7, 11} should yield the two lists {2, 3, 5} and {7,
11}. Getting this right for all the cases is harder than it looks. You should check your
solution against a few cases (length = 2, length = 3, length=4) to make sure that the list
gets split correctly near the short-list boundary conditions. If it works right for length=4,
it probably works right for length=1000. You will probably need special case code to deal
with the (length <2) cases.
Hint. Probably the simplest strategy is to compute the length of the list, then use a for
loop to hop over the right number of nodes to find the last node of the front half, and then
cut the list at that point. There is a trick technique that uses two pointers to traverse the
list. A "slow" pointer advances one nodes at a time, while the "fast" pointer goes two
nodes at a time. When the fast pointer reaches the end, the slow pointer will be about half
way. For either strategy, care is required to split the list at the right point.
/*
Split the nodes of the given list into front and back halves,
and return the two lists using the reference parameters.
If the length is odd, the extra node should go in the front list.
*/
void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {
// Your code...
16
10 RemoveDuplicates()
Write a RemoveDuplicates() function which takes a list sorted in increasing order and
deletes any duplicate nodes from the list. Ideally, the list should only be traversed once.
/*
Remove duplicates from a sorted list.
*/
void RemoveDuplicates(struct node* head) {
// Your code...
11 — MoveNode()
This is a variant on Push(). Instead of creating a new node and pushing it onto the given
list, MoveNode() takes two lists, removes the front node from the second list and pushes
it onto the front of the first. This turns out to be a handy utility function to have for
several later problems. Both Push() and MoveNode() are designed around the feature that
list operations work most naturally at the head of the list. Here's a simple example of
what MoveNode() should do...
void MoveNodeTest() {
struct node* a = BuildOneTwoThree(); // the list {1, 2, 3}
struct node* b = BuildOneTwoThree();
MoveNode(&a, &b);
// a == {1, 1, 2, 3}
// b == {2, 3}
}
/*
Take the node from the front of the source, and move it to
the front of the dest.
It is an error to call this with the source list empty.
*/
void MoveNode(struct node** destRef, struct node** sourceRef) {
// Your code
12 — AlternatingSplit()
Write a function AlternatingSplit() that takes one list and divides up its nodes to make
two smaller lists. The sublists should be made from alternating elements in the original
list. So if the original list is {a, b, a, b, a}, then one sublist should be {a, a, a} and the
other should be {b, b}. You may want to use MoveNode() as a helper. The elements in
the new lists may be in any order (for some implementations, it turns out to be convenient
if they are in the reverse order from the original list.)
/*
Given the source list, split its nodes into two shorter lists.
If we number the elements 0, 1, 2, ... then all the even elements
should go in the first list, and all the odd elements in the second.
The elements in the new lists may be in any order.
*/
void AlternatingSplit(struct node* source,
struct node** aRef, struct node** bRef) {
// Your code
17
13— ShuffleMerge()
Given two lists, merge their nodes together to make one list, taking nodes alternately
between the two lists. So ShuffleMerge() with {1, 2, 3} and {7, 13, 1} should yield {1, 7,
2, 13, 3, 1}. If either list runs out, all the nodes should be taken from the other list. The
solution depends on being able to move nodes to the end of a list as discussed in the
Section 1 review. You may want to use MoveNode() as a helper. Overall, many
techniques are possible: dummy node, local reference, or recursion. Using this function
and FrontBackSplit(), you could simulate the shuffling of cards.
/*
Merge the nodes of the two lists into a single list taking a node
alternately from each list, and return the new list.
*/
struct node* ShuffleMerge(struct node* a, struct node* b) {
// Your code
14 — SortedMerge()
Write a SortedMerge() function that takes two lists, each of which is sorted in increasing
order, and merges the two together into one list which is in increasing order.
SortedMerge() should return the new list. The new list should be made by splicing
together the nodes of the first two lists (use MoveNode()). Ideally, Merge() should only
make one pass through each list. Merge() is tricky to get right — it may be solved
iteratively or recursively. There are many cases to deal with: either 'a' or 'b' may be
empty, during processing either 'a' or 'b' may run out first, and finally there's the problem
of starting the result list empty, and building it up while going through 'a' and 'b'.
/*
Takes two lists sorted in increasing order, and
splices their nodes together to make one big
sorted list which is returned.
*/
struct node* SortedMerge(struct node* a, struct node* b) {
// your code...
15 — MergeSort()
(This problem requires recursion) Given FrontBackSplit() and SortedMerge(), it's pretty
easy to write a classic recursive MergeSort(): split the list into two smaller lists,
recursively sort those lists, and finally merge the two sorted lists together into a single
sorted list. Ironically, this problem is easier than either FrontBackSplit() or
SortedMerge().
void MergeSort(struct node* headRef) {
// Your code...
18
16 — SortedIntersect()
Given two lists sorted in increasing order, create and return a new list representing the
intersection of the two lists. The new list should be made with its own memory — the
original lists should not be changed. In other words, this should be Push() list building,
not MoveNode(). Ideally, each list should only be traversed once. This problem along
with Union() and Difference() form a family of clever algorithms that exploit the
constraint that the lists are sorted to find common nodes efficiently.
/*
Compute a new sorted list that represents the intersection
of the two given sorted lists.
*/
struct node* SortedIntersect(struct node* a, struct node* b) {
// Your code
17 — Reverse()
Write an iterative Reverse() function that reverses a list by rearranging all the .next
pointers and the head pointer. Ideally, Reverse() should only need to make one pass of the
list. The iterative solution is moderately complex. It's not so difficult that it needs to be
this late in the document, but it goes here so it can be next to #18 Recursive Reverse
which is quite tricky. The efficient recursive solution is quite complex (see next
problem). (A memory drawing and some hints for Reverse() are below.)
void ReverseTest() {
struct node* head;
head = BuildOneTwoThree();
Reverse(&head);
// head now points to the list {3, 2, 1}
Stack Heap
List reverse before and after. Before (in
ReverseTest() gray) the list is {1, 2, 3}. After (in black),
the pointers have been rearranged so the
head list is {3, 2, 1}.
1 2 3
the existing node instead of allocating a new node. You can use MoveNode() to do most
of the work, or hand code the pointer re-arrangement.
18 — RecursiveReverse()
(This problem is difficult and is only possible if you are familiar with recursion.) There is
a short and efficient recursive solution to this problem. As before, the code should only
make a single pass over the list. Doing it with multiple passes is easier but very slow, so
here we insist on doing it in one pass.. Solving this problem requires a real understanding
of pointer code and recursion.
/*
Recursively reverses the given linked list by changing its .next
pointers and its head pointer in one pass of the list.
*/
void RecursiveReverse(struct node** headRef) {
// your code...
Section 3 — Solutions
1 — Count() Solution
A straightforward iteration down the list — just like Length().
int Count(struct node* head, int searchFor) {
struct node* current = head;
int count = 0;
return count;
}
Alternately, the iteration may be coded with a for loop instead of a while...
int Count2(struct node* head, int searchFor) {
struct node* current;
int count = 0;
return count;
}
2 — GetNth() Solution
Combine standard list iteration with the additional problem of counting over to find the
right node. Off-by-one errors are common in this sort of code. Check it carefully against a
simple case. If it's right for n=0, n=1, and n=2, it will probably be right for n=1000.
int GetNth(struct node* head, int index) {
struct node* current = head;
int count = 0; // the index of the node we're currently looking at
3 — DeleteList() Solution
Delete the whole list and set the head pointer to NULL. There is a slight complication
inside the loop, since we need extract the .next pointer before we delete the node, since
after the delete it will be technically unavailable.
void DeleteList(struct node** headRef) {
struct node* current = *headRef; // deref headRef to get the real head
struct node* next;
*headRef = NULL; // Again, deref headRef to affect the real head back
// in the caller.
}
4 — Pop() Solution
Extract the data from the head node, delete the node, advance the head pointer to point at
the next node in line. Uses a reference parameter since it changes the head pointer.
int Pop(struct node** headRef) {
struct node* head;
int result;
head = *headRef;
assert(head != NULL);
result = head->data; // pull out the data before the node is deleted
5 — InsertNth() Solution
This code handles inserting at the very front as a special case. Otherwise, it works by
running a current pointer to the node before where the new node should go. Uses a for
loop to march the pointer forward. The exact bounds of the loop (the use of < vs <=, n vs.
n-1) are always tricky — the best approach is to get the general structure of the iteration
correct first, and then make a careful drawing of a couple test cases to adjust the n vs. n-1
cases to be correct. (The so called "OBOB" — Off By One Boundary cases.) The OBOB
cases are always tricky and not that interesting. Write the correct basic structure and then
use a test case to get the OBOB cases correct. Once the insertion point has been
determined, this solution uses Push() to do the link in. Alternately, the 3-Step Link In
code could be pasted here directly.
22
6 — SortedInsert() Solution
The basic strategy is to iterate down the list looking for the place to insert the new node.
That could be the end of the list, or a point just before a node which is larger than the new
node. The three solutions presented handle the "head end" case in different ways...
// Uses special case code for the head end
void SortedInsert(struct node** headRef, struct node* newNode) {
// Special case for the head end
if (*headRef == NULL || (*headRef)->data >= newNode->data) {
newNode->next = *headRef;
*headRef = newNode;
}
else {
// Locate the node before the point of insertion
struct node* current = *headRef;
while (current->next!=NULL && current->next->data<newNode->data) {
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
}
}
newNode->next = current->next;
current->next = newNode;
23
*headRef = dummy.next;
}
7 — InsertSort() Solution
Start with an empty result list. Iterate through the source list and SortedInsert() each of its
nodes into the result list. Be careful to note the .next field in each node before moving
it into the result list.
// Given a list, change it to be in sorted order (using SortedInsert()).
void InsertSort(struct node** headRef) {
struct node* result = NULL; // build the answer here
struct node* current = *headRef; // iterate over the original list
struct node* next;
while (current!=NULL) {
next = current->next; // tricky - note the next pointer before we change it
SortedInsert(&result, current);
current = next;
}
*headRef = result;
}
8 — Append() Solution
The case where the 'a' list is empty is a special case handled first — in that case the 'a'
head pointer needs to be changed directly. Otherwise we iterate down the 'a' list until we
find its last node with the test (current->next != NULL), and then tack on the 'b'
list there. Finally, the original 'b' head is set to NULL. This code demonstrates extensive
use of pointer reference parameters, and the common problem of needing to locate the
last node in a list. (There is also a drawing of how Append() uses memory below.)
void Append(struct node** aRef, struct node** bRef) {
struct node* current;
// set a to {1, 2}
// set b to {3, 4}
Append(&a, &b);
}
Stack Heap
AppendTest()
a 1 2
Append(&a, &b)
aRef 3 4
bRef
current
9 — FrontBackSplit() Solution
Two solutions are presented...
// Uses the "count the nodes" strategy
void FrontBackSplit(struct node* source,
struct node** frontRef, struct node** backRef) {
if (len < 2) {
*frontRef = source;
*backRef = NULL;
}
else {
int hopCount = (len-1)/2; //(figured these with a few drawings)
for (i = 0; i<hopCount; i++) {
current = current->next;
}
10 — RemoveDuplicates() Solution
Since the list is sorted, we can proceed down the list and compare adjacent nodes. When
adjacent nodes are the same, remove the second one. There's a tricky case where the node
after the next node needs to be noted before the deletion.
// Remove duplicates from a sorted list
void RemoveDuplicates(struct node* head) {
struct node* current = head;
26
11 — MoveNode() Solution
The MoveNode() code is most similar to the code for Push(). It's short — just changing a
couple pointers — but it's complex. Make a drawing.
void MoveNode(struct node** destRef, struct node** sourceRef) {
struct node* newNode = *sourceRef; // the front source node
assert(newNode != NULL);
newNode->next = *destRef; // Link the old dest off the new node
*destRef = newNode; // Move dest to point to the new node
}
12 — AlternatingSplit() Solution
The simplest approach iterates over the source list and use MoveNode() to pull nodes off
the source and alternately put them on 'a' and b'. The only strange part is that the nodes
will be in the reverse order that they occurred in the source list.
AlternatingSplit()
void AlternatingSplit(struct node* source,
struct node** aRef, struct node** bRef) {
struct node* a = NULL; // Split the nodes to these 'a' and 'b' lists
struct node* b = NULL;
aDummy.next = NULL;
bDummy.next = NULL;
*aRef = aDummy.next;
*bRef = bDummy.next;
}
13 SuffleMerge() Solution
There are four separate solutions included. See Section 1 for information on the various
dummy node and reference techniques.
while (1) {
if (a==NULL) { // empty list cases
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else { // common case: move two nodes to tail
tail->next = a;
tail = a;
a = a->next;
28
tail->next = b;
tail = b;
b = b->next;
}
}
return(dummy.next);
}
while (1) {
if (a==NULL) {
tail->next = b;
break;
}
else if (b==NULL) {
tail->next = a;
break;
}
else {
MoveNode(&(tail->next), &a);
tail = tail->next;
MoveNode(&(tail->next), &b);
tail = tail->next;
}
}
return(dummy.next);
}
while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}
else {
MoveNode(lastPtrRef, &a);
lastPtrRef = &((*lastPtrRef)->next);
MoveNode(lastPtrRef, &b);
lastPtrRef = &((*lastPtrRef)->next);
29
}
}
return(result);
}
SuffleMerge() — Recursive
The recursive solution is the most compact of all, but is probably not appropriate for
production code since it uses stack space proportionate to the lengths of the lists.
struct node* ShuffleMerge(struct node* a, struct node* b) {
struct node* result;
struct node* recur;
14 — SortedMerge() Solution
SortedMerge() Using Dummy Nodes
The strategy here uses a temporary dummy node as the start of the result list. The pointer
tail always points to the last node in the result list, so appending new nodes is easy.
The dummy node gives tail something to point to initially when the result list is empty.
This dummy node is efficient, since it is only temporary, and it is allocated in the stack.
The loop proceeds, removing one node from either 'a' or 'b', and adding it to tail. When
we are done, the result is in dummy.next.
struct node* SortedMerge(struct node* a, struct node* b) {
struct node dummy; // a dummy first node to hang the result on
struct node* tail = &dummy; // Points to the last result node --
// so tail->next is the place to add
// new nodes to the result.
dummy.next = NULL;
while (1) {
if (a == NULL) { // if either list runs out, use the other list
tail->next = b;
break;
}
else if (b == NULL) {
tail->next = a;
break;
}
30
return(dummy.next);
}
while (1) {
if (a==NULL) {
*lastPtrRef = b;
break;
}
else if (b==NULL) {
*lastPtrRef = a;
break;
}
return(result);
}
// Base cases
if (a==NULL) return(b);
else if (b==NULL) return(a);
return(result);
}
15 — MergeSort() Solution
The MergeSort strategy is: split into sublists, sort the sublists recursively, merge the two
sorted lists together to form the answer.
void MergeSort(struct node** headRef) {
struct node* head = *headRef;
struct node* a;
struct node* b;
FrontBackSplit(head, &a, &b); // Split head into 'a' and 'b' sublists
// We could just as well use AlternatingSplit()
*headRef = SortedMerge(a, b); // answer = merge the two sorted lists together
}
(Extra for experts) Using recursive stack space proportional to the length of a list is not
recommended. However, the recursion in this case is ok — it uses stack space which is
proportional to the log of the length of the list. For a 1000 node list, the recursion will
only go about 10 deep. For a 2000 node list, it will go 11 deep. If you think about it, you
can see that doubling the size of the list only increases the depth by 1.
16 — SortedIntersect() Solution
The strategy is to advance up both lists and build the result list as we go. When the
current point in both lists are the same, add a node to the result. Otherwise, advance
whichever list is smaller. By exploiting the fact that both lists are sorted, we only traverse
each list once. To build up the result list, both the dummy node and local reference
strategy solutions are shown...
// This solution uses the temporary dummy to build up the result list
struct node* SortedIntersect(struct node* a, struct node* b) {
struct node dummy;
32
dummy.next = NULL;
return(dummy.next);
}
return(result);
}
17 — Reverse() Solution
This first solution uses the "Push" strategy with the pointer re-arrangement hand coded
inside the loop. There's a slight trickyness in that it needs to save the value of the
"current->next" pointer at the top of the loop since the body of the loop overwrites that
pointer.
/*
Iterative list reverse.
Iterate through the list left-right.
33
current = next;
}
*headRef = result;
}
Here's the variation on the above that uses MoveNode() to do the work...
static void Reverse2(struct node** headRef) {
struct node* result = NULL;
struct node* current = *headRef;
*headRef = result;
}
/*
Plan for this loop: move three pointers: front, middle, back
down the list in order. Middle is the main pointer running
down the list. Front leads it and Back trails it.
For each step, reverse the middle pointer and then advance all
three to get the next node.
*/
struct node* front = middle->next; // the two other pointers (NULL ok)
struct node* back = NULL;
while (1) {
middle->next = back; // fix the middle node
*headRef = middle; // fix the head pointer to point to the new front
}
}
18 — RecursiveReverse() Solution
Probably the hardest part is accepting the concept that the
RecursiveReverse(&rest) does in fact reverse the rest. Then then there's a trick
to getting the one front node all the way to the end of the list. Make a drwaing to see how
the trick works.
void RecursiveReverse(struct node** headRef) {
struct node* first;
struct node* rest;
first->next->next = first; // put the first elem on the end of the list
first->next = NULL; // (tricky step -- make a drawing)
The inefficient soluition is to reverse the last n-1 elements of the list, and then iterate all
the way down to the new tail and put the old head node there. That solution is very slow
compared to the above which gets the head node in the right place without extra iteration.
35
Appendix
Basic Utility Function Implementations
Here is the source code for the basic utility functions.
Length()
// Return the number of nodes in a list
int Length(struct node* head) {
int count = 0;
struct node* current = head;
return(count);
}
Push()
// Given a reference (pointer to pointer) to the head
// of a list and an int, push a new node on the front of the list.
// Creates a new node with the int, links the list off the .next of the
// new node, and finally changes the head to point to the new node.
void Push(struct node** headRef, int newData) {
struct node* newNode =
(struct node*) malloc(sizeof(struct node)); // allocate node
newNode->data = newData; // put in the data
newNode->next = (*headRef); // link the old list off the new node
(*headRef) = newNode; // move the head to point to the new node
}
BuildOneTwoThree()
// Build and return the list {1, 2, 3}
struct node* BuildOneTwoThree() {
struct node* head = NULL; // Start with the empty list
Push(&head, 3); // Use Push() to add all the data
Push(&head, 2);
Push(&head, 1);
return(head);
}
Pointers and
Memory
By Nick Parlante Copyright ©1998-2000, Nick Parlante
Abstract
This document explains how pointers and memory work and how to use them—from the
basic concepts through all the major programming techniques. For each topic there is a
combination of discussion, sample C code, and drawings.
Audience
This document can be used as an introduction to pointers for someone with basic
programming experience. Alternately, it can be used to review and to fill in gaps for
someone with a partial understanding of pointers and memory. Many advanced
programming and debugging problems only make sense with a complete understanding
of pointers and memory — this document tries to provide that understanding. This
document concentrates on explaining how pointers work. For more advanced pointer
applications and practice problems, see the other resources below.
Pace
Like most CS Education Library documents, the coverage here tries to be complete but
fast. The document starts with the basics and advances through all the major topics. The
pace is fairly quick — each basic concept is covered once and usually there is some
example code and a memory drawing. Then the text moves on to the next topic. For more
practice, you can take the time to work through the examples and sample problems. Also,
see the references below for more practice problems.
Topics
Topics include: pointers, local memory, allocation, deallocation, dereference operations,
pointer assignment, deep vs. shallow copies, the ampersand operator (&), bad pointers,
the NULL pointer, value parameters, reference parameters, heap allocation and
deallocation, memory ownership models, and memory leaks. The text focuses on pointers
and memory in compiled languages like C and C++. At the end of each section, there is
some related but optional material, and in particular there are occasional notes on other
languages, such as Java.
Pointers and Memory – document #102 in the Stanford CS Education Library. This and
other free educational materials are available at http://cslibrary.stanford.edu/102/. This
document is free to be used, reproduced, sold, or retransmitted so long as this notice is
clearly reproduced at its beginning.
Other CS Education Library Documents
• Point Fun With Binky Video (http://cslibrary.stanford.edu/104/)
A silly video about pointer basics.
• Essential C (http://cslibrary.stanford.edu/101/)
Complete coverage of the C language, including all of the syntax used in
this document.
Table of Contents
Section 1 Basic Pointers.......................................................................... pg. 3
The basic rules and drawings for pointers: pointers, pointees, pointer
assignment (=), pointer comparison (==), the ampersand operator (&), the
NULL pointer, bad pointers, and bad dereferences.
Edition
The first edition of this document was on Jan 19, 1999. This Feb 21, 2000 edition
represents only very minor changes. The author may be reached at
[email protected]. The CS Education Library may be reached at
[email protected].
Dedication
This document is distributed for the benefit and education of all. That someone seeking
education should have the opportunity to find it. May you learn from it in the spirit in
which it is given — to make efficiency and beauty in your designs, peace and fairness in
your actions.
Section 1 —
Basic Pointers
Pointers — Before and After
There's a lot of nice, tidy code you can write without knowing about pointers. But once
you learn to use the power of pointers, you can never go back. There are too many things
that can only be done with pointers. But with increased power comes increased
responsibility. Pointers allow new and more ugly types of bugs, and pointer bugs can
crash in random ways which makes them more difficult to debug. Nonetheless, even with
their problems, pointers are an irresistibly powerful programming construct. (The
following explanation uses the C language syntax where a syntax is required; there is a
discussion of Java at the section.)
What Is A Pointer?
Simple int and float variables operate pretty intuitively. An int variable is like a
box which can store a single int value such as 42. In a drawing, a simple variable is a
box with its current value drawn inside.
num 42
A pointer works a little differently— it does not store a simple value directly. Instead, a
pointer stores a reference to another value. The variable the pointer refers to is
sometimes known as its "pointee". In a drawing, a pointer is a box which contains the
beginning of an arrow which leads to its pointee. (There is no single, official, word for
the concept of a pointee — pointee is just the word used in these explanations.)
The following drawing shows two variables: num and numPtr. The simple variable num
contains the value 42 in the usual way. The variable numPtr is a pointer which contains
a reference to the variable num. The numPtr variable is the pointer and num is its
pointee. What is stored inside of numPtr? Its value is not an int. Its value is a
reference to an int.
Pointer Dereference
The "dereference" operation follows a pointer's reference to get the value of its pointee.
The value of the dereference of numPtr above is 42. When the dereference operation is
used correctly, it's simple. It just accesses the value of the pointee. The only restriction is
that the pointer must have a pointee for the dereference to access. Almost all bugs in
pointer code involve violating that one restriction. A pointer must be assigned a pointee
before dereference operations will work.
numPtr
The C language uses the symbol NULL for this purpose. NULL is equal to the integer
constant 0, so NULL can play the role of a boolean false. Official C++ no longer uses the
NULL symbolic constant — use the integer constant 0 directly. Java uses the symbol
null.
Pointer Assignment
The assignment operation (=) between two pointers makes them point to the same
pointee. It's a simple rule for a potentially complex situation, so it is worth repeating:
assigning one pointer to another makes them point to the same thing. The example below
adds a second pointer, second, assigned with the statement second = numPtr;.
The result is that second points to the same pointee as numPtr. In the drawing, this
means that the second and numPtr boxes both contain arrows pointing to num.
Assignment between pointers does not change or even touch the pointees. It just changes
which pointee a pointer refers to.
num 42
Make A Drawing
Memory drawings are the key to thinking about pointer code. When you are looking at
code, thinking about how it will use memory at run time....make a quick drawing to work
out your ideas. This article certainly uses drawings to show how pointers work. That's the
way to do it.
5
Sharing
Two pointers which both refer to a single pointee are said to be "sharing". That two or
more entities can cooperatively share a single memory structure is a key advantage of
pointers in all computer languages. Pointer manipulation is just technique — sharing is
often the real goal. In Section 3 we will see how sharing can be used to provide efficient
communication between parts of a program.
A() A()
B() B()
Bad Pointers
When a pointer is first allocated, it does not have a pointee. The pointer is "uninitialized"
or simply "bad". A dereference operation on a bad pointer is a serious runtime error. If
you are lucky, the dereference operation will crash or halt immediately (Java behaves this
way). If you are unlucky, the bad pointer dereference will corrupt a random area of
memory, slightly altering the operation of the program so that it goes wrong some
indefinite time later. Each pointer must be assigned a pointee before it can support
dereference operations. Before that, the pointer is bad and must not be used. In our
memory drawings, the bad pointer value is shown with an XXX value...
numPtr
Bad pointers are very common. In fact, every pointer starts out with a bad value.
Correct code overwrites the bad value with a correct reference to a pointee, and thereafter
the pointer works fine. There is nothing automatic that gives a pointer a valid pointee.
6
Quite the opposite — most languages make it easy to omit this important step. You just
have to program carefully. If your code is crashing, a bad pointer should be your first
suspicion.
Pointers in dynamic languages such as Perl, LISP, and Java work a little differently. The
run-time system sets each pointer to NULL when it is allocated and checks it each time it
is dereferenced. So code can still exhibit pointer bugs, but they will halt politely on the
offending line instead of crashing haphazardly like C. As a result, it is much easier to
locate and fix pointer bugs in dynamic languages. The run-time checks are also a reason
why such languages always run at least a little slower than a compiled language like C or
C++.
Two Levels
One way to think about pointer code is that operates at two levels — pointer level and
pointee level. The trick is that both levels need to be initialized and connected for things
to work. (1) the pointer must be allocated, (1) the pointee must be allocated, and (3) the
pointer must be assigned to point to the pointee. It's rare to forget step (1). But forget (2)
or (3), and the whole thing will blow up at the first dereference. Remember to account for
both levels — make a memory drawing during your design to make sure it's right.
Syntax
The above basic features of pointers, pointees, dereferencing, and assigning are the only
concepts you need to build pointer code. However, in order to talk about pointer code, we
need to use a known syntax which is about as interesting as....a syntax. We will use the C
language syntax which has the advantage that it has influenced the syntaxes of several
languages.
num 42
numPtr
void NumPtrExample() {
int num;
int* numPtr;
num = 42;
numPtr = # // Compute a reference to "num", and store it in numPtr
// At this point, memory looks like drawing above
}
It is possible to use & in a way which compiles fine but which creates problems at run
time — the full discussion of how to correctly use & is in Section 2. For now we will just
use & in a simple way.
a 1 p
b 2 q
c 3
a 1 p
b 2 q
c 3
a 1 p
b 13 q
c 1
}
Pow!
9
• Assignment between two pointers makes them refer to the same pointee
which introduces sharing.
Section 2 —
Local Memory
Thanks For The Memory
Local variables are the programming structure everyone uses but no one thinks about.
You think about them a little when first mastering the syntax. But after a few weeks, the
variables are so automatic that you soon forget to think about how they work. This
situation is a credit to modern programming languages— most of the time variables
appear automatically when you need them, and they disappear automatically when you
are finished. For basic programming, this is a fine situation. However, for advanced
programming, it's going to be useful to have an idea of how variables work...
Local Memory
The most common variables you use are "local" variables within functions such as the
variables num and result in the following function. All of the local variables and
parameters taken together are called its "local storage" or just its "locals", such as num
and result in the following code...
// Local storage example
int Square(int num) {
int result;
return result;
}
The variables are called "local" to capture the idea that their lifetime is tied to the
function where they are declared. Whenever the function runs, its local variables are
allocated. When the function exits, its locals are deallocated. For the above example, that
means that when the Square() function is called, local storage is allocated for num and
result. Statements like result = num * num; in the function use the local
storage. When the function finally exits, its local storage is deallocated.
12
2. The memory for the locals continues to be allocated so long as the thread
of control is within the owning function. Locals continue to exist even if
the function temporarily passes off the thread of control by calling another
function. The locals exist undisturbed through all of this.
3. Finally, when the function finishes and exits, its locals are deallocated.
This makes sense in a way — suppose the locals were somehow to
continue to exist — how could the code even refer to them? The names
like num and result only make sense within the body of Square()
anyway. Once the flow of control leaves that body, there is no way to refer
to the locals even if they were allocated. That locals are available
("scoped") only within their owning function is known as "lexical
scoping" and pretty much all languages do it that way now.
} // (4) The locals are all deallocated when the function exits.
void X() {
int a = 1;
int b = 2;
// T1
Y(a);
// T3
Y(b);
// T5
void Y(int p) {
int q;
q = p + 2;
// T2 (first time through), T4 (second time through)
}
a 1 a 1 a 1 a 1 a 1
X() X() X() X() X()
b 2 b 2 b 2 b 2 b 2
(optional extra...) The drawing shows the sequence of the locals being allocated and
deallocated — in effect the drawing shows the operation over time of the "stack" which is
the data structure which the system uses to implement local storage.
Advantages Of Locals
Locals are great for 90% of a program's memory needs....
Convenient. Locals satisfy a convenient need — functions often need
some temporary memory which exists only during the function's
computation. Local variables conveniently provide this sort of temporary,
independent memory.
Disadvantages Of Locals
There are two disadvantages of Locals
Short Lifetime. Their allocation and deallocation schedule (their
"lifetime") is very strict. Sometimes a program needs memory which
continues to be allocated even after the function which originally allocated
it has exited. Local variables will not work since they are deallocated
automatically when their owning function exits. This problem will be
solved later in Section 4 with "heap" memory.
void Victim() {
int* ptr;
ptr = TAB();
*ptr = 42; // Runtime error! The pointee was local to TAB
}
15
TAB() is actually fine while it is running. The problem happens to its caller after TAB()
exits. TAB() returns a pointer to an int, but where is that int allocated? The problem is
that the local int, temp, is allocated only while TAB() is running. When TAB() exits,
all of its locals are deallocated. So the caller is left with a pointer to a deallocated
variable. TAB()'s locals are deallocated when it exits, just as happened to the locals for
Y() in the previous example.
It is incorrect (and useless) for TAB() to return a pointer to memory which is about to be
deallocated. We are essentially running into the "lifetime" constraint of local variables.
We want the int to exist, but it gets deallocated automatically. Not all uses of & between
functions are incorrect — only when used to pass a pointer back to the caller. The correct
uses of & are discussed in section 3, and the way to pass a pointer back to the caller is
shown in section 4.
3. Store the caller's current address of execution (its "return address") and
switch execution to foo().
4. foo() executes with its local block conveniently available at the end of the
call stack.
5. When foo() is finished, it exits by popping its locals off the stack and
"returns" to the caller using the previously stored return address. Now the
caller's locals are on the end of the stack and it can resume executing.
16
For the extremely curious, here are other miscellaneous notes on the function call
process...
• This is why infinite recursion results in a "Stack Overflow Error" — the
code keeps calling and calling resulting in steps (1) (2) (3), (1) (2) (3), but
never a step (4)....eventually the call stack runs out of memory.
• This is why local variables have random initial values — step (2) just
pushes the whole local block in one operation. Each local gets its own area
of memory, but the memory will contain whatever the most recent tenant
left there. To clear all of the local block for each function call would be
too time expensive.
• For a multithreaded environment, each thread gets its own call stack
instead of just having single, global call stack.
Section 3 —
Reference Parameters
In the simplest "pass by value" or "value parameter" scheme, each function has separate,
local memory and parameters are copied from the caller to the callee at the moment of the
function call. But what about the other direction? How can the callee communicate back
to its caller? Using a "return" at the end of the callee to copy a result back to the caller
works for simple cases, but does not work well for all situations. Also, sometimes
copying values back and forth is undesirable. "Pass by reference" parameters solve all of
these problems.
For the following discussion, the term "value of interest" will be a value that the caller
and callee wish to communicate between each other. A reference parameter passes a
pointer to the value of interest instead of a copy of the value of interest. This technique
uses the sharing property of pointers so that the caller and callee can share the value of
interest.
B(netWorth);
// T3 -- B() did not change netWorth
}
T1 -- The value of interest T2 -- netWorth is copied T3 -- B() exits and its local
netWorth is local to A(). to B()'s local worth. B() worth is deallocated. The
changes its local worth value of interest has not
from 55 to 56. been changed.
B() worth 55 56
B() adds 1 to its local worth copy, but when B() exits, worth is deallocated, so
changing it was useless. The value of interest, netWorth, rests unchanged the whole
time in A()'s local storage. A function can change its local copy of the value of interest,
but that change is not reflected back in the original value. This is really just the old
"independence" property of local storage, but in this case it is not what is wanted.
By Reference
The reference solution to the Bill Gates problem is to use a single netWorth variable
for the value of interest and never copy it. Instead, each function can receives a pointer to
netWorth. Each function can see the current value of netWorth by dereferencing its
pointer. More importantly, each function can change the net worth — just dereference
the pointer to the centralized netWorth and change it directly. Everyone agrees what
the current value of netWorth because it exists in only one place — everyone has a
pointer to the one master copy. The following memory drawing shows A() and B()
functions changed to use "reference" parameters. As before, T1, T2, and T3 correspond to
points in the code (below), but you can study the memory structure without looking at the
code yet.
B() worth
Passing By Reference
Here are the steps to use in the code to use the pass-by-reference strategy...
• Have a single copy of the value of interest. The single "master" copy.
• Pass pointers to that value to any function which wants to see or change
the value.
• Functions must remember that they do not have their own local copies. If
they dereference their pointer and change the value, they really are
changing the master value. If a function wants a local copy to change
safely, the function must explicitly allocate and initialize such a local
copy.
19
Syntax
The syntax for by reference parameters in the C language just uses pointer operations on
the parameters...
1. Suppose a function wants to communicate about some value of interest —
int or float or struct fraction.
3. At the time of the call, the caller computes a pointer to the value of interest
and passes that pointer. The type of the pointer (pointer to the value of
interest) will agree with the type in (2) above. If the value of interest is
local to the caller, then this will often involve a use of the & operator
(Section 1).
void A() {
int netWorth;
netWorth = 55; // T1 -- the value of interest is local to A()
Swap() Function
The values of interest for Swap() are two ints. Therefore, Swap() does not take ints
as its parameters. It takes a pointers to int — (int*)'s. In the body of Swap() the
parameters, a and b, are dereferenced with * to get at the actual (int) values of interest.
void Swap(int* a, int* b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
Swap() Caller
To call Swap(), the caller must pass pointers to the values of interest...
void SwapCaller() {
int x = 1;
int y = 2;
Swap(&x, &y); // Use & to pass pointers to the int values of interest
// (x and y).
}
Swap() a b temp 1
SwapCaller() x 1 2 y2 1
The parameters to Swap() are pointers to values of interest which are back in the caller's
locals. The Swap() code can dereference the pointers to get back to the caller's memory to
exchange the values. In this case, Swap() follows the pointers to exchange the values in
the variables x and y back in SwapCaller(). Swap() will exchange any two ints given
pointers to those two ints.
The ** Case
What if the value of interest to be shared and changed between the caller and callee is
already a pointer, such as an int* or a struct fraction*? Does that change the
rules for setting up reference parameters? No. In that case, there is no change in the rules.
They operate just as before. The reference parameter is still a pointer to the value of
interest, even if the value of interest is itself a pointer. Suppose the value of interest is
int*. This means there is an int* value which the caller and callee want to share and
change. Then the reference parameter should be an int**. For a struct
fraction* value of interest, the reference parameter is struct fraction**. A
single dereference (*) operation on the reference parameter yields the value of interest as
it did in the simple cases. Double pointer (**) parameters are common in linked list or
other pointer manipulating code were the value of interest to share and change is itself a
pointer, such as a linked list head pointer.
22
void SwapCaller() {
int x = 1;
int y = 2;
The types of the various variables and parameters operate simply as they are declared
(int in this case). The complicating layer of pointers required to implement the
reference parameters is hidden. The compiler takes care of it without allowing the
complication to disturb the types in the source code.
24
Section 4 —
Heap Memory
"Heap" memory, also known as "dynamic" memory, is an alternative to local stack
memory. Local memory (Section 2) is quite automatic — it is allocated automatically on
function call and it is deallocated automatically when a function exits. Heap memory is
different in every way. The programmer explicitly requests the allocation of a memory
"block" of a particular size, and the block continues to be allocated until the programmer
explicitly requests that it be deallocated. Nothing happens automatically. So the
programmer has much greater control of memory, but with greater responsibility since
the memory must now be actively managed. The advantages of heap memory are...
Lifetime. Because the programmer now controls exactly when memory is
allocated and deallocated, it is possible to build a data structure in
memory, and return that data structure to the caller. This was never
possible with local memory which was automatically deallocated when the
function exited.
Size. The size of allocated memory can be controlled with more detail.
For example, a string buffer can be allocated at run-time which is exactly
the right size to hold a particular string. With local memory, the code is
more likely to declare a buffer size 1000 and hope for the best. (See the
StringCopy() example below.)
More Bugs. Because it's now done explicitly in the code, realistically on
occasion the allocation will be done incorrectly leading to memory bugs.
Local memory is constrained, but at least it's never wrong.
Nonetheless, there are many problems that can only be solved with heap memory, so
that's that way it has to be. In languages with garbage collectors such as Perl, LISP, or
Java, the above disadvantages are mostly eliminated. The garbage collector takes over
most of the responsibility for heap management at the cost of a little extra time taken at
run-time.
Allocation
The heap is a large area of memory available for use by the program. The program can
request areas, or "blocks", of memory for its use within the heap. In order to allocate a
block of some size, the program makes an explicit request by calling the heap allocation
function. The allocation function reserves a block of memory of the requested size in the
heap and returns a pointer to it. Suppose a program makes three allocation requests to
25
allocate memory to hold three separate GIF images in the heap each of which takes 1024
bytes of memory. After the three allocation requests, memory might look like...
Local Heap
(Free)
(Gif3)
3 separate
heap
(Gif2) blocks —
each 1024
bytes in
(Gif1) size.
Each allocation request reserves a contiguous area of the requested size in the heap and
returns a pointer to that new block to the program. Since each block is always referred to
by a pointer, the block always plays the role of a "pointee" (Section 1) and the program
always manipulates its heap blocks through pointers. The heap block pointers are
sometimes known as "base address" pointers since by convention they point to the base
(lowest address byte) of the block.
In this example, the three blocks have been allocated contiguously starting at the bottom
of the heap, and each block is 1024 bytes in size as requested. In reality, the heap
manager can allocate the blocks wherever it wants in the heap so long as the blocks do
not overlap and they are at least the requested size. At any particular moment, some areas
in the heap have been allocated to the program, and so are "in use". Other areas have yet
to be committed and so are "free" and are available to satisfy allocation requests. The
heap manager has its own, private data structures to record what areas of the heap are
committed to what purpose at any moment The heap manager satisfies each allocation
request from the pool of free memory and updates its private data structures to record
which areas of the heap are in use.
Deallocation
When the program is finished using a block of memory, it makes an explicit deallocation
request to indicate to the heap manager that the program is now finished with that block.
The heap manager updates its private data structures to show that the area of memory
occupied by the block is free again and so may be re-used to satisfy future allocation
requests. Here's what the heap would look like if the program deallocates the second of
the three blocks...
26
Local Heap
(Free)
(Gif3)
(Free)
(Gif1)
After the deallocation, the pointer continues to point to the now deallocated block. The
program must not access the deallocated pointee. This is why the pointer is drawn in gray
— the pointer is there, but it must not be used. Sometimes the code will set the pointer to
NULL immediately after the deallocation to make explicit the fact that it is no longer
valid.
• There is some "heap manager" library code which manages the heap for
the program. The programmer makes requests to the heap manager, which
in turn manages the internals of the heap. In C, the heap is managed by the
ANSI library functions malloc(), free(), and realloc().
• The heap manager uses its own private data structures to keep track of
which blocks in the heap are "free" (available for use) and which blocks
are currently in use by the program and how large those blocks are.
Initially, all of the heap is free.
heap — its location and size are fixed once it is allocated. Generally, when
a block is allocated, its contents are random. The new owner is responsible
for setting the memory to something meaningful. Sometimes there is
variation on the memory allocation function which sets the block to all
zeros (calloc() in C).
C Specifics
In the C language, the library functions which make heap requests are malloc() ("memory
allocate") and free(). The prototypes for these functions are in the header file <stdlib.h>.
Although the syntax varies between languages, the roles of malloc() and free() are nearly
identical in all languages...
void* malloc(unsigned long size); The malloc() function
takes an unsigned integer which is the requested size of the block
measured in bytes. Malloc() returns a pointer to a new heap block if the
allocation is successful, and NULL if the request cannot be satisfied
because the heap is full. The C operator sizeof() is a convenient way to
compute the size in bytes of a type —sizeof(int) for an int pointee,
sizeof(struct fraction) for a struct fraction pointee.
void Heap1() {
int* intPtr;
// Allocates local pointer local variable (but not its pointee)
// T1
Local Heap
intPtr
intPtr 42
intPtr
• When the function exits, its local variable intPtr will be automatically
deallocated following the usual rules for local variables (Section 2). So
this function has tidy memory behavior — all of the memory it allocates
while running (its local variable, its one heap block) is deallocated by the
time it exits.
Heap Array
In the C language, it's convenient to allocate an array in the heap, since C can treat any
pointer as an array. The size of the array memory block is the size of each element (as
computed by the sizeof() operator) multiplied by the number of elements (See CS
Education Library/101 The C Language, for a complete discussion of C, and arrays and
pointers in particular). So the following code heap allocates an array of 100 struct
fraction's in the heap, sets them all to 22/7, and deallocates the heap array...
void HeapArray() {
struct fraction* fracts;
int i;
Memory Leaks
What happens if some memory is heap allocated, but never deallocated? A program
which forgets to deallocate a block is said to have a "memory leak" which may or may
not be a serious problem. The result will be that the heap gradually fill up as there
continue to be allocation requests, but no deallocation requests to return blocks for re-use.
For a program which runs, computes something, and exits immediately, memory leaks
are not usually a concern. Such a "one shot" program could omit all of its deallocation
requests and still mostly work. Memory leaks are more of a problem for a program which
runs for an indeterminate amount of time. In that case, the memory leaks can gradually
fill the heap until allocation requests cannot be satisfied, and the program stops working
or crashes. Many commercial programs have memory leaks, so that when run for long
enough, or with large data-sets, they fill their heaps and crash. Often the error detection
and avoidance code for the heap-full error condition is not well tested, precisely because
the case is rarely encountered with short runs of the program — that's why filling the
heap often results in a real crash instead of a polite error message. Most compilers have a
31
"heap debugging" utility which adds debugging code to a program to track every
allocation and deallocation. When an allocation has no matching deallocation, that's a
leak, and the heap debugger can help you find them.
Ownership
StringCopy() allocates the heap block, but it does not deallocate it. This is so the caller
can use the new string. However, this introduces the problem that somebody does need to
remember to deallocate the block, and it is not going to be StringCopy(). That is why the
comment for StringCopy() mentions specifically that the caller is taking on ownership of
the block. Every block of memory has exactly one "owner" who takes responsibility for
deallocating it. Other entities can have pointers, but they are just sharing. There's only
one owner, and the comment for StringCopy() makes it clear that ownership is being
passed from StringCopy() to the caller. Good documentation always remembers to
discuss the ownership rules which a function expects to apply to its parameters or return
value. Or put the other way, a frequent error in documentation is that it forgets to
mention, one way or the other, what the ownership rules are for a parameter or return
value. That's one way that memory errors and leaks are created.
Ownership Models
The two common patterns for ownership are...
Caller ownership. The caller owns its own memory. It may pass a pointer
to the callee for sharing purposes, but the caller retains ownership. The
callee can access things while it runs, and allocate and deallocate its own
memory, but it should not disrupt the caller's memory.
Callee allocated and returned. The callee allocates some memory and
returns it to the caller. This happens because the result of the callee
computation needs new memory to be stored or represented. The new
memory is passed to the caller so they can see the result, and the caller
must take over ownership of the memory. This is the pattern demonstrated
in StringCopy().
This article presents one of the neatest recursive pointer problems ever devised. This an advanced problem that
uses pointers, binary trees, linked lists, and some significant recursion. This article includes the problem statement,
a few explanatory diagrams, and sample solution code in Java and C. Thanks to Stuart Reges for originally showing
me the problem.
This is article #109 in the Stanford CS Education Library -- http://cslibrary.stanford.edu/109/. This and other free
educational materials are available at http://cslibrary.stanford.edu/. Permission is given for this article to be
used, reproduced, or sold so long this paragraph and the copyright are clearly reproduced. Related articles in the
library include Linked List Basics (#103), Linked List Problems (#105), and Binary Trees (#110).
Contents
Introduction
The problem will use two data structures -- an ordered binary tree and a circular doubly linked list. Both data
structures store sorted elements, but they look very different.
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 2
All the nodes in the "small" sub-tree are less than or equal to the data in the parent node. All the nodes in the
"large" sub-tree are greater than the parent node. So in the example above, all the nodes in the "small" sub-tree off
the 4 node are less than or equal to 4, and all the nodes in "large" sub-tree are greater than 4. That pattern applies
for each node in the tree. A null pointer effectively marks the end of a branch in the tree. Formally, a null pointer
represents a tree with zero elements. The pointer to the topmost node in a tree is called the "root".
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 3
The circular doubly linked list is a standard linked list with two additional features...
"Doubly linked" means that each node has two pointers -- the usual "next" pointer that points to the next
node in the list and a "previous" pointer to the previous node.
"Circular" means that the list does not terminate at the first and last nodes. Instead, the "next" from the
last node wraps around to the first node. Likewise, the "previous" from the first node wraps around to the
last node.
We'll use the convention that a null pointer represents a list with zero elements. It turns out that a length-1 list
looks a little silly...
The single node in a length-1 list is both the first and last node, so its pointers point to itself. Fortunately, the
length-1 case obeys the rules above so no special case is required.
and "large" while in the list they are labeled "previous" and "next". Ignoring the labeling, the two node types are
the same.
3. The Challenge
The challenge is to take an ordered binary tree and rearrange the internal pointers to make a circular doubly linked
list out of it. The "small" pointer should play the role of "previous" and the "large" pointer should play the role of
"next". The list should be arranged so that the nodes are in increasing order...
This drawing shows the original tree drawn with plain black lines with the "next" pointers for the desired list
structure drawn as arrows. The "previous" pointers are not shown.
Complete Drawing
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 5
Figure-5 -- original tree with "next" and "previous" list arrows added
This drawing shows the all of the problem state -- the original tree is drawn with plain black lines and the
desired next/previous pointers are added in as arrows. Notice that starting with the head pointer, the structure of
next/previous pointers defines a list of the numbers 1 through 5 with exactly the same structure as the list in
figure-2. Although the nodes appear to have different spatial arrangement between the two drawings, that's just
an artifact of the drawing. The structure defined by the the pointers is what matters.
4. Problem Statement
Here's the formal problem statement: Write a recursive function treeToList(Node root) that takes an ordered
binary tree and rearranges the internal pointers to make a circular doubly linked list out of the tree nodes. The
"previous" pointers should be stored in the "small" field and the "next" pointers should be stored in the "large"
field. The list should be arranged so that the nodes are in increasing order. Return the head pointer to the new list.
The operation can be done in O(n) time -- essentially operating on each node once. Basically take figure-1 as input
and rearrange the pointers to make figure-2.
Hints
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 6
Hint #1
The recursion is key. Trust that the recursive call on each sub-tree works and concentrate on assembling the outputs
of the recursive calls to build the result. It's too complex to delve into how each recursive call is going to work --
trust that it did work and assemble the answer from there.
Hint #2
The recursion will go down the tree, recursively changing the small and large sub-trees into lists, and then append
those lists together with the parent node to make larger lists. Separate out a utility function append(Node a,
Node b) that takes two circular doubly linked lists and appends them together to make one list which is returned.
Writing a separate utility function helps move some of the complexity out of the recursive function.
Trust that the recursive calls return correct output when fed correct input -- make the leap of faith. Look at
the partial results that the recursive calls give you, and construct the full result from them. If you try to
step into the recursive calls to think how they are working, you'll go crazy.
Decomposing out well defined helper functions is a good idea. Writing the list-append code separately
helps you concentrate on the recursion which is complex enough on its own.
/*
This is the simple Node class from which the tree and list
are built. This does not have any methods -- it's just used
as dumb storage by TreeList.
The code below tries to be clear where it treats a Node pointer
as a tree vs. where it is treated as a list.
*/
class Node {
int data;
Node small;
Node large;
this.data = data;
small = null;
large = null;
}
}
/*
TreeList main methods:
-join() -- utility to connect two list nodes
-append() -- utility to append two lists
-treeToList() -- the core recursive function
-treeInsert() -- used to build the tree
*/
class TreeList {
/*
helper function -- given two list nodes, join them
together so the second immediately follow the first.
Sets the .next of the first and the .previous of the second.
*/
public static void join(Node a, Node b) {
a.large = b;
b.small = a;
}
/*
helper function -- given two circular doubly linked
lists, append them and return the new list.
*/
public static Node append(Node a, Node b) {
// if either is null, return the other
if (a==null) return(b);
if (b==null) return(a);
return(a);
}
/*
--Recursion--
Given an ordered binary tree, recursively change it into
a circular doubly linked list which is returned.
*/
public static Node treeToList(Node root) {
// base case: empty tree -> empty list
if (root==null) return(null);
root.small = root;
root.large = root;
return(aList);
}
/*
Given a non-empty tree, insert a new node in the proper
place. The tree must be non-empty because Java's lack
of reference variables makes that case and this
method messier than they should be.
*/
public static void treeInsert(Node root, int newData) {
if (newData<=root.data) {
if (root.small!=null) treeInsert(root.small, newData);
else root.small = new Node(newData);
}
else {
if (root.large!=null) treeInsert(root.large, newData);
else root.large = new Node(newData);
}
}
System.out.println();
}
treeInsert(root, 1);
treeInsert(root, 3);
treeInsert(root, 5);
System.out.println("tree:");
printTree(root); // 1 2 3 4 5
System.out.println();
System.out.println("list:");
Node head = treeToList(root);
printList(head); // 1 2 3 4 5 yay!
}
}
C Solution Code
/*
TreeList.c
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
/* The node type from which both the tree and list are built */
struct node {
int data;
struct node* small;
struct node* large;
};
typedef struct node* Node;
/*
helper function -- given two list nodes, join them
together so the second immediately follow the first.
Sets the .next of the first and the .previous of the second.
*/
static void join(Node a, Node b) {
a->large = b;
b->small = a;
}
/*
helper function -- given two circular doubly linked
lists, append them and return the new list.
*/
static Node append(Node a, Node b) {
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 10
if (a==NULL) return(b);
if (b==NULL) return(a);
aLast = a->small;
bLast = b->small;
join(aLast, b);
join(bLast, a);
return(a);
}
/*
--Recursion--
Given an ordered binary tree, recursively change it into
a circular doubly linked list which is returned.
*/
static Node treeToList(Node root) {
Node aList, bList;
if (root==NULL) return(NULL);
return(aList);
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Tree List Recursion Problem Page: 11
while(current != NULL) {
printf("%d ", current->data);
current = current->large;
if (current == head) break;
}
printf("\n");
}
treeInsert(&root, 4);
treeInsert(&root, 2);
treeInsert(&root, 1);
treeInsert(&root, 3);
treeInsert(&root, 5);
head = treeToList(root);
printList(head); /* prints: 1 2 3 4 5 */
return(0);
}
http://cslibrary.stanford.edu/109/
TreeListRecursion.html
Unix
Programming
Tools
By Parlante, Zelenski, and many others Copyright ©1998-2001, Stanford University
Introduction
This article explains the overall edit-compile-link-debug programming cycle and
introduces several common Unix programming tools -- gcc, make, gdb, emacs, and the
Unix shell. The goal is to describe the major features and typcial uses of the tools and
show how they fit together with enough detail for simple projects. We've used a version
of this article at Stanford to help students get started with Unix.
Contents
Introduction — the compile-link process 1
The gcc compiler/linker 2
The make project utility 5
The gdb debugger 8
The emacs editor 13
Summary of Unix shell commands 15
This is document #107, Unix Programming Tools, in the Stanford CS Education Library.
This and other free educational materials are available at http://cslibrary.stanford.edu/.
This document is free to be used, reproduced, or sold so long as it is intact and
unchanged.
Other Resources
This article is an introduction — for more detailed information about a particular tool, see
the tool's man pages and xinfo entries. Also, O'Reilly & Associates publishes a pretty
good set of references for many Unix related tools (the books with animal pictures on the
cover). For basic coverage of the C programming language, see CS Education Library
#101, (http://cslibrary.stanford.edu/101/).
also bring in library object files that contain the definitions of library functions like
printf() and malloc(). The overall process looks like this...
C compiler
main.o module1.o module2.o
library
Linker functions
program
Section 1 — gcc
The following discussion is about the gcc compiler, a product of the open-source GNU
project (www.gnu.org). Using gcc has several advantages— it tends to be pretty up-to-
date and reliable, it's available on a variety of platforms, and of course it's free and open-
source. Gcc can compile C, C++, and objective-C. Gcc is actually both a compiler and a
linker. For simple problems, a single call to gcc will perform the entire compile-link
operation. For example, for small projects you might use a command like the following
which compiles and links together three .c files to create an executable named "program".
gcc main.c module1.c module2.c -o program
The above line equivalently could be re-written to separate out the three compilation
steps of the .c files followed by one link step to build the program.
gcc -c main.c ## Each of these compiles a .c
gcc -c module1.c
gcc -c module2.c
gcc main.o module1.o module2.o -o program ## This line links the .o's
## to build the program
where options is a list of command flags that control how the compiler works, and
files is a list of files that gcc reads or writes depending on the options
Command-line options
Like most Unix programs, gcc supports many command-line options to control its
operation. They are all documented in its man page. We can safely ignore most of these
options, and concentrate on the most commonly used ones: -c, -o, -g, -Wall,
-I, -L, and -l.
3
-c files Direct gcc to compile the source files into an object files without going
through the linking stage. Makefiles (below) use this option to compile
files one at a time.
-o file Specifies that gcc's output should be named file. If this option is not
specified, then the default name used depends on the context...(a) if
compiling a source .c file, the output object file will be named with the
same name but with a .o extension. Alternately, (b) if linking to create
an executable, the output file will be named a.out. Most often, the -o
option is used to specify the output filename when linking an
executable, while for compiling, people just let the default .c/.o
naming take over.
-Wall Give warnings about possible errors in the source code. The issues
noticed by -Wall are not errors exactly, they are constructs that the
compiler believes may be errors. We highly recommend that you
compile your code with -Wall. Finding bugs at compile time is soooo
much easier than run time. the -Wall option can feel like a nag, but it's
worth it. If a student comes to me with an assignment that does not
work, and it produces -Wall warnings, then maybe 30% of the time,
the warnings were a clue towards the problem. 30% may not sound
like that much, but you have to appreciate that it's free debugging.
Sometimes -Wall warnings are not actually problems. The code is ok,
and the compiler just needs to be convinced. Don't ignore the warning.
Fix up the source code so the warning goes away. Getting used to
compiles that produce "a few warnings" is a very bad habit.
Here's an example bit of code you could use to assign and test a flag
variable in one step...
int flag;
if (flag = IsPrime(13)) {
...
}
int flag;
if ((flag = IsPrime(13)) != 0) {
...
}
This gets rid of the warning, and the generated code will be the same
as before. Alternately, you can enclose the entire test in another set of
parentheses to indicate your intentions. This is a small price to pay to
get -Wall to find some of your bugs for you.
-Idir Adds the directory dir to the list of directories searched for #include
files. The compiler will search several standard directories
automatically. Use this option to add a directory for the compiler to
search. There is no space between the "-I" and the directory name. If
the compile fails because it cannot find a #include file, you need a -I to
fix it.
Extra: Here's how to use the unix "find" command to find your
#include file. This example searches the /usr/include directory for all
the include files with the pattern "inet" in them...
-lmylib (lower case 'L') Search the library named mylib for unresolved
symbols (functions, global variables) when linking. The actual name of
the file will be libmylib.a, and must be found in either the default
locations for libraries or in a directory added with the -L flag (below).
The position of the -l flag in the option list is important because the
linker will not go back to previously examined libraries to look for
unresolved symbols. For example, if you are using a library that
requires the math library it must appear before the math library on the
command line otherwise a link error will be reported. Again, there is
no space between the option flag and the library file name, and that's a
lower case 'L', not the digit '1'. If your link step fails because a symbol
cannot be found, you need a -l to add the appropriate library, or
somehow you are compiling with the wrong name for the function or
-Ldir globalthe
Adds variable.
directory dir to the list of directories searched for library files
specified by the -l flag. Here too, there is no space between the
option flag and the library directory name. If the link step fails because
a library file cannot be found, you need a -L, or the library file name is
wrong.
5
Section 2 — make
Typing out the gcc commands for a project gets less appealing as the project gets bigger.
The "make" utility automates the process of compiling and linking. With make, the
programmer specifies what the files are in the project and how they fit together, and then
make takes care of the appropriate compile and link steps. Make can speed up your
compiles since it is smart enough to know that if you have 10 .c files but you have only
changed one, then only that one file needs to be compiled before the link step. Make has
some complex features, but using it for simple things is pretty easy.
Running make
Go to your project directory and run make right from the shell with no arguments, or in
emacs (below) [esc]-x compile will do basically the same thing. In any case, make
looks in the current directory for a file called Makefile or makefile for its build
instructions. If there is a problem building one of the targets, the error messages are
written to standard error or the emacs compilation buffer.
Makefiles
A makefile consists of a series of variable definitions and dependency rules. A variable in
a makefile is a name defined to represent some string of text. This works much like
macro replacement in the C pre-processor. Variables are most often used to represent a
list of directories to search, options for the compiler, and names of programs to run.
Variables are not pre-declared, you just set them with '='. For example, the line :
CC = gcc
will create a variable named CC, and set its value to be gcc. The name of the variable is
case sensitive, and traditionally make variable names are in all upper case letters.
While it is possible to make up your own variable names, there are a few names that are
considered standard, and using them along with the default rules makes writing a
makefile much easier. The most important variables are: CC, CFLAGS, and LDFLAGS.
CC The name of the C compiler, this will default to cc or gcc in most
versions of make.
CFLAGS A list of options to pass on to the C compiler for all of your source
files. This is commonly used to set the include path to include non-
standard directories (-I) or build debugging versions (-g).
To refer to the value of a variable, put a dollar sign ($) followed by the name in
parenthesis or curly braces...
CFLAGS = -g -I/usr/class/cs107/include
$(CC) $(CFLAGS) -c binky.c
The first line sets the value of the variable CFLAGS to turn on debugging information and
add the directory /usr/class/cs107/include to the include file search path. The
second line uses CC variable to get the name of the compiler and the CFLAGS variable
6
to get the options for the compiler. A variable that has not been given a value has the
empty-string value.
The second major component of a makefile is the dependency/build rule. A rule tells how
to make a target based on changes to a list of certain files. The ordering of the rules does
not make any difference, except that the first rule is considered to be the default rule --
the rule that will be invoked when make is called without arguments (the most common
way).
A rule generally consists of two lines: a dependency line followed by a command line.
Here is an example rule :
binky.o : binky.c binky.h akbar.h
tab$(CC) $(CFLAGS) -c binky.c
This dependency line says that the object file binky.o must be rebuilt whenever any of
binky.c, binky.h, or akbar.h change. The target binky.o is said to depend on
these three files. Basically, an object file depends on its source file and any non-system
files that it includes. The programmer is responsible for expressing the dependencies
between the source files in the makefile. In the above example, apparently the source
code in binky.c #includes both binky.h and akbar.h-- if either of those two .h
files change, then binky.c must be re-compiled. (The make depend facility tries to
automate the authoring of the makefile, but it's beyond the scope of this document.)
The command line lists the commands that build binky.o -- invoking the C compiler
with whatever compiler options have been previously set (actually there can be multiple
command lines). Essentially, the dependency line is a trigger which says when to do
something. The command line specifies what to do.
The command lines must be indented with a tab characte -- just using spaces will not
work, even though the spaces will sortof look right in your editor. (This design is a result
of a famous moment in the early days of make when they realized that the tab format was
a terrible design, but they decided to keep it to remain backward compatible with their
user base -- on the order of 10 users at the time. There's a reason the word "backward" is
in the phrase "backward compatible". Best to not think about it.)
Because of the tab vs. space problem, make sure you are not using an editor or tool which
might substitute space characters for an actual tab. This can be a problem when using
copy/paste from some terminal programs. To check whether you have a tab character on
that line, move to the beginning of that line and try to move one character to the right. If
the cursor skips 8 positions to the right, you have a tab. If it moves space by space, then
you need to delete the spaces and retype a tab character.
For standard compilations, the command line can be omitted, and make will use a default
build rule for the source file based on its file extension, .c for C files, .f for Fortran files,
and so on. The default build rule for C files looks like...
$(CC) $(CFLAGS) -c source-file.c
It's very common to rely on the above default build rule -- most adjustments can be made
by changing the CFLAGS variable. Below is a simple but typical looking makefile. It
compiles the C source contained in the files main.c, binky.c, binky.h, akbar.c,
akbar.h, and defs.h. These files will produce intermediate files main.o,
binky.o, and akbar.o. Those files will be linked together to produce the executable
file program. Blank lines are ignored in a makefile, and the comment character is '#'.
7
## A simple makefile
CC = gcc
CFLAGS = -g -I/usr/class/cs107/include
LDFLAGS = -L/usr/class/cs107/lib -lgraph
PROG = program
HDRS = binky.h akbar.h defs.h
SRCS = main.c binky.c akbar.c
The first (default) target builds the program from the three .o's. The next three targets
such as "main.o : main.c binky.h akbar.h defs.h" identify the .o's that
need to be built and which source files they depend on. These rules identify what needs to
be built, but they omit the command line. Therefore they will use the default rule which
knows how to build one .o from one .c with the same name. Finally, make
automatically knows that a X.o always depends on its source X.c, so X.c can be
omitted from the rule. So the first rule could b ewritten without main.c --
"main.o : binky.h akbar.h defs.h".
The later targets, clean and TAGS, perform other convenient operations. The clean
target is used to remove all of the object files, the executable, and a core file if you've
been debugging, so that you can perform the build process from scratch . You can make
clean if you want to recover space by removing all the compilation and debugging
output files. You also may need to make clean if you move to a system with a
different architecture from where your object libraries were originally compiled, and so
8
you need to recompile from scratch. The TAGS rule creates a tag file that most Unix
editors can use to search for symbol definitions.
Compiling in Emacs
Emacs has built-in support for the compile process. To compile your code from emacs,
type M-x compile. You will be prompted for a compile command. If you have a
makefile, just type make and hit return. The makefile will be read and the appropriate
commands executed. The emacs buffer will split at this point, and compile errors will be
brought up in the newly created buffer. In order to go to the line where a compile error
occurred, place the cursor on the line which contains the error message and hit ^c-^c.
This will jump the cursor to the line in your code where the error occurred (“cc” is the
historical name for the C compiler).
Section 3 — gdb
You may run into a bug or two in your programs. There are many techniques for finding
bugs, but a good debugger can make the job a lot easier. In most programs of any
significant size, it is not possible to track down all of the bugs in a program just by staring
at the source — you need to see clues in the runtime behavior of the program to find the
bug. It's worth investing time to learn to use debuggers well.
GDB
We recommend the GNU debugger gdb, since it basically stomps on dbx in every
possible area and works nicely with the gcc compiler. Other nice debugging
environments include ups and CodeCenter, but these are not as universally available as
gdb, and in the case of CodeCenter not as cheaply. While gdb does not have a flashy
graphical interface as do the others, it is a powerful tool that provides the knowledgeable
programmer with all of the information they could possibly want and then some.
This section does not come anywhere close to describing all of the features of gdb, but
will hit on the high points. There is on-line help for gdb which can be seen by using the
help command from within gdb. If you want more information try xinfo if you are
logged onto the console of a machine with an X display or use the info-browser mode
from within emacs.
gdb program
where program is the name of the target executable that you want to debug. If you do
not specify a target then gdb will start without a target and you will need to specify one
later before you can do anything useful.
As an alternative, from within emacs you can use the command [Esc]-x gdb which
will then prompt you for the name of the executable file. You cannot start an inferior gdb
session from within emacs without specifying a target. The emacs window will then split
between the gdb buffer and a separate buffer showing the current source line.
table is the map produced by the -g compiler option that the debugger reads as it is
running your program.
The debugger is an interactive program. Once started, it will prompt you for commands.
The most common commands in the debugger are: setting breakpoints, single stepping,
continuing after a breakpoint, and examining the values of variables.
When a target executable is first selected (usually on startup) the current source file is set
to the file with the main function in it, and the current source line is the first executable
line of the this function.
As you run your program, it will always be executing some line of code in some source
file. When you pause the program (when the flow of control hits a “breakpoint” of by
typing Control-C to interrupt), the “current target file” is the source code file in which the
program was executing when you paused it. Likewise, the “current source line” is the line
of code in which the program was executing when you paused it.
Breakpoints
You can use breakpoints to pause your program at a certain point. Each breakpoint is
assigned an identifying number when you create it, and so that you can later refer to that
breakpoint should you need to manipulate it.
A breakpoint is set by using the command break specifying the location of the code
where you want the program to be stopped. This location can be specified in several
ways, such as with the file name and either a line number or a function name within that
file (a line needs to be a line of actual source code — comments and whitespace don't
count). If the file name is not specified the file is assumed to be the current target file, and
if no arguments are passed to break then the current source line will be the breakpoint.
gdb provides the following commands to manipulate breakpoints:
disable breaknum
enable breaknum Disable/enable breakpoint identified by breaknum
break binky.c:120
break DoGoofyStuff
set a breakpoint on line 120 of the file binky.c and another on the first line of the function
DoGoofyStuff. When control reaches these locations, the program will stop and give
you a chance to look around in the debugger.
Gdb (and most other debuggers) provides mechanisms to determine the current state of
the program and how it got there. The things that we are usually interested in are (a)
where are we in the program? and (b) what are the values of the variables around us?
Gdb assigns numbers to stack frames counting from zero for the innermost (currently
executing) frame. At any time gdb identifies one frame as the “selected” frame. Variable
lookups are done with respect to the selected frame. When the program being debugged
stops (at a breakpoint), gdb selects the innermost frame. The commands below can be
used to select other frames by number or address.
11
down Select and print stack frame called by this one. (The
metaphor here is that the stack grows down with
each function call.)
The list command will show the source lines with the current source line centered in
the range. (Using gdb from within emacs makes these command obsolete since it does
all of the current source stuff for you.)
Examining data
To answeer the question (b) “what are the values of the variables around us?” use the
following commands...
In gdb, there are two different ways of displaying the value of a variable: a snapshot of
the variable’s current value and a persistent display for the entire life of the variable. The
print command will print the current value of a variable, and the display command
will make the debugger print the variable's value on every step for as long as the variable
exists. The desired variable is specified by using C syntax. For example...
print x.y[3]
will print the value of the fourth element of the array field named y of a structure variable
named x. The variables that are accessible are those of the currently selected function's
activation frame, plus all those whose scope is global or static to the current target file.
Both the print and display functions can be used to evaluate arbitrarily complicated
expressions, even those containing, function calls, but be warned that if a function has
side-effects a variety of unpleasant and unexpected situations can arise.
Shortcuts
Finally, there are some things that make using gdb a bit simpler. All of the commands
have short-cuts so that you don’t have to type the whole command name every time you
want to do something simple. A command short-cut is specified by typing just enough of
the command name so that it unambiguously refers to a command, or for the special
commands break, delete, run, continue, step, next and print you need only
use the first letter. Additionally, the last command you entered can be repeated by just
hitting the return key again. This is really useful for single stepping for a range while
watching variables change.
Miscellaneous
editmode mode Set editmode for gdb command line. Supported
values for mode are emacs, vi, dumb.
Debugging Strategies
Some people avoid using debuggers because they don't want to learn another tool. This is
a mistake. Invest the time to learn to use a debugger and all its features — it will make
you much more productive in tracking down problems.
Sometimes bugs result in program crashes (a.k.a. “core dumps”, “register dumps”, etc.)
that bring your program to a halt with a message like “Segmentation Violation” or the
like. If your program has such a crash, the debugger will intercept the signal sent by the
processor that indicates the error it found, and allow you to examine the state program.
Thus with almost no extra effort, the debugger can show you the state of the program at
the moment of the crash.
Often, a bug does not crash explicitly, but instead produces symptoms of internal
problems. In such a case, one technique is to put a breakpoint where the program is
misbehaving, and then look up the call stack to get some insight about the data and
control flow path that led to the bad state. Another technique is to set a breakpoint at
some point before the problems start and step forward towards the problems, examining
the state of the program along the way.
13
Section 4 — emacs
The following is a quick introduction to the “emacs” text editor which is a free program
produced by GNU (www.gnu.org). It's a fine editor, and it happens to integrate with
many other Unix tools nicely. There's a fabulous history of various editor adherents
having long and entertaining arguments about why their editor is best, but we're just
going to avoid that subject entirely.
To start editing a new or existing file using emacs, simply type the following to the UNIX
prompt...
emacs filename
where filename is the file to be edited. The X-Windows version of emacs is called
xemacs, and if you're using it... well just look in the menus. The commands are all the
same, but you don't have to remember the funny key-combinations.
All the fancy editing commands, such as find-and-replace, are invoked through typing
special key sequences. Two important key sequences to remember are: ^x (holding down
the “ctrl” key while typing “x”) and [esc]-x (simply pressing the “esc” key followed
by typing “x”), both of which are used to start many command sequences. Note that for
historical reasons in most user manuals for emacs, the “esc” key is actually referred to as
the “Meta” or “M-” key. Therefore, you may see the [esc]-x written as equivalently
as M-x.
To save the file being edited the sequence is ^x^s. To exit (and be prompted to save)
emacs, the sequence is ^x^c. To open another file within emacs, the sequence is ^x^f.
This sequence can be used to open an existing file as well as a new file. If you have
multiple files open, emacs stores them in different “buffers”. To switch from one buffer
to another (very handy when you are editing a .c source file and need to refer to the
prototypes and definitions in the .h header file), you use the key sequence ^x-b (note
the “b” is typed plain). You can then enter the name of the file to switch to the
corresponding buffer (a default name is provided for fast switching). The arrow keys
usually work as the cursor movement keys, but there are other nagivation key
combinations listed below.
Running emacs
emacs <filename> Run emacs (on a particular file). Make sure you don't already have
an emacs job running which you can just revive with fg. Adding a
'&' after the above command will run emacs in the background,
freeing up your shell)
^z Suspend emacs— revive with % command above, or the fg
command
^x^c Quit emacs
^x^f Load a new file into emacs
^x^v Load a new file into emacs and unload previous file
^x^s Save the file
^x-k Kill a buffer
Moving About
^f Move forward one character
^b Move backward one character
^n Move to next line
^p Move to previous line
14
Searching
^s Search for a string
^r Search for a string backwards from the cursor (quit both of these
with ^f)
M-% Search-and-replace
Deleting
^d Deletes letter under the cursor
^k Kill from the cursor all the way to the end of the line
^y Yanks back all the last kills. Using the ^k ^y combination you can
get a primitive cut-paste effect to move text around
Regions
emacs defines a region as the space between the mark and the point. A mark is set with
^-space (control-spacebar). The point is at the cursor position.
^w Delete the region. Using ^y will also yank back the last region
killed or copied — this is the way to get a cut/copy/paste effect with
regions.
Screen Splitting
^x-2 Split screen horizontally
^x-3 Split screen vertically
^x-1 Make active window the only screen
^x-0 Make other window the only screen
Miscellaneous
M-$ Check spelling of word at the cursor
^g In most contexts, cancel, stop, go back to normal command
M-x goto-line num Goes to the given line number
^x-u Undo
M-x shell Start a shell within emacs
M-q Re-flow the current line-breaks to make a single paragraph of text
15
Compiling
M-x compile Compile code in active window. Easiest if you have a makefile set
up.
^c ^c Do this with the cursor in the compile window, scrolls to the next
compiler error. Cool!
Getting Help
^h emacs help
^h t Run the emacs tutorial
emacs does command completion for you. Typing M-x space will give you a list of emacs
commands. There is also a man page on emacs. Type man emacs in a shell.
This example shoes printing the two source files binky.c and lassie.c, as well as
all of the header files to printer sweet5. You can change these parameters to fit your
needs.
Directory Commands
cd directory Change directory. If directory is not specified, goes to home
directory.
pwd Show current directory (print working directory)
ls Show the contents of a directory. ls -a will also show files whose
name begins with a dot. ls -l shows lots of miscellaneous info about
each file. ls -t sorts the most recently changed to the top.
rm file Delete a file
mv old new Rename a file from old to new (also works for moving things
between directories). If there was already a file named new, it gets
overwritten.
cp old new Creates a file named new containing the same thing as old. If there
was already a file named new, it is overwritten.
mkdir name Create a directory
rmdir name Delete a directory. The directory must be empty.
Miscellaneous Commands
cat file Print the contents of file to standard output
more file Same as cat, but only a page at a time (useful for displaying)
less file Same as more, but with navigability (less is more)
w Find out who is on the system and what they are doing
ps List all your processes (use the process id's in kill below)
jobs Show jobs that have been suspended (use with fg)
source file Execute the lines in the given file as if they were typed to the shell
Getting Help
man subject Read the manual entry on a particular subject
man -k keyword Show all the manual pages for a particular keyword
History
history Show the most recent commands executed
!! Re-execute the last command (or type up-arrow with modern shells)
!number Re-execute a particular command by number
!string Re-execute the last command beginning with string
^wrong^right^ Re-execute the last command, substituting right for wrong
ctrl-P Scroll backwards through previous commands
Pipes
a > b Redirect a's standard output to overwrite file b
a >> b Redirect a's standard output to append to the file b
a >& b Redirect a's error output to overwrite file b
a < b Redirect a's standard input to read from the file b
a | b Redirect a's standard output to b's standard input