1. 程式人生 > >搜尋樹及其應用(不定期更新)

搜尋樹及其應用(不定期更新)

承接之前的『樹』,本文將目標特別鎖定在『查詢樹』;這裡整理出我遇到的各種形式的查詢樹,以後可能會不定期更新,以儘可能多的囊括所有種類的查詢樹;雖然標題為“搜尋樹”,但是我還是習慣叫“查詢樹”,以下也將沿襲著一傳統

學習『查詢樹』心裡面始終要有一個意識:對於查詢樹而言『平衡』很重要

目錄

普通查詢樹(BST)

沒有任何限制的查詢樹,只要滿足每個結點的左結點比該結點小而右結點比該結點大即可;普通查詢樹是後面介紹的所有種類查詢樹(AVL樹、伸展樹、紅黑樹……)的超集,普通的查詢樹,它既可能是平衡的,也可能是不平衡的(平衡與否直接決定了程式執行的效率)——這裡給出最壞情況(當所有元素構成單調數列時搜尋效率最低,直接退化成單鏈表)

這裡寫圖片描述

也就是說,如果樹中插入的是隨機資料則執行效果很好,但如果插入的是有序或者逆序的資料,那麼二叉查詢樹的執行速度就變得很慢(所以才需要平衡)

查詢樹的初始化(返回根結點)

TreePtr BSTInit(ElementType E)
{
    TreePtr T = malloc(sizeof(TreeNode));
    if(T)
    {
        T->data = E;
        T->left = T->right = NULL;
        return T;
    }
    return NULL;
}

查詢樹的查詢(線性下降,不需要遍歷)

這裡略作小結:如果要遍歷一棵樹我們可以採用三種方法(前、中、後),但是如果要遍歷一棵查詢樹通常從樹根開始查詢(因為查詢樹的資料結點分佈是高度有序的,所以這樣效率最高也最簡單)

TreeNode BinarySearch(ElementType E, TreePtr T)
{
    while(T)
    {
        if(E == T->data)
        {
            return *T;
        }
        else if(E > T->data)
        {
            T = T->right;
        }
        else
{ T = T->left; } } return NULL; }

查詢樹的查詢時間通常和它的高度、寬度(扁平度)有關,一棵樹越趨於平衡(高度、寬度維持在特定值)查詢就越快(也有例外,有的查詢樹在搜尋前事先確定好各個結點的位置——這樣的樹是靜態的,也就不存在平衡一說——典型的例子如哈夫曼樹)
一棵N結點查詢樹如果它是可變的(可以達到不同的平衡形態),那麼理論上它的查詢時間複雜度必然介於:O(logN)~O(N)之間

補充幾點:

1)如果要按大小順序查詢,採用中序遍歷(也很簡單)
這裡寫圖片描述
如圖,查詢順序就是 1->3->5->10->10.5->11->12->13->15->16->17->18->20

2)對結點進行的每次操作(查詢、插入、刪除、包括修改)既可能讓原來平衡的樹不在平衡,也可能幸運的繼續保持平衡;如果仍保持平衡,有的樹(比如AVL樹)就不做任何改變,而另外有些樹(比如伸展樹)不管當前平衡狀態而追求的是平均角度看的平衡,在每次操作後都會採取相應改變以調整樹的構型——這一點也體現了不同種類樹的區別
附圖:
這裡寫圖片描述
忽略最下面的“26”號……如果我們把它放到右邊“65”的下面作為其右結點,可以預見這棵樹仍然是平衡的(我們說在『插入』操作後仍然保持平衡)——而後是否調整重構(怎麼調整)就體現了不同種類的查詢樹的不同特點——從這一點看,我們說正是平衡的調整方案確定了不同的樹啊

3)用佇列結構對二叉樹進行層序遍歷(C++實現):

#include <queue>

void LevelTraversal(BSPtr *ptr)
{
    queue<BSPtr*> rel;
    rel.push(ptr);
    while(!rel.isEmpty())
    {
        BSPtr *front = rel.front();
        printf("%d\n", front->data);
        rel.pop();
        if(front->left != null)
            rel.push(frontt->left);
        if(front->right != null)
            rel.push(front->right);
    }
}

另外,學習查詢樹除了會寫程式碼還要會看圖
這裡寫圖片描述
這裡寫圖片描述

