前端学数据结构之树
前面的话
前面介绍过一种非顺序数据结构是散列表,本文将详细介绍另一种非顺序数据结构——树,它对于存储需要快速查找的数据非常有用
数据结构
树是一种分层数据的抽象模型。现实生活中最常见的树的例子是家谱,或是公司的组织架构图
一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个 节点)以及零个或多个子节点
位于树顶部的节点叫作根节点(11)。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。至少有一个子节点的节点称为内部节点(7、5、9、15、13和20是内部节点)。没有子元素的节点称为外部节点或叶节点(3、6、8、10、12、14、18和25是叶节点)。
一个节点可以有祖先和后代。一个节点(除了根节点)的祖先包括父节点、祖父节点、曾祖父节点等。一个节点的后代包括子节点、孙子节点、曾孙节点等。例如,节点5的祖先有节点7和节点11,后代有节点3和节点6。
有关树的另一个术语是子树。子树由节点和它的后代构成。例如,节点13、12和14构成了上图中树的一棵子树。
节点的一个属性是深度,节点的深度取决于它的祖先节点的数量。比如,节点3有3个祖先节点(5、7和11),它的深度为3。
树的高度取决于所有节点深度的最大值。一棵树也可以被分解成层级。根节点在第0层,它的子节点在第1层,以此类推。上图中的树的高度为3(最大高度已在图中表示——第3层)
二叉树
二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。这些定义有助于我们写出更高效的向/从树中插入、查找和删除节点的算法。二叉树在计算机科学中的应用非常广泛。
二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。上面的图中就展现了一棵二叉搜索树
【创建BinarySearchTree类】
现在开始创建自己的BinarySearchTree类。首先,声明它的结构:
function BinarySearchTree() { var Node = function(key){ //{1} this.key = key; this.left = null; this.right = null; }; var root = null; //{2} }
下图展现了二叉搜索树数据结构的组织方式:
和链表一样,将通过指针来表示节点之间的关系(术语称其为边)。在双向链表中,每个节点包含两个指针,一个指向下一个节点,另一个指向上一个节点。对于树,使用同样的方式(也使用两个指针)。但是,一个指向左侧子节点,另一个指向右侧子节点。因此,将声明一个Node类来表示树中的每个节点(行{1})。值得注意的一个小细节是,不同于将节点本身称作节点或项,我们将会称其为键。键是树相关的术语中对节点的称呼。
我们将会遵循和LinkedList类中相同的模式,这表示也将声明一个变量以控制此数据结构的第一个节点。在树中,它不再是头节点,而是根元素(行{2})。然后,我们需要实现一些方法。下面是将要在树类中实现的方法
insert(key):向树中插入一个新的键。 search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回 false。 inOrderTraverse:通过中序遍历方式遍历所有节点。 preOrderTraverse:通过先序遍历方式遍历所有节点。 postOrderTraverse:通过后序遍历方式遍历所有节点。 min:返回树中最小的值/键。 max:返回树中最大的值/键。 remove(key):从树中移除某个键。
【insert】
下面的代码是用来向树插入一个新键的算法的第一部分:
this.insert = function(key){ var newNode = new Node(key); //{1} if (root === null){ //{2} root = newNode; } else { insertNode(root,newNode); //{3} } };
要向树中插入一个新的节点(或项),要经历三个步骤
第一步是创建用来表示新节点的Node类实例(行{1})。只需要向构造函数传递我们想用来插入树的节点值,它的左指针和右指针的值会由构造函数自动设置为null
第二步要验证这个插入操作是否为一种特殊情况。这个特殊情况就是我们要插入的节点是树的第一个节点(行{2})。如果是,就将根节点指向新节点
第三步是将节点加在非根节点的其他位置。这种情况下,需要一个私有的辅助函数(行{3}),函数定义如下:
var insertNode = function(node, newNode){ if (newNode.key < node.key){ //{4} if (node.left === null){ //{5} node.left = newNode; //{6} } else { insertNode(node.left, newNode); //{7} } } else { if (node.right === null){ //{8} node.right = newNode; //{9} } else { insertNode(node.right, newNode); //{10} } } };
insertNode函数会帮助我们找到新节点应该插入的正确位置。下面是这个函数实现的步骤
1、如果树非空,需要找到插入新节点的位置。因此,在调用insertNode方法时要通过参数传入树的根节点和要插入的节点
2、如果新节点的键小于当前节点的键(现在,当前节点就是根节点)(行{4}),那么需要检查当前节点的左侧子节点。如果它没有左侧子节点(行{5}),就在那里插入新的节点。如果有左侧子节点,需要通过递归调用insertNode方法(行{7})继续找到树的下一层。在这里,下次将要比较的节点将会是当前节点的左侧子节点
3、如果节点的键比当前节点的键大,同时当前节点没有右侧子节点(行{8}),就在那里插入新的节点(行{9})。如果有右侧子节点,同样需要递归调用insertNode方法,但是要用来和新节点比较的节点将会是右侧子节点
考虑下面的情景:我们有一个新的树,并且想要向它插入第一个值
var tree = new BinarySearchTree(); tree.insert(11);
这种情况下,树中有一个单独的节点,根指针将会指向它。源代码的行{2}将会执行。现在,来考虑下图所示树结构的情况:
创建上图所示的树的代码如下,它们接着上面一段代码(插入了键为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);
下面的步骤将会被执行
1、树不是空的,行{3}的代码将会执行。insertNode方法将会被调用(root, key[6])
2、算法将会检测行{4}(key[6] < root[11]为真),并继续检测行{5}(node.left[7]不是null),然后将到达行{7}并调用insertNode(node.left[7], key[6])
3、将再次进入insertNode方法内部,但是使用了不同的参数。它会再次检测行{4}(key[6] < node[7]为真),然后再检测行{5}(node.left[5]不是null),接着到达行{7},调用insertNode(node.left[5], key[6])
4、将再一次进入insertNode方法内部。它会再次检测行{4}(key[6] < node[5]为假), 然后到达行{8}(node.right是null——节点5没有任何右侧的子节点),然后将会执行行{9}, 在节点5的右侧子节点位置插入键6
5、然后,方法调用会依次出栈,代码执行过程结束
这是插入键6后的结果:
树的遍历
遍历一棵树是指访问树的每个节点并对它们进行某种操作的过程。但是我们应该怎么去做呢?应该从树的顶端还是底端开始呢?从左开始还是从右开始呢?访问树的所有节点有三种方式:中序、先序和后序。下面将详细介绍这三种遍历方式的用法和实现
【中序遍历】
中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是对树进行排序操作。我们来看它的实现:
this.inOrderTraverse = function(callback){ inOrderTraverseNode(root, callback); //{1} };
inOrderTraverse方法接收一个回调函数作为参数。回调函数用来定义我们对遍历到的每个节点进行的操作(这也叫作访问者模式)。由于我们在BST中最常实现的算法是递归,这里使用了一个私有的辅助函数,来接收一个节点和对应的回调函数作为参数(行{1})
var inOrderTraverseNode = function (node, callback) { if (node !== null) { //{2} inOrderTraverseNode(node.left, callback); //{3} callback(node.key); //{4} inOrderTraverseNode(node.right, callback); //{5} } };
要通过中序遍历的方法遍历一棵树,首先要检查以参数形式传入的节点是否为null(这就是停止递归继续执行的判断条件——行{2}——递归算法的基本条件)。然后,递归调用相同的函数来访问左侧子节点(行{3})。接着对这个节点进行一些操作(callback),然后再访问右侧子节点(行{5})
试着在之前展示的树上执行下面的方法:
function printNode(value){//{6} console.log(value); } tree.inOrderTraverse(printNode);//{7}
但首先,需要创建一个回调函数(行{6})。要做的是在浏览器的控制台上输出节点的值。然后,调用inOrderTraverse方法并将回调函数作为参数传入(行{7})。当执行上面的代码后,下面的结果将会在控制台上输出(每个数字将会输出在不同的行):
3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
下面的图描绘了inOrderTraverse方法的访问路径:
【先序遍历】
先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。下面来看实现:
this.preOrderTraverse = function(callback){ preOrderTraverseNode(root, callback); };
preOrderTraverseNode方法的实现如下:
var preOrderTraverseNode = function (node, callback) { if (node !== null) { callback(node.key); //{1} preOrderTraverseNode(node.left, callback); //{2} preOrderTraverseNode(node.right, callback); //{3} } };
先序遍历和中序遍历的不同点是,先序遍历会先访问节点本身(行{1}),然后再访问它的左侧子节点(行{2}),最后是右侧子节点(行{3}),而中序遍历的执行顺序是:{2}、{1}和{3}
下面是控制台上的输出结果(每个数字将会输出在不同的行):
11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
下面的图描绘了preOrderTraverse方法的访问路径:
【后序遍历】
后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录中所有文件所占空间的大小。下面来看它的实现:
this.postOrderTraverse = function(callback){ postOrderTraverseNode(root, callback); };
postOrderTraverseNode方法的实现如下:
var postOrderTraverseNode = function (node, callback) { if (node !== null) { postOrderTraverseNode(node.left, callback); //{1} postOrderTraverseNode(node.right, callback); //{2} callback(node.key); //{3} } };
这个例子中,后序遍历会先访问左侧子节点(行{1}),然后是右侧子节点(行{2}),最后是父节点本身(行{3})
中序、先序和后序遍历的实现方式是很相似的,唯一不同的是行{1}、{2}和{3} 的执行顺序。下面是控制台的输出结果(每个数字将会输出在不同行):
3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
树的搜索
在树中,有三种经常执行的搜索类型:1、最小值;2、最大值;3、搜索特定的值。下面将详细介绍这三种搜索类型
【最值】
使用下面的树作为示例:
如果看一眼树最后一层最左侧的节点,会发现它的值为3,这是这棵树中最小的键。如果再看一眼树最右端的节点(同样是树的最后一层),会发现它的值为25,这是这棵树中最大的键。这条信息在我们实现搜索树节点的最小值和最大值的方法时能给予我们很大的帮助
首先来看寻找树的最小键的方法:
this.min = function() { return minNode(root); //{1} };
min方法将会暴露给用户。这个方法调用了minNode方法(行{1}):
var minNode = function (node) { if (node){ while (node && node.left !== null) { //{2} node = node.left; //{3} } return node.key; } return null; //{4} };
minNode方法允许我们从树中任意一个节点开始寻找最小的键。我们可以使用它来找到一棵树或它的子树中最小的键。因此,我们在调用minNode方法的时候传入树的根节点(行{1}),因为我们想要找到整棵树的最小键。在minNode内部,我们会遍历树的左边(行{2}和行{3})直到找到树的最下层(最左端)
以相似的方式,可以实现max方法:
this.max = function() { return maxNode(root); }; var maxNode = function (node) { if (node){ while (node && node.right !== null) { //{5} node = node.right; } return node.key; } return null; };
要找到最大的键,我们要沿着树的右边进行遍历(行{5})直到找到最右端的节点。 因此,对于寻找最小值,总是沿着树的左边;而对于寻找最大值,总是沿着树的右边
【特定值】
下面来看搜索特定值的实现
this.search = function(key){ return searchNode(root, key); //{1} }; var searchNode = function(node, key){ if (node === null){ //{2} return false; } if (key < node.key){ //{3} return searchNode(node.left, key); //{4} } else if (key > node.key){ //{5} return searchNode(node.right, key); //{6} } else { return true; //{7} } };
我们要做的第一件事,是声明search方法。和BST中声明的其他方法的模式相同,我们将会使用一个辅助函数(行{1})。searchNode方法可以用来寻找一棵树或它的任意子树中的一个特定的值。这也是为什么在行{1}中调用它的时候传入树的根节点作为参数。
在开始算法之前,先要验证作为参数传入的node是否合法(不是null)。如果是null的话,说明要找的键没有找到,返回false。如果传入的节点不是null,需要继续验证。如果要找的键比当前的节点小(行{3}),那么继续在左侧的子树上搜索(行{4})。如果要找的键比当前的节点大,那么就从右侧子节点开始继续搜索(行{6}),否则就说明要找的键和当前节点的键相等,就返回true来表示找到了这个键(行{7})。
可以通过下面的代码来测试这个方法:
console.log(tree.search(1) ? 'Key 1 found.' : 'Key 1 not found.');
console.log(tree.search(8) ? 'Key 8 found.' : 'Key 8 not found.');
输出结果如下:
Value 1 not found. Value 8 found.
下面来详细介绍查找1这个键的时候方法是如何执行的
1、调用searchNode方法,传入根节点作为参数(行{1})。(node[root[11]])不是null(行{2}),因此我们执行到行{3}
2、(key[1]<node[11])为ture(行{3}),因此来到行{4}并再次调用searchNode方法,传入(node[7],key[1])作为参数
3、(node[7])不是null({2}),因此继续执行行{3}
4、(key[1]<node[7])为ture(行{3}),因此来到行{4}并再次调用searchNode方法,传入(node[5],key[1])作为参数
5、(node[5])不是null(行{2}),因此继续执行行{3}
6、(key[1]<node[5])为ture(行{3}),因此来到行{4}并再次调用searchNode方法,传入(node[3],key[1])作为参数
7、(node[3])不是null(行{2}),因此来到行{3}
8、(key[1]<node[3])为真(行{3}),因此来到行{4}并再次调用searchNode方法,传入(null,key[1])作为参数。null被作为参数传入是因为node[3]是一个叶节点(它没有子节点,所以它的左侧子节点的值为null)
9、节点(null)的值为null(行{2},这时要搜索的节点为null),因此返回false
10、然后,方法调用会依次出栈,代码执行过程结束
下面再来查找值为8的节点:
1、调用searchNode方法,传入root作为参数(行{1})。(node[root[11]])不是null(行{2}),因此我们来到行{3}
2、(key[8]<node[11])为真(行{3}),因此执行到行{4}并再次调用searchNode方法,传入(node[7],key[8])作为参数。(node[7])不是null,因此来到行{3}
3、(key[8]<node[7])为假(行{3}),因此来到行{5}
4、(key[8]>node[7])为真(行{5}),因此来到行{6}并再次调用searchNode方法,传入(node[9],key[8])作为参数。(node[9])不是null(行{2}),因此来到行{3}
5、(key[8]<node[9])为真(行{3}),因此来到行{4}并再次调用searchNode方法,传入(node[8],key[8])作为参数。(node[8])不是null(行{2}),因此来到行{3}
6、(key[8]<node[8])为假(行{3}),因此来到行{5}
7、(key[8]>node[8])为假(行{5}),因此来到行{7}并返回true,因为node[8]就是要找的键
8、然后,方法调用会依次出栈,代码执行过程结束
【移除一个节点】
先创建这个remove方法,使它能够在树的实例上被调用
this.remove = function(key){ root = removeNode(root, key); //{1} };
这个方法接收要移除的键并且它调用了removeNode方法,传入root和要移除的键作为参数(行{1})。要注意的是,root被赋值为removeNode方法的返回值
removeNode方法的复杂之处在于我们要处理不同的运行场景,当然也包括它同样是通过递归来实现的。下面来看removeNode方法的实现:
var removeNode = function(node, key){ if (node === null){ //{2} return null; } if (key < node.key){ //{3} node.left = removeNode(node.left, key); //{4} return node; //{5} } else if (key > node.key){ //{6} node.right = removeNode(node.right, key); //{7} return node; //{8} } else { //键等于node.key //第一种情况——一个叶节点 if (node.left === null && node.right === null){ //{9} node = null; //{10} return node; //{11} } //第二种情况——一个只有一个子节点的节点 if (node.left === null){ //{12} node = node.right; //{13} return node; //{14} } else if (node.right === null){ //{15} node = node.left; //{16} return node; //{17} } //第三种情况——一个有两个子节点的节点 var aux = findMinNode(node.right); //{18} node.key = aux.key; //{19} node.right = removeNode(node.right, aux.key); //{20} return node; //{21} } };
下面来看行{2},如果正在检测的节点是null,那么说明键不存在于树中,所以返回null。然后要做的第一件事,就是在树中找到要移除的节点。因此,如果要找的键比当前节点的值小(行{3}),就沿着树的左边找到下一个节点(行{4})。如果要找的键比当前节点的值大(行{6}),那么就沿着树的右边找到下一个节点(行{7})。如果找到了要找的键(键和node.key相等),就需要处理三种不同的情况
findMinNode方法如下:
var findMinNode = function(node){ while (node && node.left !== null) { node = node.left; } return node; };
1、移除一个叶节点
第一种情况是该节点是一个没有左侧或右侧子节点的叶节点——行{9}。在这种情况下,要做的就是给这个节点赋予null值来移除它(行{9})。但是仅仅赋一个null值是不够的,还需要处理指针。在这里,这个节点没有任何子节点,但是它有一个父节点,需要通过返回null来将对应的父节点指针赋予null值(行{11})
现在节点的值已经是null了,父节点指向它的指针也会接收到这个值,这也是我们要在函数中返回节点的值的原因。父节点总是会接收到函数的返回值。另一种可行的办法是将父节点和节点本身都作为参数传入方法内部
如果回头来看方法的第一行代码,会发现我们在行{4}和行{7}更新了节点左右指针的值,同样也在行{5}和行{8}返回了更新后的节点。下图展现了移除一个叶节点的过程:
2、移除有一个左侧或右侧子节点的节点
现在来看第二种情况,移除有一个左侧子节点或右侧子节点的节点。这种情况下,需要跳过这个节点,直接将父节点指向它的指针指向子节点。如果这个节点没有左侧子节点(行{12}),也就是说它有一个右侧子节点。因此我们把对它的引用改为对它右侧子节点的引用(行{13})并返回更新后的节点(行{14})。如果这个节点没有右侧子节点,也是一样——把对它的引用改为对它左侧子节点的引用(行{16})并返回更新后的值(行{17})
下图展现了移除只有一个左侧子节点或右侧子节点的节点的过程:
3、移除有两个子节点的节点
现在是第三种情况,也是最复杂的情况,那就是要移除的节点有两个子节点——左侧子节点和右侧子节点。要移除有两个子节点的节点,需要执行四个步骤。(1)当找到了需要移除的节点后,需要找到它右边子树中最小的节点(它的继承者——行{18});(2)然后,用它右侧子树中最小节点的键去更新这个节点的值(行{19})。通过这一步,改变了这个节点的键,也就是说它被移除了;(3)但是,这样在树中就有两个拥有相同键的节点了,这是不行的。要继续把右侧子树中的最小节点移除,毕竟它已经被移至要移除的节点的位置了(行{20});(4)最后,向它的父节点返回更新后节点的引用(行{21})
findMinNode方法的实现和min方法的实现方式是一样的。唯一不同之处在于,在min方法中只返回键,而在findMinNode中返回了节点。下图展现了移除有两个子节点的节点的过程:
【完整代码】
二叉搜索树BST的完整代码如下所示
function BinarySearchTree() { var Node = function(key){ this.key = key; this.left = null; this.right = null; }; var root = null; this.insert = function(key){ var newNode = new Node(key); //special case - first element if (root === null){ root = newNode; } else { insertNode(root,newNode); } }; var insertNode = 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); } } }; this.getRoot = function(){ return root; }; this.search = function(key){ return searchNode(root, key); }; var 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 { //element is equal to node.item return true; } }; this.inOrderTraverse = function(callback){ inOrderTraverseNode(root, callback); }; var inOrderTraverseNode = function (node, callback) { if (node !== null) { inOrderTraverseNode(node.left, callback); callback(node.key); inOrderTraverseNode(node.right, callback); } }; this.preOrderTraverse = function(callback){ preOrderTraverseNode(root, callback); }; var preOrderTraverseNode = function (node, callback) { if (node !== null) { callback(node.key); preOrderTraverseNode(node.left, callback); preOrderTraverseNode(node.right, callback); } }; this.postOrderTraverse = function(callback){ postOrderTraverseNode(root, callback); }; var postOrderTraverseNode = function (node, callback) { if (node !== null) { postOrderTraverseNode(node.left, callback); postOrderTraverseNode(node.right, callback); callback(node.key); } }; this.min = function() { return minNode(root); }; var minNode = function (node) { if (node){ while (node && node.left !== null) { node = node.left; } return node.key; } return null; }; this.max = function() { return maxNode(root); }; var maxNode = function (node) { if (node){ while (node && node.right !== null) { node = node.right; } return node.key; } return null; }; this.remove = function(element){ root = removeNode(root, element); }; var findMinNode = function(node){ while (node && node.left !== null) { node = node.left; } return node; }; var removeNode = function(node, element){ if (node === null){ return null; } if (element < node.key){ node.left = removeNode(node.left, element); return node; } else if (element > node.key){ node.right = removeNode(node.right, element); return node; } else { //element is equal to node.item //handle 3 special conditions //1 - a leaf node //2 - a node with only 1 child //3 - a node with 2 children //case 1 if (node.left === null && node.right === null){ node = null; return node; } //case 2 if (node.left === null){ node = node.right; return node; } else if (node.right === null){ node = node.left; return node; } //case 3 var aux = findMinNode(node.right); node.key = aux.key; node.right = removeNode(node.right, aux.key); return node; } }; }
自平衡树
二叉树BST存在一个问题:取决于添加的节点数,树的一条边可能会非常深;也就是说,树的一条分支会有很多层,而其他的分支却只有几层,如下图所示:
这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题。为了解决这个问题,有一种树叫作阿德尔森-维尔斯和兰迪斯树(AVL树)。AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树
AVL树是一种自平衡树。添加或移除节点时,AVL树会尝试自平衡。任意一个节点(不论深度)的左子树和右子树高度最多相差1。添加或移除节点时,AVL树会尽可能尝试转换为完全树。
在AVL树中插入或移除节点和BST完全相同。然而,AVL树的不同之处在于我们需要检验它的平衡因子,如果有需要,则将其逻辑应用于树的自平衡。
下面的代码是向AVL树插入新节点的例子:
var insertNode = function(node, element) { if (node === null) { node = new Node(element); } else if (element < node.key) { node.left = insertNode(node.left, element); if (node.left !== null) { // 确认是否需要平衡 {1} } } else if (element > node.key) { node.right = insertNode(node.right, element); if (node.right !== null) { // 确认是否需要平衡 {2} } } return node; };
然而,插入新节点时,还要检查是否需要平衡树(行{1}和行{2})。
【计算平衡因子】
在AVL树中,需要对每个节点计算右子树高度(hr)和左子树高度(hl)的差值,该值 (hr-hl)应为0、1或-1。如果结果不是这三个值之一,则需要平衡该AVL树。这就是平衡因子的概念
下图举例说明了一些树的平衡因子(所有的树都是平衡的):
计算节点高度的代码如下:
var heightNode = function(node) { if (node === null) { return -1; } else { return Math.max(heightNode(node.left), heightNode(node.right)) + 1; } };
因此,向左子树插入新节点时,需要计算其高度;如果高度大于1(即不为-1、0和1之一), 就需要平衡左子树。代码如下:
// 替换insertNode方法的行{1} if ((heightNode(node.left) - heightNode(node.right)) > 1) { // 旋转 {3} }
向右子树插入新节点时,应用同样的逻辑,代码如下:
// 替换insertNode方法的行{2} if ((heightNode(node.right) - heightNode(node.left)) > 1) { // 旋转 {4} }
【AVL旋转】
向AVL树插入节点时,可以执行单旋转或双旋转两种平衡操作,分别对应四种场景:
1、右-右(RR):向左的单旋转 2、左-左(LL):向右的单旋转 3、左-右(LR):向右的双旋转 4、右-左(RL):向左的双旋转
下面来依次看看它们是如何工作的
右-右(RR):向左的单旋转。如下图所示:
假设向AVL树插入节点90,这会造成树失衡(节点50 -Y高度为+2),因此需要恢复树的平衡。下面是执行的操作:
1、与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为-2)所在的位置(行{1})
2、节点X的右子树保持不变
3、将节点Y的右子节点置为节点X的左子节点Z(行{2})
4、将节点X的左子节点置为节点Y(行{3})
下面的代码举例说明了整个过程:
var rotationRR = function(node) { var tmp = node.right; // {1} node.right = tmp.left; // {2} tmp.left = node; // {3} return tmp; };
左-左(LL):向右的单旋转。如下图所示:
假设向AVL树插入节点5,这会造成树失衡(节点50 -Y高度为+2),需要恢复树的平衡。下面是我们执行的操作:
1、与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为+2)所在的位置(行{1})
2、节点X的左子树保持不变
3、将节点Y的左子节点置为节点X的右子节点Z(行{2})
4、将节点X的右子节点置为节点Y(行{3})
下面的代码举例说明了整个过程:
var rotationLL = function(node) { var tmp = node.left; // {1} node.left = tmp.right; // {2} tmp.right = node; // {3} return tmp; };
左-右(LR):向右的双旋转。如下图所示:
假设向AVL树插入节点35,这会造成树失衡(节点50 -Y高度为+2),需要恢复树的平衡。下面是执行的操作:
1、将节点X置于节点Y(平衡因子为+2)所在的位置
2、将节点Y的左子节点置为节点X的右子节点
3、将节点Z的右子节点置为节点X的左子节点
4、将节点X的右子节点置为节点Y
5、将节点X的左子节点置为节点Z
基本上,就是先做一次RR旋转,再做一次LL旋转。下面的代码举例说明了整个过程:
var rotationLR = function(node) { node.left = rotationRR(node.left); return rotationLL(node); };
右-左(RL):向左的双旋转。如下图所示:
假设向AVL树插入节点75,这会造成树失衡(节点70 -Y高度为-2),需要恢复树的平衡。下面是我们执行的操作:
1、将节点X置于节点Y(平衡因子为-2)所在的位置
2、节点Z的左子节点置为节点X的右子节点
3、将节点Y的右子节点置为节点X的左子节点
4、将节点X的左子节点置为节点Y
5、将节点X的右子节点置为节点Z
基本上,就是先做一次LL旋转,再做一次RR旋转。下面的代码举例说明了整个过程:
var rotationRL = function(node) { node.right = rotationLL(node.right); return rotationRR(node); };
确认树需要平衡后,就需要对每种情况分别应用正确的旋转
向左子树插入新节点,且节点的值小于其左子节点时,应进行LL旋转。否则,进行LR旋转。该过程的代码如下:
// 替换insertNode方法的行{1} if ((heightNode(node.left) - heightNode(node.right)) > 1){ // 旋转 {3} if (element < node.left.key){ node = rotationLL(node); } else { node = rotationLR(node); } }
向右子树插入新节点,且节点的值大于其右子节点时,应进行RR旋转。否则,进行RL旋转。 该过程的代码如下:
// 替换insertNode方法的行{2} if ((heightNode(node.right) - heightNode(node.left)) > 1){ // 旋转 {4} if (element > node.right.key){ node = rotationRR(node); } else { node = rotationRL(node); } }
【完整代码】
AVL树的完整代码如下所示
function AVLTree() { var Node = function(key){ this.key = key; this.left = null; this.right = null; }; var root = null; this.getRoot = function(){ return root; }; var heightNode = function(node) { if (node === null) { return -1; } else { return Math.max(heightNode(node.left), heightNode(node.right)) + 1; } }; var rotationLL = function(node) { var tmp = node.left; node.left = tmp.right; tmp.right = node; return tmp; }; var rotationRR = function(node) { var tmp = node.right; node.right = tmp.left; tmp.left = node; return tmp; }; var rotationLR = function(node) { node.left = rotationRR(node.left); return rotationLL(node); }; var rotationRL = function(node) { node.right = rotationLL(node.right); return rotationRR(node); }; var insertNode = function(node, element) { if (node === null) { node = new Node(element); } else if (element < node.key) { node.left = insertNode(node.left, element); if (node.left !== null) { if ((heightNode(node.left) - heightNode(node.right)) > 1){ if (element < node.left.key){ node = rotationLL(node); } else { node = rotationLR(node); } } } } else if (element > node.key) { node.right = insertNode(node.right, element); if (node.right !== null) { if ((heightNode(node.right) - heightNode(node.left)) > 1){ if (element > node.right.key){ node = rotationRR(node); } else { node = rotationRL(node); } } } } return node; }; this.insert = function(element) { root = insertNode(root, element); }; var parentNode; var nodeToBeDeleted; var removeNode = function(node, element) { if (node === null) { return null; } parentNode = node; if (element < node.key) { node.left = removeNode(node.left, element); } else { nodeToBeDeleted = node; node.right = removeNode(node.right, element); } if (node === parentNode) { //remove node if (nodeToBeDeleted !== null && element === nodeToBeDeleted.key) { if (nodeToBeDeleted === parentNode) { node = node.left; } else { var tmp = nodeToBeDeleted.key; nodeToBeDeleted.key = parentNode.key; parentNode.key = tmp; node = node.right; } } } else { //do balancing if (node.left === undefined) node.left = null; if (node.right === undefined) node.right = null; if ((heightNode(node.left) - heightNode(node.right)) === 2) { if (element < node.left.key) { node = rotationLR(node); } else { node = rotationLL(node); } } if ((heightNode(node.right) - heightNode(node.left)) === 2) { if (element > node.right.key) { node = rotationRL(node); } else { node = rotationRR(node); } } } return node; }; this.remove = function(element) { parentNode = null; nodeToBeDeleted = null; root = removeNode(root, element); }; }
尽管AVL树是自平衡的,其插入或移除节点的性能并不总是最好的。更好的选择是红黑树。红黑树可以高效有序地遍历其节点
【红黑树】
红黑树的完整代码如下所示
function RedBlackTree() { var Colors = { RED: 0, BLACK: 1 }; var Node = function (key, color) { this.key = key; this.left = null; this.right = null; this.color = color; this.flipColor = function(){ if (this.color === Colors.RED) { this.color = Colors.BLACK; } else { this.color = Colors.RED; } }; }; var root = null; this.getRoot = function () { return root; }; var isRed = function(node){ if (!node){ return false; } return node.color === Colors.RED; }; var flipColors = function(node){ node.left.flipColor(); node.right.flipColor(); }; var rotateLeft = function(node){ var temp = node.right; if (temp !== null) { node.right = temp.left; temp.left = node; temp.color = node.color; node.color = Colors.RED; } return temp; }; var rotateRight = function (node) { var temp = node.left; if (temp !== null) { node.left = temp.right; temp.right = node; temp.color = node.color; node.color = Colors.RED; } return temp; }; var insertNode = function(node, element) { if (node === null) { return new Node(element, Colors.RED); } var newRoot = node; if (element < node.key) { node.left = insertNode(node.left, element); } else if (element > node.key) { node.right = insertNode(node.right, element); } else { node.key = element; } if (isRed(node.right) && !isRed(node.left)) { newRoot = rotateLeft(node); } if (isRed(node.left) && isRed(node.left.left)) { newRoot = rotateRight(node); } if (isRed(node.left) && isRed(node.right)) { flipColors(node); } return newRoot; }; this.insert = function(element) { root = insertNode(root, element); root.color = Colors.BLACK; }; }