1. 程式人生 > >第1章第2節 線性表的鏈式表示(1)

第1章第2節 線性表的鏈式表示(1)

由於順序表的插入,刪除操作需要移動大量的元素,影響了運算效率,由此線性表的鏈式儲存便應運而生。鏈式儲存線性表時,邏輯上連續的元素物理結構上不需要連續,它們彼此可以通過“鏈”建立起資料元素之間的邏輯關係,因此對於線性表的插入,刪除操作並不需要移動元素,只需修改指標即可。

一.單鏈表的定義

線性表的鏈式儲存又稱為單鏈表,它是通過一組任意的儲存單元來儲存線性表中的資料元素。為了建立起資料元素之間的線性關係,對每個連結串列結點,除了存放元素自身的資訊外,還需要存放一個指向其後繼的指標。
單鏈表結點的結構如下圖所示,其中,data為資料域,存放資料元素;next為指標域,存放其後繼結點的地址。

定義

對於每一個結點的描述如下:

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

利用單鏈表在解決順序表需要大量的連續儲存空間的缺點的同時,也引入了一些不可避免的缺點。比如因為需要額外的指標域,因此需要額外的儲存空間;由於單鏈表是離散的分佈在儲存空間中,所以單鏈表不能完成隨機存取的操作。
為了方便標識一個單鏈表,我們一般需要引入頭指標來操作整個單鏈表。此外,為了統一增加和刪除元素的操作,我們一般會在單鏈表的第一個結點之前附加一個結點,稱為頭結點。頭結點的指標域指向單鏈表的第一個元素結點。
注:這裡應該注意區分頭指標和頭結點。而不管單鏈表有沒有頭結點,頭指標總是指向單鏈表的第一個結點。簡單說就是如果單鏈表有頭結點,那麼頭指標將指向頭結點;如果單鏈表沒有頭結點,頭指標將指向單鏈表的第一個結點。此處我們應該注意到一般情況下頭結點內不儲存任何資訊。這裡說明下,如果後面的例題中沒有具體說明,一般都是建立有頭結點的單鏈表。

單鏈表

引入頭節點後,可以帶來兩個優點:

  • 由於開始節點的位置被存放在頭節點的指標域中,所以在連結串列的第一個位置上操作與表其他位置上的操作一致,無需進行特殊處理。
  • 無論連結串列是否為空,其頭指標都是指向頭節點的非空指標(在空表中,頭節點的指標域為空),因此也使得空表和非空表的處理方式變得統一。

二.單鏈表基本操作的實現

2.1建立單鏈表

2.1.1頭插法建立單鏈表

該方法中,首先建立一個具有頭結點的空單鏈表,然後每生成一個讀取到資料的新節點,就將其放置到頭結點之後。如下圖所示:
頭插法建立單鏈表

演算法描述如下:

typedef int ElemType;
LinkList CreateLink(LNode *head)
{
    LNode *s;
    ElemType x;
    scanf
("%d",&x); while(x!=999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=head->next; head->next=s; scanf("%d",&x); } return head; }

注:採用頭插法建立單鏈表,讀入資料的順序與生成的連結串列中的元素的順序剛好是相反的。
每個結點插入的時間複雜度為O(1),設單鏈表長為n,則總的時間複雜度為O(n)

2.1.2尾插法建立單鏈表

該方法中同樣首先建立一個具有頭結點的空單鏈表,然後每生成一個讀取到資料的新節點,就將它插入到表尾;為了達到這樣的目的,必須增加一個尾指標r,使其始終指向當前連結串列的尾結點。如下圖所示

尾插法建立單鏈表

演算法描述如下

LinkList CreatLink(LNode *head)
{   
    LNode *s;
    LNode *r=head;
    ElemType x;
    scanf("%d",&x);
    while(x!=999){
        s=(LNode*)malloc(sizeof(LNode));
        s->data=x;

        r->next=s;
        r=s;

        scanf("%d",&x);
    }
    s->next=NULL;
    return head;    
}

注:因為附加設定了一個指向表尾的尾指標r,因此每個結點插入的時間複雜度同樣為O(1),設單鏈表長為n,則總的時間複雜度為O(n)

2.2按序號查詢結點值

從單鏈表的第一個結點出發,順著指標next域逐個從上往下搜尋,直到找到第i個結點為止,否則返回最後一個結點指標域NULL。
演算法描述如下:

LNode* GetElem(LNode *head, ElemType i)
{
    LNode *p=head->next;
    int j=1;
    if(i==0){
        printf("The Link is empty!\n");
        return head;
    }
    if(i<=0){
        printf("The postion is illegal!\n");
        return NULL;
    }
    while(p){
        if(j==i){
            printf("Find success!\n");
            break;
        }
        p=p->next;
        j++;
    }
    return p;   
}

注:按序號查詢的時間複雜度為O(n)

2.3按值查詢結點值

從單鏈表的第一個結點開始,由前往後依次比較各結點資料域的值,若某結點資料域的值等於給定值x,則返回該結點的指標。若整個單鏈表中沒有這樣的結點,則返回NULL。
演算法描述如下:

