这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
一、树的介绍
- 树:n
(n >= 0)个节点构成的有限集合 - 对于任一棵非空树
(n > 0),具备以下性质:- 树中有一个称为**"根"**的特殊节点
- 其余节点可分为m
(m > 0)个互不相交的有限集T1、T2、...、Tm,其中每个集合本身又是一棵树,称为原来树的**"子树"**
树的性质是很多的...
- 节点的度:树的节点个数
- 叶节点:度为0的节点(也称为叶子节点)
- 节点的层次:规定根节点在1层,其它任一节点的层数是其父节点的层数加1
二、二叉树
二叉树的特性
-
一个二叉树第i层的最大节点数为:
2^(i-1), i>=1比如第一层(根节点)为1个;第二层最多为2节点
-
深度为k的二叉树有最大节点总数为:
2^k-1, k>=1 -
对任何非空二叉树T,若
n0表示叶节点(度为0)的个数,n2表示度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1
a. 完全二叉树
-
除二叉树最后一层外,其他各层的节点数都达到最大个数
-
且最后一层从左向右达到叶节点连续存在,只缺右侧若干节点
-
下面的图从左到右看,E有右子节点但是D没有,所以不是完全二叉树。如果给D加一个右子节点,那么它就是完全二叉树
b. 完美二叉树(满二叉树)
- 在二叉树中,除了最下一层的叶节点外,每层节点都有2个子节点,就构成了满二叉树
二叉树的存储方式可以是数组也可以是链表,但是我们一般用链表。因为使用数组的是否如果该二叉树不是满二叉树,会造成很多空间的浪费。
三、二叉搜索树
- 二叉搜索树是一颗二叉树,可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根节点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左、右子树本身也都是二叉搜索树
二叉搜索树初始化
-
首先,二叉搜索树有一个根节点,初始化让这个根节点指向空;
this.root = null; -
其次,二叉搜索树的每一个节点包含3个元素,左子节点、右子节点和键值;初始状态下指针指向空
function Node() { this.key = key; this.left = null; this.right = null; }
四、二叉搜索树常见方法
insert(key):向树中插入一个新的键search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,返回falseinOrderTraverse:通过中序遍历方式遍历所有节点preOrderTraverse:通过先序遍历方式遍历所有节点postOrderTraverse:通过后序遍历方式遍历所有节点min:返回树中的最小的值/键max:返回树中的最大的值/键remove(key):从树中移除某个键
1. insert方法
-
根据key创建节点
-
判断根节点是否有值;如果根节点为null时直接插入,否则进行下一步操作
BinarySearchTree.prototype.insert = function (key) { var newNode = new Node(key); if (this.root == null) { this.root = newNode; } else { this.insertNode(this.root, newNode); } } -
在搜索树进行插入操作的时候,如果插入的值大于根节点,那么就要把该节点插到根节点的右子节点;如果此时根节点已经有右子节点了,那就应该继续让这个要插入的节点的值和该右子节点进行比较插入;这个过程其实就是在套娃,一直到这个左或者右节点为空的时候就停止,所以这里用递归来实现,用另一个函数来进行对节点的插入。
BinarySearchTree.prototype.insertNode = function (node, newNode) { if (newNode.key < node.key) { if (node.left == null) { node.left = newNode; } else { this.insertNode(node.left, newNode); } } else { if (node.right == null) { node.right = newNode; } else { this.insertNode(node.right, newNode); } } }
2. 遍历二叉搜索树
由于树的结构是比较特殊的,在遍历节点的时候我们往往都是采用递归的方法来实现。
a. 先序遍历
- 访问根节点
- 先先序遍历其左子树,再先序遍历右子树
BinarySearchTree.prototype.preOrderTraversalNode = function (node, handler) {
if (node != null) {
handler(node.key);
this.preOrderTraversalNode(node.left, handler);
// 3. 处理经过节点的右子节点
this.preOrderTraversalNode(node.right, handler);
}
}
//--------
var resultString = ''
tree.preOrderTraversal(function (key) {
resultString += key + ' ';
})
这里的handler是一个处理结点的回调函数,便于我们看遍历的结果;在先序遍历中,我们要先遍历所有的左子节点,再处理经过节点的右子节点;比如下面这张图:
这里用的递归思路还是有一点复杂的,是一个个函数的嵌套,遍历结束之后一个个跳出来的,需要花一点时间来理解。
遍历7的左节点到5的时候,先遍历3,3是叶节点,继续遍历6(此时的node是5),遍历完5的左右节点之后,就继续遍历7的右子节点。
b. 中序遍历
- 中序遍历其左子树
- 访问根节点
- 中序遍历右子树
if (node != null) {
this.inOrderTraversalNode(node.left, handler);
handler(node.key);
this.inOrderTraversalNode(node.right, handler);
}
c. 后序遍历
- 后序遍历其左子树,再后序遍历其右子树
- 访问根节点
if (node != null) {
this.inOrderTraversalNode(node.left, handler);
this.inOrderTraversalNode(node.right, handler);
handler(node.key);
}
理解了先序遍历之后,中序遍历和后序遍历都是用递归的方式实现,不同之处在于何时来处理节点。
3. 最大值&最小值
最大值和最小值在数中是很容易找的。最左边的子节点(左下)就是最小值;最右边的子节点(右下)就是最大值。这里演示查找最大值。
- 获取根节点
- 依次向右查找,直到节点为null
BinarySearchTree.prototype.max = function () {
var node = this.root;
var key = null;
while (node != null) {
key = node.key
node = node.right
}
return node.key
}
在查找的时候,一直都找右节点,直到这个右节点的值为空的时候返回这个节点的键值。
4. 搜索特定的值
这个方法可以通过递归的方法实现,也可以通过循环来进行搜索
-
递归;将查找值和当前节点的值进行对比,如果较小就往左子树找,如果较大就往右子树上查找,直到
node.key == key时,返回true,如果经过这些步骤之后还是找不到的话就返回false。BinarySearchTree.prototype.searchNode = function (node, key) { if (node == null) { return false; } if (node.key > key) { return this.searchNode(node.left, key); } else if (node.key < key) { return this.searchNode(node.right, key); } else { return true; } return false; } -
循环 先获取根节点,再循环搜索key
循环的话会比较好理解一点,就是键值比较,决定往左子树还是右子树遍历,直到找到目标值。
var node = this.root; while (node != null) { if (node.key > key) { node = node.left; } else if (node.key < key) { node = node.right; } else { return true; } } return false;
删除操作是最复杂的,那就放在下一篇文章吧!今天学不下去了aaa...codewhy老师真的讲的好棒!