zhyDaDa的个人站点

二叉树

目录
二叉树
节点结构
遍历顺序
用递归实现遍历
用非递归实现遍历
先序遍历
中序遍历
后序遍历
直观打印二叉树
特殊遍历
深度优先遍历
广度优先遍历
二叉树分类
搜索二叉树
完全二叉树
满二叉树
平衡二叉树
线索二叉树


节点结构

class Node<V>{
  V value;
  Node left;
  Node right;
}

其实和链表类似, 但不会出现环

特别的:
+ 左孩子和右孩子都为空的节点称为叶节点
+ 最顶上的节点称为根节点 / 头节点

我们称一个子节点开头以下的所有节点为子树

遍历顺序

二叉树的遍历要用到递归的思想
我们把递归的顺序称为递归序

一般有三种遍历方法:
+ 先序遍历: -> ->
+ 中序遍历: -> ->
+ 后序遍历: -> ->

中序遍历中, A节点的下一个节点成为A的 后继节点
中序遍历中, A节点的上一个节点成为A的 前驱节点

用递归实现遍历

标准的递归代码如下:

void preOrder(Node node){
  if(node == null){
    return;
  }
  // 1
  preOrder(node.left);
  // 2
  preOrder(node.right);
  // 3
}

三种遍历的顺序都可以由上述递归序加工而来, 例如:
+ 在1处打印, 就是先序遍历
+ 在2处打印, 就是中序遍历
+ 在3处打印, 就是后序遍历

用非递归实现遍历

所有递归的代码都可以改成非递归的代码

先序遍历

先准备一个栈, 将头节点压入栈中, 接下来的流程如下:
+ 栈中弹出一个节点cur
+ 处理(打印)cur
+ (如果有的话)先右后左将cur的孩子压入栈中
+ 循环

代码如下:

void preOrderUnRecur(Node head){
  if(head != null){
    stack<Node> stack;
    stack.push(head);
    while(!stack.empty()){
      Node cur = stack.top();
      stack.pop();
      cout << cur.value << " "; // 处理
      if(cur.right != null){
        stack.push(cur.right);
      }
      if(cur.left != null){
        stack.push(cur.left);
      }
    }
    cout << endl;
  }
}

中序遍历

流程是:
+ 先整棵树所有左侧进栈
+ 每弹出一个, 就先打印
+ 接着对其右树做同样的操作

代码如下:

void inOrderUnRecur(Node head){
  if(head != null){
    stack<Node> stack;
    while(!stack.empty() || head != null){
      if(head != null){
        stack.push(head);
        head = head.left;
      }else{
        head = stack.top();
        stack.pop();
        cout << head.value << " ";
        head = head.right;
      }
    }
    cout << endl;
  }
}

可以把左的方向向下画, 右的方向向右水平画, 就比较好理解了

后序遍历

和先序遍历类似, 相当于将先序的”头左右”先改成”头右左”, 然后再逆序打印(通过一个辅助的栈实现), 相当于”左右头”
流程是:
+ 栈中弹出一个节点cur
+ cur压入收集栈
+ (如果有的话)先左后右将cur的孩子压入栈中
+ 循环

代码如下:

void posOrderUnRecur(Node head){
  if(head != null){
    stack<Node> stack;
    stack.push(head);
    stack<Node> help;
    while(!stack.empty()){
      Node cur = stack.top();
      stack.pop();
      help.push(cur);
      if(cur.left != null){
        stack.push(cur.left);
      }
      if(cur.right != null){
        stack.push(cur.right);
      }
    }
    while(!help.empty()){
      cout << help.top().value << " ";
      help.pop();
    }
    cout << endl;
  }
}

直观打印二叉树

    1
   / \
  2   3
 / \ / \

代码如下:

string getSpace(int num)
{
    string space = " ";
    string buf;
    for (int i = 0; i < num; i++)
    {
        buf += space;
    }
    return buf;
}

