資料結構與演算法分析 讀書筆記(樹)

myxs發表於2017-04-16

摘要:本節包含二叉查詢樹,AVL樹,伸展樹以及B樹

標籤: Data-Structures


注意,筆記不是講各種概念的,只是記錄一下個人學習過程中遇到的問題和實現程式碼。需要知道基本概念,理解各種樹的原理。

1 二叉查詢樹

struct TreeNode{
    int Elem;
    struct Treenode *Left,*Right;
};

SearchTree MakeEmpty(SearchTree T){
    if (T != NULL){
        MakeEmpty(T->Left);
        MakeEmpty(T->Right);
        free(T);
    }
    return NULL;
}

Position Find(int X, SearchTree T){
    if (T == NULL){
        return NULL;
    }
    if (X < T->Elem)
        return Find(X,T->Left);
    else if(X > T->Elem)
        return Find(X,T->Right);
    else
        return T;

}

//遞迴實現查詢最小值
Position FindMin(SearchTree T){
    if (T == NULL)
        return NULL;
    else if(T->Left == NULL)
        return T;
    else
        return FindMin(T->Left);
}

//非遞迴查詢最大值
Position FindMax(SearchTree T){
    if(T != NULL){
        while(T->Right != NULL)
            T  = T->Right;
    }
    return T;
}

SearchTree Insert(int X, SearchTree T){
    if (T == NULL){
        T = malloc(sizeof(struct TreeNode));
        T->Elem = X;
        T->Left = T->Right = NULL;
    }
    else if(X < T->Elem)
        T->Left = Insert(X,T->Left);
    else if (X>T->Elem)
        T->Right = Insert(X,T->Right);
    return T;
}

SearchTree Delete(int X,SearchTree T){
    Position P;
    if (T==NULL)
        return NULL;
    else if (X < T->Elem){
        T->Left = Delete(X,T->Left);
    }else if(X > T->Elem)
        T->Right = Delete(X, T->Right);
    else if (T->Left && T->Right){
        P = FindMin(T->Right);
        T->Elem = P->Elem;
        Delete(P->Elem,T->Right);
    }else{
        P = T;
        if (T->Left== NULL)
            T = T->Right;
        else if (T->Right == NULL)
            T = T->Left;
        free(P);
    }
    return T;
}

實現Delete時,有點不明白,在刪除只有一個子節點的 T 節點時,為什麼

T = T->Right;

後來仔細想了一下,紙上畫了一下,還是因為指標的本質導致的。

指標本質就是地址,當刪除子節點 T 時,父節點 parent 節點的某個指標域需要賦值,而這個值則是孫節點的地址.程式碼中 T 指標指向 兒子節點,此時 T 變數被賦值為兒子節點的地址,return T意味著返回兒子節點的地址。函式返回時,父節點parent的子樹指標域被賦值為孫節點的地址,變成了兒子節點。

感想:可以從二叉查詢樹的實現程式碼中看到很多遞迴呼叫,極大的簡化了實現。不小心指標和遞迴繞到。

2 AVL樹

AVL樹是帶有平衡條件的二叉查詢樹。在插入和刪除節點時,必須保持這個平衡條件-每個節點的左子樹和右子樹的高度最多相差為1。需要給每個節點加個附加域-高度域

sttuct AvlNode{
    int Elem;
    AvlTree *Left,*Right;
    int Height;
}

當插入節點後,需要更新插入節點處到根節點的路徑上的所有平衡資訊。如果破化了AVL樹的平衡性,需要進行旋轉來保持平衡條件。

當在某個節點 T 的某個子樹中插入節點後,高度不平衡。

不平衡會出現4種情況:

  1. T節點的左兒子的左子樹插入節點
  2. T節點的左兒子的右子樹插入節點
  3. T節點的右兒子的左子樹插入節點
  4. T節點的右子樹的右子樹插入節點

4種情況是2種映象情況的對稱,這裡只考慮左左和左右2種情況,可以分為單旋轉和雙旋轉。

單旋轉

雙旋轉

基本例程和二叉查詢樹一樣,不過多了幾個用於AVL樹旋轉的例程.

雙旋轉可以通過2次單旋轉實現,也可以通過直接調整指標。下面的旋轉例程採用了2種方式。

