一、树的术语
- 节点的度(
Degree) :节点的子树个数. - 树的度(
Degree) :树的所有节点中最大的度数. (树的度通常为节点的个数N-1) - 叶节点(
Leaf) :度为0的节点. (也称为叶子节点) - 父节点(
Parent) :有子树的节点是其子树的根节点的父节点 - 子节点(
Child) :若A节点是B节点的父节点,则称B节点是A节点的子节点;子节点也称孩子节点。 - 兄弟节点(
Sibling) :具有同一父节点的各节点彼此是兄弟节点。 - 路径和路径长度:从节点
n1到nk的路径为一个节点序列n1,n2,… ,nk,ni是ni+1的父节点。路径所包含边的个数为路径的长度。 - 节点的层次(Level) :规定根节点在
1层,其它任一节点的层数是其父节点的层数加1。 - 树的深度(Depth) :树中所有节点中的最大层次是这棵树的深度。
二、认识二叉树
- 定义
- 二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
- 二叉树重要的特性:
- 一棵二叉树第
i层的最大节点数为:2^(i-1), i >= 1; - 深度为
k的二叉树有最大节点总数为:2^k - 1, k >= 1; - 对任何非空二叉树
T,若n0表示叶节点(度为0的节点)的个数、n2是度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1。
- 特殊的二叉树
- 满二叉树(
Full Binary Tree)
在二叉树中, 除了最下一层的叶结点外, 每层节点都有2个子节点, 就构成了满二叉树。 - 完全二叉树(
Complete Binary Tree)
最后一层从左向右的叶节点连续存在, 只缺右侧若干节点。满二叉树是特殊的完全二叉树。
三、认识二叉搜索树
- 定义
- 其是二叉树的一种,但是只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大的值。
- 平衡二叉树,查找效率
O(logN)
- 缺陷
- 如果插入有序连续数据,(如10,9,8,7,6,5,4,3,2,1)造成分布不均匀,非平衡树,查找效率
O(n);
- 如果插入有序连续数据,(如10,9,8,7,6,5,4,3,2,1)造成分布不均匀,非平衡树,查找效率
四、封装二叉搜索树
一个二叉搜索树的常见操作应该包含以下:
- 插入操作
insert(key): 向树中插入一个新的数据
- 查找操作
search(key):在树中查找一个数据,如果节点存在,则返回true;如果不存在,则返回falsesearchParent(key)函数,在二叉树中查找一个节点的父节点,并返回min():返回树中最小的值max():返回树中最大的值
- 遍历操作
- 深度优先遍历:
preOrderTraverse:通过先序遍历方式遍历所有节点inOrderTraverse:通过中序遍历方式遍历所有节点postOrderTraverse:通过后序遍历方式遍历所有节点
- 广度优先遍历
levelOrderTraverse:通过层序遍历方式遍历所有节点
- 删除操作
remove(key):从树中移除某个数据
1. 基础架构
- 封装一个节点类
TreeNode和一个树类BinarySearchTree。
class TreeNode<T>{
value: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
constructor(key: T) {
this.value = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree<T>{
root: TreeNode<T> | null;
constructor() {
this.root = null;
}
}
2.插入操作
在 BSTree 中添加 insert 方法,向二叉树插入数据。
//插入
insert(key: T): void {
let newNode: TreeNode<T> = new TreeNode(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node: TreeNode<T>, newNode: TreeNode<T>): void {
if (node.value > newNode.value) {
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);
}
}
}
3.查找操作
search(key)函数,在树中查找一个数据,如果节点存在,则返回true;如果不存在,则返回false。
- 写法一:递归
//查询
seach(key: T): boolean {
return this.searchNode(this.root, key);
}
searchNode(node: TreeNode<T> | null, key: T): boolean {
if (node === null) {
return false;
}
if (node.value > key) {
return this.searchNode(node.left, key);
} else if (node.value < key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
- 写法二:迭代
seach(key: T): boolean {
let cur: TreeNode<T> | null = this.root;
if (cur === null) {
return false;
}
while (cur){
if(cur.value === key){
return true;
}else if(cur.value > key){
cur = cur.left;
}else if(cur.value < key){
cur = cur.right;
}
}
return false;
}
searchParent(key)函数,在二叉树中查找一个节点的父节点,并返回。
searchParent(key: T): TreeNode<T> | null {
let cur: TreeNode<T> | null = this.root;
let parent: TreeNode<T> | null = null;
while (cur) {
if(cur.value > key){
parent = cur;
cur = cur.left;
}else if(cur.value < key){
parent = cur;
cur = cur.right;
}else{
break;
}
}
return parent;
}
min()函数,返回树中最小的值。
min(): T | null {
let cur: TreeNode<T> | null = this.root;
while (cur !== null && cur.left !== null) {
cur = cur.left;
}
return cur?.value ?? null;
}
max()函数返回树中最大的值。
max(): T | null {
let cur: TreeNode<T> | null = this.root;
while (cur !== null && cur.right !== null) {
cur = cur.right;
}
return cur?.value ?? null;
}
4.遍历操作
preOrderTraverse()函数,通过先序遍历方式遍历所有节点。
树的先序遍历的过程:
a. 优先访问根节点
b. 之后访问左子树
c. 最后访问右子树
- 写法一:递归
preOrderTraverse(callBack: Function): void {
this.preOrderTraverseNode(this.root, callBack);
}
preOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
if(node !== null){
callBack(node.value);
this.preOrderTraverseNode(node.left, callBack);
this.preOrderTraverseNode(node.right, callBack);
}
}
- 写法二:迭代(栈)
preOrderTraverse(callBack: Function): void {
if (this.root === null) {
return;
}
const stack: TreeNode<T>[] = [];
stack.push(this.root); //根节点入栈
while (stack.length > 0) {
let curNode: TreeNode<T> = stack.pop()!;
callBack(curNode.value);
if(curNode.right){ //先入栈右节点
stack.push(curNode.right);
}
if(curNode.left){ //再入栈左节点
stack.push(curNode.left);
}
}
}
//第二种迭代写法
preOrderTraverse(callBack: Function): void {
const stack: TreeNode<T>[] = [];
let p: TreeNode<T> | null = this.root;
while (p || stack.length > 0) {
while(p){
callBack(p.value);
stack.push(p);
p = p.left;
}
let curNode: TreeNode<T> = stack.pop()!;
p = curNode.right;
}
}
inOrderTraverse函数,通过中序遍历方式遍历所有节点。
树的中序遍历的过程:
a. 优先访问左子树
b. 之后访问根节点
c. 最后访问右子树
- 写法一:递归
inOrderTraverse(callBack: Function): void {
this.inOrderTraverseNode(this.root, callBack);
}
inOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
if(node !== null){
this.inOrderTraverseNode(node.left, callBack);
callBack(node.value);
this.inOrderTraverseNode(node.right, callBack);
}
}
- 写法二:迭代(栈)
inOrderTraverse(callBack: Function): void {
const stack: TreeNode<T>[] = [];
let p: TreeNode<T> | null = this.root;
while (p || stack.length > 0) {
while (p) {
stack.push(p);//根节点入栈
p = p.left;//左子节点入栈
}
let curNode: TreeNode<T> = stack.pop()!;
callBack(curNode.value);
p = curNode.right; //右子节点当成目标节点,重新执行一遍操作
}
}
postOrderTraverse函数,通过后序遍历方式遍历所有节点。
树的后序遍历的过程:
a. 优先访问左子树
b. 之后访问右子树
c. 最后访问根节点
- 写法一:递归
postOrderTraverse(callBack: Function): void {
this.postOrderTraverseNode(this.root, callBack);
}
postOrderTraverseNode(node: TreeNode<T> | null, callBack: Function): void {
if(node !== null){
this.postOrderTraverseNode(node.left, callBack);
this.postOrderTraverseNode(node.right, callBack);
callBack(node.value);
}
}
- 写法二:迭代(栈)
借用先序遍历的方式,然后把数组反转,从而实现后序遍历。
postOrderTraverse(): Array<T> {
if (this.root === null) {
return [];
}
const arr: Array<T> = [];
const stack: TreeNode<T>[] = [];
stack.push(this.root); //根节点入栈
while (stack.length > 0) {
let curNode: TreeNode<T> = stack.pop()!;
arr.push(curNode.value);
if (curNode.left) { //再入栈左节点
stack.push(curNode.left);
}
if (curNode.right) { //先入栈右节点
stack.push(curNode.right);
}
}
//根 右 左 翻转
return arr.reverse();
}
levelOrderTraverse函数,通过层序遍历方式遍历所有节点。
- 迭代(队列)
levelOrderTraverse(callBack: Function): void {
if (this.root === null) {
return;
}
const queue: TreeNode<T>[] = [];
queue.push(this.root);
while (queue.length > 0) {
let curNode: TreeNode<T> = queue.shift()!;//头部出队
callBack(curNode.value);
if (curNode.left) {
queue.push(curNode.left);//左节点尾部入队
}
if (curNode.right) {
queue.push(curNode.right);//右节点尾部入队
}
}
}
5.删除操作
remove(key)函数,从树中移除某个数据。
删除可以分为以下三种情况:
- 第一种情况:无子节点
- 第二种情况:只有一个子节点
- 第三种情况:有两个子节点
-
- 找到右节点的最小值节点
-
- 目标节点替换要删除的节点
-
- 删除目标节点
-
//删除
remove(key: T) {
this.root = this.removeNode(this.root, key);
}
removeNode(node: TreeNode<T> | null, key: T): TreeNode<T> | null {
if (node === null) {
return null;
}
if (node.value > key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (node.value < key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
//第一种情况:无子节点
if (node.left === null && node.right === null) {
node = null;
return node;
}
//第二种情况:只有一个子节点
else if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
//第三种情况:有两个子节点
/*
1.找到右节点的最小值节点
2.目标节点替换要删除的节点
3.删除目标节点
*/
else {
let target = this.minNode(node.right)!;
node.value = target.value;
node.right = this.removeNode(node.right, target.value);
return node;
}
}
}
//找到最小节点
minNode(node: TreeNode<T> | null): TreeNode<T> | null {
let curNode: TreeNode<T> | null = node;
while (curNode !== null && curNode.left !== null) {
curNode = curNode.left;
}
return curNode;
}
五、红黑树
1.红黑树概念
- 由于非平衡的二叉搜索树存在缺陷(
时间复杂度为O(n)),所以出现红黑树,其是一种自平衡二叉搜索树(时间复杂度为O(logN)) - 存在平衡性:左子树与右子树高度差不会超过2倍。
2.红黑树性质
- 每个节点黑色或是红色
- 根节点是黑色
- 每个叶子节点(NIL)是黑色
- 每个红色节点的两个子节点都是黑色(表示不能有两个连续的红色节点)
- 任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出俩倍)
3.红黑树操作
- 变色
- 通常插入的新节点是红色
- 左旋转
- 逆时针(向左)
- 右旋转
- 顺时针(向右)
4.插入操作
- N表示当前插入节点,P表示父节点,U表示叔叔节点,G表示祖父节点
- 通常插入的新节点是红色
插入操作分以下五种情况:
- 情况一:新节点位于根节点
- 直接将插入的节点红色变成黑色
- 情况二:新节点的父节点P是黑色
- 直接插入,不需要操作
- 情况三:父P红,叔U红,祖G黑
- 将祖G变成红
- 父P,叔U变成黑
- 如果祖G没有父节点,则将祖G变成黑色;否则向上递归。
- 情况四:父P红,叔U黑,祖G黑,且N为左孩子
- 父P变成黑
- 祖G变成红
- 对祖G进行右旋转
- 情况五:父P红,叔U黑,祖G黑,且N为右孩子
- 对父P进行左旋转(变成了情况四)
- N变成黑色
- G变成红色
- 对祖G进行右旋转