二叉树

592 阅读6分钟

二叉树的性质

二叉树是特殊的树,也是我们平时程序中用的比较多的一种数据结构,它具有以下特点:

  • 每个结点最多有两颗子树,结点的度最大为2
  • 左子树和右子树是有顺序的,次序不能颠倒
  • 即使某个结点只有一个子树,也要区分左右子树
  • 在二叉树的第i(根结点为1层)层上最多有2^(i-1)个结点(i>=1)
  • 高度为k的二叉树,最多有2^k-1个结点(k>=0)
  • 对任何一棵二叉树,如果其叶结点有n个,度为2的非叶子结点有m个,则 n=m+1

一些常见的二叉树

1. 斜树

所有的结点都只有左子树(左斜树),或者只有右子树(右斜树)。斜树应用场景较少。

2. 满二叉树

所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上。

满二叉树具有以下特点:

  1. 叶子结点只能出现在最下一层
  2. 非叶子结点的度一定是2
  3. 在同样深度的二叉树中,满二叉树的结点个数最多,叶子结点最多

3. 完全二叉树

对一棵具有n个结点的二叉树按层序排号,如果编号为i的结点与同样深度的满二叉树编号为i结点在二叉树中位置完全相同,就是完全二叉树。满二叉树必须是完全二叉树,反过来不一定成立。

完全二叉树具有以下特点:

  1. 叶子结点只能出现在最底部两层
  2. 最底层叶子结点一定集中在左部连续位置
  3. 倒数第二层如果有叶子结点,一定出现在右部连续位置
  4. 同样数量结点的树,完全(满)二叉树的深度最小
  5. 对于有n个结点的完全二叉树,按层次对结点进行编号(从上到下,从左到右),对于任意编号为i的结点:
  • 如果i=1,则结点i是二叉树的根
  • 如果i>1,则其双亲结点为i/2下取整
  • 如果2i<=n,则结点i的左孩子为2i【结合上图思考】
  • 如果2i>n,则结点i无左孩子【结合上图思考】
  • 如果2i+1<=n,则结点i的右孩子为2i+1【结合上图思考】
  • 如果2i+1>n,则结点i无右孩子【结合上图思考】

4. 二叉排序树

二叉排序树,又称二叉查找树,也叫二叉搜索树

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

  1. 若左子树不为空,则左子树上所有结点的值均小于或等于它的根结点的值。
  2. 若右子树不为空,则右子树上所有结点的值均大于或等于它的根结点的值。
  3. 左、右子树也分别是二叉排序树。
  4. 二叉排序树中序遍历结果为递增有序数列。

5. 平衡二叉树

平衡二叉树(Balanced BinaryTree)又被称为AVL树(有别于AVL算法),它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

注意:满二叉树和完全二叉树一定是平衡二叉树

二叉树的遍历

1. 先序遍历

基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即根—左—右。

图中先序遍历结果是:1,2,4,5,7,8,3,6。

先序遍历JavaScript实现见下面程序。

// 递归-前序遍历
var order = (root, cb) => {
    if(root !== null) {
        cb(root.val);
        order(root.left, cb);
        order(root.right, cb);
    }
}

通过栈数据结构(先进后出),我们可以将父节点压入栈,对栈执行出栈操作,每次将出栈的节点的右孩子先压入栈,其次压入左孩子。这样就可以做到先遍历父节点,在遍历左孩子部分,后遍历右孩子部分。

// 迭代-前序遍历
var order = (root, cb) => {
    if(!root) return [];

    let stack = [], ans = [];
    stack.push(root);

    while(stack.length) {
        let node = stack.pop();
        ans.push(node.val);

        if(node.right) stack.push(node.right);
        if(node.left) stack.push(node.left);
    }

    return ans
}

2. 中序遍历

基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树即左—根—右。

图中中序遍历结果是:4,2,7,8,5,1,3,6。

中序遍历JavaScript实现见下面程序。

// 递归-中序遍历
var order = (root, cb) => {
    if(root !== null) {
        order(root.left, cb);
        cb(root.val);
        order(root.right, cb);
    }
}

我们同样可以使用栈结构来实现中序遍历,因为中序遍历左子树是优先遍历,所以父节点要先于子树的节点优先压入栈中,每当我们压入节点时,都要把节点的左子树的所有左节点压入栈中

//迭代 - 中序遍历
var order = root => {
    if(!root) return [];

    let stack = [], ans = [];
    let cur = root;

    while(stack.length || cur) {
        while(cur) {
            stack.push(cur);
            cur = cur.left;
        }

        let node = stack.pop();
        ans.push(node.val);

        if(node.right) {
            cur = node.right;
        }
    }

    return ans
}

3. 后序遍历

基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点即左—右—根。

图中后序遍历结果是:4,8,7,5,2,6,3,1。

后序遍历JavaScript实现见下面程序。

//递归 - 后序遍历
var sortedArr = [];
function postOrder(tree) {
    if(tree) {
        middleOrder(tree.left);
        middleOrder(tree.right);
        sortedArr.push(tree.key);
    }
}

后序遍历是父节点需要最后被遍历。但其实可以跟前序遍历的实现方式上差不多,只不过在插入数组中,我们总是在头部插入,这样先被插入的节点值一定是相对于左右孩子后面的

