Tree数据结构

作者: dol_re_mi | 来源:发表于2015-06-09 07:13 被阅读2105次

    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的之和。

    相关文章

      网友评论

        本文标题:Tree数据结构

        本文链接:https://www.haomeiwen.com/subject/rhcjqttx.html