以上,是兩棵不同的二叉查詢樹!雖然從物理模型角度看它們是等價的,但是在電腦科學中必須要區別對待——對每個結點,畫一條經過它的水平線,對於與它相鄰的結點(用輕繩連線),如果在這條水平線上面就說它是這個結點的父結點,否則在下面就稱為該結點的孩子結點

哈夫曼樹(Huffman Tree)

哈夫曼讓連線各個結點的路徑帶權,對於一組給定的資料,總存在唯一的讓所有帶權路徑之和最小的樹形結構,我們稱這樣的樹為關於這一組給定資料的哈夫曼樹;通常處理的資料數目龐大以至於不可能完全統計,通常使用概率代替頻率作為描述特定資料集合的哈夫曼樹的權值
這裡寫圖片描述

可以看出,哈夫曼樹並不滿足一般查詢樹“左孩子比父結點大、右孩子比符結點小”的特點(如父結點46,他的兩個孩子分別是21、25),從這一點看哈夫曼樹不是嚴格的查詢樹——但是考慮到它可以查詢資料的功能,姑且認為它也是一種特殊的查詢樹(靜態的查詢樹,只能進行查詢通常不做資料的修改、新增等操作)

哈夫曼樹的ADT

typedef struct _HuffmanNode
{
    ElementType data;
    int weight;
    struct _HuffmanNode *left;
    struct _HuffmanNode *right;
}HuffmanNode, *HuffmanPtr;

哈夫曼樹的構造

查詢的是資料,但是比較的是權值
為了使得到的哈夫曼樹的結構儘量唯一,通常規定生成的哈夫曼樹中每個結點的左子樹根結點的權小於等於右子樹根結點的權(下面的程式碼就是把小的作為左孩子)

/*
 * 函式功能:找出給定資料集合中最小、次小的兩個元素
 * E:用於接受資料的陣列
 * n:陣列元素個數
 * p1:指向最小元素下標
 * p2:指向次小元素下標
*/

static int FindMinNode(ElementType *E[], int n, int *p1, int *p2)
{
    int index;
    int fir_min = sec_min = 0xffff;

    if(E == NULL) return -1;

    for(index=0; index<n index++)
    {
        if(weight != 0)
        {
            if(E[index]->weight < fir_min)
            {
                sec_min = fir_min;
                fir_min = E[index]->weight;
                *p2 = *p1;
                *p1 = index;
            }
            else if(E[index]->weight < sec_min)
            {
                sec_min = E[index]->weight;
                *p2 = index;
            }
        }
    }
    return 0;
}
/* 函式功能:建立哈夫曼樹
 * E:給定資料陣列
 * n:給定資料個數
 */

HuffmanPtr CreateHuffmanTree(ElementType E[], int n)
{
    int i, j;
    HuffmanPtr b[], q;
    b = malloc(n*sizeof(HuffmanNode));
    for(i=0; i<n; i++)
    {
        b[i] = malloc(sizeof(HuffmanNode));
        b[i]->weight = E[i];
        b[i]->left = b[i]->right = NULL;
    }
    for(i=0; i<n-1; i++) //主體部分,迴圈n-1次建立Huffman Tree
    {
        for(j=0; j<n; j++) //讓k1、k2分別指向第一和第二棵樹
        {
            int k1=-1, k2;
            if(b[j] != NULL && k1==-1)
            {
                k1 = j;
                continue;
            }
            if(b[j] != NULL)
            {
                k2 = j;
                break;
            }
        }

        FindMinNode(E, n, &k1, &k2);

        /*由最小權值、次小權值的兩個結點建立一棵新樹,q指向樹根*/
        q = malloc(sizeof(HuffmanNode));
        q->weight = b[k1]->weight + b[k2]->weight;
        q->left = b[k1];
        q->right = b[k2];

        b[k1] = q;
        b[k2] = NULL;
    }
    free(b);
    return q;
}

AVL樹(AVL-BST)

一種簡單的高度平衡的自平衡查詢樹;它限制樹中任何節點的兩個子樹的高度最大差別為1;在每次增加、刪除元素後,要重新調整AVL樹的結構以達到新的平衡(牽一髮而動全身

AVL樹的四種失衡型別:1)LL型,2)LR型,3)RL型,4)RR型
AVL樹的四種調整型別:1)右單旋,2)左右雙旋,3)右左雙旋,4)左單旋

