本文引用至: 二叉树
树, 实际上是一个非常重要的数据结构, 比如,我们的进程树,文件树,HTML节点树等. 都是依赖这样的一个结构. 树,实际上是一种非线性的数据结构,但是他们是有序的. 如下图
每一个节点下面,都有本身的value,parent_node,child_node属性(除了根节点).
树的基本概念
每颗树都有根节点,叶子节点, 子节点,父节点的属性. 如果按 树组分的话, 还可分为子树等. 可以看一下下面这张图, 详细的阐述了基本的概念:
这里,我们先从简单的开始, 先了解一下 binary tree(二叉树)
二叉树基本内容
二叉树不同于基本的树: 他的基本性质如下:
每个节点只有两个子节点
每个子节点分为左子节点和右子节点
左子节点在索引顺序时优于右子节点
但是映射到数据结构中,我们怎么区分左右节点. 这里我们可以定义一个规则:
相对较小的值保存在左节点中,较大的值保存在右节点中
右节点的值,比父节点和左子节点都大(这条很重要)
基于这条规则,我们就可以创建一个二叉树. 二叉树,实际上就是有linked list组成的二维树. 但他不同于linked list , 该是一种非线性的结构。 所以, 他的插入方式,也是非线性的. 我们通常将该种方式查找树叫做,Binary search tree(BST). 我们可以使用代码进行模拟一下:
首先,我们得定义一个节点类:
value: 节点的值
left_child: 左子节点
right_child: 右子节点
class Node {
constructor(value, left, right) {
this.value = value;
this.left_child = left;
this.right_child = right
}
}
我们基本的BST 应该具有的方法有: add, find. 这里, 我们先模拟一下, 如果将数据插入二叉树中:
class BST {
constructor(root) {
this.root = root;
}
add(value) {
let new_node = new Node(value, null, null),
parent = this.root;
while (true) {
if (parent.value === null) {
parent = new_node;
break;
} else {
if (parent.value > value) {
if(parent.left_child==null){
parent.left_child = new_node;
break;
}
}else{
if(parent.right_child == null){
parent.right_child = new_node;
break;
}
}
}
}
}
}
现在, 我们已经存放了一些值. 那怎样去获取他呢?
常用的方法有, 中序, 先序, 后序 三种方法.
中序(in-order): 先以递归的形式访问左子树,然后再访问根节点, 最后访问右子树. 而隐含其中的关系就是: 由小到大的访问. 所以中序是遍历方法中唯一一个有顺序的.
先序(Pre-order): 先访问根节点, 然后以递归的方式访问左子树和右子树
后序(Post-order): 先以递归的方式访问左子树和右子树, 最后访问根节点.
上面的叙述,感觉有点干. 我们借助wiki提供的图,进行简单的讲解,方便理解遍历的具体过程.
这里,我们使用一种绕线法,跟着线的顺序, 接触一个点,表示被遍历到.
先序 : 线与左边相接触
中序 : 线与底边相接触
后序 : 线与右边相接触
现在,我们用代码来模拟一下中序遍历:
in_order(node){
if(node!==null){
this.in_order(node.left_child);
console.log(node.value);
this.in_order(node.right_child);
}
}
这里,利用了递归的写法, 因为二叉树本身的结构也是属于一种深层次数据结构. 只要靠自身的递归,这样才能不断的找到最里面那一层. 这样做虽然, 有点耗费内存, 但时间节省成本是相当大的.
那先序以及后序的写法,就很简单了.
先序: 根+左子树+右子树
pre_order(node) {
if (node !== null) {
console.log(node.value);
this.pre_order(node.left_child);
this.pre_order(node.right_child);
}
}
后序: 左子树+ 右子树 + 根
post_order(node){
if(node!==null){
this.in_order(node.left_child);
this.in_order(node.right_child);
console.log(node.value);
}
}
是不是很简单嘞? en. 看起来是的.
但 往往看起来很简单的,深层次都是复杂的一逼.
二叉树的相关性质
接下来, 我们通过 二叉查找树的查找内容,来探究二叉树的相关性质. 首先, 我们已经清楚
右节点> 父节点 > 左节点
首先, 我们来找一下二叉树的最小值. 推理一下, 当只有3个节点的时候, 那么最小值在左节点, 当有7 个节点时, 最小是还在左节点. 后面就不推了, 这里我们就假定最小值就在树的左下节点.
聪明的童鞋, 应该知道是什么意思, 自己看看我们插入的原理应该就清楚了.
现在我们来找一下最小值(左子节点). 方法很简单,还是用递归.
(这里使用的nodeJS 6.x版本添加的默认参数特性)
constructor(root) {
this.root = root;
this.min;
this.max;
}
min(node = this.root) {
if (node.left_child !== null) {
// 递归遍历左节点
this.min(node.left_child);
} else {
this.min = node.value;
}
return this.min;
}
同理, 最大值就是树的右下节点:
max(node = this.root) {
if (node.right_child !== null) {
// 递归遍历右节点
this.max(node.right_child);
} else {
this.max = node.value;
}
return this.max;
}
现在,我们有3条关于二叉树的性质:
右节点> 父节点 > 左节点
所有右子树 > 根节点 > 左子树
最小值是左叶子节点, 最大值是右叶子节点
接着,我们来在二叉树上查找给定的值:
由于上述性质我们可以知道, 二叉树实际上就是一个完美的二分法实验苗圃. 可以看一个总结图:
首先给你一个值, 在和根节点比较, 如果小则直接和左子树进行比较, 如果大则直接和右子树进行比较. 一次类推, 所以, 二叉树找数据是灰常快的. 不过, 我们这里就不用递归了, 使用循环来描述一下(因为简单的递归都可以使用循环来描述):
find(value, node = this.root) {
while (true) {
if(node === null){
// 如果节点为空,返回null
return null;
}else if(node.value===value){
// 找到对应的值并返回
return node;
}else if(node.value > value){
// 查找左子节点
node = node.left_child;
}else{
// 查找右子节点
node = node.right_child;
}
}
}
接下来, 又到打Boss 环节. 删除节点. 首先, 我们必须记住树的原则, 再来一遍:
右节点> 父节点 > 左节点
删除二叉树的情况,可以分为三种:
没有子节点: 直接删除, over
有一个节点: 用父节点指向节点,over
有两个节点: 用父节点,指向右子树节点,并且左节点放到右子树的左叶子节点.
有童鞋可能会问了. 需不需要考虑一下,有没有子树呢?
考虑个屁~
因为二叉树就是一个链式结构, 你只要移动父节点就可以了(即, 只要管一个节点).但当向链表的删除结构就比较复杂, 这里同样需要补充一个查找父节点的方法. 并且, 你还需要知道该节点相对于父节点而言是左还是右, 这就比较尴尬了:
child_contain(node, value) {
if (node.left_child !== null) {
if (node.left_child.value === value) {
return [true, 'left'];
}
}
if (node.right_child !== null) {
if (node.right_child.value === value) {
return [true, 'right']
}
}
return [false, 'null']
}
findParent(value, node = this.root) {
let has, child;
while (true) {
if (node === null) {
// 如果节点为空,返回null
return null;
}
[has,child] = this.child_contain(node, value)
if (has) {
// 找到父节点
return [node, child]
} else {
// 没有则继续遍历
if (node.value > value) {
// 查找左子节点
node = node.left_child;
} else {
// 查找右子节点
node = node.right_child;
}
}
}
}
有了上述两个方法之后, 我们就可以开始真正的删除节点了. 情况还是上述3种:
remove(value) {
// 找到删除节点
let node = this.find(value),
[parentNode, child] = this.findParent(value),
tmp; // 临时节点
//找到删除节点的父节点
if (node === null) {
// 节点不存在
return null;
}
if (node.left_child === null && node.right_child === null) {
// 没有子节点
parentNode[child] = null;
} else {
if (node.left_child === null) {
// 左子节点为空
parentNode[child] = node.right_child;
} else if (node.right_child === null) {
// 右子节点为空
parentNode[child] = node.left_child;
} else {
//子节点都不为空
// 找到右子树最小的节点位置
tmp = this.min(node.right_child);
tmp.left_child = node.left_child;
parentNode[child] = node.right_child;
// 解除引用
node.left_child = null;
node.right_child = null;
}
}
return node.value;
}
所有的程序放在JSFiddle中, 欢迎参考
现在,关于二叉树的基本内容,差不多详述完了. 不过,还有一个痛点还没有解决, 二叉树能不能存放相同的数据.
通常的答案是不行. 因为这样将会增大BST所有方法的复杂度. 如果你想弥补一个bug, 可以在Node 类中,添加一个count 统计重复出现的次数.
class Node {
constructor(value, left, right) {
this.value = value;
this.left_child = left;
this.right_child = right
this.count = 1;
}
}
其他方法 留个读者们去完成吧
最后的最后,总结一下:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。