树同散列表同样,是一种非顺序数据结构。现实中树的例子有家谱、公司组织架构图及其它树形结构关系。
树由一系列节点构成,每一个节点都有一个父节点(除根节点外)以及零个或多个子节点,如图:javascript
树中的每个元素叫做节点,最顶部的节点叫做根节点。至少有一个子节点的节点称为内部节点(如图中的七、九、1五、1三、20),没有子节点的节点称为外部节点或叶节点(如图中的三、 六、 八、 十、 十二、 14等)。一个节点能够包括祖先及后代,祖先包括父节点、祖父节点等,后代包括子节点、孙节点等。一个节点及其后代能够组成一个子树(如图中的1三、十二、14)。节点的深度指节点祖先节点的数量(如图中的节点6的深度为3)。树的高度指树中节点深度的最大值。上图中,节点中的数字称为键。java
二叉树:二叉树指节点最多只能包含两个子节点的树,即左侧子节点与右侧子节点。二叉树有利于高效地插入、查找、删除节点。node
二叉搜索树:二叉搜索树是二叉树的一种,它的左侧节点的值必须大于右侧节点的值,上图就是一个二叉搜索树。优化的二叉搜索树包括AVL树、红黑树(待完成)等segmentfault
insert(key):向树中插入一个新的键。数据结构
search(key):在树中查找一个键,若是节点存在,则返回true;若是不存在,则返回false。架构
inOrderTraverse:经过中序遍历方式遍历全部节点。函数
preOrderTraverse:经过先序遍历方式遍历全部节点。post
postOrderTraverse:经过后序遍历方式遍历全部节点。优化
min:返回树中最小的值/键。this
max:返回树中最大的值/键。
remove(key):从树中移除某个键。
首选实现二叉查找树类的骨架
// 二叉查找树类 function BinarySearchTree() { // 用于实例化节点的类 var Node = function(key){ this.key = key; // 节点的健值 this.left = null; // 指向左节点的指针 this.right = null; // 指向右节点的指针 }; var root = null; // 将根节点置为null }
insert方法,向树中插入一个新的键。遍历树,将插入节点的键值与遍历到的节点键值比较,若是前者大于后者,继续递归遍历右子节点,反之,继续遍历左子节点,直到找到一个空的节点,在该位置插入。
this.insert = function(key){ var newNode = new Node(key); // 实例化一个节点 if (root === null){ root = newNode; // 若是树为空,直接将该节点做为根节点 } else { insertNode(root,newNode); // 插入节点(传入根节点做为参数) } }; // 插入节点的函数 var insertNode = function(node, newNode){ // 若是插入节点的键值小于当前节点的键值 // (第一次执行insertNode函数时,当前节点就是根节点) if (newNode.key < node.key){ if (node.left === null){ // 若是当前节点的左子节点为空,就直接在该左子节点处插入 node.left = newNode; } else { // 若是左子节点不为空,须要继续执行insertNode函数, // 将要插入的节点与左子节点的后代继续比较,直到找到可以插入的位置 insertNode(node.left, newNode); } } else { // 若是插入节点的键值大于当前节点的键值 // 处理过程相似,只是insertNode函数继续比较的是右子节点 if (node.right === null){ node.right = newNode; } else { insertNode(node.right, newNode); } } }
在下图的树中插入健值为6的节点,过程以下:
树的遍历指访问树的每个节点,并对节点作必定操做。遍历树主要有三种方式:中序、先序、后序。
中序遍历严格按照从小到大的方式遍历树,也就是优先访问左子节点,至关于对树进行了排序。
this.inOrderTraverse = function(callback){ // callback用于对遍历到的节点作操做 inOrderTraverseNode(root, callback); }; var inOrderTraverseNode = function (node, callback) { // 遍历到node为null为止 if (node !== null) { // 优先遍历左边节点,保证从小到大遍历 inOrderTraverseNode(node.left, callback); // 处理当前的节点 callback(node.key); // 遍历右侧节点 inOrderTraverseNode(node.right, callback); } };
对下图的树作中序遍历,并输出各个节点的键值:
tree.inOrderTraverse(function(value){ console.log(value); });
依次输出:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25,
遍历过程如图:
先序遍历先访问节点自己,再遍历其后代节点,最后遍历其兄弟节点。它的一种应用是打印一个结构化的文档。
this.preOrderTraverse = function(callback){ // 一样的,callback用于对遍历到的节点作操做 preOrderTraverseNode(root, callback); }; var preOrderTraverseNode = function (node, callback) { // 遍历到node为null为止 if (node !== null) { callback(node.key); // 先处理当前节点 preOrderTraverseNode(node.left, callback); // 再继续遍历左子节点 preOrderTraverseNode(node.right, callback); // 最后遍历右子节点 } };
用先序遍历遍历下图所示的树,并打印节点键值,输出结果:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25,
遍历过程如图:
后序遍历先访问节点的后代节点,再访问节点自己。它的一种应用是计算一个目录和它的子目录中全部文件的大小。
this.postOrderTraverse = function(callback){ postOrderTraverseNode(root, callback); }; var postOrderTraverseNode = function (node, callback) { if (node !== null) { postOrderTraverseNode(node.left, callback); //{1} postOrderTraverseNode(node.right, callback); //{2} callback(node.key); //{3} } };
能够看到,中序、先序、后序遍历的实现方式几乎如出一辙,只是{1}、{2}、{3}行代码的执行顺序不一样。
对下图的树进行后序遍历,并打印键值:3 6 5 8 10 9 7 12 14 13 18 25 20 15 11,
遍历过程如图:
在二叉搜索树里,不论是整个树仍是其子树,最小值必定在树最左侧的最底层。所以给定一颗树或其子树,只须要一直向左节点遍历到底就好了。
this.min = function(node) { // min方法容许传入子树 node = node || root; // 一直遍历左侧子节点,直到底部 while (node && node.left !== null) { node = node.left; } return node; };
搜索最大值与搜索最小值相似,只是沿着树的右侧遍历。
this.max = function(node) { // min方法容许传入子树 node = node || root; // 一直遍历左侧子节点,直到底部 while (node && node.right !== null) { node = node.right; } return node; };
搜索特定值的处理与插入值的处理相似。遍历树,将要搜索的值与遍历到的节点比较,若是前者大于后者,则递归遍历右侧子节点,反之,则递归遍历左侧子节点。
this.search = function(key, node){ // 一样的,search方法容许在子树中查找值 node = node || root; return searchNode(key, node); }; var searchNode = function(key, node){ // 若是node是null,说明树中没有要查找的值,返回false if (node === null){ return false; } if (key < node.key){ // 若是要查找的值小于该节点,继续递归遍历其左侧节点 return searchNode(node.left, key); } else if (key > node.key){ // 若是要查找的值大于该节点,继续递归遍历其右侧节点 return searchNode(node.right, key); } else { // 若是要查找的值等于该节点,说明查找成功,返回改节点 return node; } };
移除节点,首先要在树中查找到要移除的节点,再判断该节点是否有子节点、有一个子节点或者有两个子节点,最后分别处理。
this.remove = function(key, node){ // 一样的,容许仅在子树中删除节点 node = node || root; removeNode(key, node); }; var removeNode = function (node, key) { // 找到要删除的节点 var delete_node = this.search(node, key); // 若是node不存在,直接返回 if (node === false) { return null; } // 第一种状况,该节点没有子节点 if (node.left === null && node.right === null) { node = null; return node; } // 第二种状况,该节点只有一个子节点的节点 if (node.left === null) { // 只有右节点 node = node.right; return node; } else if (node.right === null) { // 只有左节点 node = node.left; return node; } // 第三种状况,有有两个子节点的节点 // 将右侧子树中的最小值,替换到要删除的位置 // 找到最小值 var aux = this.min(node.right); // 替换 node.key = aux.key; // 删除最小值 node.right = removeNode(node.right, aux.key); return node; }
第三种状况的处理过程,以下图所示。当要删除的节点有两个子节点时,为了避免破坏树的结构,删除后要替补上来的节点的键值大小必须在已删除节点的左、右子节点的键值之间,且替补上来的节点不该该有子节点,不然会产生一个节点有多个字节点的状况,所以,找右侧子树的最小值替换上来。同理,找左侧子树的最大值替换上来也能够。