js 数据结构 树(二叉搜索树的实现)
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js 数据结构 树(二叉搜索树的实现)相关的知识,希望对你有一定的参考价值。
二叉树重要的特性
- 一个二叉树第i层的最大节点数为 2 ^ (i - 1), i>=1
- 深度为k的二叉树有最大节点总数为: 2 ^ k -1 k>=1
- 对于任何非空二叉树T,若n0表示叶节点的个数,n2是度为2的非叶节点个数,那么两者的关系是 n0 = n2 + 1
完美二叉树
除了最下面的叶子节点,其他节点的度均为2。
完全二叉树,
除了二叉树最后一层外,其他各层的节点数都达到最大个数。
且组后一层从左向右的叶子节点连续存在,只缺若干节点,
完美二叉树是特殊的完全二叉树
这个并不是完全二叉树,D下面的一个叶子节点不在了。
二叉树的存储
常见的有数组和链表。
使用数组:
- 完全二叉树:按从上往下,从左往右进行排序。(优)
- 非完全二叉树:非完全二叉树要转成完全二叉树擦能按照上面的方案存储,但是会造成很大的空间浪费。
使用链表:
每个节点封装成一个node,包含存储的数据,左节点的引用,和右节点的引用。
二叉搜索树BST
二叉搜索树可以是空
但是非空的情况下:
需要满足:
- 非空左子树的所有键值小于其根节点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左右子树本身也是二叉搜索树。
如
第一个不是,因为有左子树。非空左子树,其所有键值就是10< 18, 7 < 18,满足,非空右子树的所有键值20>18, 22 > 18满足,第三个条件,左右子树本身也是二叉搜索树。
对于10来说,非空左子树为7,小于10,满足,但是非空右子树为5,不满足。
特点:
- 相对较小的值总是保存在左节点,相对较大的值,总是保存在右节点。
- 查找效率高,因为二叉搜索树已经排好序了。
实现二叉搜索树的封装
整个基础架构就是这样,树的每个节点有三个指针,分别存放value,左子节点,右子节点。
常见的二叉搜索树操作
插入
思路:利用二叉搜索树的特性,从root结点开始判断,如果大,就跟root.right节点判断,如果小,就跟root.left的节点判断,依次类推,直到判断有一个节点比newNode小并且右子节点没有值等等的情况,就可以插入了。
如
先跟根节点比较。
递归比较,插入。
大的节点都在根节点2的右边,小的节点都在根节点的左边。完成。
遍历:先序遍历,中序遍历,后序遍历,分别对应根节点在头部,中部,尾部处理。
先序遍历
树结构大概是
先访问根节点,再先序遍历左子树,再先序遍历右子树。所以结果应该为,4,2,3,3.5,5,6
初始化先处理root节点,然后递归调用,处理left节点。再到处理right节点。这就是先序遍历。
结果一样。
中序遍历
就是先中序遍历左子树,再访问根节点,最后中序遍历右子树。
结果应该是:2,3,3.5,4,5,6
实现的话只需要换个顺序处理,先处理左子树的节点就可以。
结果同预想的一样。
后序遍历
先后续遍历左子树,再后序遍历右子树,最后遍历根节点。所以结果应该是:2,3.5,3,5,6,4
也是调换顺序就行。
结果也同预想一样,遍历就完成了。
最大值和最小值
只要找出最左边和最右边的值就行。
搜索特定的值
二叉搜索树不仅获最值效率高,而且搜索特定值的效率也很高。
搜索只需要判断搜索的值与当前值的大小相比,大的就走右边继续递归比较,小的就走左边递归比较。直到找到为止
层级遍历
就是一层一层从左往右遍历:
思路就是维护一个数组,从根节点开始处理,将其左右子树放入数组,然后索引递增,处理每个节点即可。
如上,先处理根节点,放入其左右子树,然后处理左右子树,将他们的左右子树继续放入数组,最后数组就是的顺序就是一层一层获取的。
顺序正确
删除节点操作
删除节点主要分为三种情况
- 1 被删除的节点,没有子节点,也就是叶子节点
- 2 被删除的节点,只有一个左节点,或者右节点
- 3 被删除的节点,两边都有子节点
第三个最复杂。先完成12.
先获取到删除的节点current,再获取删除的节点的父节点Paren,然后获取删除的节点current是父结点parent的右节点还是左节点。如果是叶子节点,就直接删除
如果是只有一个子节点,判断被删除的节点current的唯一一个子节点是在哪边。
然后直接让父亲指向current的子节点,跳过current节点。
第三种也是最复杂的方式,两边都有子节点。
我们可以通过一个规律:要找被删除的节点current的大一点点或者小一点的值来代替。
- 前驱:被删除节点的小一点点的值,也就是current的左子树中最大的值。
- 后继:被删除节点大一点点的值,也就是current的右子树中最小的值。
定义两个方法来获取这两个节点。
获取前驱,注意是从当前节点的左子树找最大值,所以需要循环遍历一直往右边找。需要使用parent来切断前驱节点与它的父结点的关系。
如果只有一层子节点,表示current的左子节点就是前驱,让parent.left置为null。而不止一层的话就需要往右边查找,直到找到前驱,然后切断前驱的父结点与他的关系,因为是往右边找,所以切断right。
后继也是一样的道理。
然后来看看实现:
获取前驱后继结点之后,要替换掉被删除节点,还要接手他的左右子节点。
让父结点指向后继,让后继接手被删除节点的左右子节点。
测试:
原本是这样,
删除3之后,找后继来代替,后继是右子树最小的之,也就是3.1。所以应改变这样
3.1替换了3。
看效果:
3.1代替了3的位置,并且接手了他的两个左右子树
删除完成
反转二叉搜索树
简单的将遍历,在遍历过程中左右子树直接替换就行。
如上,只要通过层级遍历,然后全部节点反转其左右子树就行了。
二叉搜索树就完成了
总结
- 相对较小的值总是保存在左节点,相对较大的值,总是保存在右节点。
- 查找效率高,因为二叉搜索树已经排好序了。
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//二叉搜索树的封装
class Node
constructor(key)
this.key = key;
this.left = null;
this.right = null;
this.tree = []; // 遍历
class Tree
constructor()
this.root = null;
static insertNode(node, newNode)
if (node.key < newNode.key)
if (!node.right)
node.right = newNode;
return;
Tree.insertNode(node.right, newNode);
else
if (!node.left)
node.left = newNode;
return;
Tree.insertNode(node.left, newNode);
insert(key)
const newNode = new Node(key);
if (!this.root)
this.root = newNode;
else
Tree.insertNode(this.root, newNode);
getMax()
//往左边找就行了
let current = this.root;
while (current.right)
current = current.right;
return current.key;
getMin()
let current = this.root;
while (current.left)
current = current.left;
return current.key;
search(searchKey)
return this._search(this.root, searchKey);
_search(node, searchKey)
if (node)
if (node.key > searchKey)
return this._search(node.left, searchKey);
else if (node.key < searchKey)
return this._search(node.right, searchKey);
else
return node;
else
return undefined;
delete(key)
// 找到删除的节点
// 三种情况 一 叶子节点 二 删除只有一个子节点 三 删除只有两个子节点的节点
let current = this.root;
let isLeftChild = true;
let parent = null;
// 获取对应的节点
while (current && current.key !== key)
parent = current;
if (current.key < key)
current = current.right;
isLeftChild = false;
else
current = current.left;
isLeftChild = true;
if (!current)
return undefined;
const replacePosition = isLeftChild ? "left" : "right";
const twoSonExit = current.left && current.right;
if (twoSonExit)
// 两个子节点都存在,
// 规律:找删除节点大一点点或者小一点点的值,比删除节点大一点点,就是删除节点右子树的最小值,
// 比删除节点小一点点,就是删除节点左子树的最大值
// 前驱,后继。当前节点的前驱,就是比current小一点点的节点,删除节点左子树的最大值 。
// 后继:当前节点的后继,就是比current大一点的节点。
// 情况 1获取前驱, 而前驱后继一定是叶子节点
//const preNode2 = this.getPreNode(current)
// 情况2 获取后继
const preNode = this.getEpiNode(current)
// preNode替换current
if (parent)
//很可能删除的就似乎根节点,那么parent就没值
parent[replacePosition] = preNode
preNode.left = current.left
preNode.right = current.right
else if (current.left || current.right)
//一个子节点存在
const isLeft = !!current.left;
if (parent)
//很可能删除的就似乎根节点,那么parent就没值
parent[replacePosition] = current[isLeft ? "left" : "right"];
else
//叶子节点
if (parent)
//很可能删除的就似乎根节点,那么parent就没值
parent[replacePosition] = null;
return current;
//获取前驱,找出左子树的最大值
getPreNode(node)
// 因为是有两个节点的处理,所以node一定有left和right
let preNode = node.left
let parent = node
// 循环遍历找出左子树最大的值,就是一直看有没有右节点即可。
while (preNode && preNode.right)
parent = preNode
preNode = preNode.right
if (parent === node)
//如果只有一层,就直接让左节点为空,因为左节点就被作为前驱拿来代替了。
parent.left = null
else
//否则就是往右边继续找。
parent.right = null
return preNode
// 后继 找出右子树的最小值
getEpiNode(node)
// 因为是有两个节点的处理,所以node一定有left和right
let preNode = node.right
let parent = node
// 循环遍历找出右子树最小的值,就是一直看有没有左节点即可。
while (preNode && preNode.left)
parent = preNode
preNode = preNode.left
if (parent === node)
//只有一层子节点,直接让右节点作为后继代替
parent.right = null
else
// 否则就是往左边继续找
parent.left = null
return preNode
//遍历树
//三种方式 先序 中序 后续
// 后序 先 后序遍历左子树,再后序遍历右子树,再遍历跟节点。
epiOrderTraversal(arr)
Tree.EpiOrderTarversal(this.root, arr);
static EpiOrderTarversal(node, arr)
if (node !== null)
//处理当前节点的右节点
Tree.EpiOrderTarversal(node.left, arr);
Tree.EpiOrderTarversal(node.right, arr); //递归遍历
arr.push(node.key); //中间处理node,中序遍历
//中序遍历
middleOrderTraversal(arr)
Tree.middleOrderTraversalNode(this.root, arr);
//中序遍历 中序遍历其左子树,访问根节点 中序遍历右子树 ,根节点是在中间处理的。
static middleOrderTraversalNode(node, arr)
if (node !== null)
//处理当前节点的左节点
Tree.middleOrderTraversalNode(node.left, arr); //递归遍历
arr.push(node.key); //中间处理node,中序遍历
Tree.middleOrderTraversalNode(node.right, arr);
// 先序,访问跟节点,先序遍历其左子树,先序遍历其右子树,
preOrderTraversal(arr)
Tree.preOrderTraversalNode(this.root, arr);
//先序遍历 为啥叫先序遍历,根节点是在最开始处理的。
static preOrderTraversalNode(node, arr)
if (node !== null)
//处理当前节点的左节点
arr.push(node.key); //先处理node,先序遍历
Tree.preOrderTraversalNode(node.left, arr); //递归遍历
Tree.preOrderTraversalNode(node.right, arr);
// 层序遍历, 一层一层从左到右执行,
// 思路就是维护一个数组,然后从根节点开始,一层一层放入,然后通过索引一个一个拿出来处理,将其左右子树继续放入数组
// 如先放入根节点,那么就是先处理根节点,将根节点的左右子树放入,然后索引向前,继续处理根节点的左子树,依次类推进行处理。
levelOrderTraverSalNode()
const stack = [this.root]
let index = 0
let currentNode;
while (currentNode = stack[index])
if (currentNode.left)
// 处理当前节点的左子树,插入进去
stack.push(currentNode.left)
if (currentNode.right)
// 处理当前节点的右子树,插入禁区
stack.push(currentNode.right)
index++;
return stack
// 反转二叉树, 直接遍历然后左右交换就可以了,利用层级遍历来处理
rever()
const stack = [this.root]
let index = 0
let currentNode;
while (currentNode = stack[index])
// 先反转,然后继续遍历,继续反转。
let temp = currentNode.left
currentNode.left = currentNode.right
currentNode.right = temp
//
if (currentNode.left)
// 处理当前节点的左子树,插入进去
stack.push(currentNode.left)
if (currentNode.right)
// 处理当前节点的右子树,插入禁区
stack.push(currentNode.right)
index++;
return stack
const test = new Tree();
test.insert(4);
test.insert(3);
test.insert(3.5);
test.insert(6);
test.insert(2);
test.insert(1);
test.insert(2.5);
test.insert(3.1)
test.insert(3.6)
test.insert(5);
// const arr = [];
// const arr2 = [];
// const arr3 = [];
// test.preOrderTraversal(arr);
// console.log(arr);
// test.middleOrderTraversal(arr2);
// console.log(arr2);
// test.epiOrderTraversal(arr3);
// console.log(arr3);
console.log(test.levelOrderTraverSalNode());
</script>
</body>
</html>
以上是关于js 数据结构 树(二叉搜索树的实现)的主要内容,如果未能解决你的问题,请参考以下文章
高阶数据结构 | 二叉搜索树(Binary Search Tree)