原文参考我的公众号文章 梳理一波「二叉树
二叉树
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
- 根节点:无父节点的节点
- 叶子结点:无子节点的节点
- 兄弟节点:有相同根节点的节点
关于“树”,还有三个比较相似的概念:高度(Height)、深度(Depth)、层(Level)。它们的定义是这样的:
二叉查找(排序)树
- 也叫二叉搜索树,支持动态数据集合的快速插入、删除、查找操作;
- 有序二叉树:在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值;
- 是一颗二叉树,每个节点最多两个子结点,即每一层最多 2^(level - 1)个节点;
树结构
/** 树节点 */
class BinaryNode {
constructor(data) {
this.data = data; //存储的数据
this.left = null; //左节点
this.right = null; //右节点
this.level = 1; //所属层级
this.count = 1; //重复数据次数
this.isDeleted = false; //是否被删除
}
}
/** 二叉树 */
class BinarySearchTree {
constructor() {
this.root = null; //根节点:在第一次insert时会被赋予值
this.nodes = 0; //节点数
this.list = []; //遍历二叉树时临时存储数据的数组
}
}
插入节点
插入规则:新结点值与当前插入位置(从根节点开始)的节点值进行比较
- 小于父节点值:插在父节点左节点上;
- 大于父节点值:插在父节点右节点上;
- 等于父节点值:父节点 count++;(这种方式看情况,如果结点还有其他数据而非只有一个 data,那可以。否则应合并到上一种情况,合并到右节点上)
- 插入过程可增加被插入的结点所属 level 的维护!
// class BinarySearchTree { ... }
/**
* 插入数据
* @param {*} data 结点的值
*/
insert(data) {
this.nodes++;
// 递归插入
// let newNode = new BinaryNode(data);
// if (this.root === null) {
// this.root = newNode;
// } else {
// this._insert(this.root, newNode);
// }
// 循环插入
this.loopInsert(data)
}
/**
* 递归的方式插入节点:新结点值与当前插入位置(从根节点开始)的节点值进行比较
* 小于根节点值:插在根节点左节点上;
* 大于等于根节点值:插在根节点右节点上;
* 增加结点所属level的维护!
* @param {*} theRootNode 被插入位置节点(从根节点开始)
* @param {*} newNode 新插入的节点
*/
_sameValAsBigInsert(theRootNode, newNode) {
newNode.level++; //从根出发,每执行一次_sameValAsBigInsert,层级便自动+1
if (theRootNode.data > newNode.data) {
if (theRootNode.left === null) {
theRootNode.left = newNode;
} else {
// 说明不是叶子结点,继续向下层插入判断
this._sameValAsBigInsert(theRootNode.left, newNode);
}
} else {
if (theRootNode.right === null) {
theRootNode.right = newNode;
} else {
// 说明不是叶子结点,继续向下层插入判断
this._sameValAsBigInsert(theRootNode.right, newNode);
}
}
}
/**
* !!!值相同,则合并到当前根节点
* @param {*} theRootNode 被插入位置节点(从根节点开始)
* @param {*} newNode 新插入的节点
*/
_sameValMergeInsert(theRootNode, newNode) {
newNode.level++; //从根出发,每执行一次_sameValMergeInsert,层级便自动+1
if (theRootNode.data > newNode.data) {
if (theRootNode.left === null) {
theRootNode.left = newNode;
} else {
// 说明不是叶子结点,继续向下层插入判断
this._sameValMergeInsert(theRootNode.left, newNode);
}
} else if (theRootNode.data < newNode.data) {
if (theRootNode.right === null) {
theRootNode.right = newNode;
} else {
// 说明不是叶子结点,继续向下层插入判断
this._sameValMergeInsert(theRootNode.right, newNode);
}
} else {
theRootNode.count++; // 值相同,则合并到当前根节点
}
}
/**
* 循环的方式插入
* @param {*} data
* @returns
*/
loopInsert(data) {
if (this.root == null) {
this.root = new BinaryNode(data);
return;
}
let p = this.root;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new BinaryNode(data);
return;
}
p = p.right;
} else {
// data <= p.data
if (p.left == null) {
p.left = new BinaryNode(data);
return;
}
p = p.left;
}
}
}
遍历树(前序、中序、后续)
遍历树采取递归的方式,出递归的条件是 node=null
/** 遍历树: [type] 遍历方式 */
print(type) {
this.list = [];
let callee = `${type}Order`;
this[callee](this.root);
}
- 前序遍历
prevOrder(node){
if(node==null){
return
}
console.log(node.data)
this.prevOrder(node.left)
this.prevOrder(node.right)
}
- 中序遍历(遍历结果正好是有序的)
inOrder(node){
if(node==null){
return
}
this.inOrder(node.left)
console.log(node.data)
this.inOrder(node.right)
}
- 后序遍历
postOrder(node){
if(node==null){
return
}
this.postOrder(node.left)
this.postOrder(node.right)
console.log(node.data)
}
查找节点
先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。
/**
* 查找一个节点
* @param {*} data 要查找的值
* @returns
*/
find(data) {
let node = this.root;
while (node != null) {
if (data < node.data) {
node = node.left;
} else if (data > node.data) {
node = node.right;
} else {
return node;
}
}
return null;
}
删除节点
物理删除:删除操作比较复杂,一般要分以下三种情况分别处理。
- 被删除节点没有子节点:直接将父节点中,指向要删除节点的指针置为 null;
- 被删除节点只有一个子节点:只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点(左或右)就可以了;
- 被删除节点右两个子节点(最复杂):我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点;
逻辑删除节点(简易版)
/**
* 删除节点(简易版)
* 对要删除的节点做标记,表示已删除
* @param {*} data
*/
delete(data){
let targetNode = this.find(data);
// 未找到要删除的数据
if(targetNode == null){
return null
}
// 标记为“已删除”
targetNode.isDeleted = true;
}
}
逻辑删除对应的树遍历方式修改!
prevOrder(node) {
if (node == null) {
return;
}
/**+++ 此处增加判断,inOrder和postOrder同样处理! */
if (node.isDeleted != true) {
this.list.push(node.data);
console.log(node.data);
}
this.prevOrder(node.left);
this.prevOrder(node.right);
}
测试有序二叉树
// 创建一颗有序二叉树
let bsTree = new BinarySearchTree();
// 插入数据
let arr = [6,8,7,6,3,5,9,4]
arr.map(item=>{
bsTree.insert(item)
})
// 二叉树结构
console.log(bsTree);
// 三种方式遍历整棵树!
console.log('前序遍历');
bsTree.prevOrder(bsTree.root)
console.log('\n\n')
console.log('中序遍历(有序输出)');
bsTree.inOrder(bsTree.root)
console.log('\n\n')
console.log('后序遍历');
bsTree.postOrder(bsTree.root)
console.log('\n\n')
// 查找某个节点
console.log("\n查找值6");
console.log(bsTree.find(6));
// 删除某个节点
console.log("\n删除6");
console.log(bsTree.delete(6));
console.log("\n\n");
console.log("删除后-前序遍历");
bsTree.print("prev");
总结(来自极客时间课程总结,总归比我自己总结的好 🤷♂️)
这篇文章学习了一种特殊的二叉树,二叉查找树。它支持快速地查找、插入、删除操作。
二叉查找树中,每个节点的值都大于左子树节点的值,小于右子树节点的值。不过,这只是针对没有重复数据的情况。对于存在重复数据的二叉查找树,我介绍了两种构建方法,
- 一种是让每个节点存储多个值相同的数据;
- 另一种是,每个节点中存储一个数据。针对这种情况,只需要稍加改造原来的插入、删除、查找操作即可。
在二叉查找树中,查找、插入、删除等很多操作的时间复杂度都跟树的高度成正比。两个极端情况的时间复杂度分别是 O(n) 和 O(logn),分别对应二叉树退化成链表的情况和完全二叉树。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。