int Height(Position P){
    if (P == NULL)
        return -1;
    else
        return P->Height;
}

 int Max(int a,int b){
    return a>b?a:b;
}
//左旋轉
 Position SingleRotationWithLeft(Position K2){//情況 1
    Position 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),K2->Height) + 1;
    return K1;
}

//左雙旋轉
 Position DoubleRotationWithLeft(Position K3){//情況 2
//    K3->Left = SingleRotationWithRight(K3->Left);
//    return SingleRotationWithLeft(K3);
    Position K1, K2;
    K1 = K3->Left;
    K2 = K1->Right;
    K1->Right = K2->Left;
    K3->Left = K2->Right;
    K2->Left = K1;
    K2->Right = K3;
    K1->Height = Max( Height(K1->Left), Height(K1->Right) ) + 1;
    K3->Height = Max( Height(K3->Left), Height(K3->Right) ) + 1;
    K2->Height = Max( K1->Height, K3->Height ) + 1;
    return K3;
}

 Position DoubleRotationWithRight(Position K3){//情況 3
    K3->Right = SingleRotationWithLeft(K3->Right);
    return SingleRotationWithRight(K3);
}

Position SingleRotationWithRight(Position K2){//情況 4
    Position K1;
    K1 = K2->Right;
    K2->Right = K1->Left;
    K1->Left = K2;
    K2->Height = Max(Height(K2->Left),Height(K2->Right))+1;
    K1->Height = Max(K2->Height,Height(K1->Right))+1;
    return K1;
}

插入和刪除:

AvlTree Insert(int X, AvlTree T){
    if (T == NULL){
        T = malloc(sizeof(struct AvlNode));
        T->Elem = X;
        T->Left = T->Right = NULL;
        T->Height = 0;
    }else if (X < T->Elem){
        T->Left = Insert(X, T->Left);
        if (Height(T->Left) - Height(T->Right) == 2)
            if (X < T->Left->Elem)
                T = SingleRotationWithLeft(T);
            else
                T = DoubleRotationWithLeft(T);
    }else if ( X > T->Elem){
        T ->Right = Insert(X, T->Right);
        if (Height(T->Right) - Height(T->Left) == 2)
            if (X > T->Right->Elem)
                T = SingleRotationWithRight(T);
            else
                T = DoubleRotationWithRight(T);
    }
    T->Height = Max(Height(T->Left),Height(T->Right))+1;
    return T;
}

AvlTree Delete(int X, AvlTree T){
    Position P;
    if (T == NULL)
        return NULL;
    else if( X < T->Elem){
        T->Left = Delete(X, T->Left);
        if (Height(T->Right) - Height(T->Left) == 2)
            T = SingleRotationWithRight(T);
    }
    else if (X > T->Elem){
        T->Right = Delete(X, T->Right);
        if (Height(T->Left) - Height(T->Right) == 2)
            T = SingleRotationWithLeft(T);
    }else if(T->Left && T->Right){
        P = FindMin(T->Right);
        T->Elem = P->Elem;
        T->Right = Delete(P->Elem,T->Right);
    }else{
        P = T;
        if (T->Left == NULL)
            T = T->Right;
        else if (T->Right == NULL)
            T = T->Left;
        free(P);
    }
    return T;
}

在實現時,依次插入節點 1 2 3 4 5 6 7 16 17,節點17插入後,需要對7、16、17進行右單旋轉調整保持平衡,但由於對遞迴和指標不熟悉,未寫某個關鍵程式碼如下

 //錯誤程式碼
SingleRotationWithRight(T);

 //正確程式碼
T = SingleRotationWithRight(T);

直到單步除錯時,發現單旋轉沒有錯誤,但是函式返回後,T節點依然是7,而且丟失了右子樹,才反應過來調整後的樹的根沒有被指向。

遍歷:

void PrintTree(AvlTree T){
    if (T != NULL){
        PrintTree(T->Left);
        printf(" %d ",T->Elem);
        PrintTree(T->Right);
    }
}

3 伸展樹

伸展樹建立在二叉查詢樹的基礎上,不過為了避免每次操作最壞時間 O(N)。

通過將一個被訪問過的節點旋轉到根上,那麼對於原先處於深度比較深的其它節點在被訪問時,會降低訪問時間。區域性性原理(剛被訪問的內容下次可能仍會被訪問)。