幾種旋轉的英文叫法:右單旋 => Zig、左單旋 => Zag、左右雙旋 => Zag-Zig、右左雙旋 => Zig-Zag

這裡寫圖片描述
這裡寫圖片描述

AVL樹的性質

對於每一個結點
0)高度自平衡
1)左右子樹也都是AVL樹
2)左右子樹的高度之差不會超過1

AVL樹的ADT

typedef struct _AVLNode
{
    ElementType data;
    int height;
    struct _AVLNode *left;
    struct _AVLNode *right;
}AVLNode, *AVLPtr;

AVL樹的插入

/*嚴格來說,這並不是結點的高度;但是我們約定用這樣的二進位制數表示它,好處就是簡化了資訊的複雜度,同樣當height的差等於2時旋轉——這一點看和一般意義上的height是等價的*/
static int Height(AVLPtr P)
{
    if(P == NULL)
    {
        return -1;
    }
    else{
        return P->height;
    }
}
/*左單旋*/
static AVLPtr SingleRotateWithLeft(AVLPtr K2)
{
    AVLPtr K1;
    K1 = K2->left;
    K2->left = K1->right;
    K1->right = K2;
    K2->height = Max(Height(K2->left), Height(K2->right)) + 1;
    K1->height = Max(Height(K1->left), Height(K2->height)) + 1;
    return K1;
}
/*右雙旋*/
static AVLPtr DoubleRotateWithLeft(AVLPtr K3)
{
    K3->left = SingleRotateWithRight(K3->left);
    return SingleRotateWithLeft(K3);
}
/*插入函式主體部分,呼叫上面的函式*/
int AVLInsert(ElementType E, TreePtr T)
{
    if(T == NULL)
    {
        T = malloc(sizeof(TreeNode));
        if(T)
        {
            T->data = E;
            T->left = T->right = NULL;
            T->height = 0;
        }
        else{
            return -1;
        }
    }
    else if(E < T->data)
    {
        T->left = AVLInsert(E, T->left);
        if(Height(T->left) - Height(T->right) == 2)
        {
            if(X < T->left->data)
            {
                T = SingleRotateWithLeft(T);
            }
            else{
                T = DoubleRotateWithLeft(T);
            }
        }
    }
    else if(E > T->data)
    {
        T->right = AVLInsert(E, T->right);
        if(Height(T->right) - Height(T->left) == 2)
        {
            if(X > T->left->data)
            {
                T = SingleRotateWithRight(T);
            }
            else{
                T = DoubleRotateWithRight(T);
            }
        }
    }
    T->height = Max(Height(T->left), Height(T->right));
    return T;
}

樹堆(Treap)

樹堆的每個結點都有一個在插入時隨機賦予的優先順序,其結構相當於以隨機資料插入的二叉查詢樹,在平衡被打破時它也要通過旋轉建立新的平衡

優先順序越小的結點越靠近根結點;從這一點看,樹堆和哈夫曼樹倒有幾分相似(樹堆的優先順序相當於哈夫曼樹的權值,不過二者作用相反——哈夫曼樹是權值越大的越靠近樹根,而且樹堆的每個結點的優先順序也不滿足等於兩個子結點的相加關係->看下圖13+14≠6)

這裡寫圖片描述

樹堆的性質

樹堆的性質總結起來就是,1. 最小堆原則:從樹根往下,每個結點的優先順序都必須小於其任意孩子結點的優先順序(注意不一定是一個比它大一個比它小,那是二叉查詢樹儲存資料的方法)2. 樹堆既可以看成一棵二叉查詢樹,也可以看成是一個堆結構(Treap=Tree+Heap)

樹堆的ADT

typedef struct _TreapNode
{
    ElementType data;
    int priority;
    struct _TreapNode *left;
    struct _TreapNode *right;
}TreapNode, *TreapPtr;

樹堆的刪除

因為樹堆既可以看成樹,也可以看成堆,所以這裡刪除結點就有兩種不同的思路(分別從「二叉樹」和「堆」的角度)

