一,树的定义
树由一组以边连接的节点组成。。一棵树最上面的节点称为根节点,如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子节点。一个节点可以有0 个、1 个或多个子节点。没有任何子节点的节点称为叶子节点。
沿着一组特定的边,可以从一个节点走到另外一个与它不直接相连的节点。从一个节点到另一个节点的这一组边称为路径,在图中用虚线表示。以某种特定顺序访问树中所有的节点称为树的遍历。
树可以分为几个层次,根节点是第0 层,它的子节点是第1 层,子节点的子节点是第2层,以此类推。树中任何一层的节点可以都看做是子树的根,该子树包含根节点的子节点,子节点的子节点等。我们定义树的层数就是树的深度。
二,二叉树和二叉查找树
二叉树每个节点的子节点不允许超过两个。通过将子节点的个数限定为2,可以写出高效的程序在树中插入、查找和删除数据。
一个父节点的两个子节点分别称为左节点和右节点。在一些二叉树的实现中,左节点包含一组特
定的值,右节点包含另一组特定的值。
当考虑某种特殊的二叉树,比如二叉查找树时,确定子节点非常重要。二叉查找树是一种
特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。这一特性使得
查找的效率很高,对于数值型和非数值型的数据,比如单词和字符串,都是如此。
(1)实现二叉查找树
二叉查找树由节点组成,所以我们要定义的第一个对象就是Node,该对象和前面介绍链表
时的对象类似。Node 类的定义如下:
function Node(data, left, right) {
this.data = data;
this.left = left;
this.right = right;
this.show = show;
}
function show() {
return this.data;
}
Node 对象既保存数据,也保存和其他节点的链接(left 和right),show() 方法用来显示保存在节点中的数据。
创建一个类,用来表示二叉查找树(BST)。我们让类只包含一个数据成员:一个表示二叉查找树根节点的Node 对象。该类的构造函数将根节点初始化为null,以此创建一个空节点。
首先要创建一个Node 对象,将数据传入该对象保存。
其次检查BST 是否有根节点,如果没有,那么这是棵新树,该节点就是根节点,这个方法到此也就完成了;否则,进入下一步。
如果待插入节点不是根节点,那么就需要准备遍历BST,找到插入的适当位置。该过程类似于遍历链表。用一个变量存储当前节点,一层层地遍历BST。
进入BST 以后,下一步就要决定将节点放在哪个地方。找到正确的插入点时,会跳出循环。查找正确插入点的算法如下。
- 设根节点为当前节点。
- 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第4 步。
- 如果当前节点的左节点为null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
- 设新的当前节点为原节点的右节点。
- 如果当前节点的右节点为null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
function Node(data, left, right) {
this.data = data;
this.left = left;
this.right = right;
this.show = show;
}
function show() {
return this.data;
}
function BST() {
this.root = null;
this.insert = insert;this.inOrder = inOrder;
}
function insert(data) {
var n = new Node(data, null, null);
if (this.root == null) {
this.root = n;
}
else {
var current = this.root;
var parent;
while (true) {
parent = current;
if (data < current.data) {
current = current.left;
if (current == null) {
parent.left = n;
break;
}
}
else {
current = current.right;
if (current == null) {
parent.right = n;
break;
}
}
}
}
}
(2)遍历二叉查找树
有三种遍历BST 的方式:中序、先序和后序。中序遍历按照节点上的键值,以升序访问BST 上的所有节点。先序遍历先访问根节点,然后以同样方式访问左子树和右子树。后序遍历先访问叶子节点,从左子树到右子树,再到根节点。中序遍历:该方法需要以升序访问树中所有节点,先访问左子树,再访问根节点,最后访问右子树。
function inOrder(node) { if (!(node == null)) { inOrder(node.left); console.log(node.show() + " "); inOrder(node.right); } }
先序遍历:
function preOrder(node) {
if (!(node == null)) {
console.log(node.show() + " ");
preOrder(node.left);
preOrder(node.right);
}
}
inOrder() 和preOrder() 方法的唯一区别,就是if 语句中代码的顺序。在inOrder()方法中,show() 函数像三明治一样夹在两个递归调用之间;在preOrder() 方法中,show()函数放在两个递归调用之前。
后序遍历:
function postOrder(node) {
if (!(node == null)) {
postOrder(node.left);postOrder(node.right);
putstr(node.show() + " ");
}
}
三,在二叉查找树上进行查找
对BST 通常有下列三种类型的查找:
- 查找给定值;
- 查找最小值;
- 查找最大值。
(1)查找最小值和最大值
查找BST 上的最小值和最大值非常简单。因为较小的值总是在左子节点上,在BST 上查找最小值,只需要遍历左子树,直到找到最后一个节点。
该方法沿着BST 的左子树挨个遍历,直到遍历到BST 最左边的节点
function getMin() {
var current = this.root;
while (!(current.left == null)) {
current = current.left;
}
return current.data;
}
在BST 上查找最大值,只需要遍历右子树,直到找到最后一个节点,该节点上保存的值即为最大值。
function getMax() {
var current = this.root;
while (!(current.right == null)) {
current = current.right;
}
return current.data;
}
(2)查找给定值
在BST 上查找给定值,需要比较该值和当前节点上的值的大小。通过比较,就能确定如果给定值不在当前节点时,该向左遍历还是向右遍历。
function find(data) {
var current = this.root;
while (current != null) {
if (current.data == data) {
return current;
}
else if (data < current.data) {
current = current.left;
}
else {
current = current.right;
}
}
return null;
}
如果找到给定值,该方法返回保存该值的节点;如果没找到,该方法返回null。
四,从二叉查找树上删除节点
从BST 上删除节点的操作最复杂,其复杂程度取决于删除哪个节点。如果删除没有子节点的节点,那么非常简单。如果节点只有一个子节点,不管是左子节点还是右子节点,就变得稍微有点复杂了。删除包含两个子节点的节点最复杂。
为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法:remove() 和removeNode()。
从BST 中删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果待删除数据小于当前节点上的数据,则移至当前节点的左子节点继续比较;如果删除数据大于当前节点上的数据,则移至当前节点的右子节点继续比较。
如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接指向null。
如果待删除节点只包含一个子节点,那么原本指向它的节点久得做些调整,使其指向它的子节点。
最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树上的最大值,要么查找其右子树上的最小值。这里我们选择后一种方式。我们需要一个查找子树上最小值的方法,后面会用它找到的最小值创建一个临时节点。将临时节点上的值复制到待删除节点,然后再删除临时节点。整个删除过程由两个方法完成。remove() 方法只是简单地接受待删除数据,调用removeNode()
删除它,后者才是完成主要工作的方法。两个方法的定义如下:
function remove(data) {
root = removeNode(this.root, data);
}
function removeNode(node, data) {
if (node == null) {
return null;
}
if (data == node.data) {
// 没有子节点的节点
if (node.left == null && node.right == null) {
return null;
}
// 没有左子节点的节点
if (node.left == null) {
return node.right;
}
// 没有右子节点的节点
if (node.right == null) {
return node.left;
}
// 有两个子节点的节点
var tempNode = getSmallest(node.right);
node.data = tempNode.data;
node.right = removeNode(node.right, tempNode.data);
return node;
}
else if (data < node.data) {
node.left = removeNode(node.left, data);
return node;
}
else {
node.right = removeNode(node.right, data);
return node;
}
}
五,计数
BST 的一个用途是记录一组数据集中数据出现的次数。比如,可以使用BST 记录考试成绩的分布。给定一组考试成绩,可以写一段程序将它们加入一个BST,如果某成绩尚未在BST 中出现,就将其加入BST;如果已经出现,就将出现的次数加1。
我们来修改Node 对象,为其增加一个记录成绩出现频次的成员,同时我们还需要一个方法,当在BST 中发现某成绩时,需要将出现的次数加1,并且更新该节点。
function update(data) {
var grade = this.find(data);
grade.count++;
return grade;
}
function prArray(arr) {
console.log(arr[0].toString() + ' ');
for (var i = 1; i < arr.length; ++i) {
console.log(arr[i].toString() + ' ');
if (i % 10 == 0) {
console.log("\n");
}
}
}
function genArray(length) {
var arr = [];
for (var i = 0; i < length; ++i) {
arr[i] = Math.floor(Math.random() * 101);
}
return arr;
}