《大話數據結構》第八章 查找


第八章 查找

定義

搜索引擎工作

查找表:是由同一類型的數據元素構成的集合。

關鍵字:是數據元素中某個數據項的值。

主關鍵字:若此關鍵字可以唯一地標識一個記錄,則稱此關鍵字爲主關鍵字(Primary Key)。

次關鍵字:那些可以識別多個數據元素的關鍵字,稱之爲次關鍵詞(Secondary Key)。

查找:根據給定的某個值,在查找表中確定一個關鍵字等於給定值的數據元素。

靜態查找表:只作查找操作的查找表,主要操作有1. 查詢某個“特定的”數據元素是否在表中;2. 檢索某個“特定的”數據元素和各種屬性。

動態查找表:在查找過程中同時插入查找表中不存在的數據元素,或者刪除已經存在的元素。


順序表查找

思路:從頭到尾遍歷比較。

實現代碼

int Sequential_Search(int *a, int n, int key)
{
    int i;
    for (i=1; i<=n; i++)
    {
        if (a[i] == key)
            return i;
    }
    return 0;
}

優化順序查找
設置一個哨兵,可以不需要 i 每次都和 n 比較。
做一個長度爲n+1的數組,把0位置的值設爲key,倒着查找每次下標減一,一定會出現值爲key的時候,若下標爲0表示沒找到,否則表示找到。

int Sequential_Search2(int *a, int n, int key)
{
    int i;
    a[0] = key;
    i = n;
    while (a[i] != key)
    {
        i--;
    }
    return i;
}

時間複雜度
最好的情況爲O[1]O[1]
最壞的情況爲O[n]O[n]
關鍵字在任一位置的概率是相同的,所以平均查找次數爲n+12\frac{n+1}{2},最終時間複雜度爲O[n]O[n]


JAVA實現順序查找

public class OrderFine {
    public static void main(String[] args) {
        int[] a = {2,3,4,5,1,6};
        int b = 4;
        Find1(a, b);
        // 第一個位置爲空,寫爲0
        int[] c = {0, 2,3,4,5,1,6};
        Find2(c, b);
    }

    private static void Find2(int[] a, int b) {
        a[0] = b;
        int len = a.length;
        while (a[len-1] != b){
            len--;
        }
        if (len==1){
            System.out.println("沒找到");
        } else {
            System.out.println(len-2);
        }
    }

    private static void Find1(int[] a, int b) {
        for (int i = 0; i < a.length; i++) {
            if (a[i] == b){
                System.out.println(i);
                break;
            }
        }
    }
}


有序表查找

折半查找

思路:前提是線性表中的關鍵詞有序,線性表示順序結構。取中間記錄作爲比較對象,若給定值小於中間值,就在左邊區間找;若大於中間值,就在右邊區間找。

代碼實現

int Binary_Search(int *a, int n, int key)
{
    int low, high, mid;
    low = 1;    // 最低下標爲記錄首位
    high = n;    // 最高下標爲記錄末位
    while (low <= high)
    {
        mid = (low + high) / 2;    // 折半
        if (key < a[mid])
            high = mid - 1;
        else if (key > a[mid])
            low = mid + 1;
        else
            return mid;    // 相等表示找到
    }
    return 0;
}

插值查找

思路:優化二分查找法,根據key在數值域中大小比例,來確定在哪找。

推導

mid=low+high2=low+12(highlow)=low+keya[low]a[high]a[low](highlow)mid = \frac{low + high}{2}=low + \frac{1}{2}(high-low)=low+\frac{key-a[low]}{a[high] - a[low]}(high-low)

核心代碼

mid = low + (high-low)*(key-a[low]) / (a[high]-a[low]);

斐波那契查找(沒理解)

找數組的長度在F數組中的位置。
根據F中的數字來擴充a數組,後面的值用a數組中的最大值填充
mid的值是由F決定的,mid=low+F[k-1] - 1
比較後修改high,low,k// 爲什麼要這樣改k呢
如果小了,k-1,大了k-2.high並不會影響mid的選擇,low纔會

k在逐漸變小,得到的F值也在變小,F值是a的下標,如果key比a大,那麼low變大了,F也會變得很大,此時k-2的話,得到的F值仍然是大的,

沒看懂啊,後面再看吧

int Fibonacci_Search(int *a, int n, int key)
{
    int low, high, mid, i, k;
    low = 1;    // 最低小標爲記錄首位
    high = n;
    k = 0;
    while (n > F[k]-1)    // 計算n位於斐波那契數列的位置
        k++;
    for (i=n; i<F[k]-1; i++)
        a[i] = a[n];
    while (n > F[k]-1)    // 看看長度位於F數組的什麼位置
        k++;
    for (i=n; i<F[k]-1; i++)    // a數組的長度順着上面F的取值,要擴充
        a[i] = a[n];
    while (low <= high)
    {
        mid = low + F[k-1] - 1;    // 計算當前分割下標
        if (key < a[mid])
        {
            high = mid-1;
            k = k-1;
        }
        else if (key > a[mid])
        {
            low = mid+1;
            k = k-2;
        }
        else
        {
            if (mid <=n )
                return mid;
            else
                return n;
        }
    }
    return 0;
}

JAVA實現有序表查找

折半