/*使用二叉查詢樹的方式刪除結點*/
if(結點是葉子結點)
{
    直接刪除之;
}
else if(結點有一個孩子)
{
    刪除該結點,並讓它的唯一孩子代替它原來的位置;
}
else
{
    刪除該結點,並讓其右子樹中最小值(或左子樹中最大值)代替它原來的位置;
}
/*使用堆的方式刪除結點:把要刪除的結點旋轉到葉子上再直接刪除*/

伸展樹(Splay Tree)

伸展樹(Splay Tree)也叫分裂樹,是一種自調整的平衡查詢樹;它能在O(log N)內完成插入、查詢和刪除操作;由丹尼爾·斯立特Daniel Sleator和羅伯特·恩卓·塔揚Robert Endre Tarjan在1985年發明;和AVL樹不同,它的特點是伸展(本質上是和AVL一樣都是二叉查詢樹,不過在失衡後的調整措施不同)

不會保證樹一直是平衡的,但各種操作的平攤時間複雜度是O(logN),因而,從平攤複雜度上看,伸展樹也是一種平衡二叉樹(專業術語:『攤還』)

伸展樹的自調整體現在:每一次對結點的查詢、插入、刪除操作,就把該結點移到根結點,如果需要刪除在這之後進行伸展樹不需要記錄用於平衡樹的冗餘資訊(結點的高度)

這裡寫圖片描述

伸展樹的伸展

所謂伸展就是把指定結點旋轉到樹根,可以把伸展看成是特殊的旋轉,而旋轉是所有平衡查詢樹的特性(為什麼要旋轉到樹根?因為查詢樹樹的遍歷都是從樹根開始,這樣可以讓那些訪問頻率高的結點優先被訪問到)說到優先順序,我們說『伸展樹的優先順序是在人為干涉下動態變化的,而樹堆的優先順序和外界干涉無關是根據隨機數演算法隨機分配的』;另外,伸展樹的優先順序(不嚴謹的說,優先順序就是結點距離樹根的路徑長度)是概念上的,實際上並不存在

伸展樹每次Splay前要考察的層數為三層(祖父、父親、自己),提出Splay要『向上追溯兩層而非一層』的是著名的電腦科學家Tarjan在1985年提出的

伸展樹的伸展不是被動而是主動的,不論查詢、刪除、或者插入結點,不論當前狀態是否已經平衡都要進行伸展;具體來說,當一個結點被查詢時說明該結點被訪問的可能性增大故進行伸展使該結點上移,同理可以分析刪除、插入操作,總之伸展行為總是主動發生在每一次的訪問(查、刪、插)操作後

我們說『伸展樹』是犧牲時間保護空間的典型代表 —— 不用額外的空間儲存結點的高度資訊,但是作為替代每次查詢、刪除、插入操作後都要進行一次伸展

伸展樹的伸展型別根據被訪問的結點位置分為三種情形:

1)被訪問結點是樹根的左或右孩子(最簡單)

這裡寫圖片描述

這裡寫圖片描述

這時候只要進行一次單旋(是左孩子->右旋)

2)被訪問結點不是根結點的孩子,且和它的父親都是各自父親的左或右結點

這裡寫圖片描述

這裡寫圖片描述

觀察構型,應該右右雙旋或左左雙旋(這裡是右右)

3)被訪問結點不是根結點的孩子,且和它的父親分別是各自父親的左孩子和是右孩子

這裡寫圖片描述

這裡寫圖片描述

觀察構型,應該左右雙旋或右左雙旋(這裡是左右)

看不懂?沒關係!且聽我慢慢道來……

首先對於伸展樹而言,它的目標是要『把當前操作結點旋轉到樹根』,這句話的理解很關鍵(複習二叉樹相關概念),我們知道一個結點到樹根的路徑是唯一的(『路徑』就是指『最短路徑』),於是最簡單的approach就是『向上回溯,遇到左孩子就右旋,遇到右孩子就左旋』,看上去很完美——BUT!

看圖說話(其實也可以看本文的第一張圖),what if 『構成樹的所有元素可以構成一個單調數列』?
這裡寫圖片描述
伸展操作(不同於『普通旋轉』,『伸展操作』影響的範圍或者說被操作結點的位置變化更大,因為人家的目標很明確就是樹根)後你旋轉得到的新樹是這樣的:對於單調數列,在『伸展操作』後,就像沒有旋轉一樣,因為O(N)還是O(N)時間複雜度沒變!
這裡寫圖片描述