LNode* GetElem(LNode *head, ElemType x)
{
    LNode *p=head->next;
    if(head==NULL){
        printf("The LinkList is empty!\n");
        return head;
    }
    while(p){
        if(p->data==x){
            printf("Find the number!\n");
            return p;
        }
        p=p->next;  
    }
    printf("Not Find the number!\n");
    return NULL;
}

注:按值查詢的時間複雜度為O(n)

2.4插入結點

2.4.1插入後繼結點

插入操作是將值為x的新結點插入倒單鏈表的第i個位置上。先檢查插入位置的合法性,然後找到待插入位置的前驅結點,即第i1個結點,再在其後插入新的結點。
演算法首先需要呼叫GetElem(L,i-1)查詢第i1個結點。假設返回的第i1個結點為*p,然後令新結點*s的指標域指向*p的後繼結點,再令結點*p的指標域插入新的結點*s。其操作過程如下圖所示:

插入後繼結點

演算法描述如下:

LNode* GetElem(LNode *head, int i)
{
    if(i==0){
        printf("The LinkList is empty!\n");
        return head;
    }
    if(i<=0){
        printf("The LinkList is illegal!\n");
        return NULL;
    }

    LNode *p=head->next;
    int j=1;
    while(p){
        if(j==i){
            printf("Find it!The postion is %dth\n", j);
            break;
        }
        j++;
        p=p->next;
    }
    return p;
}

void InertElem(LNode *p, ElemType x){
    LNode *s;
    s=(LNode*)malloc(sizeof(LNode));
    s->data=x;

    s->next=p->next;
    p->next=s;
}

演算法中,時間的主要開銷是查詢第i1個元素,時間複雜度為O(n)。若是在給定的結點後面插入新結點,則時間複雜度僅為O(1)

2.4.1插入前驅結點

在方法一中,我們可以在指標p指向的結點後面插入新的結點s,但是有時如果我們需要在指標p指向的結點前面插入新的結點s時,上述演算法明顯是辦不到的。
但是如果我們換個思路,將指標p指向的結點和s結點它們之間的資料域做一次交換,依舊將s結點插入到指標p指向的結點後面,如此我們便將前插操作變為了向指標p指向的結點的後插操作,並且在邏輯上仍舊滿足條件。
演算法描述如下:

void InertElem(LNode *p, ElemType x){
    LNode *s;
    s=(LNode*)malloc(sizeof(LNode));
    s->data=x;

    ElemType temp;
    temp=p->data;
    p->data=s->data;
    s->data=temp;

    s->next=p->next;
    p->next=s;  
}

注:同方法一相同,時間的主要開銷是查詢第i1個元素,時間複雜度為O(n)。若是在給定的結點後面插入新結點,則時間複雜度僅為O(1)

2.5刪除結點

2.5.1刪除後繼結點

刪除結點操作即將單鏈表的第i個結點刪除。先檢查刪除位置的合法性,然後查詢表中第i1個結點(即將要被刪除結點的前驅結點),然後在刪除第i個結點。其操作過程如下圖所示:

刪除結點

假設我們要刪除指標q指向的結點,那麼我們首先通過遍歷所有結點找到指標q指向的結點的前驅結點p,為了實現演算法,我們只需修改指標p的指標域,將指標p的指標域next直接指向指標q指向的結點的指標域next所指的下一個結點便可。
演算法描述如下:

void DelLNode(LNode *head, int i)
{
    LNode *p=head;
    LNode *q=head->next;
    while(i){
        p=q;
        q=q->next;
        i--;
    }
    p->next=q->next;
    free(q);
}

注:刪除操作中,時間的主要開銷是查詢第i1個元素,因此時間複雜度為O(n)。若是刪除給定結點的後繼結點,則時間複雜度為O(1)

2.5.2刪除自身結點

有時我們需要刪除指標所指向的自身結點(比如指標p指向的結點),此時如果繼續使用上述方法明顯是不可能的。我們採用與2.4.1插入前驅結點相似的方法,將指標p所指向的結點的資料域與指標q所指向的結點的資料域進行一次交換(因為是一次刪除操作,我們只需要將指標q指向的結點的資料域直接賦值為指標p指向的結點的資料域便可),這樣,我們就又變成了刪除指標q指向的結點的操作。
演算法描述如下:

void DelList(LNode *head, int i)
{
    LNode *p=head;
    LNode *q=p->next;

    while(i){
        p=q;
        q=q->next;
        i--;
    }

    p->data=q->data;

    p->next=q->next;
    free(q);
}

注:與上述演算法一樣,刪除操作中,時間的主要開銷是查詢第i個元素,因此時間複雜度為O(n)。若是刪除給定的結點,則時間複雜度為O(1)

2.5求連結串列長度

求表長實際上就是計算單鏈表中資料結點(不含頭節點)的個數。為了達到這個目的,我們只需對連結串列進行一次遍歷,同時設定計數器對每次訪問到的結點計數便可。
演算法描述如下:

int LenLink(LNode *head)
{
    int cnt=0;
    LNode *p=head->next;
    while(p){
        p=p->next;
        cnt++;
    }
    return cnt;
}

注:遍歷操作中需要訪問所有結點,因此時間複雜度為O(n)