树的特点
树的抽象
优秀的哈希函数(补充)
- 快速计算:霍纳法则
- 均匀分布:质数(长度、幂的底)
数组
链表
哈希表
树
而且模拟某些场景,我们使用树结构会更加方便
不过大部分术语都与真实世界的树相关,或者和家庭关系相关(如父节点和子节点)
树(Tree):n(n>=0)个节点构成的有限集合
对于任一颗非空树(n>0),它具备以下性质:
所有的数本质上都可以使用二叉树模拟出来
如果树中每个节点 最多只能有两个子节点,这样的数就称为 “二叉树”
二叉树有几个比较重要的特性,笔试题中比较常见:
2^(i-1), i>=1
2^k-1, k>=1
完美二叉树(Perfect Binary Tree),也称满二叉树(Full Binary Tree)
完全二叉树(Complete Binary Tree)
二叉树的存储常见的方式是数组和链表
二叉树最常见的方式还是使用链表存储
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
二叉搜索树是一颗二叉树,可以为空,如果不为空,满足以下性质:
这种方式就是二分查找的思想:
插入操作:
查找操作:
遍历操作:
删除操作:
class BSTree<T> {
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
// 代码1
if (newNode.value < node.value) {
// 去左边继续查找空白位置
if (node.left === null) {
node.left = newNode
} else {
this.insertNode(node.left, newNode)
}
// 代码2
} else {
// 去右边继续查找空白位置
if (node.right === null) {
node.right = newNode
} else {
this.insertNode(node.right, newNode)
}
}
}
}
二叉树遍历常见的四种方式:
先序/中序/后序:取决于访问根节点(root)的时机
class TreeNode<T> extends Node<T> {
preOrderTraverse() {
this.preOrderTraverseNode(this.root)
}
private preOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
console.log(node.value)
this.preOrderTraverseNode(node.left)
this.preOrderTraverseNode(node.right)
}
}
}
class TreeNode<T> extends Node<T> {
preOrderTraversalNoRecursion() {
let stack: TreeNode<T>[] = []
let current: TreeNode<T> | null = this.root
while (current !== null || stack.length !== 0) {
while (current !== null) {
console.log(current.value)
stack.push(current)
current = current.left
}
current = stack.pop()!
current = current.right
}
}
}
class TreeNode<T> extends Node<T> {
inOrderTraverse() {
this.inOrderTraverseNode(this.root)
}
private inOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.inOrderTraverseNode(node.left)
console.log(node.value)
this.inOrderTraverseNode(node.right)
}
}
}
class TreeNode<T> extends Node<T> {
inOrderTraversalNoRecursion() {
let stack: TreeNode<T>[] = []
let current: TreeNode<T> | null = this.root
while (current !== null || stack.length !== 0) {
while (current !== null) {
stack.push(current)
current = current.left
}
current = stack.pop()!
console.log(current.value)
current = current.right
}
}
}
class TreeNode<T> extends Node<T> {
postOrderTraverse() {
this.postOrderTraverseNode(this.root)
}
private postOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.postOrderTraverseNode(node.left)
this.postOrderTraverseNode(node.right)
console.log(node.value)
}
}
}
class TreeNode<T> extends Node<T> {
preOrderTraversalNoRecursion() {
let stack: TreeNode<T>[] = []
let current: TreeNode<T> | null = this.root
while (current !== null || stack.length !== 0) {
while (current !== null) {
console.log(current.value)
current = current.left
}
current = stack.pop()!
stack.push(current)
current = current.right
}
}
}
层序遍历很好理解,就是从上向下逐层遍历
层序遍历通常我们会借助队列来完成
class TreeNode<T> extends Node<T> {
levelOrderTraverse() {
// 1.如果没有根节点,那么不需要遍历
if (!this.root) return
// 2.创建队列结构
const queue: TreeNode<T>[] = []
queue.push(this.root)
// 3.遍历队列中所有的节点(依次出队)
while (queue.length) {
// 3.1.访问节点的过程
const current = queue.shift()!
console.log(current.value)
// 3.2.将左子节点放入队列
if (current.left) {
queue.push(current.left)
}
// 3.3.将右子节点放入到队列
if (current.right) {
queue.push(current.right)
}
}
}
}
在二叉搜索树中搜索最值是一件非常简单的事情,其实用眼睛就可以看出来了
class TreeNode<T> extends Node<T> {
/** 获取最值操作:最大值 */
getMaxValue(): T | null {
let current = this.root
while (current && current.right) {
current = current.right
}
return current?.value ?? null
}
/** 获取最值操作:最小值 */
getMinValue(): T | null {
let current = this.root
while (current && current.left) {
current = current.left
}
return current?.value ?? null
}
}
二叉搜索树不仅仅获取最值效率非常高,搜索特定的值效率也非常高
class TreeNode<T> extends Node<T> {
searchNoRecursion(value: T): boolean {
let current = this.root
while (current) {
// 找到了节点
if (current.value === value) return true
if (current.value < value) {
current = current.right
} else {
current = current.left
}
}
return false
}
search(value: T): boolean {
return this, this.searchNode(this.root, value)
}
searchNode(node: TreeNode<T> | null, value: T): boolean {
// 1.如果节点为null,那么就直接退出递归
if (node === null) return false
// 2.判断node节点的value和传入的value的大小
if (node.value > value) {
return this.searchNode(node.left, value)
} else if (node.value < value) {
return this.searchNode(node.right, value)
} else {
return true
}
}
}
二叉搜索树的删除有些复杂,我们一点点完成
删除节点要从查找到删除的节点开始,找到节点后,需要考虑三种情况:
我们先从查找要删除的节点入手
class TreeNode<T> extends Node<T> {
left: TreeNode<T> | null = null
right: TreeNode<T> | null = null
parent: TreeNode<T> | null = null
get isLeft(): boolean {
return !!(this.parent && this.parent.left === this)
}
get isRight(): boolean {
return !!(this.parent && this.parent.right === this)
}
}
class TreeNode<T> extends Node<T> {
private searchNode(value: T): TreeNode<T> | null {
let current = this.root
let parent: TreeNode<T> | null = null
while (current) {
if (current.value === value) return current
parent = current
if (current.value < value) {
current = current.right
} else {
current = current.left
}
if (current) current.parent = parent
}
return null
}
}
如果只有一个单独的根,直接删除即可
class TreeNode<T> extends Node<T> {
remove(value: T): boolean {
// 1.搜索当前是否有这个value
const current = this.searchNode(value)
if (!current) return false
// 2.获取到三个东西:当前节点/父节点是否属于父节点的左子节点还是右子节点
// 2.1.如果删除的是叶子节点
if (current.left === null && current.right === null) {
if (current === this.root) {
// 根节点
this.root = null
} else if (current.isLeft) {
// 父节点的左子节点
current.parent!.left = null
} else {
current.parent!.right = null
}
}
return true
}
}
class TreeNode<T> extends Node<T> {
remove(value: T): boolean {
if (current.right === null) {
// 2.2.只有一个子节点,只有左子节点
if (current === this.root) {
this.root = current.left
} else if (current.isLeft) {
current.parent!.left = current.left
} else {
current.parent!.right = current.left
}
} else if (current.left === null) {
// 2.3.只有一个子节点,只有右子节点
if (current === this.root) {
this.root = current.right
} else if (current.isLeft) {
current.parent!.left = current.right
} else {
current.parent!.right = current.right
}
}
return true
}
}
class TreeNode<T> extends Node<T> {
private getSuccessor(delNode: TreeNode<T>) {
// 获取右子树
let current = delNode.right
let successor: TreeNode<T> | null = null
while (current) {
successor = current
current = current.left
if (current) {
current.parent = successor
}
}
// 拿到后继节点
if (successor !== delNode.right) {
successor!.parent!.left = successor!.right
successor!.right = delNode.right
}
// 将删除节点的 left,赋值给后继节点的 left
successor!.left = delNode.left
return successor
}
remove(value: T): boolean {
else {
// 2.4.两个子节点
const successor = this.getSuccessor(current)
if (current === this.root) {
this.root = successor
} else if (current.isLeft) {
current.parent!.left = successor
} else {
current.parent!.right = successor
}
}
return true
}
}
如果我们要 删除的节点有两个子节点,甚至子节点还有子节点,这种情况下我们需要 从下面的子节点中找到一个子节点,来替换当前的节点
但是找到这个节点有什么特征呢?应该是 current 节点下面所有节点中 最接近 current 节点 的
这个节点怎么找呢?
前驱和后继
也就是为了能够删除有两个子节点的 current,要么找到它的前驱,要么找到它的后继
class TreeNode<T> extends Node<T> {
remove(value: T): boolean {
// 1.搜索当前是否有这个value
const current = this.searchNode(value)
if (!current) return false
// 2.获取到三个东西:当前节点/父节点是否属于父节点的左子节点还是右子节点
let replaceNode: TreeNode<T> | null = null
if (current.left === null && current.right === null) {
// 2.1.如果删除的是叶子节点
replaceNode = null
} else if (current.right === null) {
// 2.2.只有一个子节点,只有左子节点
replaceNode = current.left
} else if (current.left === null) {
// 2.3.只有一个子节点,只有右子节点
replaceNode = current.right
} else {
// 2.4.两个子节点
const successor = this.getSuccessor(current)
replaceNode = successor
}
if (current === this.root) {
this.root = replaceNode
} else if (current.isLeft) {
current.parent!.left = replaceNode
} else {
current.parent!.right = replaceNode
}
return true
}
}
删除操作非常复杂,一些程序员都尝试着避开删除操作
import { btPrint } from 'hy-algokit'
class INode<T> {
value: T
constructor(value: T) {
this.value = value
}
}
class TreeNode<T> extends Node<T> {
left: TreeNode<T> | null = null
right: TreeNode<T> | null = null
parent: TreeNode<T> | null = null
get isLeft(): boolean {
return !!(this.parent && this.parent.left === this)
}
get isRight(): boolean {
return !!(this.parent && this.parent.right === this)
}
}
class BSTree<T> {
private root: TreeNode<T> | null = null
print() {
btPrint(this.root)
}
private searchNode(value: T): TreeNode<T> | null {
let current = this.root
let parent: TreeNode<T> | null = null
while (current) {
if (current.value === value) return current
parent = current
if (current.value < value) {
current = current.right
} else {
current = current.left
}
if (current) current.parent = parent
}
return null
}
/** 插入数据的操作 */
insert(value: T) {
// 1.根据传入value创建Node(TreeNode)节点
const newNode = new TreeNode(value)
// 2.判断当前是否已经有了根节点
if (!this.root) {
// 当前树为空
this.root = newNode
} else {
// 树中已经有其他值
this.insertNode(this.root, newNode)
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
if (newNode.value < node.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)
}
}
}
// 遍历的操作
/** 先序遍历 */
preOrderTraverse() {
this.preOrderTraverseNode(this.root)
}
private preOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
console.log(node.value)
this.preOrderTraverseNode(node.left)
this.preOrderTraverseNode(node.right)
}
}
/** 中序遍历 */
inOrderTraverse() {
this.inOrderTraverseNode(this.root)
}
private inOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.inOrderTraverseNode(node.left)
console.log(node.value)
this.inOrderTraverseNode(node.right)
}
}
/** 后序遍历 */
postOrderTraverse() {
this.postOrderTraverseNode(this.root)
}
private postOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.postOrderTraverseNode(node.left)
this.postOrderTraverseNode(node.right)
console.log(node.value)
}
}
/** 层序遍历 */
levelOrderTraverse() {
// 1.如果没有根节点,那么不需要遍历
if (!this.root) return
// 2.创建队列结构
const queue: TreeNode<T>[] = []
queue.push(this.root)
// 3.遍历队列中所有的节点(依次出队)
while (queue.length) {
// 3.1访问节点的过程
const current = queue.shift()!
console.log(current.value)
// 3.2将左子节点放入队列
if (current.left) {
queue.push(current.left)
}
// 3.3将右子节点放入到队列
if (current.right) {
queue.push(current.right)
}
}
}
/** 获取最值操作:最大值 */
getMaxValue(): T | null {
let current = this.root
while (current && current.right) {
current = current.right
}
return current?.value ?? null
}
/** 获取最值操作:最小值 */
getMinValue(): T | null {
let current = this.root
while (current && current.left) {
current = current.left
}
return current?.value ?? null
}
/** 搜索特定的值 */
search(value: T): boolean {
return !!this.searchNode(value)
}
searchNodeValue(node: TreeNode<T> | null, value: T): boolean {
// 1.如果节点为null,那么就直接退出递归
if (node === null) return false
// 2.判断node节点的value和传入的value的大小
if (node.value > value) {
return this.searchNodeValue(node.left, value)
} else if (node.value < value) {
return this.searchNodeValue(node.right, value)
} else {
return true
}
}
/** 删除操作 */
private getSuccessor(delNode: TreeNode<T>) {
// 获取右子树
let current = delNode.right
let successor: TreeNode<T> | null = null
while (current) {
successor = current
current = current.left
if (current) {
current.parent = successor
}
}
// 拿到后继节点
if (successor !== delNode.right) {
successor!.parent!.left = successor!.right
successor!.right = delNode.right
}
// 将删除节点的 left,赋值给后继节点的 left
successor!.left = delNode.left
return successor
}
remove(value: T): boolean {
// 1.搜索当前是否有这个value
const current = this.searchNode(value)
if (!current) return false
// 2.获取到三个东西:当前节点/父节点是否属于父节点的左子节点还是右子节点
let replaceNode: TreeNode<T> | null = null
if (current.left === null && current.right === null) {
// 2.1.如果删除的是叶子节点
replaceNode = null
} else if (current.right === null) {
// 2.2.只有一个子节点,只有左子节点
replaceNode = current.left
} else if (current.left === null) {
// 2.3.只有一个子节点,只有右子节点
replaceNode = current.right
} else {
// 2.4.两个子节点
const successor = this.getSuccessor(current)
replaceNode = successor
}
if (current === this.root) {
this.root = replaceNode
} else if (current.isLeft) {
current.parent!.left = replaceNode
} else {
current.parent!.right = replaceNode
}
return true
}
}
二叉搜索树作为数据存储的结构有重要的优势
但是,二叉搜索树有一个很麻烦的问题:
非平衡树
为了能以较快的时间 O(log N)来操作一棵树,我们需要保证树总是平衡的
AVL 树
红黑树