另外考慮這樣的例子:
這裡寫圖片描述
這裡寫圖片描述
這種奇怪構型的查詢樹在進行『姑且認為是粗糙版的伸展操作』後,原來的k3結點卻被推到了和k1同樣深的位置——同樣的,單旋轉也沒能降低這種情況下的時間複雜度

基於上述兩個代表性例子——這些旋轉的效果是將新結點一直推向樹根,使得對該結點的進一步訪問很容易(暫時的);不足之處是它把另外一個結點幾乎推向和新結點以前同樣的深度,而對那個結點的訪問又將把另外的結點向深處推進……我們想『看來單旋轉是不行了,不妨試試雙旋轉』

『雙旋』也是可以轉換成兩步單旋的,我們假設被訪問結點為x
這裡寫圖片描述

在旋轉之前要明確『目標是什麼』:伸展樹的目標是把訪問結點移動到整棵樹的樹根

在明確了目標後,自然想到討論結點和樹根的關係

case1)x的父結點是樹根
訪問結點9後,只需要單旋一次(這裡是左旋)把該結點搞成樹根就完事了

這樣最簡單,想要雙旋還搞不了
這裡寫圖片描述

並且我們說單次旋轉操作涉及到要判斷的樹結點層數不會太多(最多到『祖父』結點,這就是極限了)

case2)之字形
被訪問結點X 介於父節點P 和 祖父節點G 之間,確切地說是 P≤X≤G(在『裡面』)
這時候執行Zig-Zag或Zag-Zig(兩次方向相反的單旋)

對於『RL』、『LR』的『之字形』,AVL樹也要旋轉兩次,從結果上看AVL樹的Rotate和這裡伸展樹的Splay是一樣的,都是把X旋轉到了樹根位置(AVL是無意為之,而Splay則是故意為之)
這裡寫圖片描述
這裡寫圖片描述

case3)一字型
被訪問結點X 不介於父節點P 和 祖父節點G 之間,確切地說是 X≤P≤G 或 G≤P≤X(在『外面』)
這之後執行Zig-Zig或Zag-Zag(兩次方向相同的單旋,依次把最上面的樹根翻下來)

可以看到,和AVL樹的一字型Rotate相比,看下圖如果是AVL樹到第二步把G翻下來就完事了,而Splay則多了一個把P翻下來的過程(實際上是要把X翻上去——Splay的思想就是把操作結點翻到對應子樹的樹根位置)
這裡寫圖片描述
這裡寫圖片描述

總結:以後看到『伸展樹』就要想到『伸展』,想到『把當前結點移動到樹根位置』,還要想到我上面舉的兩個例子,要形成反射弧——

通過本章節你學到了什麼,你的思維導圖:

伸展樹 => 伸展,就是把當前結點想辦法移動到樹根位置 => 儘量不要加深樹的深度 => 使用雙旋轉(就像DNA的雙螺旋一樣)

另外,伸展樹一律使用『雙旋轉』,除非特殊情況,即被訪問結點的父親是樹根,這時候我們也想雙旋轉但是因為想搞搞不了只得使用單旋轉

/*函式功能:實現伸展樹的Splay操作
 *函式說明:使用迭代,如果當前結點不是樹根就根據情況旋轉,當它的父親是樹根時跳出迴圈(因為時另一種情況),並根據case進行單旋轉,其中封裝了PushUp函式
 *請參說明:T為樹根結點,N時新插入結點
 */
void Splay(SplayPtr *T, SplayPtr *N)
{
    while(T != N && T->left != N && T->right != N)
        PushUp(T, N);
    if(T->left == N) SingleRotateWithRight(T, N);
    else if(T->right == N) SingleRotateWithLeft(T, N);
}
/*函式功能:實現Splay的主體功能,被封裝在splay中
 *請參說明:i和j是判斷條件的標記,也可以用兩個boolean型別代替
 */
void PushUp(SplayPtr T, SplayPtr N)
{
    SplayPtr parent, grandparent;
    int i, j;
    parent = N->parent;
    grandparent = N->parent->parent;
    i = grandparent->left == parent ? 0 : 1;
    j = parent->left == N ? 0 : 1;
    if(i==0 && j==1)
        zig-zig(T, N);  //右右雙旋
    else if(i==0 && j==1)
        zag-zig(T, N);  //左右雙旋
    else if(i==0 && j==1)
        zig-zag(T, N);  //右左雙旋
    else
        zag-zag(T, N);  //左左雙旋
}