void printInOrder(Node *head, int height, string to, int len)
{
    if (head == nullptr)
    {
        return;
    }
    printInOrder(head->right, height + 1, "v", len);
    string val = to + " " + to_string(head->value) + " " + to;
    int lenM = val.length();
    int lenL = (len - lenM) / 2;
    int lenR = len - lenM - lenL;
    val = getSpace(lenL) + val + getSpace(lenR);
    cout << getSpace(height * len) + val << endl;
    printInOrder(head->left, height + 1, "^", len);
}

void printTree(Node *head)
{
    cout << "Binary Tree:" << endl;
    printInOrder(head, 0, "H", 17);
    cout << endl;
}

使用示例:

int main()
{
    Node *head = new Node(1);
    head->left = new Node(-222222222);
    head->right = new Node(3);
    head->left->left = new Node(4444);
    head->right->left = new Node(55555555);
    head->right->right = new Node(66);
    head->left->left->right = new Node(777);

    printTree(head);
    return 0;
}

特殊遍历

深度优先遍历

就是先序遍历的非递归实现

广度优先遍历

也叫 宽度优先遍历 / 按层遍历
用队列实现, 流程是:
+ 头节点进队列
+ 每次从队列中弹出(从右边出)一个节点, 就处理(打印)
+ (如果有的话)先左后右将弹出节点的孩子进队列(左边进)

代码如下:

void levelOrder(Node head){
  if(head != null){
    queue<Node> queue;
    queue.push(head);
    while(!queue.empty()){
      Node cur = queue.front();
      queue.pop();
      cout << cur.value << " ";
      if(cur.left != null){
        queue.push(cur.left);
      }
      if(cur.right != null){
        queue.push(cur.right);
      }
    }
    cout << endl;
  }
}

二叉树分类

搜索二叉树

搜索二叉树是一种特殊的二叉树, 也叫二叉查找树, 二叉排序树

英文名: Binary Search Tree, BST

特点是: 左树<头<右树
+ 每个节点都大于其左子树的所有节点的值
+ 每个节点都小于其右子树的所有节点的值
+ 每个节点的左右子树都是搜索二叉树

直观图:

      6
     / \
    4   8
   / \ / \
  2  5 7  9
 /
1...

判断方法: 中序遍历
+ 如果是搜索二叉树, 那么中序遍历的结果是递增的
+ 一旦中序遍历有降序, 那么就不是搜索二叉树

代码如下:
非递归实现

bool isBST(Node head){
  if(head != null){
    stack<Node> stack;
    int pre = INT_MIN;
    while(!stack.empty() || head != null){
      if(head != null){
        stack.push(head);
        head = head.left;
      }else{
        head = stack.top();
        stack.pop();
        if(head.value < pre){
          return false;
        }
        pre = head.value;
        head = head.right;
      }
    }
    return true;
  }
  return false;
}

递归实现

// 全局变量
int pre = INT_MIN;
bool isBST(Node head){
  if(head == nullptr){
    return true;
  }
  if(!isBST(head.left)){
    return false;
  }
  // 将原本是处理的逻辑, 写成比较的环节
  if(head.value < pre){
    return false;
  }else{
    pre = head.value;
  }
  return isBST(head.right);
}

完全二叉树

完全二叉树是一种特殊的二叉树, 也叫完美二叉树

英文名: Complete Binary Tree, CBT

特点是: 从上到下, 从左到右依次填满节点
+ 除了最后一层, 其他层的节点数都是满的
+ 最后一层的节点都靠左排列

直观图:

      a
     / \
    b   c
   / \ / \
  d  e f  g
 / \
h   i ...

判断方法: 层序遍历
+ 如果有右孩子, 没有左孩子, 那么不是完全二叉树, 直接false
+ 在不违反上一条规则的情况下: 如果出现了左右孩子不双全的节点, 那么后面的节点都必须是叶节点, 否则false

