二叉树的性质
二叉树是特殊的树,也是我们平时程序中用的比较多的一种数据结构,它具有以下特点:
- 每个结点最多有两颗子树,结点的度最大为2
- 左子树和右子树是有顺序的,次序不能颠倒
- 即使某个结点只有一个子树,也要区分左右子树
- 在二叉树的第i(根结点为1层)层上最多有2^(i-1)个结点(i>=1)
- 高度为k的二叉树,最多有2^k-1个结点(k>=0)
- 对任何一棵二叉树,如果其叶结点有n个,度为2的非叶子结点有m个,则 n=m+1
一些常见的二叉树
1. 斜树
所有的结点都只有左子树(左斜树),或者只有右子树(右斜树)。斜树应用场景较少。
2. 满二叉树
所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上。
满二叉树具有以下特点:
- 叶子结点只能出现在最下一层
- 非叶子结点的度一定是2
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子结点最多
3. 完全二叉树
对一棵具有n个结点的二叉树按层序排号,如果编号为i的结点与同样深度的满二叉树编号为i结点在二叉树中位置完全相同,就是完全二叉树。满二叉树必须是完全二叉树,反过来不一定成立。
完全二叉树具有以下特点:
- 叶子结点只能出现在最底部两层
- 最底层叶子结点一定集中在左部连续位置
- 倒数第二层如果有叶子结点,一定出现在右部连续位置
- 同样数量结点的树,完全(满)二叉树的深度最小
- 对于有n个结点的完全二叉树,按层次对结点进行编号(从上到下,从左到右),对于任意编号为i的结点:
- 如果i=1,则结点i是二叉树的根
- 如果i>1,则其双亲结点为i/2下取整
- 如果2i<=n,则结点i的左孩子为2i【结合上图思考】
- 如果2i>n,则结点i无左孩子【结合上图思考】
- 如果2i+1<=n,则结点i的右孩子为2i+1【结合上图思考】
- 如果2i+1>n,则结点i无右孩子【结合上图思考】
4. 二叉排序树
二叉排序树,又称二叉查找树
,也叫二叉搜索树
。
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
- 若左子树不为空,则左子树上所有结点的值均小于或等于它的根结点的值。
- 若右子树不为空,则右子树上所有结点的值均大于或等于它的根结点的值。
- 左、右子树也分别是二叉排序树。
- 二叉排序树中序遍历结果为递增有序数列。
5. 平衡二叉树
平衡二叉树(Balanced BinaryTree)又被称为AVL树(有别于AVL算法),它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
注意:满二叉树和完全二叉树一定是平衡二叉树
二叉树的遍历
1. 先序遍历
基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即根—左—右。
图中先序遍历结果是:1,2,4,5,7,8,3,6。
先序遍历JavaScript实现见下面程序。
// 递归-前序遍历
var order = (root, cb) => {
if(root !== null) {
cb(root.val);
order(root.left, cb);
order(root.right, cb);
}
}
通过栈数据结构(先进后出),我们可以将父节点压入栈,对栈执行出栈操作,每次将出栈的节点的右孩子先压入栈,其次压入左孩子。这样就可以做到先遍历父节点,在遍历左孩子部分,后遍历右孩子部分。
// 迭代-前序遍历
var order = (root, cb) => {
if(!root) return [];
let stack = [], ans = [];
stack.push(root);
while(stack.length) {
let node = stack.pop();
ans.push(node.val);
if(node.right) stack.push(node.right);
if(node.left) stack.push(node.left);
}
return ans
}
2. 中序遍历
基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树即左—根—右。
图中中序遍历结果是:4,2,7,8,5,1,3,6。
中序遍历JavaScript实现见下面程序。
// 递归-中序遍历
var order = (root, cb) => {
if(root !== null) {
order(root.left, cb);
cb(root.val);
order(root.right, cb);
}
}
我们同样可以使用栈结构来实现中序遍历,因为中序遍历左子树是优先遍历,所以父节点要先于子树的节点优先压入栈中,每当我们压入节点时,都要把节点的左子树的所有左节点压入栈中
//迭代 - 中序遍历
var order = root => {
if(!root) return [];
let stack = [], ans = [];
let cur = root;
while(stack.length || cur) {
while(cur) {
stack.push(cur);
cur = cur.left;
}
let node = stack.pop();
ans.push(node.val);
if(node.right) {
cur = node.right;
}
}
return ans
}
3. 后序遍历
基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点即左—右—根。
图中后序遍历结果是:4,8,7,5,2,6,3,1。
后序遍历JavaScript实现见下面程序。
//递归 - 后序遍历
var sortedArr = [];
function postOrder(tree) {
if(tree) {
middleOrder(tree.left);
middleOrder(tree.right);
sortedArr.push(tree.key);
}
}
后序遍历是父节点需要最后被遍历。但其实可以跟前序遍历的实现方式上差不多,只不过在插入数组中,我们总是在头部插入,这样先被插入的节点值一定是相对于左右孩子后面的
//迭代 - 后序遍历
var order = root => {
if(!root) return [];
let stack = [], ans = [];
stack.push(root);
while(stack.length) {
let curNode = stack.pop();
ans.unshift(curNode.val);
curNode.left && stack.push(curNode.left);
curNode.right && stack.push(curNode.right);
}
return ans
}
4. 层序遍历
基本思想:实现二叉树的层序遍历--从根开始,依次向下,对于每一层从左向右遍历。
// 递归 - 层序遍历
var levelOrder = function(root) {
let res = [];
order(root, 0, res);
res = res.reduce((acc, cur) => [...acc, ...cur], []);
console.log(res);
};
var order = (root, level, arr) => {
if(!root) return [];
arr[level] = arr[level] || [];
arr[level].push(root.val);
root.left && order(root.left, level*1 + 1, arr);
root.right && order(root.right, level*1 + 1, arr);
}
我们使用队列来保存节点,每轮循环中,我们都取一层出来,将它们的左右孩子放入队列
// 迭代 - 层序遍历
var order = function(root) {
let queue = [], ans = [];
if(!root) return [];
queue.push(root);
while(queue.length) {
let len = queue.length;
while(len) {
let node = queue.shift();
ans.push(node.val);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
len -= 1;
}
}
return ans;
};
二叉树的实现
class Node {
constructor(key, leftChild, rightChild) {
this.left = leftChild;
this.key = key;
this.right = rightChild;
}
}
class BinarySearchTree {
constructor() {}
create(data) {
if (!(data instanceof Array) || data.length < 1) return;
let len = data.length,
tree = new Node(data[0], null, null);
if (len === 1) return tree;
for (let i = 1; i < len; i++) this.insert(tree, data[i]);
return tree;
}
//查询节点
search(tree, key) {
if (!tree) return false;
if (tree.key === key) return true;
if (key < tree.key) {
if (tree.left) return this.search(tree.left, key);
return false;
} else {
if (tree.right) return this.search(tree.right, key);
return false;
}
}
insert(currentNode, key) {
let node = new Node(key, null, null);
if (key < currentNode.key) {
if (currentNode.left === null) {
currentNode.left = node;
} else {
this.insert(currentNode.left, key);
}
} else {
if (currentNode.right === null) {
currentNode.right = node;
} else {
this.insert(currentNode.right, key);
}
}
}
// 找到右子树中最小的值
findMinNodeKey(rightTree) {
if (rightTree) {
while (rightTree && rightTree.left) rightTree = rightTree.left;
return rightTree.key;
}
return null;
}
delete(currentNode, key) {
//如果节点无效,return
if (!currentNode) return null;
//找不到该节点
if (!this.search(currentNode, key)) return currentNode;
if (key < currentNode.key) {
currentNode.left = this.delete(currentNode.left, key);
return currentNode;
} else if (key > currentNode.key) {
currentNode.right = this.delete(currentNode.right, key);
return currentNode;
} else {
// 叶子节点
if (!currentNode.left && !currentNode.right) {
currentNode = null;
return currentNode;
}
if (!currentNode.right) {
//没有右子树
currentNode = currentNode.left;
return currentNode;
} else if (!currentNode.left) {
//没有左子树
currentNode = currentNode.right;
return currentNode;
} else {
/**
* 存在左子树也存在右子树的情况:
* 1. 找出左子树中最大或者右子树中最小的值val
* 2. 将当前节点的值替换为val
* 3. 在左子树或者右子树中找到val删除
*/
let minRightNodeKey = this.findMinNodeKey(currentNode.right);
currentNode.key = minRightNodeKey;
currentNode.right = this.delete(currentNode.right, minRightNodeKey);
return currentNode;
}
}
}
// 前序遍历
preOrderTraverse(node, cb) {
if (node !== null) {
cb(node.key);
this.preOrderTraverse(node.left, cb);
this.preOrderTraverse(node.right, cb);
}
}
// 中序遍历
inOrderTraverse(node, cb) {
if (node !== null) {
this.inOrderTraverse(node.left, cb);
cb(node.key);
this.inOrderTraverse(node.right, cb);
}
}
// 后序遍历
postOrderTraverse(node, cb) {
if (node !== null) {
this.postOrderTraverse(node.left, cb);
this.postOrderTraverse(node.right, cb);
cb(node.key);
}
}
}
const arr = [62, 88, 58, 47, 35, 73, 51, 99, 37, 93];
const bst = new BinarySearchTree();
const rootNode = bst.create(arr);
// console.log("\n");
// console.warn("preOrderTraverse");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));
// console.log("\n");
// console.warn("inOrderTraverse");
// bst.inOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));
// console.log("\n");
// console.warn("postOrderTraverse");
// bst.postOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));
// search
// const isExist = bst.search(rootNode, 100);
// console.warn("isExist");
// console.log(isExist);
// console.log("\n");
// search
// bst.insert(rootNode, 30);
// console.log("\n");
// console.warn("preOrderTraverse - after inserted:");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));
// delete
// bst.delete(rootNode, 62);
// console.warn("preOrderTraverse - after deleted:");
// bst.preOrderTraverse(rootNode, (e) => console.log(`key : ${e}`));