Code below for the "is complete" - with a comment showing the half-line to remove to test for a perfect binary tree (i.e. one with all leaves at the same depth). This is compilable code, with 3 test cases at the end.
The algorithmic bit is depths()
, reproduced here for discussion: with extra comments inline and below (and without cout
trace).
LR depths(Node& n)
{
// NULL/NULL is handled correctly below anyway, but nice not to have to think about it
if (n.l == NULL && n.r == NULL) return LR();
// get the depths of the child nodes... LR() is (0,0)
LR l12 = n.l ? ++depths(*n.l) : LR();
LR r12 = n.r ? ++depths(*n.r) : LR();
// >= ensures left-hand branches are as deep or deeper, i.e. left packing
if (l12.l >= l12.r && l12.r >= r12.l && r12.l >= r12.r &&
// also check the leftmost-to-rightmost depth range is 0 (full tree below)
// or 1 (perfect tree)
(l12.l == r12.r || l12.l == 1 + r12.r))
return LR(l12.l, r12.r);
throw false; // children can't be part of a full or complete tree
}
To explain the concept - consider any tree (or part thereof) that looks like this, where child / grandchild nodes might or might not exist:
Node
/ \
*l r
/ \ / \
*l1 l2 r1 r2
The algorithm generates a "depth" number for how far down the l1
, l2
, r1
and r2
paths you can go. Say only the nodes with *
s actually exist - i.e. l
and l1
exist but l2
doesn't - then l1
's depth would be 2, but l2
's would be 1. r
doesn't exist at all, then r1
and r2
would be 0.
The algorithm then observes: if l1
, l2
, r1
and r2
are equal, then the tree is "perfect". If they differ, then it may still be a complete tree, but the depths must decrease by 1 somewhere: e.g. if the grandchildren are leaf nodes, then (2, 2, 2, 1) or (2, 2, 1, 1) or (2, 1, 1, 1) or (1, 1, 1, 0) or (1, 1, 0, 0) or (1, 0, 0, 0). The easiest way to test for that is to check that l1 >= l2 >= r1 >= r2 && l1 == r2 + 1
- i.e. there's never an increase in depth, and the depth of the ends are only 1 apart.
Finally, the algorithm recurses deeply into the tree until it's dealing with a node with at least 1 child, then does the validation, then the lower and higher ends of the spanned range (which are at most 1 apart) are passed up for consideration of the up-to-four child paths from the parent node.
This consideration of four child paths at a time is a bit unusual and complicated, but it allows us to easily detect a situation where across four nodes the depth differs by more than 1.
If I'd only considered 2 nodes at a time, then a tree with two steps couldn't be detected without an extra has-already-stepped or max-depth-seen variable... but as Vikram's answer proves - using such a variable is easier over-all.
Full code:
#include <iostream>
#include <algorithm>
#include <string>
#include <cassert>
struct Node
{
std::string n;
Node* l;
Node* r;
Node(const char n[]) : n(n), l(0), r(0) { }
Node(const char n[], Node& l, Node& r) : n(n), l(&l), r(&r) { }
Node(const char n[], Node& l, int) : n(n), l(&l), r(0) { }
Node(const char n[], int, Node& r) : n(n), l(0), r(&r) { }
};
struct LR
{
int l, r;
LR(int l, int r) : l(l), r(r) { }
LR() : l(0), r(0) { }
LR& operator++() { ++l; ++r; return *this; }
bool operator==(const LR& rhs) const { return l == rhs.l && r == rhs.r; }
};
std::ostream& operator<<(std::ostream& os, const LR& lr)
{ return os << lr.l << ',' << lr.r; }
LR depths(Node& n)
{
if (n.l == NULL && n.r == NULL) return LR();
LR l12 = n.l ? ++depths(*n.l) : LR();
LR r12 = n.r ? ++depths(*n.r) : LR();
if (l12.l >= l12.r && l12.r >= r12.l && r12.l >= r12.r &&
(l12.l == r12.r || l12.l == 1 + r12.r))
{
LR result = LR(l12.l, r12.r);
std::cout << "depths(" << n.n << "), l12 " << l12 << ", r12 " << r12
<< ", result " << result << '\n';
return result;
}
std::cerr << "!complete @ " << n.n << ' ' << l12 << ' ' << r12 << '\n';
throw false;
}
bool is_complete_tree(Node& root)
{
try
{
depths(root);
return true;
}
catch (...)
{
return false;
}
}
int main()
{
{
std::cerr << "left node no children, right node two children\n";
Node rl("rl"), rr("rr"), r("r", rl, rr), l("l"), root("root", l, r);
assert(!is_complete_tree(root));
}
{
std::cerr << "left node two children, right node no children\n";
Node ll("ll"), lr("lr"), l("l", ll, lr), r("r"), root("root", l, r);
assert(is_complete_tree(root));
}
{
std::cerr << "left node two children, right node two children\n";
Node ll("ll"), lr("lr"), l("l", ll, lr);
Node rl("rl"), rr("rr"), r("r", rl, rr);
Node root("root", l, r);
assert(is_complete_tree(root));
}
std::cerr << ">>> test 3-level tree with 1 missing leaf at 3rd level\n";
{
std::cerr << "left node left child, right node two children\n";
Node ll("ll"), l("l", ll, 0);
Node rl("rl"), rr("rr"), r("r", rl, rr);
Node root("root", l, r);
assert(!is_complete_tree(root));
}
{
std::cerr << "left node right child, right node two children\n";
Node lr("lr"), l("l", 0, lr);
Node rl("rl"), rr("rr"), r("r", rl, rr);
Node root("root", l, r);
assert(!is_complete_tree(root));
}
{
std::cerr << "left node two children, right node left child\n";
Node ll("ll"), lr("lr"), l("l", ll, lr);
Node rl("rl"), r("r", rl, 0 );
Node root("root", l, r);
assert(is_complete_tree(root));
}
{
std::cerr << "left node two children, right node right child\n";
Node ll("ll"), lr("lr"), l("l", ll, lr);
Node rr("rr"), r("r", 0, rr);
Node root("root", l, r);
assert(!is_complete_tree(root));
}
}