將要查詢的節點自底向上旋轉,直至該節點成為樹根。

保證了在任意 M 次訪問時,最多花費 O(MlogN) 的執行時間,伸展樹的每次操作的攤還代價為 O(logN)。

很遺憾,伸展樹的思路實現還是很好理解的,但是在實現過程中,遇到了一個很嚴重的問題。

比如依次插入7 6 5 4 3 2 1,當查詢 1 時,只能將3 2 1進行一字型旋轉後,就不能進行下去了。

錯誤程式碼

Position Find(int X, SplayTree T){
    if (T == NULL || T->Elem == X)
        return T;
    else if (X < T -> Elem ){
        if( T->Left->Elem == X){//根的左子節點就是所要查詢的節點,則和根旋轉,單旋轉
            T->Left = T->Left->Right;
            T->Left->Right = T;
            return T->Left;
        }else if (X < T->Left->Elem){
            if( X == T->Left->Left->Elem)
                T = OneRotationWithLeft(T);
            else
                T->Left = Find(X,T->Left);
        }else if(X > T->Left->Elem){
            if( X == T->Left->Right->Elem){
                T = DoubleRotationWithLeft(T);
            }else
                T->Left = Find(X,T->Left);
        }
    }else if (X > T->Elem){
        if (T->Right->Elem == X){
            T->Right = T->Right->Left;
            T->Right->Left = T;
            return T->Right;
        }else if(X < T->Right->Elem){
            if (X == T->Right->Left->Elem){
                T = DoubleRotationWithRight(T);
            }else{
                T->Right = Find(X,T->Right);
            }
        }else if (X > T->Right->Elem){
            if ( X == T->Right->Right->Elem){
                T = OneRotationWithRight(T);
            }else{
                T->Right = Find(X,T->Right);
            }
        }
    }
    return T;
}

寫得很囉嗦,判斷很多種情況。其中幾個旋轉例程是針對4種情況的,Zig-zag和Zig-zig,以及對稱情況

//旋轉例程,分為Zig-zag和Zig-zig,即之字型旋轉和一字型旋轉
//左右雙旋轉
Position DoubleRotationWithLeft(Position G){
    Position P,X;
    P = G->Left;
    X = P->Right;

    P->Right = X->Left;
    X->Left = P;
    G->Left = X->Right;
    X->Right = G;

    return X;
}
//右左雙旋轉
Position DoubleRotationWithRight(Position G){
    Position P,X;
    P = G->Right;
    X = P->Left;

    G->Right = X->Left;
    X->Left = G;
    P->Left = X->Right;
    X->Right = P;

    return X;
}
//左一字型
Position OneRotationWithLeft(Position G){
    Position P,X;
    P = G->Left;
    X = P->Left;

    G->Left = P->Right;
    P->Left = X->Right;
    P->Right = G;
    X->Right = P;

    return X;
}
//右一字型
Position OneRotationWithRight(Position G){
    Position P,X;
    P = G->Right;
    X = P->Right;
    G->Right = P->Left;
    P->Left = G;
    P->Right = X->Left;
    X->Left = P;
}

反思自己的實現過程中的錯誤,幾個旋轉例程很容易實現,不過建議在紙上畫一下如何旋轉。關鍵是如何處理好將找到的節點不斷地進行回溯旋轉。

前面是自己寫的程式碼,下列是參考程式碼

也被折磨好幾天,最後參考了部落格

仔細看了一遍,再回來看看自己那程式碼實現太簡陋。

主要是給節點多附加了一個指向父節點的域

struct SplayNode{
    Tree parent; //該結點的父節點,方便操作
    ElementType val; //結點值
    Tree lchild;
    Tree rchild;
    SplayNode(int val=0) //預設建構函式
    {
        parent=NULL;
        lchild=rchild=NULL;
        this->val=val;
    }
};

其次,在處理節點時,通過劃分模組,邏輯很清晰。

void SplayTree(Tree &root,Tree node)
{
    while (root->lchild!=node && root->rchild!=node && root!=node) //當前結點不是根,或者不是其根的左右孩子,則根據情況進行旋轉操作
        up(root, node);
    if (root->lchild==node) //當前結點為根的左孩子,只需進行一次單右旋
        root=right_single_rotate(root, node);
    else if(root->rchild==node) //當前結點為根的右孩子,只需進行一次單左旋
        root=left_single_rotate(root, node);
}

