读【数据结构与算法】之二叉树

树是在计算机科学中经常用到的一种数据结构。树是非线性的数据结构,以分层的方式存储数据。在二叉树上进行查找非常快(在链表上查找就不是这样了),为二叉树添加或删除元素也非常快的(对数组添加或删除则不是这样)。

树由一组以边连接的节点组成。一棵树最上面的节点叫根节点,如果一个节点下面连接多个节点,那么该节点就称为父节点,他下面的节点称为子节点。没有任何子节点的节点称为叶子节点。沿着一组特定的边,可以从一个节点走到另外一个与他不直接连接的节点。从一个节点到另一个节点的这一组边称为路径。以某种特定顺序访问树中所有的节点称为树的遍历。

树可以分为几个层次,根节点就是第0层,他的子节点就是第1层,子节点的子节点是第2层,以此类推。树中的任何一层的节点都可以看做是子树的根。我们定义树的层数就是树的深度。

一个父节点的两个子节点分别称为左节点和右节点。

二叉树是一种特殊的树,他的子节点个数不超过两个。在一些二叉树的实现中,左节点包含一组特定的值,右节点包含另一组特定的值。

二叉查找树(BST)是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。这样的特性使得他的查找效率很高。

BST由节点组成,所以需要定义节点Node类。下边的是其基本实现:

/**
 * Class Node
 * 节点 Node
 */

function Node(data, left, right) {
	this.data = data;
	this.left = left;
	this.right = right;
}

Node.prototype = {

	constructor: Node,

	/**
	 * 显示保存节点中的数据
	 * @return {Any} 数据
	 */
	show: function show() {
		return this.data;
	}

};

有了节点,现在就可以实现一个简单的BST类了,目前只有一个加入节点insert操作:

/**
 * Class BST
 * 二叉查找树 BST
 */

function BST() {
	this.root = null;
}

BST.prototype = {

	constructor: BST,

	/**
	 * 加入新节点
	 * @param {Any} data 节点数据
	 * @return {Undefined} undefined
	 */
	insert: function insert(data) {
		var n = new Node(data, null, null);
		if (this.root == null) {
			this.root = n;
		} else {
			var current = this.root, parent, k;
			while (true) {
				parent = current;
				if (data < current.data) {
					// 进入左节点
					k = 'left';
				} else {
					// 进入左节点
					k = 'right';
				}
				current = current[k];
				if (current == null) {
					// 没有k节点
					parent[k] = n;
					break;
				}
			}
		}
	}

};

下一步就需要对二叉树进行遍历了,有三种遍历方式:中序、先序和后序。中序遍历就是按照节点上的键值,以升序访问BST节点上的所有节点。先序遍历就是先访问根节点,然后以同样的方式访问左子树和右子树。后序遍历就是先访问叶子节点,从左子树到右子树,然后再到根节点。

下边的代码是加入了三种遍历之后的样子:

/**
 * Class BST
 * 二叉查找树 BST
 */

function BST() {
	this.root = null;
}

