一,树的定义
树由一组以边连接的节点组成。。一棵树最上面的节点称为根节点,如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子节点。一个节点可以有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; }