//根據情況,選擇不同的旋轉方式
void up(Tree &root,Tree node)
{
    Tree parent,grandparent;
    int i,j;
    parent=node->parent;
    grandparent=parent->parent;
    i=grandparent->lchild==parent ? -1:1;
    j=parent->lchild==node ?-1:1;
    if (i==-1 && j==-1) //AVL樹中的LL型
        right_double_rotate(root, node);
    else if(i==-1 && j==1) //AVL樹中的LR型
        LR_rotate(root, node);
    else if(i==1 && j==-1) //AVL樹中的RL型
        RL_rotate(root, node);
    else                    //AVL樹中的RR型
        left_double_rotate(root, node);
}

**總結:

        1 遺憾自己沒有寫出伸展樹
     2 小Tip:當覺得自己對二叉查詢樹和AVL樹有了一定程度瞭解後,建議嘗試實現伸展樹**

4 B樹

B樹基本操作,查詢,插入和刪除

  • 查詢類似於二叉查詢樹,不過不是二路,而是多路;一趟查詢,要麼找到關鍵字的位置,要麼能確定關鍵字所在的子樹的指標
  • 插入關鍵字:按照查詢的思路,從根節點開始查詢,遇到節點的關鍵字已滿2M-1,需要分裂出新節點,每個節點包含M-1個節點,將中間節點移到父節點,在2個節點指標之間。為何是M-1,因為B樹的性質保證B樹除根節點外的內部節點的關鍵字數必須介於2*M-1和M-1之間,M稱為B樹的階
  • 刪除關鍵字:分為幾種情況 1 : 當前T節點為葉子節點,關鍵字key在T節點中,直接刪除 2 : 當前T節點為內部節點,關鍵字key在T節點中,根據子節點關鍵字的數目和M-1比較,決定將關鍵字key左右子樹中的一個關鍵字替換key,或者需要合併節點 3 : 關鍵字不在當前內部節點T中,需要確定可能包含關鍵字key的子樹節點。再根據子樹關鍵字數目,確定是否需要從左右兄弟節點中移動一個節點替代父節點中關鍵字,把父節點中被替代的關鍵字放在被刪除的位置上

在實現的過程中,一個關鍵問題就是插入要保證不會滿,否則分類;刪除要保證節點的關鍵字數目不能低於M-1,否則破壞B樹性質。

B樹以及相關變體,關鍵在插入關鍵字判斷是否需要分裂節點,向上回溯直到滿足B樹性質,刪除關鍵字的節點是否需要合併。

此部分參考部落格以及演算法導論18章

struct BNodeRecord{
  int num;//關鍵字個數
  int leaf;// 1 表示葉子 0 表示內部節點
  int key[2*M - 1];
  BNode c[2*M];
};

建立B樹

BTree CreateBTree(){
    BTree T;
    T = malloc(sizeof(struct BNodeRecord));
    T->num = 0;
    T->leaf = 1;
    return T;
}

在T節點中查詢關鍵字key的索引

BNode BTreeSearch(BTree T, int key, int *index){
    int i = 0;
    while(i < T->num && T->key[i] < key)
        i++;
    if (i < T->num && T->key[i] == key){
        *index = i;
        return T;
    }
    else if ( T->leaf == 1){
        printf("no key in BTree\n");
        return NULL;
    }
    return BTreeSearch(T->c[i], key, index);
}

分裂節點

BTree BTreeSplitChild(BTree T, int index){
    BNode Y,Z;//Y表示滿節點,Z表示新節點
    Y = T->c[index];
    Z = malloc(sizeof(struct BNodeRecord));
    Z-> num = M - 1;
    Y-> num = M - 1;
    Z->leaf = Y->leaf;
    //關鍵字轉移
    for (int i = M; i < 2*M-1; ++i) {
        Z->key[i-M] = Y->key[i];
    }
    //如果是內部節點則進行指標轉移
    if (!Y->leaf){
        for (int j = M; j < 2*M; ++j) {
            Z->c[j-M] = Y->c[j];
        }
    }

    for(int k = T->num-1; k>= index; k--){
        T->key[k+1] = T->key[k];
    }
    for (int l = T->num; l >= index+1; ++l) {
        T->c[l+1] = T->c[l];
    }
    T->key[index] = Y->key[M-1];
    T->c[index+1] = Z;
    T->num++;
    return T;
}

