二叉搜尋樹
假設有一萬個數,需要查詢某個數存在不存在
按照以往的方法,暴力迴圈
let arr = new Array(10000)
for(let i = 0; i < 10000; i++) {
arr[i] = Math.floor(Math.random() * 10000)
}
let num = 0
function search(arr, target) {
for (let i = 0; i < arr.length; i++) {
num += 1
if (arr[i] == target) return true
}
return false
}
console.log(search(arr, 1000)) // false / true
console.log(num) // 10000 / 7260
複製程式碼
可以看出,這樣寫,迴圈了非常多次,這是非常浪費效能的
- 如果一個演算法的效能很爛的話,有兩個方面的原因
- 資料結構很爛
- 演算法不對
很明顯上方的演算法沒有什麼問題,就是比較嘛。那麼問題就只能出現在資料結構上了,這個資料結構很爛!
二叉搜尋樹
這是一顆二叉樹
這顆二叉樹有排序效果,左子樹的節點都比當前節點小,右子樹的節點都比當前節點大
構建二叉搜尋樹
- 任選一個數字做根節點
- 將剩下的數與節點比較,比節點小的放左邊,比節點大的放右邊
- 重複第2步
程式碼實現
interface INodeType {
value: number;
left: INodeType | null;
right: INodeType | null;
}
type INode = INodeType | null;
class BuildSearchTree {
private arr: number[]
readonly root: INode
constructor() {
this.arr = this.createArr()
this.root = this.init()
}
// 初始化陣列
private createArr(): number[] {
let arr: number[] = new Array(10000)
for (let i = 0; i < 10000; i++) {
arr[i] = Math.floor(Math.random() * 10000)
}
return arr
}
private node(value): INode {
return {
value,
left: null,
right: null,
}
}
/**
* 連線節點
* @param root 根節點
* @param num 需要連線的數
*/
private addNode(root: INode, num: number): void {
if (root == null || root.value == num) return // 如果這個數存在,則不作處理
if (root.value < num) { // 大的數放右邊
if (root.right == null) root.right = this.node(num)
this.addNode(root.right, num)
} else { // 小的數放左邊
if (root.left == null) root.left = this.node(num)
this.addNode(root.left, num)
}
}
/**
* 建立二叉搜尋樹
*/
private init(): INode {
if (this.arr == null || this.arr.length == 0) return null
let root: INode = this.node(arr[0]) // 選定陣列第0位作為根節點
for (let i = 0; i < this.arr.length; i++) {
this.addNode(root, this.arr[i])
}
return root
}
}
const root = new BuildSearchTree().root
複製程式碼
二叉搜尋樹建立好了之後,搜尋其實很簡單,很像前序遍歷
/**
* 二叉樹搜尋
* @param root 根節點
* @param target 目標數
*/
let num2 = 0
function searchByTree(root: INode, target: number): boolean{
if(root == null) return false
num2 += 1
if (root.value == target) return true
if (root.value < target) return searchByTree(root.right, target)
if (root.value > target) return searchByTree(root.left, target)
}
console.log(searchByTree(root, 1000)) // false
console.log(num2) // 15
console.log(search(arr, 1000)) // false
console.log(num) // 10000
複製程式碼
從迴圈次數上面來看,二叉搜尋樹的效果簡直完爆嘛,二叉搜尋樹的強大之處,不言而喻。
雖然現在的效能看起來已經很強大了,但是不要忘了,前序遍歷的迴圈次數是受二叉樹層數影響的,層數越少,遍歷的次數也就越少。也就是說,如果能把這顆二叉樹儘量構造成平衡二叉樹,那麼就還能提升效能,用電腦科學的話來說,就是還未到效能的極致。
優化二叉搜尋樹 - 平衡二叉樹
平衡二叉樹概念
- 根節點的左子樹與右子樹的高度差不超過1
- 這棵樹的每個子樹都符合第一條
判斷二叉樹是否平衡
獲取二叉樹的深度 從上往下一層一層判斷。不平衡就停止,平衡則繼續向下判斷
class Pingheng {
// 獲取二叉樹深度
public static getDeep(root: INode): number {
if (root == null) return 0
let leftDeep = this.getDeep(root.left),
rightDeep = this.getDeep(root.right);
return Math.max(leftDeep, rightDeep) + 1; // 當前還有一層, 所以要 + 1
}
// 判斷是否是平衡二叉樹
public static isBlance(root: INode): boolean {
if (root == null) return true;
let leftDeep = this.getDeep(root.left),
rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) > 1) {
// 差值大於1 不平衡
return false;
} else {
return this.isBlance(root.right) && this.isBlance(root.left);
}
}
}
複製程式碼
二叉樹的單旋(左單旋,右單旋)
某一節點不平衡,如果左邊淺,右邊深,進行左單旋。 反之亦然
上面的類裡面加一點方法
class Change extends Pingheng {
// 左單旋
protected static leftRotate(root: INode): INode {
// 找到新根
let newRoot = root.right
// 找到變化分支
let changeTree = root.right.left
// 當前旋轉節點的右孩子為變化分支
root.right = changeTree
// 新根的左孩子為旋轉節點
newRoot.left = root
// 返回新根
return newRoot
}
// 右單旋
protected static rightRotate (root: INode):INode {
// 找到新根
let newRoot: INode = root.left
// 找到變化分支
let changeTree = root.left.right
// 當前旋轉節點的左孩子為變化分支
root.left = changeTree
// 新根的右孩子為旋轉節點
newRoot.right = root
// 返回新根
return newRoot
}
// 旋轉二叉樹
public static change(root: INode): INode {
if (this.isBlance(root)) return root;
if (root.left != null) root.left = this.change(root.left)
if (root.right != null) root.right = this.change(root.right)
let leftDeep = this.getDeep(root.left)
let rightDeep = this.getDeep(root.right)
if (Math.abs(leftDeep - rightDeep) < 2) {
return root
} else if (leftDeep > rightDeep) { // 左邊深, 右單旋
return this.rightRotate(root)
} else { // 右邊深, 左單旋
return this.leftRotate(root)
}
}
}
複製程式碼
二叉樹的雙旋(右左雙旋, 左右雙旋)
- 當要對某個節點進行左單旋時: 如果變化分支是唯一的最深分支,要先對新根進行右單旋,然後進行左單旋,這樣的旋轉叫做右左雙旋
- 當要對某個節點進行右單旋時: 如果變化分支是唯一的最深分支,要先對新根進行左單旋,然後進行右單旋,這樣的旋轉叫做左右雙旋
class Shuangxuan extends Change {
public static change(root: INode): INode {
if (!root) { return null; }
if (this.isBlance(root) || (root.right == null && root.left == null)) {
return root;
}
if (root.left != null) {
root.left = this.change(root.left);
}
if (root.right != null) {
root.right = this.change(root.right);
}
const leftDeep = this.getDeep(root.left);
const rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) < 2) {
return root;
} else if (leftDeep > rightDeep) { // 左邊深, 右單旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.left = this.rightRotate(root.left as INodeType);
}
return this.rightRotate(root);
} else { // 右邊深, 左單旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.right = this.rightRotate(root.right as INodeType);
}
return this.leftRotate(root);
}
}
}
複製程式碼
二叉樹的雙旋
前面經過了二叉樹的單旋,左右雙旋,右左雙旋,二叉樹依舊有可能不平衡。那就還需要考慮一種情況:如果變化分支的深度比旋轉節點的另一側高度差距超過2,那麼單旋之後依舊不平衡。
那再改造一下change方法
class DoubleRotate extends Change {
public static change(root: INode): INode {
if (!root) { return null; }
if (this.isBlance(root) || (root.right == null && root.left == null)) {
return root;
}
if (root.left != null) {
root.left = this.change(root.left);
}
if (root.right != null) {
root.right = this.change(root.right);
}
const leftDeep = this.getDeep(root.left);
const rightDeep = this.getDeep(root.right);
if (Math.abs(leftDeep - rightDeep) < 2) {
return root;
} else if (leftDeep > rightDeep) { // 左邊深, 右單旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.left = this.rightRotate(root.left as INodeType);
}
let newRoot = this.rightRotate(root);
if (newRoot) {
newRoot.right = this.change(newRoot.right);
}
newRoot = this.change(newRoot);
return newRoot;
} else { // 右邊深, 左單旋
const changeTreeDeep = this.getDeep(root.right && root.right.left),
noChangeTreeDeep = this.getDeep(root.right && root.right.right);
if (changeTreeDeep > noChangeTreeDeep) {
root.right = this.rightRotate(root.right as INodeType);
}
let newRoot = this.leftRotate(root);
if (newRoot) {
newRoot.left = this.change(newRoot.left);
}
newRoot = this.change(newRoot);
return newRoot;
}
}
}
複製程式碼
234樹
思考一下
影響二叉平衡樹的效能的點是什麼
- 在於二叉平衡搜尋樹的叉只有兩個,導致在節點鋪滿時也有很多層。
- 如果一個節點存多個數,可以提升空間效能
- 樹的層級越少,查詢的效率越高
怎麼樣能使二叉平衡排序樹的層數變少
- 如果不是二叉,層數會更少
叉越多,層數越小,但是叉閱讀,樹的結構就越複雜,樹的叉最多為4層最好
234樹
-
希望一顆樹,最多有四個叉(度最大為4)
-
234樹的子節點永遠在最後一層,
-
234樹永遠是平衡的(每一個路徑的高度都相同)
-
達成的效果
- 分支變多了,層數變少了
- 節點中存的樹變多了,節點變少了
- 因為分支變多了, 所以複雜度上升了
期望
- 希望對二三四樹進行簡化
- 簡化為二叉樹
- 依舊保留多叉
- 單節點依舊保留存放多個值
由此出現了紅黑樹
紅黑樹
to be continue--