二叉查找树 (Binary Search Tree)

286 阅读6分钟

一、树

  1. 树(Tree)是n(n>=0)个结点的有限集。 在任意一棵非空树中: 有且仅有一个特定的称为(Root)的结点;
  2. 树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的(Degree)。(b)中A的度为3,C的度为1,F的度为0.
  3. 度为0的结点称为叶子(Leaf)或者终端结点
  4. 结点的层次(Level)从根开始定义起,根为第一层,跟的孩子为第二层。树中结点的最大层次称为树的深度(Depth)或高度。(b)的树的深度为4。

二、二叉树

  1. 二叉树(Binary Tree),它的特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分(其次序不能任意颠倒。)
  2. 一棵深度为k且有2的k次方-1个结点的二叉树称为满二叉树
  3. 深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树
  4. 二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树) 它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空, 则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空, 则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。 特点: 左子树小于父节点,右子树大于父节点

三、创建二叉查找树

接下来就是代码部分,实现上图中的创建一个二叉查找树的逻辑。

  1. 创建类
// 创建树节点类
class Node {
  constructor (value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}
// 创建二叉查找树类
class BinaryTree {
  constructor () {
    this.root = null;
  }
  // 若根 (root) 为null,则直接设为根;否则递归插入
  insert(value) {
    let newNode = new Node(value);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }
  insertNode(node, newNode) {
    // 依照树的特点,小值递归插入左树,大值递归插入右树
    if (newNode.value < node.value) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }
}
  1. 遍历树

遍历分为前序遍历、中序遍历、后序遍历

  • 中序遍历: 先输出左树,再输出根,最后输出右树
  • 前序遍历: 先输出根,再输出左树,最后输出右树
  • 后序遍历: 先输出左树,再输出右树,最后输出根

(1)递归遍历

  //三种遍历的区别在于输出顺序
  //中序遍历
  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback)
  }
  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.value);
      this.inOrderTraverseNode(node.right, callback);
    }
  }
  //先序遍历
  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback)
  }
  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.value);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }
  //后序遍历
  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback)
  }
  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.value);
    }
  }

(2)非递归遍历

  //栈 的先进后出特点
  
  //中序遍历
  //1.进入循环,先将根入栈,再将左节点作为新的根,进入下次循环
  //2.若没有左节点则将当前节点出栈,并将右节点作为下次循环根,进入下次循环
  //3.若没有右节点,则将当前节点出栈,重复第二步
  inOrderTraverseByStack(callback) {
    let stack = [],
        node = this.root;
    while (stack.length > 0 || node) {
      if (node) {
        stack.push(node);
        node = node.left;
      } else {
        node = stack.pop();
        callback(node.value);
        node = node.right;
      }
    }
  }
  //先序遍历
  //1.先将根节点入栈
  //2.然后进入循环,当前节点出栈,右节点、左节点依次入栈 (注意顺序,栈先进后出)
  preOrderTraverseByStack(callback) {
    let stack = [],
        node = this.root;

    stack.push(node);

    while (stack.length > 0) {
      node = stack.pop();
      callback(node.value);

      if (node.right) {
        stack.push(node.right);
      }
      if (node.left) {
        stack.push(node.left);
      }
    }
  }
  //后序遍历
  //1.创建两个栈,先把根放入栈1
  //2.进入循环,从栈1出栈一个节点,放入栈2
  //3.判断这个节点的左右节点是否存在,存在则分别放入放入栈1 (注意左节点先入栈1,右节点后入,这样到栈2就是反过来的顺序)
  //4.依次循环,直到栈1为空,循环栈2,依次出栈即可
  postOrderTraverseByStack(callback) {
    let stack1 = [],
        stack2 = [],
        node = this.root;
    stack1.push(node);
      
    while (stack1.length > 0) {
      node = stack1.pop();
      stack2.push(node);
      if (node.left !== null) {
        stack1.push(node.left);
      }
      if (node.right !== null) {
        stack1.push(node.right);
      }
    }
    while (stack2.length > 0) {
      node = stack2.pop();
      callback(node.value);
    }
  }
  1. 查找最小、最大值以及特定值
  // 查找最小、最大值
  findMin() {
    let minNode = this.findMinNode(this.root);
    return minNode ? minNode.value : null;
  }
  findMinNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
      }
      return node;
    }
    return null;
  }
  findMax() {
    let maxNode = this.findMaxNode(this.root);
    return maxNode ? maxNode.value : null;
  }
  findMaxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right;
      }
      return node;
    }
    return null;
  }
  //查找特定值
  find(value) {
    return this.findSearchNode(this.root, value);
  }
  findSearchNode(node, value) {
    if (node === null) {
      return false;
    }
    if (value < node.value) {
      return this.findSearchNode(node.left, value);
    } else if (value > node.value) {
      return this.findSearchNode(node.right, value);
    } else {
      return true;
    }
  }
  1. 删除一个指定节点
  //首先找到要删除节点的位置
  //然后分3种情况
  //1.没有子节点,直接删除
  //2.有一个子节点,用子节点替换当前节点
  //3.有两个子节点,则在右树中找到最小值替换当前节点的值,并删除替换的节点,以保持树的特性
  remove(value) {
    this.root = this.removeNode(this.root, value);
  }
  removeNode(node, value) {
    if (node === null) {
      return node;
    }
    if (value < node.value) {
      node.left = this.removeNode(node.left, value);
      return node;
    } else if (value > node.value) {
      node.right = this.removeNode(node.right, value);
      return node;
    } else {
      //情况1 没有子节点
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }
      //情况2 一个子节点
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }
      //情况3 两个子节点
      let aux = this.findMinNode(node.right);
      node.value = aux.value;
      node.right = this.removeNode(node.right, aux);
      return node;
    }
  }

