Tree数据结构

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<TreeNode *> 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<TreeNode *> 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<TreeNode *> 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<TreeNode *> 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<TreeNode *> 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<TreeNode *> tmp;
         stack<int> 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<Node *> 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<vector<int>> zigzagLevelOrder(TreeNode *root){
       vector<vector<int> result;
       if(root == nullptr) return result;
  
       vector<int> level;
       queue<TreeNode *> 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<TreeNode *> qn;
        queue<int> 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的之和。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容