Binary Tree
Binary Tree中每一个节点有两个子节点,区别于Binary Search Tree, Binary Tree子节点之间不存在大小顺序关系,首先来看几个简单的问题:
采用post order来计算树的大小
int size(TreeNode *root){
if(root == nullptr) return 0;
return size(root->left) + 1 + size(root->right);
}
接着看如何判断两棵树是否相同:
bool identicalTrees(Node *a, Node *b){
if(a ==nullptr && b == nullptr)
return true;
if(a && b){
return (a->val == b->val && identicalTrees(a->left, b->left) && identicalTrees(a->right, b->right);
}
return false;
}
另一个简单的例子计算数的高度或者最高深度:
int maxDepth(TreeNode *root){
if(root == nullptr) return 0;
return max(MaxDepth(root->left), maxDepth(root->right)) + 1;
}
那么如何删除tree呢?
void deleteTree(TreeNode *root){
if(root == nullptr) return;
deleteTree(root->left);
deleteTree(root->right);
delete root;
root = nullptr;
}
这里采用了postorder的方式进行树的删除.
树的遍历
对于树这个数据结构,通常有几个基本的遍历方式perorder, inorder, postorder和level order,下面我们将分别详细描述这几种遍历方式:
preorder traversal
preorder traversal一般有三种方式:recursive, iterative和Morris method. 从最简单的resursive的方式讨论:
void preorder(TreeNode *root){
if(root){
cout << root->val << " ";
preorder(root->left);
preorder(root->right);
}
}
对于recursive的方式,注意判断root是否为空,从recursive转化为iterative,我们需要辅助新的stack空间来实现:
void preorder(TreeNode *root){
stack tmp;
if(root){
tmp.push(root):
while(!tmp.empty()){
TreeNode *cur = temp.top();
tmp.pop();
cout << cur->val << " ";
if(cur->right) tmp.push(cur->right);
if(cur->left) tmp.push(cur->left);
}
}
这里需要注意的是每一次有新的元素推入栈中,需要判断这个元素是否为空,另外左右子节点的顺序与recursive的方式相反。我们知道每一个recursive的函数都可以又一个iterative的方式表示,因此我们可以通过模拟resursive function call来实现preorder遍历。
void preorder(TreeNode *root){
stack tmp;
while(root || !tmp.empty()){
if(root){
cout << root->val << " ";
tmp.push(root);
root = root->left;
}else{
root = tmp.top();
tmp.pop();
root = root->right;
}
}
这里我们利用更新root本身来实现遍历,以上解法需要O(lgN)的额外空间,Morris方法可以不需要额外空间,具体如下:
void perorderMorrisTraversal(TreeNode *root){
TreeNode *cur = root, *prev = nullptr;
while(cur){
if(cur->left == nullptr){
cout << cur->val << " ";
cur = cur->right;
}else{
prev = cur->left;
while(prev->right && prev->right != cur)
prev = prev->right;
if(prev->right == nullptr){
cout << cur->val << " ";
prev->right = cur;
cur = cur->left;
}else{
prev->right = nullptr;
cur = cur->right;
}
}
}
这里仍然通过更新root来遍历,Binary Tree遍历问题有一个关键点就是当指针走到底层以后如何返回的问题,之前的方法都是通过stack或者function stack来解决,Morris方法有一个地方特别的是如果左子节点不为空的情况下,寻找它的前驱节点然后link起来,这样就可以让下层的节点和上层节点联系起来,等到第二次遍历的时候再让这种联系断开通过prev->right = nullptr;
inorder traversal
Recursive method:
void inorder(TreeNode *root){
if(root){
inorder(root->left);
cout << root->val << " ";
inorder(root->right);
}
}
Iterative method:
void inorder(TreeNode *root){
stack tmp;
while(root || !tmp.empty()){
if(root){
tmp.push(root);
root = root->left;
}else{
root = tmp.top();
tmp.pop();
cout << root->val << " ";
root = root->right;
}
}
}
Morris tree traversal:
void inOrderMorrisTraversal(TreeNode *root){
TreeNode *cur = root, *prev = nullptr;
while(cur){
if(cur->left == nullptr){
cout < cur->val << " ";
cur = cur->right;
}else{
prev = cur->left;
while(prev->right && prev->right != cur)
prev = prev->right;
if(prev->right == nullptr){
prev->right = cur;
cur = cur->left;
}else{
prev->right = nullptr;
cout << cur->val << " ";
cur = cur->right;
}
}
}
}
相比较于preorder traversal, inorder Morris方法只是在节点打印的顺序上有一个改变就是当unlink前驱节点的同时打印当前节点,而preorder traversal是在link前驱节点的时候打印当前节点。
postorder traversal
postorder在几个遍历方法中是比较复杂,首先看一下recursive的方法:
void postorder(TreeNode *root){
if(root){
postorder(root->left);
postorder(root->right);
cout << root->val << " ";
}
}
然后再看recursive的方法:
void postOrder(TreeNode *root){
stack tmp;
TreeNode *prev, *curr;
if(root) tmp.push(root);
while(!tmp.empty()){
curr = tmp.top();
if(prev == nullptr || prev->left == curr || prev->right == curr){
if(curr->left)
tmp.push(curr->left);
else if(curr->right)
tmp.push(curr->right);
}else if (curr->left == prev){
if(curr->right)
tmp.push(curr->right);
}else{
cout << curr->val << " ";
tmp.pop();
}
prev = curr;
}
}
仔细分析不难看出这里使用了两个指针来记录节点访问情况,在第一个if中prev节点是curr节点的parent node, 因此curr还没有被访问于是将curr的子节点推入栈中。第二种情况prev是curr的左子节点,因此需要讲其右子节点推入栈中。其他情况则可以打印curr节点,更新prev节点。 对于recursive方法可以改写为以下这种形式:
void postOrder(TreeNode *root){
TreeNode *p, *q;
stack s;
p = root;
do{
while(p){
s.push(p);
p = p->left;
}
q = nullptr;
while(!s.empty()){
p = s.top();
s.pop();
if(p->right == q){
cout << p->val << " ";
q = p;
}else{
s.push(p);
p = p->right;
break;
}
}
}whie(!s.empty());
}
从以上可以看出来刚开始一路向左推入栈中,然后检查right node是否被访问过p->right == q,以此来选择是否打印curr.另外还有一种省事的方法,即将preorder的方法将结果输入到一个stack中,然后反向输出即可:
void postOrder(TreeNode *root){
stack tmp;
stack result;
if(root) tmp.push(root);
while(!tmp.empty()){
TreeNode *cur = tmp.top();
tmp.pop();
result.push(cur->val);
if(cur->left) tmp.push(cur->left);
if(cur->right) tmp.push(cur->right);
}
while(!result.empty(){
cout << result.top() << " ";
result.pop();
}
}
PostOrder Morris方法比较复杂,需要utility function来辅助:
void reverse(TreeNode *from , TreeNode *to){
if(from == to)
return;
TreeNode * x = from, *y = from ->right, *z;
while(true){
z = y->right;
z->right = x;
x= y;
y = z;
if(x == to) break;
}
}
void printReverse(TreeNode *from, TreeNode *to){
reverse(from , to);
TreeNode *p = to;
while(true){
cout << p->val<<" ";
if(p == from)
break;
p = p->right;
}
reverse(to, from);
}
void postOrderMorrisTraversal(TreeNode *root){
TreeNode dump(0);
dump.left = root;
TreeNode *cur = &dump, *pre = nullptr;
while(cur){
if(cur->left == nullptr)
cur = cur->right;
else{
prev = cur->left;
while(prev->right && prev->right == cur)
prev = prev->right;
if(prev->right == nullptr){
prev->right = cur;
cur = cur->right;
}else{
printReverse(cur->left, prev);
prev->right = nullptr;
cur = cur->right;
}
}
PostOrder Morris Tree Traversal在几个地方与之前有很大不同,首先用了临时节点dump,它的左子节点为root,并且注意cur是从临时节点开始的。其他逻辑结构与之前两种遍历类似,不同的地方是需要一个子过程反向输出两个节点之间所有的节点。
接下来就是level order traversal,与之前遍历方法的最大变化就是使用queue来代替stack的作用:
void levelorder(Node *root){
queue tmp;
if(root) tmp.push(root):
while(!tmp.empty()){
Node *cur = tmp.front();
tmp.pop();
cout << cur->val << " ";
if(cur->left) tmp.push(cur->left);
if(cur->right) tmp.push(cur->right);
}
}
再写一个recursive版本, 这里需要几个辅助函数,一个是确定树的高度,另一个打印制定高度的node, 这里的时间复杂度是O(n^2):
int height(TreeNode *node){
if(node == nullptr)
return 0;
return max(height(node->left), height(node->right)) + 1;
}
void printLevelOrder(TreeNode *root){
int h = height(root);
for(int i = 1; i < h; ++i)
printGivenLevel(root, i);
}
void printGivenLevel(TreeNode *root, int level){
if(root == nullptr)
return;
if(level == 1)
cout << root->val << " ";
else if (level > 1){
printGivenLevel(root->left, level - 1);
printGivenLevel(root->right, level - 1);
}
}
下面来看几个level order traversal 的变种题目,level order in a zigzag fashion,如果采用recursive的方式可以在上面的解答中加入一个变量判断是否需要反向输出, 其时间复杂度与前面一致都是O(n^2), 具体过程如下:
void printSpiral(TreeNode *root){
int h = height(root);
bool rev = false;
for(int i = 0; i <= h; ++i){
printGivenLevel(root, i, rev);
rev = !rev;
}
}
void printGivenLevel(TreeNode *node, int level, bool rev){
if(node == nullptr)
return;
else if (level = 1)
cout << node->val << " ";
else{
if(rev){
printGivenLevel(node->left, level-1, rev);
printGivenLevel(node->right, level-1, rev);
}else{
printGivenLevel(node->right, level-1, rev);
printGivenLevel(node->left, level-1, rev);
}
}
}
除此之外,iterative的解法则需要额外的空间stack来储存,并且记录当前层和下一层的node个数以及当前层访问过的node个数:
vector> zigzagLevelOrder(TreeNode *root){
vector result;
if(root == nullptr) return result;
vector level;
queue helper;
helper.push(root);
int curLev = 1, nextLev = 0, curVisit = 0;
bool revers = false;
while(!help.empty()){
TreeNode *tmp = help.front();
helper.pop();
level.push(tmp->val);
curVisit++;
if(tmp->left){
helper.push(tmp->left);
nextLev++;
}
if(tmp->right){
helper.push(tmp->right);
nextLev++;
}
if(curVisit = curLev){
curVisit = nextLev;
nextLev = 0;
curVisit = 0;
if(revers)
reverse(level.begin(), level.end());
result.push_back(level);
level.clear();
}
return result;
}
仔细发现,这道题的构架可以用来进行一起关于level order这样的操作,也就是说通过记录当前层和下一层的node个数可以知道目前是否访问到当前所有的node,用来判断是否要进行更新。
下面是另外一种level order traversal的变种:find next right node of a given key in level order traversal
TreeNode *nextRight(TreeNode *root, int k){
if(root == nullptr)
return 0;
queue qn;
queue ql;
int level=0;
qu.push(root);
ql.push(level);
while(!qn.empty()){
TreeNode *node = qn.front();
level = ql.front();
qn.pop();
ql.pop();
if(node->val == k){
if(ql.size() == 0 || ql.front() != level)
return nullptr;
return qn.front();
}
if(node->left){
qn.push(node->left);
ql.push(level+1);
}
if(node->right){
qu.push(node->right);
ql.push(level+1);
}
}
return nullptr;
}
再看一个简单的例子, Sum of all the numbers that are formed from root to leaf paths. Each node has a digit from 1 to 9.
int treePathSumUtil(TreeNode *root, int val){
if(!root) return 0;
val = (val * 10 + root->val);
if(! root->left && ! root->right)
return val;
return treePathSumUntil(root->left, val) + treePathSumUntil(root->right, val);
}
int TreePathSum(TreeNode *root){
return treePathSumUntil(root, 0);
}
不难看出,这里使用了preorder的思想来计算所有node的之和。