Recursion is beautiful. It is also a fundamental (as in "core", not "introductory") mathematical concept closely related to induction. Many algorithms are described more simply and elegantly using recursive formulations.
Verifying red-black tree black-height property (alternatives):
// stand-alone functions
static const int INVALID_HEIGHT
bool Node::verify() {
if (is_leaf(this)) return true;
int black_height_left = height(left);
if (black_height_left == INVALID_HEIGHT) return false;
return black_height_left == height(right);
}
bool is_leaf(Node* node) {
assert(node);
if (node->left) return false;
assert(!node->right);
return true;
}
int height(Node* node) {
if (is_leaf(node)) return 1;
int black_height_left = height(node->left);
if (black_height_left == INVALID_HEIGHT) return INVALID_HEIGHT;
int black_height_right = height(node->right);
if (black_height_left != black_height_right) return INVALID_HEIGHT;
return black_height_left + (node->color == BLACK);
}
// support methods
bool Node::verify() {
if (is_leaf()) return true;
int black_height_left = left->height();
if (black_height_left == Node::INVALID_HEIGHT) return false;
return black_height_left == right->height();
}
bool Node::is_leaf() {
if (left) return false;
assert(!right);
return true;
}
int Node::height() {
if (is_leaf()) return 1;
int black_height_left = left->height();
if (black_height_left == Node::INVALID_HEIGHT) return Node::INVALID_HEIGHT;
int black_height_right = right->height();
if (black_height_left != black_height_right) return Node::INVALID_HEIGHT;
return black_height_left + (color == BLACK);
}
// returning 2 results
bool Node::verify() {
int black_height;
return verify(&black_height);
}
bool Node::verify(int* height) {
assert(height);
if (!left) {
assert(!right);
*height = 1;
return true;
}
int height_left;
if (!left->verify(&height_left)) return false;
int height_right;
if (!right->verify(&height_right)) return false;
*height = height_left + (color == BLACK);
return height_left == height_right;
}
The shortest path between two vertice can be found easily by running a depth-first search. However, there is a related problem: least-cost path, minimum-distance path, or weighted-graph shortest path. The lowest-cost path is not necessarily the most direct routing.
Dijkstra's algorithm requires non-negative weights. The algorithm
uses a greedy approach. Two fields are added to the node class:
tentative_cost
and
comes_from
(or
predecessor).
Every node is assigned an initial tentative cost. The start node has cost zero and every other node has infinite cost. Initially, every node is unprocessed. When a node is processed, the tentative cost becomes its minimum cost.
The algorithm procededes by (repeatedly) processing the lowest-cost node from the unprocessed set. The minimum-cost path to the selected node can only pass through processed nodes. For each neighbor of the selected node, if the cost of the selected node plus the cost of the edge to the neighbor is lower than the current tentative cost of the neighbor, update the neighbor's tentative cost and set the comes-from field.
The algorithm proceeds until the goal node or all nodes are processed.
The run-time cost of the algorithm is
O(|V|2 + |E|).
If the graph is dense,
|E| = O(V2),
so
T = O(|V|2).
If the graph is sparse, we can use a priority queue to select the
minimum-cost node, with run-time performance of
O(|V| log |V| + |E|).
Example: shortest-path from AUS to MDW (click on image to enlarge
or download
frame-by-frame
zip):