JavaScript实现二分搜索树(递归)

477 阅读4分钟

二分搜索树看了我整整三天,又是被人类智慧折服的每一天,冲冲冲!

1. 什么是二分搜索树

二分搜索树(Binary Search Tree, BST),属于树形数据结构的一种,二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树,它的定义如下:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点;

image.png

优点

  1. 相比于其他数据结构的优势在于有着高效的插入、删除、查找操作,平均时间复杂度为O(logn)
  2. 可以方便地回答数据之间的关系的问题:如min,max,floor,ceil,rank,select
插入删除查找
普通数组O(n)O(n)O(n)
顺序数组O(n)O(n)O(logn)
二分搜索树O(logn)O(logn)O(logn)

普通数组插入O(n):这里实现的是无重复元素的数据结构,所以普通数组插入时需要先查找是否已经存在该元素,如果存在则更新,不存在则插入。

局限性

  1. 不能随机访问
  2. 二分搜索树为不平衡树,若树分布极不平衡,则会大大影响时间性能 同样的数据,可以对应不同的二分搜索树,如果节点数和树深度相同(类似链表),则所有操作退化为O(n) image.png

    可改进为平衡二叉搜索树,如红黑树等.本文不做探讨

2. JavaScript实现BST

以下为构建一个无重复元素的二分搜索树(类似字典),可以通过查找 key 得到 value

👇以下为主要结构

// 构造节点  
class Node {
	constructor(key, value) {
		this.key = key
		this.value = value
		this.left = null
		this.right = null
    }
}
// 构造二分搜索树
class BST {
	constructor(root = null) {
		this.root = root
		this.count = 0
	}
  // 主要方法
  insert(key){} // 插入
  search(key){} // 查找 
  preOrder(){} // 前序遍历
  inOrder(){}  // 中序遍历
  postOrder(){} // 后序遍历
  levelOrder(){} // 广度优先遍历(层序)
  searchMin(){} // 查找最小
  searchMax(){} // 查找最大
	deleteMin(){} // 删除最小
  deleteMax(){} // 删除最大
  delete(key){} // 删除
}

2.1 插入

// 向以node为根的二叉搜索树中,插入节点(key, value)
// 返回根节点

// 递归实现
insert(key, value) {
  return this.root = this._insert(this.root, key, value)
}
_insert(node, key, value) {
  if (node === null) {
    return node = new Node(key, value)
  }
  if (key === node.key) {
    node.value = value
  } else if (key < node.key) {
    node.left = this._insert(node.left, key, value)
  } else {
    node.right = this._insert(node.right, key, value)
  }
  return node
	}
}

2.2 查找

// 向以node为根的二叉搜索树中,搜索是否包含key的节点。
// 包含返回node,不包含则返回null
search(key) {
  let node = this.root
  while (node !== null) {
    if (key === node.key) {
      return node
    } else if (key > node.key) {
      node = node.right
    } else {
      node = node.left
    }
  }
  return null
}

2.3 遍历

二分搜索树遍历分为两大类,深度优先遍历广度优先遍历

深度优先遍历分为三种,先序遍历、中序遍历、后序遍历。(以根的位置划分) 先序遍历:根左右 中序遍历:左根右 后序遍历:左右根

// 先序遍历(递归)
preOrder(node = this.root, arr = []) {
  if (node !== null) {
    arr.push(node.key)
    this.preOrder(node.left, arr)
    this.preOrder(node.right, arr)
  }
  return arr
}

// 中序遍历(递归)
inOrder(node = this.root, arr = []) {
  if (node !== null) {
    this.inOrder(node.left, arr)
    arr.push(node.key)
    this.inOrder(node.right, arr)
  }
  return arr
}

// 后序遍历(递归)
postOrder(node = this.root, arr = []) {
  if (node !== null) {
    this.postOrder(node.left, arr)
    this.postOrder(node.right, arr)
    arr.push(node.key)
  }
  return arr
}

// 广度优先遍历(层级)
levelOrder() { 
  const stack = []
  const arr = []
  stack.push(this.root)
  while (stack.length > 0) {
    let node = stack.shift() // 先进先出
    arr.push(node.key)
    node.left && stack.push(node.left)
    node.right && stack.push(node.right)
  }
  return arr
}

2.4 删除

  1. 查找要删除的节点 (1)如果该节点不存在,则返回null (2)如果该节点存在,则继续

  2. 判断

    (1)当节点没有子节点,那么只需要将从父节点指向它的链接指向变为null

    (2)当节点只有左子树时,父节点指向该节点的子节点

    (2)当节点只有右子树时,父节点指向该节点的子节点

    (3)【重点】当节点包含左右子树时,该节点替换为左子树中最大的节点或右子树中最小的节点(1962年,Hubbard deletion)

image.png

delete(key) {
  return this.root = this._deleteNode(this.root, key)
}
_deleteNode(node, key) {
  if (node === null) {
    return null
  }
  if (key > node.key) {
    node.right = this._deleteNode(node.right, key)
    return node
  } else if (key < node.key) {
    node.left = this._deleteNode(node.left, key)
    return node
  } else { // key === node.key
    if (node.left === null) {
      return node.right
    } else if (node.right === null) {
      return node.left
    } else {
      // 左右节点均不为空
      let rightMinNode = this.searchMin(node.right)
      let successor = new Node(rightMinNode.key, rightMinNode.value)
      successor.left = node.left
      successor.right = this.deleteMin(node.right)
      return successor
    }
  }
}

// delete(key) 需要调用searchMin(),deleteMin()(均为递归)
searchMin(node = this.root) {
  if (node.left === null) {
    return node
  }
  node = node.left
  return this.searchMin(node)
}

deleteMin(){
  // 删除以node为根节点的最小节点
  // 返回删除后的根节点
  if(this.root === null){
    return null
  }
  return this.root = this._deleteMin(this.root)
}
_deleteMin(node){
  if(node.left === null) {
    return node.right
  } 
  node.left = this._deleteMin(node.left)
  return node
}

更多

除了对数据进行增删改查,二分搜索树还可以回答数据之间的顺序性问题。(具体实现略)

  • minimum、maximum
  • successor(后继)、predecessor(前驱)
  • floor(地板)、ceil(天花板)
  • rank(58是排名第几的元素)、select(排名第10的元素是谁)

3. 不同设计的BST

image.png

4. 参考

慕课刘宇波【算法与数据结构 】 :大爱波波老师❤️
菜鸟教程-数据结构:具体的实现过程图示,nice!