歷經了一個多月,終於完成了二叉搜索樹的學習和整理。以前只是零散的發佈出來,並無作一個完整的分享。此次,我將以前的內容整理到一塊兒,一次性的對這個話題作個終結。若是看了這篇,仍是不懂二叉搜索樹,那你就來後臺留言找我,我將給您一一解答。這篇文章主要介紹二叉搜索樹、平衡二叉樹裏的AVL樹、2-3-4樹、紅黑樹。能夠點擊分篇連接查看更細緻的內容javascript
在生活中咱們常常會使用到搜索的功能。在咱們數據量不大的狀況下,可使用每次遍歷所有數據,查詢咱們的目標數據。當數據量增長時,咱們遍歷的方式就有些力不從心了;也能夠將數據的數據排序,使用比較高效的二分查找方式,可是在插入或刪除數據時,數組表現就會很慢。因此咱們能夠結合二分查找查詢的高效 + 鏈表添加刪除的高效性來實現高效搜索(符號表)的狀況java
下面我將列舉一些樹的內容定義(後續全部的代碼使用Java語言實現)node
樹由節點構成,每一個節點都有一個父節點(根結點不存在父節點)算法
節點包含的連接能夠指向不存在的NULL或者其餘真實存在的節點數組
每一個節點均可以包含多個子連接,將子連接的個數稱爲度;樹的度是指全部的子節點中最大的度(將度爲2的樹稱爲二叉樹、度爲3的樹稱爲三叉樹等)。如圖1~3示markdown
葉節點:沒有子節點的節點 如圖-1的B、C、D節點數據結構
父節點:有子樹的節點是其子樹的根節點的父節點 圖-1的A節點是B、C、D節點的父節oop
子節點:若A節點是B節點的父節點,那麼B是A的子節點,子節點也能夠成爲孩子節點 圖-3的A節點是B、C、D的父節點,同時也是H節點的子節點性能
兄弟節點:具備相同一個父節點的個節點彼此是兄弟節點 圖-1的B、C、D學習
每一個節點只指向一個父節點,最多包含左右兩個子連接
左子邊節點的Key小於父節點、右子節點的Key大於父節點 如圖-4示
T data;
TreeNode<T> left;
TreeNode<T> right;
int size;
複製代碼
每一個節點只指向一個父節點,最多包含左右兩個子連接
T data;
TreeNode<T> left;
TreeNode<T> right;
int size;
複製代碼
if (Objects.isNull(node)) {
return null;
}
T val = node.data;
int res = val.compareTo(element); //和node節點比較
if (res == 0) { //等於node的值,表示查詢到
return node;
}
if (res < 0) { //節點的值小於要查詢的值,向右遞歸
return find(element, node.getRight());
}
return find(element, node.getLeft()); //節點的值大於查詢的值,向左遞歸
}
複製代碼
根據查找二叉樹的特性,極值存在於葉節點或者只包含一個子節點的父節點中
//查詢極小值,一直向左查詢,若是沒有左節點,則認爲當前節點最小 例子:A節點
public TreeNode<T> findMin(TreeNode<T> node){
if (Objects.isNull(node.getLeft())) {
return node;
}
return findMin(node.getLeft());
}
//查詢極大值,一直向右查詢,若是沒有右節點,則認爲當前節點最大 例子:Z節點
public TreeNode<T> findMax(TreeNode<T> node){
if (Objects.isNull(node.getRight())) {
return node;
}
return findMax(node.getRight());
}
複製代碼
圖-7展現了插入Z的做爲F右子節點的狀況(插入到左子節點的狀況相似,再也不贅敘)
圖-8展現了被插入節點存在的狀況。
public void add(T element) {
if (element == null) {
throw new RuntimeException("數據不能爲NULL");
}
TreeNode<T> node = new TreeNode<>();
node.data = element;
if (Objects.isNull(root)) {
root = node;
return;
}
addImpl(root, node);
}
private void addImpl(TreeNode<T> root, TreeNode<T> node) {
T val = root.data;
T element = node.data;
int sub = element.compareTo(val);
//包含要插入的值,不處理
if (sub == 0) {
return;
}
//插入的值大於根節點的值,將新節點做爲根節點的右子節點
if (sub > 0) {
TreeNode<T> right = root.getRight();
if (Objects.isNull(right)) {
root.setRight(node);
return;
}
addImpl(right, node);
} else { //插入的值小於根節點的值,將新節點做爲根節點的左子節點
TreeNode<T> left = root.getLeft();
if (Objects.isNull(left)) {
root.setLeft(node);
return;
}
addImpl(root.getLeft(), node);
}
}
複製代碼
因爲刪除節點比較複雜,咱們先看下刪除極大值(極小值)的狀況,爲節點刪除作好準備工做
刪除最小值
因爲二叉搜索樹的特色二(左子邊節點的Key小於父節點、右子節點的Key大於父節點)那麼最小值節點要麼是葉子節點或者包含右子節點的狀況
極小值節點是葉子節點,能夠直接移除
極小值節點有一個右子節點,將右子節點替換爲父節點(若是還包含左子節點,那麼當前節點非最小值)
//移除最小的節點,將返回的值做爲根節點
private TreeNode<T> deleteMin(TreeNode<T> node) {
if (Objects.isNull(node.getLeft())) { //沒有左子節點,返回右子節點
return node.getRight();
}
TreeNode<T> min = deleteMin(node.getLeft()); //遞歸左子樹
node.setLeft(min);
return node;
}
複製代碼
和刪除最小值的狀況類似。只不過遞歸的是右子樹
極大值節點是葉子節點,能夠直接移除
極大值節點有一個左子節點,將左子節點替換爲父節點(若是還包含右子節點,那麼當前節點非最大值)
if (Objects.isNull(node.getRight())) {
return node.getLeft();
}
TreeNode<T> max = deleteMax(node.getRight());
node.setRight(max);
return node;
}
複製代碼
咱們將刪除節點的狀況概括以下
被刪除節點是葉子節點,能夠直接移除
被刪除節點只包含一個子節點(左子節點或者右子節點),咱們須要須要將子節點替換到父節點
被刪除節點包含兩個子節點,若是直接移除E節點,那麼子節點D、F將會丟失。咱們須要轉換思路,將包含兩個子節點的狀況轉換爲上兩種狀況。下面咱們介紹下如何處理(T.Hibbard 1962年提出的方法,膜拜巨佬)
咱們使用前驅節點(後續節點)的值替換被刪除節點,而後刪除前驅節點(後繼節點)
前驅節點:當前節點的左子樹中的最大值
後繼節點:當前節點的右子樹中的最小值
//刪除element的節點,返回根結點的引用
public TreeNode<T> delete(T element, TreeNode<T> node){
if (Objects.isNull(node)) {
return null;
}
T val = node.data;
int res = val.compareTo(element);
if (res < 0) { //被刪除節點在node的右子樹
TreeNode<T> rNode = delete(element, node.getRight());
node.setRight(rNode);
} else if (res > 0) { //被刪除節點在node的左子樹
TreeNode<T> lNode = delete(element, node.getLeft());
node.setLeft(lNode);
} else { //node爲被刪除節點
//包含一個子節點,使用子節點替換父節點
if (Objects.isNull(node.getLeft())) {
return node.getRight();
}
if (Objects.isNull(node.getRight())) {
return node.getLeft();
}
//左右節點均存在,使用後繼節點代替,移除後繼節點
TreeNode<T> tmp = node;
node = findMin(node.getRight());
TreeNode<T> rNode = deleteMin(tmp.getRight());
node.setRight(rNode);
node.setLeft(tmp.getLeft());
}
return node;
}
複製代碼
至此,咱們已經完成了二叉搜索樹的增長、查詢、刪除的方法。咱們發現二叉搜索樹的實現並不困難,而且在大多數場景下也能正常運行。二叉搜索樹在極端狀況的性能也是不可忍受的。
後面咱們將講述一種在任何場景初始化,運行時間都將是對數級的
接上面二叉樹搜索樹瞭解到二叉搜索樹在極端狀況也不能知足咱們對於查詢性能的要求。
二叉樹的一些統計特性
第n層最多的節點個數2n-1
高度爲h的二叉樹,最多包含2h-1個節點,因此n個節點的二叉樹的最小高度是log2n + 1
查找成功時,查找次數不會超過樹的高度h
二叉樹查詢性能的衡量
咱們下面來使用 A - H字符來觀察二叉搜索樹在不一樣的插入順序下構造的樹的結果
天然順序的平均查找長度爲ASL=(1+ 2 + 3 + 4+ 5+ 6+ 7 +8) / 8 = 4.5
計算特定順序的平均查找長度ASL=(1 + 2*2 + 3*4 + 4*1) / 8 = 2.6
當咱們數據相同,可是採用不一樣的插入順序,使平均查找長度不同。因此咱們要解決這個問題,先觀察兩個初始化方式兩個樹的特色,大體發現使用特定順序初始化的樹,感受樹的節點分佈比較平衡。因爲統計特色3和特色2,咱們但願n個節點的二叉樹的接近log2n + 1,那麼咱們就能夠最大化的提高查詢性能.
因此爲了解決這個問題,咱們引入新的二叉搜索樹實現-平衡二叉樹(AVL樹)
AVL樹內容定義
平衡因子BalanceFactor:左右子樹的高度差BF=HL - HR
規定左右子樹的高度差的絕對值不超過1 |BF| ≤ 1
節點定義
原有節點的基礎上增長height屬性
class AVLNode<T extends Comparable<T>> {
private T data;
//左節點
private AVLNode<T> left;
//右節點
private AVLNode<T> right;
//當前節點的高度
private int height;
}
複製代碼
高度計算
因爲平衡二叉樹的平衡指高度方面的平衡,咱們先來計算樹的高度
樹的高度H指:左HL右HR子樹高度的最大值 + 1
int height(AVLNode<T> node){
if (Objects.isNull(node)) {
return 0;
}
int rHeight = height(node.getRight());
int lHeight = height(node.getLeft());
return Math.max(rHeight, lHeight) + 1;
}
複製代碼
查找
調整平衡
插入分類
接下來咱們將處理全部的狀況
當插入節點在右子樹的右節點上(ADF路徑)
操做步驟:
將右子節點D做爲根節點
原根節點A做爲新根節點D的左子節點
將D節點的左子節點B設置爲原根節點A的右子節點
實現代碼以下:
AVLNode<T> singleRightRotation(AVLNode<T> node) {
AVLNode<T> result = node.getRight();
AVLNode<T> left = result.getLeft();
node.setRight(left);
result.setLeft(node);
return result;
}
複製代碼
LL插入
當插入的節點在左子樹的左節點上(GDB路徑)
操做步驟:
將左子節點D做爲根結點
原根節點G做爲新根節點D的右子節點
將D節點的右子節點F做爲原結點G的左子節點
實現代碼:
AVLNode<T> singleLeftRotation(AVLNode<T> node) {
AVLNode<T> result = node.getLeft();
AVLNode<T> right = result.getRight();
node.setLeft(right);
result.setRight(node);
return result;
複製代碼
當插入的節點在右子樹的左節點上(ADB路徑)
操做步驟:
針對A節點的右子節點D作左旋轉
針對A節點作右旋轉
實現代碼:
AVLNode<T> doubleRightLeftRotation(AVLNode<T> node){
AVLNode<T> right = singleLeftRotation(node.getRight());
node.setRight(right);
return singleRightRotation(node);
}
複製代碼
當插入的節點在右子樹的左節點上(GDF路徑)
操做步驟:
針對G節點的左子節點D作右旋轉
針對G節點作左旋轉
實現代碼:
AVLNode<T> doubleLeftRightRotation(AVLNode<T> node) {
AVLNode<T> left = singleRightRotation(node.getLeft());
node.setLeft(left);
return singleLeftRotation(node);
}
複製代碼
刪除節點
咱們在刪除節點時,思路以下:
葉子節點直接刪除
包含一個子節點,將子節點替換到父節點
包含兩個子節點,使用後繼節點替換被刪除節點,刪除後繼節點便可
平衡調整的思路:節點被刪除後,至關於在兄弟節點插入新的節點
代碼以下:
return null;
}
T nodeData = node.getData();
int flag = data.compareTo(nodeData);
if (flag > 0) { //右子樹
AVLNode<T> right = delete(node.getRight(), data);
node.setRight(right);
AVLNode<T> lNode = node.getLeft();
int rHeight = getHeight(right);
int lHeight = getHeight(lNode);
int bf = lHeight - rHeight;
if (bf == 2) {//右子樹被刪除節點,不平衡
//查看左兄弟節點,若是左兄弟有右子節點高度大於左子節點須要進行左右旋轉 (刪除狀況2)
if (getRightNodeHeight(lNode) > getLeftNodeHeight(lNode)) {
node = doubleLeftRightRotation(node);
} else { //右節點的高度小於或者等於左子節點的高度,左單旋便可(刪除狀況1)
node = singleLeftRotation(node);
}
}
} else if (flag < 0) { //左子樹
AVLNode<T> left = delete(node.getLeft(), data);
node.setLeft(left);
AVLNode<T> right = node.getRight();
int lHeight = getHeight(node.getLeft());
int rHeight = getHeight(right);
int bf = rHeight - lHeight;
if (bf == 2) {//左子樹被刪除節點,不平衡
//查看右兄弟節點,若是左子節點高度大於右子節點高度,進行右左旋轉 (刪除狀況4)
if ( getLeftNodeHeight(right) > getRightNodeHeight(right)) {
node = doubleRightLeftRotation(node);
} else {
//左子樹的高度小於等於右子節點的高度,左單旋轉便可(刪除狀況3)
node = singleRightRotation(node);
}
}
} else { //found
if (Objects.nonNull(node.getLeft()) && Objects.nonNull(node.getRight())) { //存在左右子節點
AVLNode<T> rMin = findMin(node.getRight()); //後繼節點替代
node.setData(rMin.getData());
delete(node.getRight(), rMin.getData()); //刪除後繼節點
} else {
node = Objects.isNull(node.getLeft()) ? node.getRight() : node.getLeft();
}
}
if (Objects.nonNull(node)) {
buildHeight(node);
}
return node;
}
複製代碼
因爲AVL是一個高度嚴格平衡的二叉搜索樹,查找效率在log2n級別。可是在維護節點高度平衡時,須要進行旋轉操做(插入時最多兩次旋轉;刪除節點時AVL樹須要調整整個查詢路徑的高度平衡,最多須要log2n次旋轉)後面,咱們將介紹另一種平衡搜索二叉樹(紅黑樹)!
引言
紅黑樹、B樹、B+樹,都是軟件開發中一個比較難理解和掌握的知識點。他們的本質依然是平衡二叉搜索樹。若是直接去學習紅黑樹、B樹、B+樹的知識點,無異於霧裏看花。此次咱們從這些數據結構的底層邏輯設計出發,不牽扯任何代碼層面上的內容。
二節點
一個key和左右兩個連接;其中key大於左連接、小於右連接
三節點
包含兩個key和三個連接(兩個key分別稱爲key1和key2,key1小於key2)
一、二、3三個子連接(子連接1的key小於根結點key一、子連接2的key大於根結點key1且小於根結點key二、子連接3的key大於根結點key2)
四節點
包含三個key和四個子連接(三個key分別爲key一、key二、key3且從小到大排列)
一、二、三、4三個子連接(子連接1的key小於根結點key一、子連接2的key大於根結點key1且小於根結點key二、子連接3的key大於根結點key2且小於根結點key三、子連接4的key大於根結點key3)
上述的節點計數指子連接的數量,而非節點包含的key的數量
因爲二、三、4樹的查詢操做和二叉搜索樹的操做一致,再也不贅敘。本次主要完成插入和刪除的操做描述
能夠參考前面,熟悉二叉樹一些基本定義和操做
咱們把1-10的數字拆入到一棵234樹中
依次插入一、二、3節點
插入4節點,須要將4節點分裂成3個2節點的操做
至此,插入邏輯介紹完畢
節點的刪除邏輯,和二叉樹的刪除邏輯區別不大。若是是葉子節點,能夠直接刪除;若是是非葉子節點,須要轉換爲後繼/前驅節點的刪除方式,全部均可以轉換爲極值的刪除
至此,咱們的234樹的插入和刪除操做介紹完了。搞清楚234樹的插入和刪除操做將是後續紅黑樹、B樹、B+樹的前置條件。
從上面的2-3-4樹瞭解到底層原理和操做邏輯,但按照對應邏輯實現代碼和各類狀況的處理,卻不容易。因此咱們要減小因爲2-3-4樹爲了實現平衡,而致使的實現複雜度上升的狀況。咱們如今使用普通的二叉樹+顏色來表示2-3-4樹(紅黑樹是多路平路查找樹的一種實現)
紅連接必須是左連接,根結點必須是黑色的
不能同時存在兩條連續的紅連接
任一空連接到根節點的路徑上經歷的黑色節點個數同樣
下面咱們使用1-3的插入來觀察紅黑樹是如何保持平衡的
根據上面根據上面的操做咱們能夠發現紅黑樹對2-3-4樹的實現原理:
使用黑+紅兩個節點來實現3節點(如上圖插入2後)
使用三個黑色節點實現4節點(如上圖插入3後)
RedBlackNode<T extends Comparable<T>> {
/*顏色的常量定義 red:false black:true 新建節點默認爲紅色*/
public static final boolean RED = false;
public static final boolean BLACK = true;
private T data;
private RedBlackNode<T> left;
private RedBlackNode<T> right;
private boolean color;
}
複製代碼
咱們將紅黑樹的操做分開描述
查找和普通的二叉搜索樹一致,再也不贅敘。
能夠參考二叉搜索樹關於查找的部分
實現步驟:
右子節點的顏色 = 原根結點的顏色
根結點node做爲右子節點的左子節點,刷新爲紅色節點
將右子節點的左子節點設置爲原根結點的右子節點
代碼示例:
RedBlackNode<T> rotateLeft(RedBlackNode<T> node){
RedBlackNode<T> right = node.getRight();
right.setColor(node.isColor());
RedBlackNode<T> middle = right.getLeft();
node.setRight(middle);
node.setColor(RedBlackNode.RED);
right.setLeft(node);
return right;
}
複製代碼
將根結點的左子節點替換到根結點,將左子節點做爲根結點返回
實現步驟:
左子節點的顏色 = 原根結點的顏色
根結點node替換到左子節點的右子節點,刷新爲紅色節點
將左子節點的右子節點設置爲原根結點的左子節點
代碼示例:
RedBlackNode<T> rotateRight(RedBlackNode<T> node){
RedBlackNode<T> result = node.getLeft();
result.setColor(node.isColor());
RedBlackNode<T> resultRight = result.getRight();
node.setLeft(resultRight);
result.setRight(node);
node.setColor(RedBlackNode.RED);
return result;
}
複製代碼
變色
/**若是左右節點都是紅色的那麼將左右子節點修改成黑色,父節點修改成紅色*/
void flushColor(RedBlackNode<T> node){
node.setColor(RedBlackNode.RED);
RedBlackNode<T> left = node.getLeft();
left.setColor(RedBlackNode.BLACK);
RedBlackNode<T> right = node.getRight();
right.setColor(RedBlackNode.BLACK);
}
複製代碼
插入算法代碼示例:
RedBlackNode<T> insert(RedBlackNode<T> node, T data){
if (Objects.isNull(node)) {
node = new RedBlackNode<>();
node.setData(data);
node.setColor(RedBlackNode.RED);
return node;
}
T nodeData = node.getData();
int flag = data.compareTo(nodeData);
if (flag < 0) { //插入數據小於節點數據,入左子樹
RedBlackNode<T> left = insert(node.getLeft(), data);
node.setLeft(left);
} else if (flag > 0) { //插入數據大於節點數據,入右子樹
RedBlackNode<T> right = insert(node.getRight(), data);
node.setRight(right);
}
//插入位置在右子節點,且左子樹非紅色,進行左旋轉
if (isRed(node.getRight()) && !isRed(node.getLeft())) {
node = rotateLeft(node);
}
//插入的節點在左子樹的左子節點上,右旋
if (isRed(node.getLeft()) && isRed(node.getLeft().getLeft())) {
node = rotateRight(node);
}
if (isRed(node.getLeft()) && isRed(node.getRight())) {
flushColor(node);
}
return node;
}
複製代碼
因爲咱們在二叉搜索樹BST裏介紹過,咱們能夠將節點刪除的邏輯調整爲極值的刪除
2-3-4樹文章裏,已經知道單獨的2節點是不能直接刪除的,須要將2節點轉換爲3或4節點(2節點對應紅黑樹中的黑色節點)
綜上所述:咱們須要極大/小值的刪除和2節點的刪除方法
主要分爲3節點和4節點刪除最小值(其中4節點根結點有紅或黑兩種顏色。CASE比較多,請放大查看)
代碼示例:
/**
最小值的刪除方法,返回刪除後的根節點
*/
RedBlackNode<T> deleteMin(RedBlackNode<T> node){
RedBlackNode<T> left = node.getLeft();
//左節點不爲null,最小值在node的左節點,繼續向左
if (Objects.isNull(left)) {
return null;
}
//左右節點都不是紅色,須要將黑色節點調整爲紅色Del-2至Del-5示
RedBlackNode<T> ll = left.getLeft();
if (!isRed(left) && !isRed(ll)) {
node = removeRedLeft(node);
}
left = deleteMin(node.getLeft());
node.setLeft(left);
return blance(node);
}
/** 移除紅色最小節點
*/
RedBlackNode<T> removeRedLeft(RedBlackNode<T> node) {
flipsColor(node);
RedBlackNode<T> right = node.getRight();
RedBlackNode<T> rl = Objects.isNull(right) ? null : right.getLeft();
//若是右左節點是紅色節點(對應圖中的Del-三、Del-5圖)
if (isRed(rl)) {
right = rotateRight(right);
node.setRight(right);
node = rotateLeft(node);
}
return node;
}
/**變色Del-2至Del-5示*/
void flipsColor(RedBlackNode<T> node) {
node.setColor(RedBlackNode.BLACK);
RedBlackNode<T> left = node.getLeft();
RedBlackNode<T> right = node.getRight();
if (Objects.nonNull(left)) {
left.setColor(RedBlackNode.RED);
}
if (Objects.nonNull(right)) {
right.setColor(RedBlackNode.RED);
}
}
/**
節點刪除後的平衡調整方法
*/
RedBlackNode<T> balance(RedBlackNode<T> node){
if (isRed(node.getRight())) { //右節點爲紅,左旋(圖中的2列)
node = rotateLeft(node);
}
if (isRed(node.getRight()) && !isRed(node.getLeft())) {
node = rotateLeft(node);
}
if (isRed(node.getLeft()) && isRed(node.getLeft().getLeft())) {
node = rotateRight(node);
}
if (isRed(node.getLeft()) && isRed(node.getRight())) {
flushColor(node);
}
return node;
}
複製代碼
最大值的刪除邏輯以下圖示
代碼示例:
/**
最大值的刪除方法,返回刪除後的根節點
*/
RedBlackNode<T> deleteMax(RedBlackNode<T> node){
if(isRed(node.getLeft())){
node = rotateRight;
}
RedBlackNode<T> right = node.getRight();
if(right == null){
return null;
}
if (!isRed(right) && !isRed(right.getLeft())) {
node = removeRedRight(node);
}
right = deleteMax(right);
node.setRight(right);
return balance(node);
}
/** 移除紅色右節點
*/
RedBlackNode<T> removeRedRight(RedBlackNode<T> node) {
flipsColor(node);
RedBlackNode<T> left = node.getLeft();
RedBlackNode<T> lr = Objects.isNull(left) ? null : left.getRight();
//若是左右節點是紅色節點
if (!isRed(rl)) {
return rotateRight(node);
}
return node;
}
複製代碼
咱們將以上兩個方法結合就能夠獲得紅黑樹的刪除方法,再也不贅敘。
至此,咱們就將二叉搜索樹的內容介紹完畢了。若是你以爲對你有幫助,記得點個贊和在看哦。同時也期待你們的留言討論。