前两天接到了蚂蚁金服的面试电话,面试官很直接,上来就抛出了三道算法题。。。node
其中有一道关于二叉树实现中序遍历的,当时没回答好,因此特地学习了一把二叉树的知识,行文记录总结。git
节点: 树中的每一个元素称为一个节点,github
根节点: 位于整棵树顶点的节点,它没有父节点, 如上图 5面试
子节点: 其余节点的后代算法
叶子节点: 没有子节点的元素称为叶子节点, 如上图 3 8 24 数组
二叉树:二叉树就是一种数据结构, 它的组织关系就像是天然界中的树同样。官方语言的定义是:是一个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。数据结构
二叉查找树:
二叉查找树也叫二叉搜索树(BST),它只容许咱们在左节点存储比父节点更小的值,右节点存储比父节点更大的值,上图展现的就是一颗二叉查找树。post
首先建立一个类来表示二叉查找树,它的内部应该有一个Node类,用来建立节点学习
function BinarySearchTree () { var Node = function(key) { this.key = key, this.left = null, this.right = null } var root = null }
它还应该有一些方法:测试
向树中插入一个新的键,首页应该建立一个用来表示新节点的Node类实例,所以须要new一下Node类并传入须要插入的key值,它会自动初始化为左右节点为null的一个新节点
而后,须要作一些判断,先判断树是否为空,若为空,新插入的节点就做为根节点,如不为空,调用一个辅助方法insertNode()方法,将根节点和新节点传入
this.insert = function(key) { var newNode = new Node(key) if(root === null) { root = newNode } else { insertNode(root, newNode) } }
定义一下insertNode() 方法,这个方法会经过递归得调用自身,来找到新添加节点的合适位置
var insertNode = function(node, newNode) { if (newNode.key <= node.key) { if (node.left === null) { node.left = newNode }else { insertNode(node.left, newNode) } }else { if (node.right === null) { node.right = newNode }else { insertNode(node.right, newNode) } } }
要实现中序遍历,咱们须要一个inOrderTraverseNode(node)方法,它能够递归调用自身来遍历每一个节点
this.inOrderTraverse = function() { inOrderTraverseNode(root) }
这个方法会打印每一个节点的key值,它须要一个递归终止条件————检查传入的node是否为null,若是不为空,就继续递归调用自身检查node的left、right节点
实现起来也很简单:
var inOrderTraverseNode = function(node) { if (node !== null) { inOrderTraverseNode(node.left) console.log(node.key) inOrderTraverseNode(node.right) } }
有了中序遍历的方法,只须要稍做改动,就能够实现先序遍历和后序遍历了
上代码:
这样就能够对整棵树进行中序遍历了
// 实现先序遍历 this.preOrderTraverse = function() { preOrderTraverseNode(root) } var preOrderTraverseNode = function(node) { if (node !== null) { console.log(node.key) preOrderTraverseNode(node.left) preOrderTraverseNode(node.right) } } // 实现后序遍历 this.postOrderTraverse = function() { postOrderTraverseNode(root) } var postOrderTraverseNode = function(node) { if (node !== null) { postOrderTraverseNode(node.left) postOrderTraverseNode(node.right) console.log(node.key) } }
发现了吧,其实就是内部语句更换了先后位置,这也恰好符合三种遍历规则:先序遍历(根-左-右)、中序遍历(左-根-右)、中序遍历(左-右-根)
如今的完整代码以下:
function BinarySearchTree () { var Node = function(key) { this.key = key, this.left = null, this.right = null } var root = null //插入节点 this.insert = function(key) { var newNode = new Node(key) if(root === null) { root = newNode } else { insertNode(root, newNode) } } var insertNode = function(node, newNode) { if (newNode.key <= node.key) { if (node.left === null) { node.left = newNode }else { insertNode(node.left, newNode) } }else { if (node.right === null) { node.right = newNode }else { insertNode(node.right, newNode) } } } //实现中序遍历 this.inOrderTraverse = function() { inOrderTraverseNode(root) } var inOrderTraverseNode = function(node) { if (node !== null) { inOrderTraverseNode(node.left) console.log(node.key) inOrderTraverseNode(node.right) } } // 实现先序遍历 this.preOrderTraverse = function() { preOrderTraverseNode(root) } var preOrderTraverseNode = function(node) { if (node !== null) { console.log(node.key) preOrderTraverseNode(node.left) preOrderTraverseNode(node.right) } } // 实现后序遍历 this.postOrderTraverse = function() { postOrderTraverseNode(root) } var postOrderTraverseNode = function(node) { if (node !== null) { postOrderTraverseNode(node.left) postOrderTraverseNode(node.right) console.log(node.key) } } }
居然已经完成了添加新节点和遍历的方式,咱们来测试一下吧:
定义一个数组,里面有一些元素
var arr = [9,6,3,8,12,15]
咱们将arr中的每一个元素依此插入到二叉搜索树中,而后打印结果
var tree = new BinarySearchTree() arr.map(item => { tree.insert(item) }) tree.inOrderTraverse() tree.preOrderTraverse() tree.postOrderTraverse()
运行代码后,咱们先来看看插入节点后整颗树的状况:
输出结果
中序遍历:
3
6
8
9
12
15
先序遍历:
9
6
3
8
12
15
后序遍历:
3
8
6
15
12
9
很明显,结果是符合预期的,因此,咱们用上面的JavaScript代码,实现了对树的节点插入,和三种遍历方法,同时,很明显能够看到,在二叉查找树树种,最左侧的节点的值是最小的,而最右侧的节点的值是最大的,因此二叉查找树能够很方便的拿到其中的最大值和最小值
怎么作呢?其实只须要将根节点传入minNode/或maxNode方法,而后经过循环判断node为左侧(minNode)/右侧(maxNode)的节点为null
实现代码:
// 查找最小值 this.findMin = function() { return minNode(root) } var minNode = function(node) { if (node) { while (node && node.left !== null) { node = node.left } return node.key } return null } // 查找最大值 this.findMax = function() { return maxNode(root) } var maxNode = function (node) { if(node) { while (node && node.right !== null) { node =node.right } return node.key } return null }
this.search = function(key) { return searchNode(root, key) }
一样,实现它须要定义一个辅助方法,这个方法首先会检验node的合法性,若是为null,直接退出,并返回fasle。若是传入的key比当前传入node的key值小,它会继续递归查找node的左侧节点,反之,查找右侧节点。若是找到相等节点,直接退出,并返回true
var searchNode = function(node, key) { 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 true } }
移除节点的实现状况比较复杂,它会有三种不一样的状况:
和实现搜索指定节点一元,要移除某个节点,必须先找到它所在的位置,所以移除方法的实现中部分代码和上面相同:
// 移除节点 this.remove = function(key) { removeNode(root,key) } var removeNode = function(node, key) { if (node === null) { return null } if (key < node.key) { node.left = removeNode(node.left, key) return node }else if(key > node.key) { node.right = removeNode(node.right,key) return node }else{ //须要移除的节点是一个叶子节点 if (node.left === null && node.right === null) { node = null return node } //须要移除的节点包含一个子节点 if (node.letf === null) { node = node.right return node }else if (node.right === null) { node = node.left return node } //须要移除的节点包含两个子节点 var aux = findMinNode(node.right) node.key = aux.key node.right = removeNode(node.right, axu.key) return node } } var findMinNode = function(node) { if (node) { while (node && node.left !== null) { node = node.left } return node } return null }
其中,移除包含两个子节点的节点是最复杂的状况,它包含左侧节点和右侧节点,对它进行移除主要须要三个步骤:
有点绕儿,但必须这样,由于删除元素后的二叉搜索树必须保持它的排序性质
tree.remove(8) tree.inOrderTraverse()
打印结果:
3
6
9
12
15
8 这个节点被成功删除了,可是对二叉查找树进行中序遍历依然是保持排序性质的
到这里,一个简单的二叉查找树就基本上完成了,咱们为它实现了,添加、查找、删除以及先中后三种遍历方法
可是实际上这样的二叉查找树是存在一些问题的,当咱们不断的添加更大/更小的元素的时候,会出现以下状况:
tree.insert(16) tree.insert(17) tree.insert(18)
来看看如今整颗树的状况:
很容易发现,它是不平衡的,这又会引出平衡树的概念,要解决这个问题,还须要更复杂的实现,例如:AVL树,红黑树 哎,以后再慢慢去学习吧
关于实现二叉排序树,我也找到慕课网的一系列的视频:Javascript实现二叉树算法,
内容和上述实现基本一致