public class BinarySearch {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4, 5, 6, 7, 8};
        int key = 3;
        BinarySc(a, key);
    }

    private static void BinarySc(int[] a, int key) {
        int high = a.length-1;
        int low = 0;
        int min = 0;
        while (low <= high){
            min = (low + high) / 2;
            if (a[min] > key){
                high = min - 1;
            } else if (a[min] < key){
                low = min + 1;
            } else {
                System.out.println(min);
                break;
            }
        }
    }
}

線性索引查找

稠密索引

稠密索引:數據集中每個記錄都有一個索引,索引項一定按照關鍵碼有序排列。


分塊索引

分塊有序:將數據集分塊,按塊給索引,這些塊要滿足下面兩個條件,這樣的序列叫做分塊有序:

  • 塊內無序:每個塊內的元素不需要有序
  • 塊間有序:比如第二塊的記錄的關鍵字均大於第一塊中所有記錄的關鍵字。

分塊索引表結構

  • 最大關鍵碼,存儲每一個塊中的最大關鍵字,好處是可使下一塊的最小關鍵字也能比上一塊最大的關鍵字大。
  • 存儲了塊中的記錄個數,以便循環時使用。
  • 用於指向首數據元素的指針,便於開始對這一塊中記錄進行遍歷。

分塊索引表查找步驟

  1. 先用簡單的算法找到位於哪個塊
  2. 然後利用塊的指針, 在塊中順序搜索即可

時間複雜度分析
共有n個記錄,設有m塊,每塊t條記錄,所以塊的查找假設爲m+12\frac{m+1}{2}次,
塊中的查詢設爲t+12\frac{t+1}{2}次,所以總查找爲:

m+12+t+12=12(m+t)+1=12(nt+t)+1\frac{m+1}{2} + \frac{t+1}{2} = \frac{1}{2}(m+t) +1 = \frac{1}{2}(\frac{n}{t} + t) +1

最好的情況是m與t相等,所以次數爲n+1\sqrt{n}+1,時間複雜度爲O[n]O[\sqrt{n}],比折半查找的O[logn]O[\log n]差不少。


倒排索引

索引項的通用結構

  • 次關鍵碼,例如上面的英文單詞
  • 記錄號表,例如上面的文章編號

倒排索引:就是和上面的分塊索引相反,左邊放元素,右邊放在哪個塊。記錄號表存儲具有相同次關鍵碼的所有記錄的記錄號,這樣的索引方法就是倒排索引。
因爲生活中有時需要根據屬性來查找記錄,例如搜索引擎。


二叉排序樹

定義:二叉排序樹(Binary Sort Tree),又稱爲二叉查找樹,它或者是一顆空樹,或者是有下列性質的二叉樹:

  • 若左子樹不爲空,則左子樹上所有的結點都小於根結點
  • 若右子樹不爲空,則右子樹上所有的結點都大於根結點
  • 左右子樹也分別爲二叉排序樹
  • 使用中序遍歷可得從小到大的序列

作用:並不是爲了排序,而是爲了提高查找和插入刪除關鍵字的速度。

二叉樹結構

typedef struct BiTNode    // 結點結構
{
    int data;
    struct BitNode *lchild, *rchild;
} BiTNode, *BitTree;

二叉排序樹的查找

思路:f用來指向雙親,p用來保存結果,找到了就爲此結點,到最後一直沒找到就返回離該點最接近的一個結點。

代碼實現

Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
    if (!T)    // 大小比較完的最後,到了null就表示找不到了
    {
        *p = f;    // 返回空的父結點,就是二叉樹中最接近的一個頂點
        return FALSE:
    }
    else if (key == T->data)
    {
        *p = T;
        return TRUE;
    }
    else if (key < T->data)
        return SearchBST(T->lchild, key, T, p);    // 在左子樹繼續找
    else
        return SearchBST(T->rchild, key, T, p);    // 在右子樹繼續找
}

二叉排序樹插入操作

思路:先檢查樹裏有沒有和插入點重複的,有就不插,沒有就找到最接近插入點的結點,根據該結點和插入點的大小關係來判斷爲左孩子還是右孩子。

代碼實現

Status InsertBST(BiTree *T, int key)
{
    BiTree p, s;
    if (!SearchBST(*T, key, NULL, &p))    // 沒找到就添加
    {
        // 先把結點建好
        s = (BiTree) malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        // 添加環節
        if (!p)    // 如果是個空樹,就把插入的點當成根結點
            *T = s;
        else if (key < p-data)
            p->lchild = s;    // 插入s爲左孩子
        else
            p->rchild = s;
        return TRUE;
    }
    else
        return FALSE;    // 已經有了相同的關鍵點,不再插入
}

二叉排序樹刪除操作

思路: 找刪除點的中序前驅結點來替換被刪除點,前驅和該刪除點在排序上是相鄰,所以是最適合替換的,同理也可用後驅替換。所以應有三個元素,一是被刪除點,二是前驅點,三是前驅的父結點,前驅的父結點用來把斷的接上。

查找代碼
刪除前要找到結點,找到後針對結點刪除

Status DeleteBST(BiTree *T, int key)
{
    if (!*T)    // 不存在的話
        return FALSE;
    else
    {
        if (key == (*T)->data)    // 找到關鍵字等於key的數據元素
            // 此處和查找不同
            return Delete(T);
        else if (key < (*T)->data)
            return DeleteBST(&(*T)->lchild, key);
        else
            return DeleteBST(&(*T)->rchild, key);
    }
}

