二叉树和二叉搜索树
二叉树中的节点最多只能有2个子节点:一个是左侧子节点,另外一个是右侧子节点。
二叉搜索树(BST
)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或等于)的值
创建二叉搜索树
1.我们要先声明二叉搜索树的结构:
function BinarySearchTree(){
let Node = function(key){
this.key = key;
this.left = null;
this.right = null;
};
let root = null;
function show(){
return this.key;
}
}
上面的数据结构,和链表很相似,都是东莞指针表示节点之间的关系(术语称其为边),对于树来说,我们也是通过指针来指定下一个元素的,但是从图中,我们也看到了,节点的指针,一个指向左侧子节点,另外一个指向右侧的子节点,其中,键是树相关术语对节点的称呼。
向树中插入一个人键(节点)
向树中插入一个新的节点,要经历三个步骤
第一步:创建用来表示新节点的Node
的实例,只要向构造函数中传入我们想要传入的节点的值,做指针和右指针会由构造函数自动的设置为null
第二步:需要验证这个插入的操作是否为一种特殊的情况,这个特殊的情况就是我们插入的节点是树的第一个节点,这个时候只需要将根节点指向新节点。
第三步:将节点加在非根节点的其他位置。
let insert = function(key){
let newNode = new Node(key);
if(root === null){
root = newNode;
}else{
insertNode(root,newNode)
}
};
//当为非空树的时候,我们进行节点的插入的时候,进行的步骤
let indsertNode = function(node,newNode){
if(newNode.key < node.key){
if(node.left === null){
node.left = newNode;
}else{
insertNode(node.left, newNode);
}
}else{
if(node.right === null){
node.right = newNode;
}else{
insertNode(node.right, newNode);
}
}
}
首先我们先构造出一个二叉搜索树:
let tree = new BinarySearchTree();
tree.insert (11);
tree.insert (7);
tree.insert (15);
tree.insert (5);
tree.insert (3);
tree.insert (9);
tree.insert (8);
tree.insert (10);
tree.insert (13);
tree.insert (12);
tree.insert (14);
tree.insert (20);
tree.insert (18);
tree.insert (25);
我们现在构造出来的二叉树是:
当我们想要插入一个键值为6的键,执行下面的代码:
tree.insert(6);
看一下我们究竟是怎么插入的:
中序遍历
中序遍历是一种以上行顺序访问BST
所有节点的遍历方式,也就是是从小到大的顺序访问所有的节点,让我们看看具体的实现过程。
let inOrderTraverse = function (root){
inOrderTraverseNode(root);
}
inOrderTraverse
方法接受一个根节点作为参数。在回调函数中定义我们对遍历到的每一个节点进行操作。由于我们在BST中最常实现的算法就是递归。
var inOrderTraverseNode = function(node){
if(node !== null){
inOrderTraverseNode(node.left);
console.log(node.show());
inOrderTraverseNode(node.right);
}
};
tree.inOrderTraverse (tree.root);
最终输出的结果是:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
先序遍历
先序遍历是以优先于后代节点的顺序访问每一个节点,先序遍历的一种应用是打印一个结构化的文件。我们看一下代码的实现方式
let preOrderTraverse = function (root){
preOrderTraverseNode(root);
}
先序遍历和中序遍历的不同点是:先序遍历会访问节点的本身,然后访问它的左侧节点,最后是右侧节点,而中序遍历,先访问左子节点,自身,右子节点,还是上面的二叉搜索树,我们对其进行先序遍历。
var preOrderTraverseNode = function(node){
if(node !== null){
console.log(node.show());
preOrderTraverseNode (node.left);
preOrderTraverseNode (node.right);
}
};
tree.preOrderTraverse (tree.root);
最终输出的结果是:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
后序遍历
后续遍历就是先访问节点的后代节点,再访问节点本身,后序遍历的一种应用是计算一个目录和它的子目录中所有的文件所占空间的大小。
let postOrderTraverse = function (root){
postOrderTraverseNode(root);
}
var postOrderTraverseNode = function(node){
if(node !== null){
postOrderTraverseNode (node.left);
postOrderTraverseNode (node.right);
console.log(node.show());
}
};
tree.preOrderTraverse (tree.root);
搜索树中的值
搜索树上的最大值和最小值
在上面的二叉搜索树中我们可以清楚的看见,最左侧的子节点的值是最小的,最右侧的子节点的值是最大的,这条信息为我们实现二叉搜索树上的最大值和最小值提供了很大的帮助。
现在编写一下查找最小值的代码:
let min = function (){
return minNode(root);
};
let minNode = function(node){
if(node){
while(node && node.left !== null){
node = node.left;
}
return node.key;
}
return null;
}
minNode
方法允许我们从树中任意一个节点开始寻找最小的键,我们可以使用来找到一棵树或是他子树最小的键。因此我们在调用minNode
方法的时候传入了树的根节点,最终体现的结果是我们找到了最左边的子节点。类似的我们可以找到树中的最大值,代码如下:
let max= function (){
return maxNode(root);
};
let maxNode= function(node){
if(node){
while(node && node.right !== null){
node = node.right;
}
return node.key;
}
return null;
}
搜索特定的值
当我们想要查找在二叉树中存不存在一个特定的值的时候,我们需要搜索二叉树,现在看一下我们的搜索过程。
let search = function(key){
return searchNode(root,key);
};
let searchNode = function(node, key){
if(node === null){
return false;
}
if(key < node.key){
return searchNode(node.left,key);
} else if(key > node.key){
return searchNode(node.right,key);
}else{
return true;
}
}
searchNode
方法可以用来寻找一棵树或是它的任意子树中的一个特定的值,这也是为什么在调用的时候传入一个节点作为参数。我们在程序开始的时候,需要判断节点的合法性,如果是null
的话,说明要找的键没有找到,返回false
。如果传入的节点不为null
,就要比较节点的键值和传入值的键值大小。如果传入值要大,那就向右边遍历二叉搜索树,否则,就向左边遍历二叉树。
移除节点
移除节点是我们在二叉搜索树中最复杂的一个,首先我们先创建这个方法。
let remove = function(key){
root = removeNode (root,key);
};
这个方法接收要移除的键并且调用了removeNode
方法,传入root
和要移除的键作为参数。我们在移除的过程中,root
被赋予removeNode
方法的返回值,是有一定的意义的。我们会在下面进行详细的讲述。
let removeNode = function(node, key){
if (node === null){
return null;
}
if(key < node.key){
node.left = removeNode (node.left, key);
return mdoe;
}else if(key > node.key){
node.right = removeNode(node.right, key);
return node;
}else{ // 键等于node.key
// 第一种情况-- 一个叶节点
if(node.left === null && node.right === null){
node = null;
return node;
}
// 第二种情况-- 一个只有一个子节点的节点
if(node.left=== null){
node = node.right;
return node;
}else if(node.right === null){
node = node.left;
return node;
}
// 第三种情况-- 一个有两个子节点的节点
let aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode (node.right,aux.key);
return node;
}
}
检测到的节点是null
,说明键值不存在于树中,返回null
。我们要做的第一件事情就是从树中找到这个节点。因此,如果要找的键比当前节点的值小,就沿着树的左边一直找到下一个节点,如果找到的键比当前节点的键值大,那么就沿着树的右边找到下一个节点。
如果我们要找到的键(键和node.key
相等),据需要三种不同的情况
let findMinNode = function (node){
while(node && node.left !== null ){
node = node.left;
}
return node;
}
移除一个叶节点
这种情况是最简单最易懂的删除方式。
我们要做的就是给这个节点赋予null
值移除它。但是当学习了链表的实现之后,我们知道仅仅是赋予了一个null值是不够的,还需要处理指针,这个节点没有任何的子节点,但是有一个父节点,需要通过返回null
来将对应的父节点指针赋予null
值。
现在节点的值已经是null
,父节点指向它的指针也会接受到这个值,这也是我们要在函数中返回节点的值的原因。父节点总是会接受到函数的返回值。另外一种可行的办法是将父节点和节点本身都作为参数传入方法的内部。
移除有一个左侧或右侧子节点的节点
现在让我们来看第二种情况,移除一个有左侧子节点,或是右侧子节点的节点,在这种情况下,我们需要跳过这个节点,直接将父节点指向它指针指向的子节点。
如果这个节点没有左侧子节点,也就是它只有一个右侧子节点,因此我们那对他的引用改成了对它右侧子节点的引用,并返回更新后的节点。现在让我们以一张图片的形式来展现移除只有一个左子节点或右侧子节点的节点的过程。
移除有两个子节点的节点
现在是第三种情况,也就是最复杂的情况,那么就是要移除节点的两个子节点-- 左侧子节点和右侧子节点。要移除有两个知己诶单的节点的时候,需要执行四个步骤:
(1)找到需要删除的节点。找到他右边子树最小的节点(来代替该节点的位置)
(2)然后,用他右边子树最小的节点来更新这个节点的值,通过这一步,我们改变了这个节点的键,也就是说,他被移除了
(3)但是,这样的话,树上就有两个拥有相同键的节点了,怎样是不行的,要继续把右子树的最小节点进行移除,毕竟它已经被移到被删除的节点的位置了。
(4) 最后,向它的父节点返回更新后的节点的引用。
findMinNode
方法的实现和min方法的实现方式是一样的,唯一的不同之处在于,在min
方法中只返回键,但是findMinNode
返回了节点。