//迭代 - 后序遍历
var order = root => {
    if(!root) return [];

    let stack = [], ans = [];
    stack.push(root);

    while(stack.length) {
        let curNode = stack.pop();
        ans.unshift(curNode.val);

        curNode.left && stack.push(curNode.left);
        curNode.right && stack.push(curNode.right);
    }

    return ans
}

4. 层序遍历

基本思想:实现二叉树的层序遍历--从根开始,依次向下,对于每一层从左向右遍历。

// 递归 - 层序遍历
var levelOrder = function(root) {
    let res = [];
    order(root, 0, res);
    res = res.reduce((acc, cur) => [...acc, ...cur], []);
    console.log(res);
};

var order = (root, level, arr) => {
    if(!root) return [];

    arr[level] = arr[level] || [];
    arr[level].push(root.val);

    root.left && order(root.left, level*1 + 1, arr);
    root.right && order(root.right, level*1 + 1, arr);
}

我们使用队列来保存节点,每轮循环中,我们都取一层出来,将它们的左右孩子放入队列

// 迭代 - 层序遍历
var order = function(root) {
    let queue = [], ans = [];
    if(!root) return [];

    queue.push(root);

    while(queue.length) {
        let len = queue.length;

        while(len) {
            let node = queue.shift();
            ans.push(node.val);
            node.left && queue.push(node.left);
            node.right && queue.push(node.right);
            len -= 1;
        }
    }

    return ans;
};

二叉树的实现

class Node {
  constructor(key, leftChild, rightChild) {
    this.left = leftChild;
    this.key = key;
    this.right = rightChild;
  }
}

class BinarySearchTree {
  constructor() {}

  create(data) {
    if (!(data instanceof Array) || data.length < 1) return;

    let len = data.length,
      tree = new Node(data[0], null, null);

    if (len === 1) return tree;

    for (let i = 1; i < len; i++) this.insert(tree, data[i]);
    return tree;
  }

  //查询节点
  search(tree, key) {
    if (!tree) return false;
    if (tree.key === key) return true;

    if (key < tree.key) {
      if (tree.left) return this.search(tree.left, key);
      return false;
    } else {
      if (tree.right) return this.search(tree.right, key);
      return false;
    }
  }

  insert(currentNode, key) {
    let node = new Node(key, null, null);

    if (key < currentNode.key) {
      if (currentNode.left === null) {
        currentNode.left = node;
      } else {
        this.insert(currentNode.left, key);
      }
    } else {
      if (currentNode.right === null) {
        currentNode.right = node;
      } else {
        this.insert(currentNode.right, key);
      }
    }
  }

  // 找到右子树中最小的值
  findMinNodeKey(rightTree) {
    if (rightTree) {
      while (rightTree && rightTree.left) rightTree = rightTree.left;
      return rightTree.key;
    }
    return null;
  }

  delete(currentNode, key) {
    //如果节点无效,return
    if (!currentNode) return null;

    //找不到该节点
    if (!this.search(currentNode, key)) return currentNode;

    if (key < currentNode.key) {
      currentNode.left = this.delete(currentNode.left, key);
      return currentNode;
    } else if (key > currentNode.key) {
      currentNode.right = this.delete(currentNode.right, key);
      return currentNode;
    } else {
      // 叶子节点
      if (!currentNode.left && !currentNode.right) {
        currentNode = null;
        return currentNode;
      }

      if (!currentNode.right) {
        //没有右子树
        currentNode = currentNode.left;
        return currentNode;
      } else if (!currentNode.left) {
        //没有左子树
        currentNode = currentNode.right;
        return currentNode;
      } else {
        /**
         * 存在左子树也存在右子树的情况:
         * 1. 找出左子树中最大或者右子树中最小的值val
         * 2. 将当前节点的值替换为val
         * 3. 在左子树或者右子树中找到val删除
         */
        let minRightNodeKey = this.findMinNodeKey(currentNode.right);
        currentNode.key = minRightNodeKey;
        currentNode.right = this.delete(currentNode.right, minRightNodeKey);
        return currentNode;
      }
    }
  }

  // 前序遍历
  preOrderTraverse(node, cb) {
    if (node !== null) {
      cb(node.key);
      this.preOrderTraverse(node.left, cb);
      this.preOrderTraverse(node.right, cb);
    }
  }

  // 中序遍历
  inOrderTraverse(node, cb) {
    if (node !== null) {
      this.inOrderTraverse(node.left, cb);
      cb(node.key);
      this.inOrderTraverse(node.right, cb);
    }
  }

  // 后序遍历
  postOrderTraverse(node, cb) {
    if (node !== null) {
      this.postOrderTraverse(node.left, cb);
      this.postOrderTraverse(node.right, cb);
      cb(node.key);
    }
  }
}

const arr = [62, 88, 58, 47, 35, 73, 51, 99, 37, 93];
const bst = new BinarySearchTree();
const rootNode = bst.create(arr);

// console.log("\n");
// console.warn("preOrderTraverse");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));

// console.log("\n");
// console.warn("inOrderTraverse");
// bst.inOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));

// console.log("\n");
// console.warn("postOrderTraverse");
// bst.postOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));

// search
// const isExist = bst.search(rootNode, 100);
// console.warn("isExist");
// console.log(isExist);
// console.log("\n");

// search
// bst.insert(rootNode, 30);
// console.log("\n");
// console.warn("preOrderTraverse - after inserted:");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));

// delete
// bst.delete(rootNode, 62);
// console.warn("preOrderTraverse - after deleted:");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));