BST.prototype = {

	constructor: BST,

	/**
	 * 加入新节点
	 * @param {Any} data 节点数据
	 * @return {Undefined} undefined
	 */
	insert: function insert(data) {
		var n = new Node(data, null, null);
		if (this.root == null) {
			this.root = n;
		} else {
			var current = this.root, parent, k;
			while (true) {
				parent = current;
				if (data < current.data) {
					// 进入左节点
					k = 'left';
				} else {
					// 进入左节点
					k = 'right';
				}
				current = current[k];
				if (current == null) {
					// 没有k节点
					parent[k] = n;
					break;
				}
			}
		}
	},

	/**
	 * 中序遍历
	 * @param  {Node}  node 节点
	 * @param  {Array} ret  结果
	 * @return {Array}      结果数组
	 */
	inOrder: function inOrder(node, ret) {
		if (!ret) ret = [];
		if (node != null) {
			inOrder(node.left, ret);
			ret.push(node.show());
			inOrder(node.right, ret);
		}
		return ret;
	},

	/**
	 * 先序遍历
	 * @param  {Node}  node 节点
	 * @param  {Array} ret  结果
	 * @return {Array}      结果数组
	 */
	preOrder: function preOrder(node, ret) {
		if (!ret) ret = [];
		if (node != null) {
			ret.push(node.show());
			preOrder(node.left, ret);
			preOrder(node.right, ret);
		}
		return ret;
	},

	/**
	 * 后序遍历
	 * @param  {Node}  node 节点
	 * @param  {Array} ret  结果
	 * @return {Array}      结果数组
	 */
	postOrder: function postOrder(node, ret) {
		if (!ret) ret = [];
		if (node != null) {
			postOrder(node.left, ret);
			postOrder(node.right, ret);
			ret.push(node.show());
		}
		return ret;
	}

};

对BST通常有三种类型查找:

  1. 查找给定值

  2. 查找最小值

  3. 查找最大值

在BST上查找最小值和最大值很简单,因为较小的值总是在左子节点上,在BST上查找最小值,只需要遍历左子树,直到找到最后一个节点。查找最大值类似,遍历右子树,直到找到最后一个节点。

那么怎么查找给定值呢,就需要比较该值和当前节点上的值大小,如果比当前值小,那么就遍历左子树,反之就是遍历右子树。

实现的代码(部分):

/**
 * 得到最小值
 * @return {Any} 最小值
 */
getMin: function getMin() {
	var current = this.root;
	while (current.left != null) {
		current = current.left;
	}
	return current.data;
},

/**
 * 得到最大值
 * @return {Any} 最大值
 */
getMax: function getMax() {
	var current = this.root;
	while (current.right != null) {
		current = current.right;
	}
	return current.data;
},

/**
 * 查找指定值
 * @param  {Any} data  指定值
 * @return {Node|Null} 保存该值的节点或null
 */
find: function find(data) {
	var current = this.root;
	while (current != null) {
		if (current.data === data) {
			return current;
		} else if (current.data > data) {
			current = current.left;
		} else {
			current = current.right;
		}
	}
	return null;
}

在BST上还有一个最复杂的操作,那就是删除节点,其复杂程度取决于删除哪个节点。如果删除没有子节点的节点就很简单,直接删除。如果节点只有一个子节点,不管是左节点还是右节点,就复杂了;当然删除带有两个子节点的节点是最为复杂的。

删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果说待删除的数据小于当前节点上的数据,那么就移至当前节点的左子节点继续比较;否则就移至当前节点的右节点继续比较。

如果待删除节点是一个叶子节点,那么只需要将父节点指向他的链接指向null即可。

如果待删除节点只包含一个子节点,那么原本指向他的节点就得做一些调整,使其指向他的子节点。

最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树上的最大值,要么查找其右子树上的最小值。

删除实现代码(部分):

/**
 * 删除指定数据
 * @param  {Any} data  指定数据
 * @return {Node|Null} 移除后节点或者null
 */
remove: function remove(data) {
	return this.removeNode(this.root, data);
},

/**
 * 从节点上删除指定数据
 * @param  {Node|Null} node 节点
 * @param  {Any}       data 待删除数据
 * @return {Node|Null}      移除后节点或者null
 */
removeNode: 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 tmpNode = this.getSmallest(node.right);
		node.data = tmpNode.data;
		node.right = removeNode(node.right, tmpNode.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;
	}
},

/**
 * 得到节点上有最小值的子节点
 * @param  {Node} node 节点
 * @return {Node}      有最小值的节点
 */
getSmallest: function getSmallest(node) {
	// 没有左节点,意味着当前节点是最小
	if (node.left == null) return node;

	return getSmallest(node.left);
}

具体运行例子请看demojs实现二叉树

发布于: 2014年 09月 11日