刪除代碼

Status Delete(BiTree *p)
{
    BiTree q, s;
    if ((*p)->rchild == NULL)    // 右子樹空則只需要重接左子樹
    {
        q = *p;
        *p = (*p)->lchild;
        free(q);
    }
    else if ((*p)->lchild == NULL)    // 同理
    {
        q = *p;
        *p = (*p)->rchild;
        free(q);
    }
    else    // 左右子樹均不爲空
    {
        q = *p;
        s = (*p)->lchild;
        // 找p的左孩子的右孩子的盡頭,s
        while (s->rchild)    // s指向被刪除結點的前驅,q指向s的父結點
        {
            q = s;
            s = s->rchild;
        }
        (*p)->data = s->data;    // 將前驅的值賦給被刪除結點位置
        // 當q的左孩子擁有右孩子的時候
        if (q != *p)
            q->rchild = s->lchild;    // 重接q的右子樹
        // 當q的左孩子沒有右孩子的時候,q=p,沒動
        else
            // 刪除點的左孩子接替刪除點的位置
            // 正因沒有右孩子,所以不會有影響
            q->lchild = s->lchild;
        free(s);
    }
    return TRUE;
}

刪除操作圖示
綠色的線表示結點的變動,藍色的數字表示中序遍歷順序,黃色表示結點


二叉排序樹總結

二叉樹雖然插入刪除比順序表簡單,但也存在問題,樹的結構是很影響速度的。
同樣的數據元素,不同的排列順序,會有不同的樹的結構:

查找結點99,左邊只需比較兩次,而右邊需要比較10次。
所以希望二叉排序樹是比較平衡的,深度與完全二叉樹相同,均爲[log2n]+1[\log_2n]+1,所以茶中的複雜度也爲O[logn]O[\log n]


JAVA實現二叉排序樹查找、插入、刪除

因爲java中,沒有Tree實例就不能操作,沒有指針,所以定義了一個變量,來標記該結點是否存在。

結點類

public class Tree {
    private int data;
    // 因爲java中,沒有Tree實例就不能操作,沒有指針
    // 所以再定義一個變量,來標記該結點是否存在
    public boolean exist=false;
    public Tree lchild, rchild;

    public int getData() {
        return data;
    }

    public void setData(int data) {
        exist = true;
        this.data = data;
    }

    public Tree() {
        lchild = rchild = null;
    }

    public Tree(int data) {
        exist = true;
        this.data = data;
        lchild = rchild = null;
    }

    public void equal(Tree t){
        if (t != null){
            this.exist = true;
            this.data = t.getData();
            this.lchild = t.lchild;
            this.rchild = t.rchild;
        } else {
            this.exist = false;
        }

    }
}

測試類

public class BaseBT {
    public static void main(String[] args) {
        int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
        Tree T = new Tree();
        // 創建二叉排序樹,就是一直插入的過程
        for (int i = 0; i < a.length; i++) {
            SearchOrInsert(T, a[i], null, false);
        }
        System.out.println("中序遍歷--------");
        // 中序遍歷即可得到升序
        TraverseTree(T);
        // 刪除結點
        int key = 52;
        System.out.println("刪除的值爲:" + key);
        DeleteNode(T, key);
        System.out.println("中序遍歷--------");
        TraverseTree(T);
    }

    private static void DeleteNode(Tree t, int key) {
        // 首先要找到位置
        if (t.exist == false){
            System.out.println("無此元素");
        } else {
            if (key == t.getData()){
                Delete(t);
            } else if (key < t.getData()){
                DeleteNode(t.lchild, key);
            } else {
                DeleteNode(t.rchild, key);
            }
        }
    }

    private static void Delete(Tree t) {
        // 如果左右孩子存在空,是最簡單的
        // 如果左孩子爲空,那麼可能就是隻有右孩子或都沒有
        if (t.lchild == null){
            t.equal(t.rchild);
        } else if (t.rchild == null){
            t.equal(t.lchild);
        } else {
            // 找前驅
            Tree q = t;
            // 先找一個左孩子
            Tree s = q.lchild;
            // 再一直找左孩子的右孩子
            while (s.rchild!=null){
                q = s;
                s = s.rchild;
            }
            // 此時可以確定t的新值
            t.setData(s.getData());
            // 此時要看s是否有右孩子,沒有就直接接上
            if (q == t){
                // 如果左孩子沒有右孩子,那麼t的值就是左孩子,所以去掉左孩子
                t.lchild = s.lchild;
            } else {
                q.rchild.equal(s.lchild);
            }
        }
    }

    private static void TraverseTree(Tree t) {
        if (t!=null && t.exist){
            TraverseTree(t.lchild);
            System.out.println(t.getData());
            TraverseTree(t.rchild);
        }
    }