注意下,每次Splay最多也只是把當前結點移動到它的祖父結點位置——之後如果要移動到整棵樹的樹根位置,還要繼續Splay(如果此時父結點就是樹根,那就直接單旋轉完事)——反正記著,伸展樹的Splay的最終目的是把操作結點移至整棵樹的樹根位置

伸展樹的區間操作

首先想一想,伸展樹為什麼適合『區間操作』(不是說別的樹不可以,只是說它的構型特點比較適合)?因為每次Splay(伸展樹的核心操作)都要把相應的結點旋轉的樹根,這樣旋轉後該結點的位置就確定了,既然位置確定了又考慮到伸展樹作為一棵查詢樹它所有結點分佈應該滿足有序的特點,所以對該新樹根做一些遍歷之類的操作就很簡單

所以,與其說『伸展樹的區間操作』,不如說『借用Splay函式的區間操作』

給裡給出思路:要求區間[s, e]的極值,將結點s-1伸展成樹根,再將結點e+1伸展為樹根的右孩子,那麼結點e+1的左子樹就代表了區間[s, e],結點e+1的左孩子的極值域就是所求範圍(以上,必須要保結點s-1和結點e+1存在)

結點大小平衡樹(SBT)

名字不要和普通查詢樹弄混了(那是BST)

SBT,即Size Balanced Tree,節點大小平衡樹,是一種自平衡二叉查詢樹,它是由中國廣東中山紀念中學的陳啟峰發明的;實踐中,SBT是所有種類的平衡樹中效率較高的一種;SBT的高度是O(logN),Maintain是O(1),所有主要操作都是O(logN);SBT的特點是,它需要專門去維護其大小,從而實現構建平衡二叉樹的目的

SBT的自平衡是通過size域實現的

結點大小平衡樹最主要的特色或者說核心操作就是『維護子樹大小』,這裡聰明的讀者肯定一眼就看出來為了維護所謂的子樹大小必然要有一個附加的『size』域用來存放每個結點的子樹大小

SBT的高度是O(logN),Maintain是O(1),所有主要操作都是O(logn)——用陳老師的原話就是『目前為止速度最快的高階二叉搜尋樹』

SBT的實現結構高度

P.s. Treap、Splay、SBT,號稱是『三大查詢樹』(並無考證,姑且信之吧)

SBT的性質

SBT是一種通過Size域來保持平衡的查詢樹,它的性質總結起來一句話:對於SBT的每個結點,每棵子樹的大小不小於其兄弟的子樹大小

寫成數學表示式就是:

Size[Right[T]] ≥ Size[Left[Left[T]]], Size[Right[Left[T]]]
Size[Left[T]] ≥ Size[Right[Right[T]]], Size[Left[Right[T]]]

做成PPT也好理解:
這裡寫圖片描述

SBT的操作集

這裡寫圖片描述

SBT的ADT

因為旋轉總是發生在當前結點和其父結點之間,所以要取得這個結點的父親的引用或者指標(當然,或者轉換思路把要傳給Rotate函式的引數寫成上面的結點就不用加上parent域了,加上parent域是為了便於查詢);另一種思路是用一個數組維護整個SBT

/*寫法一*/
typedef struct _SBTNode
{
    ElementType key;
    int size;
    struct _SBTNode *left;
    struct _SBTNode *right;
}SBTNode;
/*寫法二:用陣列維護整棵樹*/
typedef struct
{
    int key, left, right, size;
}tree[MAXSIZE]; 

SBT的旋轉

SBT的旋轉函式最後是要封裝到Maintain函式中去的,大體操作和普通的AVL旋轉沒有區別,不過SBT有SBT的特點:發現SBT每次旋轉,包括判斷是否要旋轉(我是說『Maintain』,就是一系列旋轉),波及到的結點都大於2層但是沒有層數的上界限制,也就是說可以有3層、4層、5層……這一點要有一個印象,就是『SBT可以很深』

SBT的旋轉和AVL的旋轉沒有區別,無非是多了個size域……