至此,二叉查找树的创建、插入、遍历、查找、删除已全部实现。下面是实例化代码。

// 实例化二叉树
let nodes = [8, 6, 16, 5, 10, 9, 7, 18, 15, 17, 23, 29];
let binaryTree = new BinaryTree();
nodes.forEach(key => {
    binaryTree.insert(key);
});

let inOrderNodes = [];
let preOrderNodes = [];
let postOrderNodes = [];
let callback = function (key) {
  inOrderNodes.push(key)
}
let callback2 = function (key) {
  preOrderNodes.push(key)
}
let callback3 = function (key) {
  postOrderNodes.push(key)
}
// 中序遍历数组
binaryTree.inOrderTraverse(callback);
// binaryTree.inOrderTraverseByStack(callback);
console.log(inOrderNodes);

// 先序遍历数组
binaryTree.preOrderTraverse(callback2);
// binaryTree.preOrderTraverseByStack(callback2)
console.log(preOrderNodes);

// 后序遍历数组
binaryTree.postOrderTraverse(callback3);
// binaryTree.postOrderTraverseByStack(callback3)
console.log(postOrderNodes);

//[8, 6, 16, 5, 10, 9, 7, 18, 15, 17, 23, 29];
//=========== 删除节点前 ==========//
console.log('=========删除节点前==========');
// 查找最小、最大值
console.log(`最小值为 ${binaryTree.findMin()}`);
console.log(`最大值为 ${binaryTree.findMax()}`);
// 查找特定值是否存在
console.log(`9 ${binaryTree.find(9) ? '存在' : '不存在'}`);
console.log(`88 ${binaryTree.find(88) ? '存在' : '不存在'}`);

//=========== 删除节点后 ==========//
console.log('=========删除节点5, 9 插入节点88==========');
// 删除子节点
binaryTree.remove(5);
binaryTree.remove(9);
//插入88
binaryTree.insert(88);
// 查找最小、最大值
console.log(`最小值为 ${binaryTree.findMin()}`);
console.log(`最大值为 ${binaryTree.findMax()}`);
// 查找特定值是否存在
console.log(`9 ${binaryTree.find(9) ? '存在' : '不存在'}`);
console.log(`88 ${binaryTree.find(88) ? '存在' : '不存在'}`);

binaryTree.inOrderTraverseByStack(callback);
console.log(inOrderNodes)
// [ 6,  7,  8, 10, 15, 16, 17, 18, 23, 29, 88 ]
// 对比原输出
// [ 5,  6,  7,  8,  9, 10, 15, 16, 17, 18, 23, 29 ]

查看 完整代码

Generator与for...of的运用

递归遍历还可以结合ES6中的Generator函数,yield*以及for...of来实现

  inOrderTraverseByGen(callback) {
    //for...of循环可以应用于任何部署了Iterator接口的数据结构
    for (let value of this.inOrderTraverseNodeByGen(this.root)) {
      callback(value);
    }
  }
  // 函数名前面加上* 代表这是一个Generator函数
  * inOrderTraverseNodeByGen(node) {
    if (node !== null) {
    //yield* 可以在一个Generator函数内部执行另一个Generator函数
      yield* this.inOrderTraverseNodeByGen(node.left);
      yield node.value;
      yield* this.inOrderTraverseNodeByGen(node.right);
    }
  }
  //...
  //用法和输出结果和之前一致
  binaryTree.inOrderTraverseByGen(callback);
  console.log(inOrderNodes);
  //[ 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 23, 29 ]