    // 查詢插入一體化
    private static boolean SearchOrInsert(Tree t, int i, Tree f, boolean search){
        // 考慮根結點爲空的情況
        // tree不爲空但exits爲false,只有這一種情況
        if (t!=null && t.exist==false){
            System.out.println("插入了:" + i);
            t.setData(i);
            return false;
        }

        // t是樹的根結點,i是被查找元素,f是當前結點的父結點
        if (t == null){
            if (search){
                System.out.println("沒找到");
            } else {
                System.out.println("插入了:" + i);
                if (i > f.getData()){
                    f.rchild = new Tree(i);
                } else {
                    f.lchild = new Tree(i);
                }
            }
            return false;
        }
        // 找到了
        if (t.getData() == i){
            System.out.println("找到了");
            return true;
        }
        // 往右子樹走
        if (t.getData() < i){
            return SearchOrInsert(t.rchild, i, t, search);
        } else {
            return SearchOrInsert(t.lchild, i, t, search);
        }
    }
}

平衡二叉樹AVL

定義:平衡二叉樹是一種二叉排序樹,其中每一個結點的左子樹和右子樹的高度差至多等於1。

平衡因子BF:將二叉樹上結點的左子樹深度減去右子樹深度的值稱爲平衡因子BF。

例子

最小不平衡子樹:距離插入結點最近的,且平衡因子的絕對值大於1的結點爲根的子樹,稱爲最小不平衡子樹。

案例
插入了37,58的高度變成了2,BF也變成了2。


平衡二叉樹實現原理

思想:在構建二叉排序樹的過程中,每當插入一個結點時,先檢查是否因插入而破壞了樹的平衡性,若是,則找出最小不平衡子樹。再保持二叉排序樹特性的前提下,調整最小不平衡子樹中各結點之間的鏈接關係,進行相應的旋轉,使之稱爲新的平衡子樹。

案例:數組爲{3,2,1,4,5,6,7,10,9,8},最終結果爲下圖,步驟就不上了,太多了。

方法

  • 出現不平衡問題的時候要立即修正
  • 如果最小不平衡樹的根結點爲負數,該最小不平衡樹就左旋,正數就右旋
  • 如果最小不平衡樹的根結點和孩子結點的BF符號不一樣,就得調整到符號一樣,調整的方法可能是改變順序(11和12),也可能是旋轉(14和15)




平衡二叉樹實現算法

改進結點,添加BF因子