代码如下:

bool isCBT_flag = false;

bool isCBT(Node *head)
{
    queue<Node *> q;
    q.push(head);
    Node *cur;
    while (!q.empty())
    {
        cur = q.front();
        q.pop();

        Node *left = cur->left;
        Node *right = cur->right;

        if (isCBT_flag && (left != nullptr || right != nullptr) || (left == nullptr && right != nullptr))
        {
            return false;
        }

        if (left != nullptr)
        {
            q.push(left);
        }
        if (right != nullptr)
        {
            q.push(right);
        }
        else
        {
            isCBT_flag = true;
        }
    }
    return true;
}

满二叉树

满二叉树是一种特殊的二叉树, 也叫真二叉树

英文名: Full Binary Tree, FBT

特点是: 每一层的节点数都是满的(包括最后一层)
+ 每一层的节点数都是满的
+ 每个节点的左右子树都是满二叉树

特别的, 从数学上来说, 如果该树的最大深度为h, 节点数为n, 那么有:
$$
n = 2^h – 1
$$

这是满二叉树的充要条件

直观图:

      a
     / \
    b   c
   / \ / \
  d  e f  g

判断代码如下:

struct FBT_return
{
    int h;
    int n;
    FBT_return(int h, int n){
        this->h = h;
        this->n = n;
    }
};

FBT_return FBT_process(Node *head)
{
    if(head == nullptr)
        return FBT_return(0, 0);
    FBT_return left = FBT_process(head->left);
    FBT_return right = FBT_process(head->right);

    int h = max(left.h, right.h)+1;
    int n = left.n + right.n + 1;

    return FBT_return(h, n);
}

bool isFBT(Node *head)
{
    FBT_return ans = FBT_process(head);
    return ans.n == (1 << ans.h) - 1;
}

平衡二叉树

平衡二叉树是一种特殊的二叉树, 也叫AVL树(Adelson-Velskii and Landis)

英文名: Balanced Binary Tree, BBTree

之所以研究ta, 主要是希望二叉树查询的时间复杂度能够降低到O(logN)

特点是: 左右子树的高度差不超过1
+ 每个节点的左右子树的高度(深度)差不超过1
+ 每个节点的左右子树都是平衡二叉树

直观图:

       a
     /   \
    b     c
     \   / \
      d e   f
             \
              g

图中是高度为4的最小平衡二叉树

代码如下:

struct BBT_return
{
    bool isBBT;
    int height;
    BBT_return(bool isBBT, int height)
    {
        this->isBBT = isBBT;
        this->height = height;
    }
};

BBT_return BBT_process(Node *head)
{
    if (head == nullptr)
    {
        return BBT_return(true, 0);
    }
    BBT_return left = BBT_process(head->left);
    BBT_return right = BBT_process(head->right);

    bool isBBT = left.isBBT && right.isBBT && abs(left.height - right.height) < 2;
    int height = 1 + (left.height > right.height ? left.height : right.height);

    return BBT_return(isBBT, height);
}

bool isBBT(Node *head)
{
    return BBT_process(head).isBBT;
}

线索二叉树

线索二叉树是一种特殊的二叉树, 也叫Threaded Binary Tree

英文名: Threaded Binary Tree, TBT

特点是: 在二叉树的节点上, 除了有左右孩子指针, 还有指向前驱和后继的指针
+ 二叉树的每个节点都有指向前驱和后继的指针
+ 二叉树的每个节点都有左右孩子指针, 除了左右孩子指针, 还有指向前驱和后继的指针

其意义就是为了解决二叉树的遍历难问题, 使得遍历的时间复杂度降低到O(1)

缺点是: 会占用额外的空间, 并且这样的线索二叉树不可共用

Avatar photo
我是 zhyDaDa

前端/UI/交互/独立游戏/JPOP/电吉他/游戏配乐/网球/纸牌魔术

发表回复