我们把每个节点最多有两个子节点的树称为二叉树,即英文的Binary Tree。我们把这两个子节点分别叫做左子节点和右子节点。一般来说,二叉树的节点包括以下的内容:
可以用以下的JavaScript代码表示二叉树的节点:
class BinaryTreeNode {
constructor(data, leftChild, rightChild) {
this.data = data;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
};
根据二叉树的定义,我们可以很容易得到以下的两个特性:
二叉树的第n层最多有2n-1个节点
这个特性可以数学归纳法来得到结论:
- 根节点时为第1层,节点数为21-1=1
- 假设第n层时,节点数为2n-1
- 因为二叉树每个节点最多有两个子节点,那么我们可以知道第n+1层的节点数2*2n-1,即2(n+1)-1,所以上面的第二步假设成立
高度为h的二叉树最多有2h-1个节点
这个结论可以通过假设二叉树的每个节点都有最多的节点数:2,那么高度为h的二叉树的总节点数为:1+2+4…+2h-1,这是一个简单的等比数列,所以和等于2h-1
灵活运用二叉树的这两个基础特性可以比较方便的解决一些二叉树相关的算法问题。
创建二叉树有很多种方式,我们在这里做这样的前提假设:给定一个数组按层顺序创建一个完全二叉树。
完全二叉树:二叉树中除了最后一层所有的层都完全填满,最后一层尽可能的填满左子树
创建二叉树算法的核心是如何确定节点和子节点的对应关系。根据完全二叉树的定义,除了最后一层所有的节点都有2个子节点,我们可以得到这样的一个结论:
数组中索引为i的元素的左子节点是数组中索引为(2*i)+1的元素,右子节点是索引为(2*i)+2的元素
创建二叉树的算法用JavaScript表示如下:
function createCompleteBinaryTreeFromArray(dataArray) {
let treeNodeArray = dataArray.map(function(data){
return new BinaryTreeNode(data, null, null);
});
for (let i = 0; i < treeNodeArray.length; ++i) {
let node = treeNodeArray[i];
node.leftChild = treeNodeArray[i * 2 + 1];
node.rightChild = treeNodeArray[i * 2 + 2];
}
return treeNodeArray[0];
};
需要说明的是这里先调用Array的map方法把数据数组转化称二叉树节点数组,然后在节点数组上设置好节点和子节点的对应关系从而达到创建二叉树的目的,此时节点数组的第一个元素就是二叉树的根节点。
要向已有的二叉树插入新节点关键在于怎么找到要插入的位置,假如还需要保证插入后的二叉树保持某种顺序,那么还要有额外的处理。在这里我们考虑一种简单的情况:给定数据,按层顺序找到一个合适的位置并插入新节点。
所谓层顺序即按广度优先的方式遍历二叉树的一种顺序
说到广度优先来遍历二叉树,有经验的程序员会想到一种数据结构:队列。队列是一种先入先出(FIFO)的数据结构,我们只须依次把节点加入队列,访问时让节点移除队列的方式就可以实现广度优先遍历。在访问时只需要找到第一个左子节点或右子节点为空的节点,让后把新节点作为该节点的左子节点或右子节点就完成了插入。算法简单描述如下:
function insertInLevelOrder(root, data) {
let visitNodeQueue = [];
visitNodeQueue.push(root);
while(visitNodeQueue.length != 0)
{
let currentNode = visitNodeQueue.shift();
if (!currentNode.leftChild){
currentNode.leftChild = new BinaryTreeNode(data, null, null);
break;
}
else if (!currentNode.rightChild){
currentNode.rightChild = new BinaryTreeNode(data, null, null);
break;
} else {
visitNodeQueue.push(currentNode.leftChild);
visitNodeQueue.push(currentNode.rightChild);
}
}
return root;
}
要删除一个二叉树上的节点,通常需要两个步骤:一,找到要删的节点;二,删除节点后要确保二叉树的完整性。第一个步骤容易理解,第二个步骤是因为假如要删除的节点不是叶子节点,那么删除后假如不做处理,那么二叉树结构就被破坏了,所以我们要想办法把删除节点后的结构恢复称二叉树的结构。相对来说第二个步骤的实现要复杂一点,所以我们可以想用另外的方式来规避这第二个步骤但是又能实现删除节点的操作。
我们知道假如删除的节点是叶子节点,那么就不用做额外的处理就能保证删除后的二叉树还是二叉树。所以我们可以想办法把删除操作转化称对叶子节点的删除,算法用文字描述如下:
在算法第二步中,我们可以假定合适的叶子节点是最后一层中靠最右的叶子节点,一样的我们可以通过广度优先的方式来找到这个叶子节点。整个删除算法用Javascript描述如下:
function deleteTreeNode(root, data) {
if (root == null) {
return null;
}
if (root.leftChild == null && root.rightChild == null) {
if (root.data === data) {
return null;
}
return root;
}
let nodeToDeleted = null;
let nodeDeepest = null;
let visitNodeQueue = [root];
while (visitNodeQueue.length != 0) {
nodeDeepest = visitNodeQueue.shift();
if (nodeDeepest.data == data) {
nodeToDeleted = nodeDeepest;
}
if (nodeDeepest.leftChild) {
visitNodeQueue.push(nodeDeepest.leftChild);
}
if (nodeDeepest.rightChild) {
visitNodeQueue.push(nodeDeepest.rightChild);
}
}
if (nodeToDeleted) {
nodeToDeleted.data = nodeDeepest.data;
deleteDeepestTreeNode(root, nodeDeepest);
}
return root;
}
在以上代码中我们用广度优先的方式遍历二叉树,定位要删除的节点(nodeToDeleted)和用来替换的叶子节点(nodeDeepest)。定位这两个节点之后要做的就是把要删除节点的数据替换称该叶子节点的数据,即这一行代码:
nodeToDeleted.data = nodeDeepest.data;
然后就是删除叶子节点,即这一行代码:
deleteDeepestTreeNode(root, nodeDeepest);
deleteDeepestTreeNode这个函数就是用来删除叶子节点的,它的实现如下:
function deleteDeepestTreeNode(root, treeNode) {
let visitNodeQueue = [];
visitNodeQueue.push(root);
while(visitNodeQueue.length != 0) {
let currentNode = visitNodeQueue.shift();
if (currentNode == treeNode) {
// only one node in this tree, and this node is to be deleted
return null;
}
if (currentNode.leftChild) {
if (currentNode.leftChild == treeNode) {
currentNode.leftChild = null;
return root;
} else {
visitNodeQueue.push(currentNode.leftChild);
}
}
if (currentNode.rightChild){
if (currentNode.rightChild == treeNode) {
currentNode.rightChild = null;
return root;
} else {
visitNodeQueue.push(currentNode.rightChild);
}
}
}
return root;
}
deleteDeepestTreeNode的核心也是通过广度优先遍历二叉树,找到要删除的叶子节点的父节点,如果叶子节点是该父节点的左子节点或右子节点就把左子节点或右子节点引用置空就可以了。
通过实现以上的算法我们发现在遍历像树这样的非线性结构时,经常用队列来实现广度优先的遍历,从而实现了很巧妙的算法。类似的还有通过栈来实现深度优先来遍历树或图,这个以后有时间再介绍。