插入

BTree BTreeInsert(BTree T, int key){
    if(T->num == 2*M - 1){
        BNode S;
        S = malloc(sizeof(struct BNodeRecord));
        S->num = 0;
        S->leaf = 0;
        S->c[0] = T;
        T = BTreeSplitChild(S,0);
        BTreeInsertNotFull(T,key);
    }else
        BTreeInsertNotFull(T,key);
}

刪除

BTree BTreeDelete(BTree T, int key){
    int index, i, flag;
    //在T節點中查詢關鍵字是否存在,在的話,返回其下標index,
    index = BTreeSearchIndex(T, key, &flag);
    //falg標誌T->key[index] == key,即節點T中有關鍵字key

    /* 第一種情況:關鍵字在葉子節點中,直接刪除它 */
    if (T->leaf == 1 && flag == 0){
        //flag=0表示T節點中有關鍵字,且T是葉子節點
        //把index後面的所有關鍵字向前移動一位,關鍵字數目減1
        memmove(&T->key[index], &T->key[index+1], sizeof(int) * (T->num - index -1));
        T->num--;
        return T;
    }else if (T->leaf && flag != 0){
        //關鍵字不在T節點中
        return T;
    }

    /* 第二種情況:關鍵字在內部節點 T 中,根據3種情況- T 的子節點的關鍵字數目來刪除和移動*/

    if (flag != 0){
        // Y :T->key[index]的前一個指標所指向的子節點,Z :T->key[index]的後一個指標所指向的子節點
        //                T->key[index](關鍵字)
        //    T->c[index](T中指標)       T->c[index+1](T中指標)
        //      |                              |
        //      Y                              Z
        BNode Y, Z;
        Y = T->c[index];
        Z = T->c[index + 1];

        // a : Y中至少有M個關鍵字,在Y子樹中找到key的前驅關鍵字key',遞迴刪除key',將key'移到key中;
        if (Y->num >= M){
            int pre_key = Y->key[Y->num - 1];
            //Y子樹中刪除pre_key
            T->c[index] = BTreeDelete(Y, pre_key);
            T->key[index] = pre_key;
            return T;
        }

        // b : Y中關鍵字數目低於M個,對稱地將 Z子樹 中的第一個節點移動到T->key[index]中
        if (Z->num >= M){
            int next_key = Z->key[0];
            //Z子樹中刪除next_key
            T->c[index+1] = BTreeDelete(Z,next_key);
            T->key[index] = next_key;
            return T;
        }

        // c :  Y和Z每一個關鍵字數目都是M - 1,則合併Y、Z以及關鍵字key,在Y中遞迴刪除關鍵字Key
        if( (Y->num == M-1) && (Z->num == M-1)){
            //將關鍵字key放在Y和Z關鍵字之間
            Y->key[Y->num++] = key;
            //關鍵字複製
            memmove(&Y->key[Y->num], &Z->key[0], sizeof(int) * Z->num);
            //指標複製
            memmove(&Y->c[Y->num], &Z->key[0], sizeof(struct BNodeRecord) * (Z->num + 1));
            Y->num += Z->num;

            //判斷T節點刪除一個關鍵字後是否為空,是則Y子節點作為當前的返回節點
            if (T->num > 1){
                memmove(&T->key[index], &T->key[index+1], sizeof(int) * (T->num - index - 1));
                memmove(&T->c[index+1], &T->c[index+2], sizeof(struct BNodeRecord)*(T->num - index - 1));
                T->num--;
            }else{//為空,釋放節點
                free(T);
                T = Y;
            }
            free(Z);
            BTreeDelete(Y, key);
            return T;
        }
    }

    // 第三種情況 : 關鍵字key不在內部節點T中,如果key關鍵字在T的子節點T->c[index]中,根據子節點的關鍵字數目是否低於M,是則需要父節點中一個關鍵字移動到兄弟節點子節點T->c[index]中,再把兄弟節點中一個關鍵字移動到父節點,
    BNode child;//可能含有關鍵字的子節點
    if ((child = T->c[index]) && child->num == M-1){

        // 1:child子節點中含有M-1個關鍵字,相鄰左或者右兄弟節點中含有至少M個節點,先將父節點中移一個關鍵字到child節點中,再將兄弟節點中一個關鍵字移到父節點中

        // 先假設左兄弟節點至少有M個節點
        BNode sibling;
        if ( (index > 0) && (sibling = T->c[index-1]) && sibling->num >= M){
            //child子節點關鍵字和指標向後移動一位,父節點關鍵字放入child關鍵字下表0的位置
            memmove(&child->key[1],&child->key[0], sizeof(int) * child->num);
            memmove(&child->c[1],&child->c[0], sizeof(struct BNodeRecord) * (child->num + 1));
            child->key[0] = T->key[index-1];
            T->key[index-1] = sibling->key[sibling->num-1];
            child->c[0] = sibling->c[sibling->num];

            child->num++;
            sibling->num--;
        }else
        //右兄弟節點至少有M個節點
        if((index<T->num) && (sibling = T->c[index+1]) && (sibling->num >= M)){
            child->key[child->num++] = T->key[index];
            T->key[index] = sibling->key[0];
            child->c[child->num] = sibling->c[0];
            sibling->num--;
            memmove(&sibling->key[0],&sibling->key[1], sizeof(int) *(sibling->num));
            memmove(&sibling->c[0],&sibling->c[1], sizeof(struct BNodeRecord) * sibling->num+1);

        }

        // 2 :左右兄弟節點關鍵字數目都是M-1,合併其中一個兄弟節點和child節點

        //合併child節點和右兄弟節點
        else if ((index<T->num) && (sibling = T->c[index+1]) && (sibling->num == M-1)){
            child->key[child->num++] = T->key[index];
            memmove(&child->key[child->num],&sibling->key[0], sizeof(int) * sibling->num);
            memmove(&child->c[child->num], &sibling->c[0], sizeof(struct BNodeRecord) * (sibling->num + 1));
            child->num +=sibling->num;

            if (T->num > 1){
                memmove(&T->key[index],&T->key[index+1], sizeof(int) * (T->num - index - 1));
                memmove(&T->c[index+1],&T->c[index+2], sizeof(struct BNodeRecord) * (T->num-index -1));
                T->num--;
            }else{
                free(T);
                T= child;
            }
            free(sibling);
        }else if((index>0) && (sibling = T->c[index-1]) && (sibling->num == M-1)){
            sibling->key[sibling->num++] = T->key[index - 1];
            memmove(&sibling->key[sibling->num], &child->key[0], sizeof(int) * child->num);
            memmove(&sibling->c[sibling->num], &child->c[0], sizeof(struct BNodeRecord) * (child->num + 1));
            sibling->num += child->num;

            if (T->num - 1 > 0)
            {
                memmove(&T->key[index - 1], &T->key[index], sizeof(int) * (T->num - index));
                memmove(&T->c[index], &T->c[index + 1], sizeof(struct BNodeRecord) * (T->num - index));
                T->num--;
            }
            else
            {
                free(T);
                T = sibling;
            }

            free(child);

            child = sibling;
        }

    }

    BTreeDelete(child,key);
    return T;
}

給非空節點插入

BTree BTreeInsertNotFull(BTree T, int key){
    int i = T->num - 1;
    if (T->leaf){
        while(i>=0 && T->key[i]>key){
            T->key[i+1] = T->key[i];
            i--;
        }
        T->key[i+1] = key;
        T->num++;
    }else{
        while(i>=0 && T->key[i]>key)
            i--;
        i++;
        if (T->c[i]->num == 2*M-1){
            BTreeSplitChild(T,i);
            if (key > T->key[i])
                i++;
        }
        BTreeInsertNotFull(T->c[i],key);
    }
    return T;
}

查詢節點T是否包含關鍵字key

int BTreeSearchIndex(BTree T, int key, int *flag){
    int i;
    for (int i = 0; i < T->num && (*flag = T->key[i] - key) < 0; ++i) {
        ;
    }
    return i;
}

總結: B樹很不好理解,花了好幾天,從概念上理解,在細節上注意,最好每一步都在圖上畫,尤其是刪除部分,許多情況,結構複雜易出錯。此外,最讓我頭疼的是,那個移動關鍵字很是小心,最後發現,完全可以提取出來作為一個函式,不用每次都重新實現。

相關文章