二叉树
01. 什么是二叉树
定义:每个节点都最多只能有两个子节点的树结构
特点:
-
通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)
-
任何一个树都可以转成二叉树
02. 完全二叉树
定义:一棵二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k层所有的结点都连续集中在最左边。
特点:
- 深度为k的完全二叉树,至少有2^(k-1)个节点,至多有2^k-1个节点
03. 满二叉树
定义:一棵二叉树深度为k,则第k层有2^(k-1)个节点的树是满二叉树。
特点:
-
所有内部节点都有两个子节点,叶子节点全都在最底层;
-
第k层的结点数是:2^(k-1);
-
总结点数是:2^k-1,总结点数一定是奇数
04. 为什么定义这样特殊的二叉树?
因为一颗完全二叉树是可以用数组来存储的,不会浪费内存空间。
如下数组,根节点 A 的下标是 0,那么它的左孩子就是 (2 * 0 + 1),右孩子就是 (2 * 0 + 2)。以此类推,0 替换为变量 i 后,就可以推导每个节点的左右孩子,而不需要特别定义节点类,使用两个左右指针去指向左右孩子,这也是一种更高效的存储方案。此处可以延伸到二叉堆,它无论何时都是一颗符合堆的性质的完全二叉树,所以它的最优存储方案就是用数组
const tree = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
05. 二叉树的遍历方式
二叉树有先序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)、层序遍历四种方式。
i. 先序遍历(根左右)
遍历规则:
-
先访问根结点
-
然后先序遍历左子树
-
然后先序遍历右子树
实现:
const root = {
val: 'A',
left: {
val: 'B',
left: {
val: 'D',
left: {
val: 'H',
},
right: {
val: 'I',
},
},
right: {
val: 'E',
},
},
right: {
val: 'C',
left: {
val: 'F',
right: {
val: 'J',
},
},
right: {
val: 'G',
},
},
}
function preorder(root){
if(!root){
return
}
console.log(root.val) // 打印当前遍历的节点
preorder(root.left) // 递归遍历左子树
preorder(root.right) // 递归遍历右子树
}
遍历结果:ABDHIECFJG
ii. 中序遍历(左根右)
遍历规则:
-
先中序遍历左子树
-
访问根结点
-
然后中序遍历右子树
实现:
function midorder(root) {
if(!root) {
return
}
midorder(root.left) // 递归遍历左子树
console.log(root.val) // 打印当前遍历的结点
midorder(root.right) // 递归遍历右子树
}
遍历结果:HDIBEAFJCG
iii. 后序遍历(左右根)
遍历规则:
-
先后序遍历左子树
-
然后后序遍历右子树
-
然后访问根结点
实现:
function postorder(root) {
if(!root) {
return
}
postorder(root.left) // 递归遍历左子树
postorder(root.right) // 递归遍历右子树
console.log(root.val) // 打印当前遍历的结点
}
遍历结果:HDIBEACFJG
iv. 层序遍历
遍历规则:从上到下,从左到右遍历。
实现:
function levelTraversal(root){
if(!root){
return [];
}
var queue = [root];
var result = [];
while (queue.length!==0){
var node = queue.shift();
result.push(node.val);
if(node.left){
queue.push(node.left);
}
if(node.right){
queue.push(node.right);
}
}
return result;
}
遍历结果:ABCDEFGHIJ
二叉搜索树(BST)
01. 基本概念
二叉搜索树BST(Binary Search/ Sort Tree),也称为二叉查找树,二叉排序树,顾名思义是二叉树的特种树,专注于检索,特点是让节点按照数据的一定的规律摆放,从而让搜索某个节点特别的高效
特点:
-
左子树上所有结点的值均小于等于它的根结点的值
-
右子树上所有结点的值均大于等于它的根结点的值
02. 代码实现
1、定义
首先定义两个类,一个节点类,一个树类:
class TreeNode {
constructor(val) {
this.val = val;
this.left = null; // 左孩子
this.right = null; // 右孩子
}
}
class BST {
constructor() {
this.root = null; // 根结点
}
}
2、新增
新增节点时,需要保证无论增加多少个节点,都不被破坏二叉搜索树的定义(即左小右大)。
步骤:
-
新节点与根节点进行比较
-
若新节点 < 根节点,将根节点指向根节点的左孩子,返回第一步
-
若新节点 > 根节点,将根节点指向根节点的右孩子,返回第一步
-
若根节点不存在,此处即为新节点的位置
class BST {
...
add(val) {
const _helper = (node, val) => {
if (node === null) { // 某个节点为空,创建新节点返回
return new TreeNode(val);
}
if (node.val > val) { // 如果当前节点值比val大,新节点需要放在其左子树里
node.left = _helper(node.left, val); // 改以左孩子为起点,返回新的左孩子节点
return node; // 返回新的左子树
} else if (node.val < val) {
node.right = _helper(node.right, val); // 同上
return node;
}
}
this.root = _helper(this.root, val); // 返回新的树
}
}
3、查询
步骤:
-
新节点与根节点进行比较
-
若新节点 < 根节点,将根节点指向根节点的左孩子,返回第一步
-
若新节点 > 根节点,将根节点指向根节点的右孩子,返回第一步
-
若新节点 === 根结点,找到啦!
-
若根节点不存在,说明树中不存在要查询的节点
class BST {
...
compare(val) {
const _helper = (node, val) => {
if (node === null) { // 某个节点为空,没查询到
return false;
}
if (node.val === val) { // 相等,查询成功
return true;
}
if (node.val > val) { // 根结点大于查询节点,去左子树找
return _helper(node.left, val);
} else { // 根结点小于查询节点,去右子树找
return _helper(node.right, val);
}
}
return _helper(this.root, val);
}
}
4、删除
-
叶子节点:直接删除
-
只有一边有孩子节点:让孩子节点接替被删除节点的位置
-
两边都有孩子节点:
-
在待删除节点的左子树里面找到最大的值替代
-
在待删除节点的右子树里面找到最小的值替代
class BST {
...
maxmum(node) { // 返回树的最大值节点
if (node.right === null) {
return node;
}
return this.maxmum(node.right);
}
removeMax(node) { // 删除树的最大值节点
if (node.right === null) {
const leftNode = node.left;
node.left = null;
return leftNode;
}
node.right = this.removeMax(node.right);
return node;
}
remove(val) { // 删除指定值
const _helper = (node, val) => {
if (node === null) { // 没找到要删除的
return node;
}
if (node.val === val) { // 找到了
if (node.left === null) { // 左孩子为空时,让右孩子续上
const rightNode = node.right;
node.right = null;
return rightNode;
} else if (node.right === null) { // 右孩子为空时,让左孩子续上
const leftNode = node.left;
node.left = null;
return leftNode;
} else { // 如果左右都有孩子
const successor = this.maxmum(node.left); // 在左子树里找最大的节点
node.left = this.removeMax(node.left); // 并且删除左子树里最大的节点
successor.left = node.left; // 让新节点指向之前的左孩子
successor.right = node.right; // 让新节点指向之前的右孩子
return successor; // 返回新节点
}
} else if (node.val > val) {
node.left = _helper(node.left, val);
return node
} else {
node.right = _helper(node.right, val);
return node;
}
}
this.root = _helper(this.root, val);
}
}
03. 优缺点
优点:增删查速度快,平均时间复杂度均为O(logn)
缺点:可能出现类似线性链表的结构,查找的时间复杂度会变成O(n)
平衡二叉树(AVL)
01. 基本概念
平衡二叉树(AVL)全称平衡二叉搜索树,是一种可以自平衡的树,是从上面二叉搜索树升级过来的。
以二叉搜索树为基础,增加平衡的规定:左子树和右子树的高度差不得超过1,并且左右两个子树都是一棵平衡二叉树。
02. 如何实现平衡?
通过左旋或右旋两种方法。
03. 优缺点
优点:解决了二叉查找树退化为近似链表的缺点,最坏的查找时间复杂度也为 O(logn)。
缺点:因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,很容易会破坏平衡树的平衡的规则,进而我们需要频繁地通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁进行调整,这会使平衡树的性能大打折扣。