二叉树
树是一种非线性的数据结构,以分层的方式存储数据。树被用来存储具有层级关系的数据,比如文件系统中的文件;树还被用来存储有序列表。本章将研究一种特殊的树:二叉树。选择树而不是那些基本的数据结构,是因为在二叉树上进行查找非常快(而在链表上查找则不是这样),为二叉树添加或删除元素也非常快(而对数组执行添加或删除操作则不是这样)。
二叉树是一种特殊的树,它的子节点个数不超过两个。二叉树具有一些特殊的计算性质,使得在它们之上的一些操作异常高效。
二叉树的高度和深度的区别:
高度和深度是相反的表示,深度是从上到下数的
,而**高度是从下往上数
**。
(对某个节点来说)
深度是指从根节点到该节点的最长简单路径边的条数;
高度是指从最下面的叶子节点到该节点的最长简单路径边的条数;
(对二叉树)
深度是从根节点数到它的叶节点;
高度是从叶节点数到它的根节点;
注意: 树的深度和高度一样,但是具体到树的某个节点,其深度和高度不一样。
二叉查找树由节点组成,所以我们要定义的第一个对象就是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() 方法用来显示保存在节点中的数据
查找正确插入点的算法如下
(1) 设根节点为当前节点。
(2) 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反之,执行第4 步。
(3) 如果当前节点的左节点为null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
(4) 设新的当前节点为原节点的右节点。
(5) 如果当前节点的右节点为null,就将新的节点插入这个位置,退出循环;反之,继续执行下一次循环。
// BST 二叉查找树
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;
}
}
}
}
}
三种遍历BST 的方式:中序、先序和后序
。
中序遍历按照节点上的键值,以升序访问BST 上的所有节点。先序遍历先访问根节点,然后以同样方式访问左子树和右子树。后序遍历先访问叶子节点,从左子树到右子树,再到根节点。
如图所示是二叉树中的一个节点,这个节点有左子树与右子树,通过两根绿色的连接线,将此节点划分成了三个区域,我们分别用前、中、后
这三个辅助点来表示。这三个点表明在二叉树的遍历中什么时候要取出此节点的值。任何一个节点去遍历都是 前-左绿线-中-右绿线-后
这样的顺序遍历的。
先序遍历
使用递归方式实现前序遍历的具体过程为:
- 先访问根节点
- 再序遍历左子树
- 最后序遍历右子树
我们来对上面的动画进行一个充分的说明来理解前序遍历的递归实现方式。
- 首先访问了
28
这个节点,我们看前辅助点
,由于是前序遍历,那么此刻我们取出该节点的值28
- 而后通过左绿线访问
28
的左子树 - 在
16
这个节点中,我们看前辅助点
,由于是前序遍历,取出该节点的值16
- 通过左绿线访问
16
的左子树 - 在
13
这个节点中,我们看前辅助点
,由于是前序遍历,取出该节点的值13
13
这个节点左子树为空,那么我们左绿线就没有,然后看中辅助点
,由于是前序遍历,因此不做处理13
这个节点右子树为空,那么我们右绿线就没有,然后看后辅助点
,由于是前序遍历,因此不做处理- 而后回到
16
这个节点中,看中辅助点
,由于是前序遍历,因此不做处理 - 而后看
16
这个节点的右子树22
这个节点,看前辅助点
,由于是前序遍历,取出该节点的值22
中序遍历
使用递归方式实现中序遍历的具体过程为:
- 先中序遍历左子树
- 再访问根节点
- 最后中序遍历右子树
我们来对上面的动画进行一个充分的说明来理解中序遍历的递归实现方式。
- 首先访问了
28
这个节点,我们看前辅助点
,由于是中序遍历,因此不做处理 - 而后通过左绿线访问
28
的左子树 - 在
16
这个节点中,我们看前辅助点
,由于是中序遍历,因此不做处理 - 通过左绿线访问
16
的左子树 - 在
13
这个节点中,我们看前辅助点
,由于是中序遍历,因此不做处理 13
这个节点左子树为空,那么我们左绿线就没有,然后看中辅助点
,由于是中序遍历,取出该节点的值13
13
这个节点右子树为空,那么我们右绿线就没有,然后看后辅助点
,由于是中序遍历,因此不做处理- 而后回到
16
这个节点中,看中辅助点
,由于是中序遍历,取出该节点的值16
- 而后看
16
这个节点的右子树22
这个节点,看前辅助点
,由于是中序遍历,因此不做处理 - 看
中辅助点
,由于是中序遍历,取出该节点的值22
后序遍历
使用递归方式实现后序遍历的具体过程为:
- 先后序遍历左子树
- 再后序遍历右子树
- 最后访问根节点
我们来对上面的动画进行一个充分的说明来理解后序遍历的递归实现方式。
- 首先访问了
28
这个节点,我们看前辅助点
,由于是后序遍历,因此不做处理 - 而后通过左绿线访问
28
的左子树 - 在
16
这个节点中,我们看前辅助点
,由于是后序遍历,因此不做处理 - 通过左绿线访问
16
的左子树 - 在
13
这个节点中,我们看前辅助点
,由于是后序遍历,因此不做处理 13
这个节点左子树为空,那么我们左绿线就没有,然后看中辅助点
,由于是后序遍历,因此不做处理13
这个节点右子树为空,那么我们右绿线就没有,然后看后辅助点
,由于是后序遍历,取出该节点的值13
- 而后回到
16
这个节点中,看中辅助点
,由于是后序遍历,因此不做处理 - 而后看
16
这个节点的右子树22
这个节点,看前辅助点
,由于是中序遍历,因此不做处理 - 看
中辅助点
,由于是后序遍历,因此不做处理 - 看
后辅助点
,由于是后序遍历,取出该节点的值16
二叉查找树上查找
对BST 通常有下列三种类型的查找:
(1) 查找给定值;
(2) 查找最小值;
(3) 查找最大值。
查找最小值和最大值
getMin() 方法查找BST 上的最小值,该方法的定义如下:
function getMin() {
var current = this.root;
while (!(current.left == null)) {
current = current.left;
}
return current.data;
}
getMax() 方法的定义如下:
function getMax() {
var current = this.root;
while (!(current.right == null)) {
current = current.right;
}
return current.data;
}
查找给定值
// find() 方法用来在BST 上查找给定值,定义如下:
// 利用二叉树的性质,如果data小于该数据,就往左寻找,data大于该数据,就往右寻找
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;
}
二叉查找树上删除节点
为了管理删除操作的复杂度,我们使用递归操作,同时定义两个方法: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;
}
}