/*S表示旋轉前在上面的結點,旋轉後翻到下面*/
int RotateWithLeft(SBTNode *S)
{
    SBTNode *tmp = S->right->left;
    S->right->left = S;
    S->right = tmp;
    S->right.size = S.size;
    /*S的size,因為S被翻下來後獲得了它原來孩子的孩子,所以加一*/
    S.size = S.left.size + S.right.size;
}

SBT的Maintain函式(核心關鍵)

當我們刪除、插入一個結點到SBT中,SBT的大小會發生改變(從而破壞SBT的兩個重要性質),這時候就需要呼叫Maintain函式對SBT進行修復

1)Maintain(T)用於修復以T為根的SBT

2)呼叫Maintain(T)的前提是T的子樹都已經是SBT了
看到注意事項2,你要想到兩點:1)既然說『呼叫Maintain的前提是T的子樹都是SBT』,那麼我們說不看旋轉單看Maintain,Maintain操作一定是自下而上進行的,即先Maintain下面的再Maintain上面的,2)雖然話是這麼說沒錯,但是如果把Maintain和Rotate結合起來看的話(事實上,Maintain就是包含Rotate的),由於首先Rotate操作已經讓發生問題的結點轉到上面去了,我們想要『先Maintain下面的再Maintain上面的』,不過這時候是要接著Rotate操作後對翻下來的結點進行Maintain就好了

要細分的話,Maintain呼叫有四種情況:分別是1)Size[A]>Size[R],2)Size[B]>Size[R],3)Size[D]>Size[L],4)Size[C]>Size[L];不過二叉樹好就好在它比較對稱,後兩種情況和前兩種情況對應為映象情形,這裡以前兩種要Maintain的情形分析

Maintain函式其實並不複雜,如果我們忽略那些多餘的自子樹結點(途中三角形部分),Maintain總是發生在一定範圍內(從圖中看好像是三四層的『感覺』)

case1:Size[A] > Size[R]

這裡寫圖片描述

0x01)順時針旋轉根結點T(不是以T為轉軸,而是拎著T)讓T下,L上(R不動)

這裡寫圖片描述

0x02)由於旋轉後不確定T是否滿足SBT的性質即不能確定是否仍然是SBT,我們對翻下來的結點T進行Maintain操作(熟練後就形成慣性思維了,知道要對翻下來的結點先進行維護),Maintain操作的基礎也是Rotate——在Maintain過T後,我們知道又解決掉一層,接下來只要對Maintain發生的最高一層結點也就是L進行維護即可;在這之後,不要再想著是不是要繼續上溯對L的父結點進行Maintain,那是完全沒有必要的(回頭看之前的紅色部分),因為我們說『呼叫Maintain(T)的前提是T的子樹都已經是SBT了』,這句話的理解我認為是理解Maintain函式的關鍵(每一次『操作』包括刪除、插入的影響範圍都是以操作位置為核心向周圍散開的,如果沒有特殊要求——比如Splay要求伸展比如要搞到樹根位置這就是特殊要求——並不會影響範圍不會太大)

這裡寫圖片描述

這裡寫圖片描述

case2:Size[B] > Size[R]

情形二和情形一相比,不同在於『新插入點在Maintain影響範圍最高層的左右或右左位置,或者說插入點位於旋轉點的內側(情形一插入點位於旋轉點的外側)』

這裡寫圖片描述

0x01)圖中是『LR』型別,所以先旋轉L結點劃歸為『LL』型別後(這不,又是case1了)再旋轉T結點(兩步合為一步)

這裡寫圖片描述

這裡寫圖片描述

0x02)旋轉(Rotate)過後,就是維護(Maintain)了,這裡L和T由於在同一層,都是B的孩子,所以誰先Maintain誰後Maintain是沒關係的,不過在Maintain(L)& Maintain(T)過後,不要忘了現在處在『影響範圍最高層』的B結點,也要Maintain——Maintain和Rotate一樣,都有點『自下而上』的感覺(上溯,直到影響範圍消失)

這裡寫圖片描述

這裡寫圖片描述

程式碼部分:

/*函式說明:維護函式,觸發條件是size域不滿足相關性質;其中前兩個if對應圖片的case1、case2,後兩個就是前兩種的映象而已
 *請參說明:T是旋轉結點也是影響範圍的最高層位置;flg:方便後面Insert呼叫,每次插入把平衡操作壓入棧中(注意註釋前後的語句變化,加上註釋的版本方便後面函式的呼叫)
 *備註:沒有使用陣列維護整個SBT樹,因為如果使用陣列的話,key域的型別就定死了,我們使用一個結構體left、right來封裝各自的資料域;另外SBT的Maintain有四種情況這個記住
 */
void Maintain(int *T, bool flg)
{
    if(flg)
    {
        if(T->left->left.size > T->right->size)
        {
            RotateWithRight(T);
            //Maintain(T->left);
            //Maintain(T);
        }
        else if(T->left->right->size > T->right->size)
        {
            RotateWithLeft(T->left);
            RotateWithRight(T);
            //Maintain(T->right);
            //Maintain(T->left);
            //Maintain(T);
        }
    }
    else
    {
        else if(T->right->right->size > T->left->size)  //對應case1映象
        {
            RotateWithLeft(T);
            //Maintain(T->left);
            //Maintain(T);
        }
        else if(T->right->left->size > T->left->size)  //對應case2映象
        {
            RotateWithRight(T->right);
            RotateWithLeft(T);
            //Maintain(T->left);
            //Maintain(T->right);
            //Maintain(T);
        }
    }
    Maintain(T->left, false);  //#
    Maintain(T->right, true);  //#
    Maintain(T, false);  //#
    Maintain(T, true);  //#
}

略作小結:SBT的平衡是靠Size域維護的,當Size域不滿足SBT性質時,要進行Maintain操作;而Maintain操作是包括Rotate操作的,在Rotate後,再要根據具體插入位置判斷Maintain的結點次數——整個Maintain函式就是一個判斷並遞迴呼叫而已

SBT的插入(實現Maintain介面)

Insert函式封裝Maintain函式,Maintain函式封裝Rotate函式,Rotate函式封裝size域的變化……

/*函式說明:實現SBT的結點插入
 *引數T:為根樹結點;引數newNode:為新結點;parent:用來儲存T變為其孩子之前的值(確切地說是儲存T變為NULL之前的值)
 *備註:在實際使用當中,寫法是靈活的——比如可以封裝一個NewNode函式用來建立新結點,那麼這裡只要傳入它的key域即可
 */
void Insert(SBTPtr *newNode, SBTPtr *T)
{
    if(T == NULL)
    {
        T = newNode;
    }

    SBTPtr *parent = NULL;
    while(T)
    {
        if(newNode->key > T->key)
        {
            parent = T;
            T = T->right
        }
        else if(newNode->key < T->key)
        {
            parent = T;
            T = T->left;
        }
    }
    if(parent->key < newNode->key)
    {
        parent->right = newNode;
    }
    else
    {
        praent->left = newNode;
    }
    Maintain(T, key>=T->size);
}

SBT的Select函式

SBT的Select函式返回以某結點為樹根的子樹包含第k小的值的結點指標
還是那句話,對於給定的查詢樹結點,左孩子的值>當前結點的值>右孩子的值

int Select(SBTPtr *T, int k)
{
    int r = T->left->size+1;
    if(r == k)
        return T->key;
    else
        if(r < k)
            return Select(T->right, k-r);
        else
            return Select(T->left, k);
}           

SBT的Rank函式

SBT的Rank函式返回某結點的key值排名;Rank和Select互為補充

int Rank(SBTPtr *T, int key)
{
    if(key < T->key)
        return Rank(T->left, key);
    else
        if(key > T->key)
            retrun rank(T->right, key) + T->left->size + 1;
        else
            retrun T->left->size + 1;
}

替罪羊樹(Scapegoat Tree)

替罪羊樹是一種基於重構的重量級(為什麼說它是『重量級』?因為每次都要暴力重構啊,不懂往下看)查詢樹

替罪羊樹的『平衡』是這樣定義的:給出定值alpha(0.5≤alpha≤1),對於對樹中的每個結點x,如果Size(left(x))≤α·size(x)和Size(right(x))≤α·size(x),則稱這棵樹滿足替罪羊樹的平衡;也就是說,結點的兩棵子樹包含結點數不超過整棵子樹的α倍,那麼就稱這個節點x是α-大小平衡的(∂大小自取)——始終要謹記,不同的樹對於『平