typedef struct BiTNode    // 結點結構
{
    int data;
    int bf;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

旋轉可行的原因
二叉排序樹的構建和元素的輸入順序有很大關係,而下面的旋轉操作並不像影響排序的正確性,所以旋轉相當於把輸入的順序重新排了,數還是那些數,中序遍歷起來也還是升序的,所以沒問題。

右旋

  • 讓原本根結點的左孩子作新的根結點,原本根結點作爲新根結點的右孩子,重點是,旋轉後的中序遍歷順序不能變。
  • BF在別的地方改變,所以不需要在這裏考慮BF
// 對以p爲根的二叉排序樹作右旋
// 之後p指向新的結點
void R_Rotate(BiTree *p)
{
    BiTree L;
    L = (*p)->lchild;
    // 可能存在,也可能不存在
    // 如果存在的話,爲了保證正確的的大小順序,具體看下圖
    (*p)->lchild = L->rchild;
    // 重點就在這,讓p的左孩子成爲新的根結點
    L->rchild = (*p);
    *p = L;
}

右旋中的排序理解
紅色爲BF,藍色爲中序順序,注意,此處的BF只是一個參考。
右旋

右旋案例

左旋

void L_Rotate(BiTree *p)
{
    BiTree R;
    R = (*p)->rchlid;
    (*p)->rchild = R->lchild;
    R->lchild = (*p);
    *p = R;
}

左平衡旋轉處理
思路:這裏已經知道是要處理左平衡,所以知道T的BF大於0,直接從根結點的左孩子下手,先判斷左孩子的BF,如果是同號,那麼做簡單的右旋並修改各個結點的BF即可;如果是異號,則以左孩子爲根結點左旋,變爲正BF,再對根結點右旋,同時修改各個結點的BF,這裏的BF修改還跟插在了哪顆子樹相關。

下圖算是比較清晰例子:

#define LH +1    // 左高
#define EH 0    // 等高
#define RH -1    // 右高
// 對T所指結點爲根的二叉樹作左平衡旋轉處理
// 結束時T指向新的根結點
void LeftBalance(BiTree *T)
{
    BiTree L, Lr;
    L = (*T)->lchild;    // 處理左平衡,所以直接左子樹
    // 判斷同號還是異號
    switch(L->bf)
    {
        case LH:    // 同號,新點插在了T的左孩子的左子樹上,做單右旋處理
            (*T)->bf = L->bf = EH;
            R_Rotate(T);
            break;
        case RH:    // 異號,插在了左孩子的右子樹上,做雙旋處理
            Lr = L->rchild;    // 左孩子的右子樹根
            // 判斷是在右子樹的何處,藉此修改各結點bf
            // 不過=0沒看懂
            switch(Lr->bf)
            {
                case LH:
                    (*T)->bf = RH;
                    L->bf = EH;
                    break;
                case EH:
                    (*T)->bf = L->bf = EH;
                    break;
                case RH:
                    (*T)->bf = EH;
                    L->bf = LH;
                    break;
            }
            Lr->bf = EH;
            L_Rotate(&(*T)->lchild);    // 對T的左子樹做左旋
            R_Rotate(T);    // 對T做右旋
    }
}

左平衡的三種case圖例
對應異號部分的三種情況,藍色爲中序順序,紅色爲BF,N爲新插入的點,在EH情況下,Lr就是N。

  1. EH
  2. LH
  3. RH

左平衡規律總結

  1. Lr會成爲新的根結點,且bf爲0。
  2. 因爲是左平衡且異號,所以變化很固定,先根結點的左孩子左旋,再根結點右旋。
  3. T和L的bf和孩子取決於,N是Lr的左孩子還是又孩子。如果是左孩子,那麼會小於根結點Lr,所以就會分配給L,所以此時L的bf=0,T的bf=-1。
  4. 如果N是Lr的右孩子,那麼會大於根結點Lr,就會分配給T,所以此時L的bf=1,T的bf=0。

主函數
思路

  • 先說插入:這是一個插入函數,每次插入調用一次。想插入就得先找到位置,所以該函數用了遞歸來尋找位置,使用InsertAVL(&(*T)->lchild...來遞歸查找,如果輸入的參數T變成了Null,說明找到了位置,即可在該指針處創建結點。
  • 再說平衡:由if (e<(*T)->data)這個判斷可知道,插入後緊接着就會檢查新插入結點的父結點的BF,並根據BF的值來判斷是否需要對父結點進行平衡並該BF操作,不需要的話就把父結點的BF改了,畢竟插入了,所以一定會變。如果原來的父結點的BF爲0的話,此時高度就會變,所以taller會變成TRUE,此時回到遞歸的上一次,即父結點爲T的時候,此時因爲taller變了,所以還要再判斷是否平衡。因此只要高度變了,就會從下往上根據判斷條件平衡。從下往上的話即不會漏掉,也可以在第一時間平衡。
  • 這裏面最近T是被插入的結點的父節點,然後T會一層一層再往上移動,然後層層改變BF。
  • 做了左、右平衡後,taller=False,此時就不需要再網上判斷了,所以說每插入一次,最多做一次平衡就行了
// 若不存在和e相同的,則插入並返回1,否則返回0
// 若插入後使二叉排序樹失去平衡,則作平衡旋轉處理
// taller反應是否長高
Status InsertAVL(BiTree *T, int e, Status *taller)
{
    if (!*T)
    {
        // 插入新結點,樹長高
        *T = (BiTree) malloc(sizeof(BiTNode));
        (*T)->data = e;
        (*T)->lchild = (*T)->rchild = NULL:
        (*T)->bf = EH;
        *taller = TRUE;
    }
    else
    {
        if (e == (*T)->data)
        {    // 已有,不再插入
            *taller = FALSE;
            return FALSE;
        }
        if (e < (*T)->data)
        {   // 在T的左子樹繼續搜索
            if (!InsertAVL(&(*T)->lchild, e, taller))
                // 插入失敗
                return FALSE;
            // 長高了,就得修改BF並考慮平衡問題
            if (taller)
            {
                switch((*T)->bf)
                {
                    case LH:    // 原來爲左高,左邊再加一個就不平衡了
                        LeftBalance(T);
                        *taller = FALSE;
                        break;
                    case EH:    // 原來一樣高
                        (*T)->bf = LH;
                        *taller = TRUE;
                        break;
                    case RH:
                        // 爲什麼這個也有False?
                        // 左右變平衡,說明是在短的一邊加的,所以高度不變
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                }
            }
        }
        else
        {
            if (!InsertAVL(&(*T)->rchild, e, taller))
                return FALSE;
            if (taller)
            {
                switch((*T)->bf)
                {
                    case LH:
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                    case EH:
                        (*T)->bf = RH;
                        *taller = TRUE;
                        break;
                    case RH:
                        LeftBalance(T);
                        *taller = FALSE;
                        break;
                }
            }
        }
    }
    return TRUE;
}

生成平衡二叉樹

int i;
int a[10] = {...};
BiTree T = NULL;
Status taller;
for (i=0; i<10; i++)
{
    InsertAVL(&T, a[i], &taller);
}

時間複雜度
查找、刪除、插入都是O[logn]O[\log n]


JAVA實現平衡二叉樹相關

結點類

public class BitNode {
    public int BF;
    public BitNode lchild, rchild;
    private int data;
    public boolean exist=false;

    public int getData() {
        return data;
    }

    public void setData(int data) {
        exist = true;
        this.data = data;
    }



    public BitNode() {
        lchild = rchild = null;
    }

    public BitNode(int data) {
        exist = true;
        this.data = data;
        lchild = rchild = null;
    }

    public void equal(BitNode bitNode){
        if (bitNode!=null)
        {
            exist = bitNode.exist;
            data = bitNode.data;
            BF = bitNode.BF;
            lchild = bitNode.lchild;
            rchild = bitNode.rchild;
        } else {
            exist = false;
        }
    }
}

方法類

public class AVLutils {
    public static void L_Rotate(BitNode T) {
        // T的分身
        BitNode L = new BitNode();
        L.equal(T);
        // T的右孩子上位新跟結點
        T.equal(T.rchild);
        L.rchild = T.lchild;
        T.lchild = L;
    }

    public static void R_Rotate(BitNode T) {
        // T的分身
        BitNode L = new BitNode();
        L.equal(T);
        // T的右孩子上位新跟結點
        T.equal(T.lchild);
        L.lchild = T.rchild;
        T.rchild = L;
    }

    public static void LeftBalance(BitNode T){
        // 做平衡,T.bf=1,是在變化之前傳進函數的
        BitNode L = T.lchild;
        // 檢查同號或異號
        switch (L.BF){
            // 同號的情況
            case 1:
                // 右轉T
                R_Rotate(T);
                T.BF = L.BF = 0;
                break;
            // 異號的情況,這裏要考慮Lr的符號
            case -1:
                BitNode Lr = L.rchild;
                switch (Lr.BF){
                    // 插在了Lr的右邊,此時新結點跟T走
                    case -1:
                        T.BF = 0;
                        L.BF = 1;
                        break;
                    // 插在了Lr的左邊,此時新結點跟L走
                    case 1:
                        T.BF = -1;
                        L.BF = 0;
                        break;
                    case 0:
                        T.BF = L.BF = 0;
                        break;
                }
                // 根據規律可得以下固定內容
                Lr.BF = 0;
                L_Rotate(L);
                R_Rotate(T);
        }
    }

    public static void RightBalance(BitNode T){
        BitNode R = T.rchild;
        switch (R.BF){
            // 此時同號爲負
            case -1:
                L_Rotate(T);
                T.BF = R.BF = 0;
            case 1:
                BitNode Rl = R.lchild;
                switch (Rl.BF){
                    case 0:
                        T.BF = R.BF = 0;
                    // 插在了左邊,跟着T
                    case 1:
                        T.BF = 0;
                        R.BF = -1;
                    case -1:
                        T.BF = 1;
                        R.BF = 0;
                }
                Rl.BF = 0;
                R_Rotate(R);
                L_Rotate(T);
        }
    }

    // 通過比較來找位置
    public static boolean InsertAVL(BitNode T, BitNode f, int e, Status sta){



        // 不能存在說明找到要插入的位置了,假設只要插入了就變高
        // 其實就是個檢查機制,有插入就檢查
        if (T==null){

            // 父結點不爲空
            if (e > f.getData()){
                f.rchild = new BitNode(e);
            } else {
                f.lchild = new BitNode(e);
            }
            System.out.println("插入結點:" + e);
            sta.taller = true;
            return true;
        } else {
            // f表示父結點,f和T都爲空說明是根結點
            if (f==null && T.exist==false){
                T.setData(e);
                System.out.println("插入結點:" + e);
                return true;
            }

            if (e == T.getData()){
                System.out.println("重複了:" + e);
                sta.taller = false;
                return false;
            } else if (e > T.getData()){
                // 往右子樹找
                // 如果插入失敗,則跳過
                if (!InsertAVL(T.rchild, T, e, sta)){
                    return false;
                }
                // 插入成功後,檢查高度
                if (sta.taller){
                    // 現在是插到了右邊
                    switch (T.BF){
                        case 0:
                            // 插入打破了平衡,說明最高高度變了
                            T.BF = -1;
                            sta.taller = true;
                            break;
                        case 1:
                            // 左右變平衡,說明是在短的一邊加的,所以最高高度不變
                            T.BF = 0;
                            sta.taller = false;
                            break;
                        case -1:
                            RightBalance(T);
                            sta.taller = false;
                            break;
                    }
                }
            } else {
                // 往左子樹找
                if (!InsertAVL(T.lchild, T, e, sta)){
                    return false;
                }
                if (sta.taller){
                    switch (T.BF){
                        case 0:
                            // 插入打破了平衡,說明最高高度變了
                            sta.taller = true;
                            T.BF = 1;
                            break;
                        case -1:
                            // 左右變平衡,說明是在短的一邊加的,所以最高高度不變
                            sta.taller = false;
                            T.BF = 0;
                            break;
                        case 1:
                            LeftBalance(T);
                            sta.taller = false;
                            break;
                    }
                }
            }
        }
        // 能走到這裏就說明插入成功了,否則在前面就返回false了
        return true;
    }

    public static void TraverseTree(BitNode T){
        if (T!= null){
            TraverseTree(T.lchild);
            System.out.println(T.getData());
            TraverseTree(T.rchild);
        }
    }
}

高度標記類

public class Status {
    public boolean taller;

    public Status(boolean taller) {
        this.taller = taller;
    }
}

測試類

public class main {
    public static void main(String[] args) {
        int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
        BitNode T = new BitNode();
        Status sta = new Status(false);
        for (int i = 0; i < a.length; i++) {
            AVLutils.InsertAVL(T, null, a[i], sta);
        }
        System.out.println("中序遍歷-------------");
        AVLutils.TraverseTree(T);
    }
}

多路查找樹(B樹)

多路查找樹(mutil-way search tree):其每一個結點的孩子數可以多與兩個,且每一個結點處可以存儲多個元素。
由於它是查找樹,所有元素之間存在某種特定的排序關係。

2-3樹

2-3:每一個結點都有兩個孩子(稱它爲2結點)或三個孩子(稱它爲3結點)

2結點:包含一個元素和兩個孩子,排序和二叉排序樹類似,但2結點要麼沒有孩子,要麼有兩個孩子。

3結點:包含一大一小兩個元素和三個孩子,左子樹包含小於較小元素的元素,中間子樹包含介於較小較大之間的元素,右子樹包含大於較大元素的元素。3結點要麼沒有孩子,要麼有兩個孩子。

性質:2-3樹中所有葉子都在同一層次。

2-3樹的插入

  1. 空樹插入2結點即可
  2. 插入元素到2結點中。如下圖所示,3介於1,4之間,將左下角的2結點1改爲3結點1、3。
  3. 插入元素到3結點中。此時3結點元素已經滿了,所以要求修改3結點的父結點,把父結點改造成3結點。

    4.插入元素到3結點的其他情況。如果父結點已經是3結點,那就繼續找父結點的父結點,直到找到2結點。

2-3樹的刪除

  1. 刪除3結點上的葉子結點,直接和刪除即可
  2. 刪除2結點的葉子結點,可能會破壞2結點的定義,有四種情況在後面介紹
  3. 所刪除的元素位於非葉子的分支結點,通常是將樹按中序遍歷後得到此元素的前驅或後繼,考慮讓他們補位

刪除2結點的葉子結點四種情況

  1. 雙親是2結點,且擁有3結點的右孩子
  2. 雙親是2結點,右孩子也是2結點
  3. 雙親是一個3結點
  4. 如果當前樹是一個滿二叉樹的情況,此時刪除任何一個結點都不會滿足2-3結點的定義

2-3-4樹

概念:2-3樹的拓展,包含了4結點的使用,包含大中小三個元素和四個孩子,也是要麼四個要麼沒有。然後根據三個數分成四個區間,對應區間的數分配到對應的子樹中。

案例
數組:{7,1,2,5,6,9,8,4,3}。
創建流程:

刪除流程,刪除順序是1、6、3、4、5、2、9:


B樹

B樹:B樹是一種平衡的多路查找樹,2-3樹和2-3-4樹都是B樹的特例。

B樹的階:結點最大的孩子數目。所以2-3樹是3階B樹,2-3-4樹是4階B樹。

一個m階的B樹有以下屬性

  • 如果根結點不是葉結點,則至少有兩棵子樹。
  • 每一個非根的分支結點都有k-1個元素和k個孩子,其中[m/2]km[m/2] \le k \le m;每一個葉結點n都有k-1個元素,其中[m/2]km[m/2] \le k \le m
  • 所有葉子結點都位於同一層次。

B+樹(沒看)

這個沒代碼也不知道如何用,沒耐心看,先跳過


散列表(哈希表)查找概述

散列技術:在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。

散列函數/哈希函數:把這種對應關係f稱爲散列函數,又稱哈希函數。

散列表/哈希表:採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱爲散列表或哈希表。

散列表查找步驟

  1. 根據散列函數計算地址,然後按地址存儲該記錄。
  2. 查找記錄時,根據同樣的散列函數計算記錄的地址,直接訪問地址。

適合場景:適合的求解問題是查找與給定值相等的記錄。

衝突key1key2\text{key}_1 \not = \text{key}_2的時候,卻又f(key1)=f(key2)f(\text{key}_1) = f(\text{key}_2),這種現象稱爲衝突,把這兩個關鍵字稱爲這個散列函數的同義詞。


散列函數的構造方法

散列函數的設計原則

  1. 計算簡單,計算時間不應超過其他查找技術與關鍵字比較的時間。
  2. 散列地址分佈均勻,讓散列地址均勻分佈在存儲空間中,保證存儲空間的有效利用。

直接定址法

取關鍵詞的某個線性函數值爲散列地址:

f(key)=axkey+b(ab)f(key) = a x key + b (a、b爲常數)
使用場景:需事先知道關鍵字分佈情況,適合查找表較小且連續的情況,由於這樣的限制,並不常用。

數組分析法

抽取一部分,再進行反轉、移位、疊加等操作。
使用場景:處理關鍵字位數較大的情況,如果事先知道關鍵字的分佈且關鍵字的若干位分佈較均勻,就可以考慮用這個方法。

平方取中法
假設關鍵字是1234,平方就是1522756,取中間三位是227,用作散列地址,也可以是275。
使用場景:不知道關鍵字的分佈,而位數又不是很大的情況。

摺疊法
從左到右分割成位數相等的幾部分,最後一部分位數不夠可以短些,然後將這幾部分疊加求和,並按散列表表長,取後記爲做散列地址。
比如關鍵字是9876543210,散列表表長爲3爲,分成四組,987、654、321、0,然後疊加求和987+654+312+0=1962,再求後三位得散列地址962。
此時還不能保證分佈均勻,可以摺疊後再相加,如變成789+654+123+0=1566,此時地址爲566。
使用場景:事先不需要知道關鍵字的分佈,適合關鍵字位數較多的情況。

除留餘數法
對於散列表長爲m的散列函數公式:

f(key)=keymodp(pm)f(key)=key mod p (p小於等於m)
mod是取模的意思,不僅可以對關鍵字直接取模,也可在摺疊、平方取中後再取模。
比如 29 mod 12 = 5,就放在下標爲5的位置:

使用經驗:若表長爲m,通常p爲小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。

隨機數法

f(key)=random(key)f(key)=random(key)

不同方法的考慮因素

  1. 計算地址所需時間
  2. 關鍵字的長度
  3. 散列表的大小
  4. 關鍵字的分佈情況
  5. 記錄查找的頻率

處理散列衝突的方法

開放定址法
一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
衝突了就用下面的公式找新地址:

fi(key)=(f(key)+di)MODm(di=1,2,3,...,m1) f_i(key) = (f(key) +d_i) MOD m (d_i=1,2,3,...,m-1)

上面是線性探測法,但是會出現本來不是同義詞(第一次f的時候得到的值不相同)卻要爭奪一個地址的情況,這種現象稱爲堆積,但是堆積會大大降低存入和查找的效率。

因此提出二次探測法,目的是爲了不讓關鍵字都聚集在某一塊區域:

fi(key)=(f(key)+di)MODm(di=12,12,22,22,...,q2,q2,qm/2) f_i(key) = (f(key) +d_i) MOD m (d_i=1^2,-1^2,2^2,-2^2,...,q^2, -q^2, q \le m/2)

還有一種隨機探測法,因爲可設定隨機數種子,所以隨機數不會重複:

fi(key)=(f(key)+di)MODm(di)f_i(key) = (f(key) +d_i) MOD m (d_i是一個隨機數列)

再散列函數法
準備多個散列函數一起用,每當發生散列地址衝突的時候,就換一個計算方法,這樣做消耗的時間比較多:

fi(key)=RHi(key)(i=1,2,...,k)f_i(key) = RH_i(key) (i=1,2,...,k)

鏈地址法
將所有關鍵字爲同義詞的記錄存儲在一個單鏈表中,我們稱這種表爲同義詞子表,在散列表中值存儲所有同義詞子表的頭指針。
該方法一定會爲元素提供地址,但是會多出查找時遍歷單鏈表的性能損耗。
例如集合{12,67,56,16,25,37,22,29,15,47,48,34},以12爲出書,得表:

公共溢出區法
創建一個地方專門存放衝突的關鍵字。

使用場景:適合衝突數據很少的情況,該結構對查找很友好。


散列表查找實現

定義散列表結構

#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 // 定義散列表長爲數組的長度
#define NULLKEY -32768
typedef struct HashTable
{
    int *elem;    // 元素存儲基址,動態分配數組
    int count;    // 當前數據元素個數
}
int m=0;    // 散列表表長,全局變量

散列表的初始化

Status Init HashTable(HashTable *H)
{
    int i;
    m = HASHSIZE;
    H->count = m;
    H->elem = (int *)malloc(m * sizeof(int));
    for (i=0; i<m; i++)
        H->elem[i] = NULLKEY;
    return OK;
}

散列函數

int Hash(int key)
{
    return key % m;    // 除留餘數法
}

插入操作
插入的關鍵字集合:{12、67、56、16、25、37、22、29、15、47、48、34}

void InsertHash(HashTable *H, int key)
{
    int addr = Hash(key);
    while (H->elem[addr] != NULLKEY)    // 不爲空,表衝突
        addr = (addr + 1) %m;    // 開放定址法的線性探測
    H->elem[addr] = key;    // 有空後插入關鍵字
}

查找記錄

Status SearchHash(HashTable H, int key, int *addr)
{
    *addr = Hash(key);
    while(H.elem[*addr] != key)    // 不相等則說明發生了衝突
    {
        *addr = (*addr + 1) % m;
        if(H.elem[*addr]==NULLKEY || *addr==Hash(key)
        {
            //如果一直沒找到到了空地址,或者地址又轉回來了
            return UNSUCCESS;    // 說明關鍵字不存在
        }
    }
    return SUCCESS;
}

散列表查找性能分析

如果沒有衝突的話複雜度就是O[1]O[1],但是衝突是無法避免的,平均查找長度取決於以下幾個因素:

  1. 散列函數是否均勻。
  2. 處理衝突的方法。性能比較:鏈地址法 > 二次探測法 > 線性探測法
  3. 散列表的裝填因子。裝填因子α\alpha=記錄個數 / 三列表長度。記錄越多,α\alpha越大,產生衝突的可能性就越大。

總結:通常將散列表的空間設置得比查找集合大,雖然浪費了一定的空間,但是換來查找效率的大大提升。


JAVA實現散列表查找

public class main {
    public static int HashSize = 12;
    public static int NullKey = 65535;
    public static void main(String[] args) {
        // 初始化存儲數組
        int[] save = new int[12];
        for (int i = 0; i < HashSize; i++) {
            save[i] = NullKey;
        }
        // 存放數組
        int[] a = {2, 4, 55, 22, 550};
        SaveIntoHash(save, a);
        // 查找數組
        System.out.println("------------開始查找----------");

        int[] b = {2, 4, 55, 22, 550, 11};
        for (int i = 0; i < b.length; i++) {
            SearchInHash(save, b[i]);
        }

    }

    private static void SearchInHash(int[] save, int key) {
        boolean flag = true;
        int add = getHash(key);
        while (save[add]!=key){
            add = (add + 1) % HashSize;
            // 如過轉了一圈都沒找到
            // 或者說add沒有再指向值,說明不是衝突而是不存在
            if (save[add]==NullKey || add==getHash(key))
            {
                System.out.println("數組中不存在:" + key);
                flag = false;
                break;
            }
        }
        if (flag){
            System.out.println("找到了:" + key + " 地址爲:" + add);
        }
    }

    private static void SaveIntoHash(int[] save, int[] a) {
        for (int i = 0; i < a.length; i++) {
            int add = getHash(a[i]);
            // 如果發生衝突的話
            while (save[add]!=NullKey){
                add = (add + 1) % HashSize;
            }
            save[add] = a[i];
            System.out.println("地址:" + add + " 值:" + a[i]);
        }
    }

    public static int getHash(int k){
        return k % HashSize;
    }
}

思維導圖

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章