1. 程式人生 > >資料結構(三):線性表

資料結構(三):線性表

一、線性表及其邏輯結構

1、線性表的定義

線性表是具有相同特性的資料元素的一個有限序列。

該序列中所含的元素個數叫做線性表的長度,用 n表示(n>=0)。當 n=0時,表示線性表是一個空表,即表中不包含任何資料元素。

線性表中的第一個元素叫做表頭元素,最後一個元素叫做表尾元素。

線性表在位置上是有序的,即第 i個元素處在第 i-1個元素後面、第 i+1個元素前面,這種位置上的有序性就是線性關係,所以線性表是一個線性結構。

2、線性表的抽象資料型別描述

ADT List{
    //資料物件 括號內的 i是下標
    D = {a(i)|1=<i<=n, n>=0, a(i)屬於 ElemType型別}
    //資料關係
    R = {<a(i), a(i+1)>, a(i+1)屬於 D, i=1,···,n-1}
    //基本運算
    InitList(&L) //初始化線性表  該方法會將 L初始化為一個空的線性表
    DestroyList(&L) //銷燬線性表  該方法會釋放 L所佔的儲存空間
    ListEmpty(L) //確定線性表是否為空  若 L為空表則返回真,否則返回假
    DisPlayList(L) //輸出線性表  按順序輸出線性表中各個節點的值
    GetElem(L, i, &e) //獲取線性表中第 i個數據元素的值  並將該資料元素的值賦給 e(1=<i<=ListLength(L))
    LocateElem(L, e)  //返回 L中第一個和 e的值相等的資料元素的序號
    ListInsert(&L, i, e)  //在 L的第 i個元素之前插入 e,L的長度增加 1
    ListDelete(&L, i, &e)  //刪除 L的第 i個元素並用 e返回其值
}

二、線性表的順序儲存結構

1、順序表

線性表的順序儲存結構就是:把線性表中的資料元素按照其邏輯順序一次儲存到計算機儲存器中指定儲存位置的一塊連續的儲存空間中。這樣,表頭元素的儲存位置就是指定儲存空間的首地址,第 i個元素就緊挨著第 i+1個元素前面。

假設線性表中的資料元素為 ElemType,則每個資料元素所佔的儲存空間就是 sizeof(ElemType),整個線性表佔的儲存空間就是 n*sizeof(ElemType),其中 n是線性表的長度。

在 C/C++中陣列中相鄰的兩個元素在記憶體中的位置也是相鄰的,和順序表的儲存結構剛好一樣,所以在 C/C++中我們可以使用陣列來實現順序表。

2、順序表的基本演算法實現

我們先定義順序表和順序表中的資料元素的型別:

#define INIT_LIST_LEN 50
#define INCREACEMENT_NUM 20
#define ERROR -1
#define OK 1
#define NULL_VALUE -51

typedef char ElemType;
typedef struct {
    ElemType *data;
    int length;
    int max_size;
}LinerList;

這裡 ElemType是順序表裡的資料元素型別,LinerList是順序表的型別。

在順序表中,我們定義了一個 ElemType型別的陣列指標 data,這個指標指向一塊用來儲存 ElemType型別資料的儲存空間。

在使用中我們可以把 data直接看作一個 ElemType型別的陣列,不過和陣列不同的是 data的大小(相當於陣列的長度)是可以動態改變的。

length就是順序表當前的長度,max_size就是順序表當前能夠儲存的最大元素數量。

當 length要大於 max_size時,我們會通過 realloc函式來為 data分配一塊更大的記憶體(大小是原來的大小加上 INIT_LIST_LEN再乘以 sizeof(ElemType))。

接下來我們來定義順序表的基本運算:

void InitList(LinerList*& L) {
    L = (LinerList*)malloc(sizeof(LinerList));
    L->data = (ElemType*)malloc(INIT_LIST_LEN * sizeof(ElemType));
    L->max_size = INIT_LIST_LEN;
    L->length = 0;
}

void DestroyList(LinerList*& L) {
    free(L->data);
    free(L);
    L = NULL;
}

bool ListEmpty(LinerList L) {
    //當 L未初始化時 L.length小於 0
    //當 L初始化但為空時 L.length等於 0
    //兩種情況都認為 L為空
    if (L->length <= 0) {
        return true;
    }
    else {
        return false;
    }
}

int ListLength(LinerList L) {
    //當 L未初始化時返回 -1
    if (L->length >= 0) {
        return L->length;
    }
    else{
        return ERROR;
    }
}

void DisplayList(LinerList L) {
    if (L->length < 0) {
        printf("This list is not inited.\n");
    }
    else if(L->length == 0){
        printf("This list is empty.\n");
    }
    else {
        for (int i = 1; i <= L->length; i++) {
            printf("ElmType %2d: %c\n", i, L->data[i]);
        }
    }
}

void GetElem(LinerList L, int i, ElemType* e) {
    if (i <= 0 || i > L.length) {
        printf("invalid integer i.\n");
    }
    else{
        *e = L.data[i];
    }
}

int LocateElem(LinerList L, ElemType e) {
    //包含對錯誤的處理
    for (int i = 1; i <= L.length; i++) {
        if (L.data[i] == e) {
            return i;
        }
    }
    return 0;
}

ListInsert:

int ListInsert(LinerList* L, int i, ElemType e) {
    if (L->length == L->max_size) {
        L->data = (ElemType*)realloc(L->data, (L->max_size + INCREACEMENT_NUM) * sizeof(ElemType));
        L->max_size += INCREACEMENT_NUM;
    }

    if (i > L->length + 1 || i <= 0) {
        return ERROR;
    }
    else {
        for (int k = L->length; k >= i; k--) {
            L->data[k + 1] = L->data[k];
        }
        L->data[i] = e;
        L->length++;
    }

    return OK;
}

在插入資料元素之前,我們要先檢查順序表長度是否已達到最大容量,如果順序表已經達到最大長度,我們用 realloc重新分配一塊更大的記憶體,並且順序表的最大容量 max_size增加 INCREACEMENT_NUM(也就是 20)。

若順序表還沒達到最大容量,我們先對插入位置 i的有效性進行檢查。

顯然當 i小於或等於 0和 i大於順序表長度加 1的時候是無效的。

當確定 i是有效的時候,我們才執行插入操作。

首先我們先把第 i個元素及其後面的所有元素向後移一個位置,然後再將資料元素 e插入到第 i個位置,並將順序表的長度加 1.

ListDelete:

int ListDelete(LinerList* L, int i, ElemType* e) {
    if (i > L->length || i <= 0) {
        return ERROR;
    }
    else {
        *e = L->data[i];
        for (int k = i; k < L->length - 1; k++) {
            L->data[k] = L->data[k + 1];
        }
        L->data[L->length] = NULL_VALUE;
        L->length--;
    }
}

和插入資料元素時一樣,我們在執行刪除操作之前先檢查 i的有效性,當 i的值有效時我們才進行下一步執行刪除操作。

在刪除目標資料元素之前,我們先將它的值賦給資料元素 e,然後再將其在順序表中刪除,並且順序表的長度減 1。

在刪除一個數據元素的時候,我們跟在該資料元素之後的所有資料元素向前移一個位置,然後將最後一個數據元素的值賦值為空值,最後將順序表的長度減一。

測試:

int main() {
    LinerList* L = NULL;
    
    InitList(L);

    ElemType t;
    ElemType a = 'a';
    ElemType b = 'b';

    //檢查順序表是否為空
    if (ListEmpty(*L)) {
        printf("true\n");
    }
    else {
        printf("false\n");
    }

    //順序表為空時刪除
    ListDelete(L, 1, &t);
    //順序表為空時輸出順序表
    DisplayList(*L);
    //順序表為空時定位
    printf("the locate is %d\n", LocateElem(*L, b));
    //獲取順序表長度
    printf("list length: %d\n", ListLength(*L));

    for (int i = 0; i < 100; i++) {
        ListInsert(L, 1, a);
    }

    //檢查順序表是否為空
    if (ListEmpty(*L)) {
        printf("true\n");
    }
    else {
        printf("false\n");
    }


    //順序表達到最大長度時插入
    ListInsert(L, 10, b);
    //順序表達到最大長度時刪除
    ListDelete(L, 1, &t);
    ListDelete(L, 1, &t);
    //給定過大的 i
    ListInsert(L, 100, a);
    ListInsert(L, 101, a);
    //順序表不為空時定位
    printf("the locate is %d\n", LocateElem(*L, b));
    //獲取順序表長度
    printf("list length: %d\n", ListLength(*L));
    //輸出順序表
    DisplayList(*L);


    int c;
    scanf_s("%d", &c);
}

三、線性表的鏈式儲存結構

1、連結串列

在鏈式儲存中,每個節點不僅包含有元素本省的資訊(這稱為資料域),還包含了元素之間的邏輯關係的資訊,即前驅節點包含了後繼節點的地址資訊(這稱為指標域),這樣可以通過前驅節點指標域中的資訊方便地找到後繼節點地位置。

由於順序表中每個資料元素最多隻有一個前驅節點和一個後繼節點,所以當採用鏈式儲存時,一般在每個節點中只設置一個指標域,用來指向後繼節點地位置,這樣構成的連結表稱為單向連結表,簡稱單鏈表。

另一種方法是,在每個節點中設定兩個指標域,分別用來指向前驅節點和後繼節點,這樣構成的連結串列稱為雙鏈表。

在單鏈表中,由於每個節點只有一個指向後繼節點的指標,所以當我們訪問過一個節點後,只能接著訪問它的後繼節點,而無法訪問它的前驅節點。在雙鏈表中,由於每個節點既包含有指向前驅節點的指標,也包含了指向後繼節點的指標,所以我們在訪問過一個節點後,既可以依次訪問它的前驅節點,也可以依次訪問它的後繼節點。

線上性表的鏈式儲存中,為了方便插入和刪除演算法的實現,每個連結串列帶有一個頭節點,並通過頭節點的指標唯一標識該連結串列。

在單鏈表中,每個節點應該包含儲存元素的資料域和指向下一個節點的指標域,我們使用 C語言的結構體來定義單鏈表的節點型別:

typedef char ElemType;
typedef struct ListNode {
    ElemType data;
    ListNode* next;
} LinkList;

對於雙鏈表。採用類似單鏈表的型別定義,不過和單鏈表不同的是,雙鏈表有兩個指標域。

typedef char ElemType;
typedef struct DListNode {
    ElemType data;
    DListNode* pre;
    DListNode* next;
} DLinkList;

2、單鏈表的基本運算實現

(1)頭插法建立單鏈表

頭插法建立連結串列的方法是:先建立一個頭節點,然後將新節點插入到頭節點的後面。注意這裡的頭節點只儲存連結串列開始節點的地址,並不保資料,也就是說連結串列的第一個節點應該是頭節點後面的第一個節點,頭插法的演算法實現如下:

void CreateLinkList(LinkList*& L, ElemType data[], int n) {
    L = (LinkList*)malloc(sizeof(LinkList));
    L->next = NULL;

    for (int i = 0; i < n; i++) {
        LinkList* p = (LinkList*)malloc(sizeof(LinkList));
        p->data = data[i];
        p->next = L->next;
        L->next = p;
    }
}

思考:

既然頭節點不儲存任何資料,能否另外再定義一個頭節點型別來表示一個連結串列?

如:

typedef char ElemType;
typedef struct ListNode {
    ElemType data;
    ListNode* next;
} ListNode;
typedef struct {
    int length;
    ListNode *first_node
} LinkList;

這裡 ElemType是要儲存的資料型別,ListNode是連結串列的節點型別,LinkLIst是連結串列型別。

我們用 LInkList型別的變數來表示一個連結串列,它包含了一個指向連結串列開始節點的指標和表示連結串列長度的變數 length。

(2)尾插法建立單鏈表

頭插法建立連結串列雖然簡單,但是頭插法建立的連結串列中的資料元素的順序和原陣列元素的順序相反。如果希望兩者的順序一致,我們可以使用尾插法來建立連結串列。

尾插法建表時將資料元素新增到連結串列的尾部,所以我們需要一個指標來指向連結串列的尾部(這個指標指只在建立連結串列時使用)。

尾插法建立連結串列的演算法如下:

void ECreateLinkList(LinkList*& L, ElemType data[], int n) {
    L = (LinkList*)malloc(sizeof(LinkList));
    L->next = NULL;

    LinkList* end = L; // end始終指向連結串列尾部

    for (int i = 0; i < n; i++) {
        LinkList* p = (LinkList*)malloc(sizeof(LinkList));
        p->data = data[i];
        end->next = p;
        end = p;
    }
    end->next = NULL;
}

銷燬連結串列需要把每個節點的空間都釋放:

void DestroyList(LinkList*& L){
    LinkList* p = L;
    LinkList* q = p->next;

    while(q != NULL) {
        free(p);
        p = q;
        q = p->next;
    }
    free(p);
}
(3)單鏈表的基本運算
void DestroyList(LinkList*& L){
    LinkList* p = L;
    LinkList* q = p->next;

    while(q != NULL) {
        free(p);
        p = q;
        q = p->next;
    }
    free(p);
}

bool ListEmpty(LinkList* L) {
    if (L->next == NULL) {
        return true;
    }
    else {
        return false;
    }
}

int ListLength(LinkList* L) {
    int i = 0;
    LinkList* t = L;

    while (t->next != NULL) {
        i++;
        t = t->next;
    }

    return i;
}

void DisplayList(LinkList* L) {
    if (L->next == NULL) {
        printf("list is empty.\n");
    }
    else {
        while (L->next != NULL) {
            L = L->next;
            printf("node data: %c\n", L->data);

        }
    }
}

void GetElem(LinkList* L, int i, ListNode*& e) {
    int count = 0;
    while (L->next != NULL && count < i) {
        L = L->next;
        count++;
    }

    if (count == i) {
        e = (ListNode*)malloc(sizeof(ListNode));
        e->data = L->data;
        e->next = L->next;
    }
    else {
        e = NULL;
    }
}

int LocateElem(LinkList* L, ListNode* e) {
    int i = 1;
    L = L->next;

    while (L != NULL && L->data != e->data) {
        L = L->next;
        i++;
    }

    if (L == NULL) {
        return 0;
    }
    else {
        return i;
    }
}

向連結串列中插入節點:

int ListInsert(LinkList* L, int i, ListNode* e) {
    int count = 0;
    while (count < i - 1 && L != NULL) {
        count++;
        L = L->next;
    }

    if (L == NULL || count == 0) {
        return 0;
    }
    else {
        LinkList* t = L->next;
        L->next = e;
        e->next = t;
        return 1;
    }
}

在向連結串列中插入節點時,我們先定位到第 i-1個節點。

如果第 i-1個節點存在,則 count=i-1,且 L不為空;如果第 i-1個節點不存在,則 L為空;如果輸入的 i為非法值(比如負數),則 count為 0。

當第 i-1個節點存在時,直接將第 i-1個節點的 next指標指向要插入的節點,並將要插入的節點的 next指標指向第 i+1個節點(原來的第 i個節點)。

當第 i-1個節點不存在時,第 i個節點沒有前驅節點,所以不能將節點插入到第 i個節點處。

刪除連結串列中的節點:

int ListDelete(LinkList* L, int i, ListNode*& e) {
    int count = 0;
    while (count < i - 1 && L != NULL) {
        count++;
        L = L->next;
    }
    
    if (L != NULL && L->next != NULL && count != 0) {
        ListNode *t = L->next;
        e = (ListNode*)malloc(sizeof(ListNode));
        e->data = t->data;
        e->next = t->next;
        L->next = t->next;
        free(t);
        return 1;
    }
    else {
        e = NULL;
        return 0;
    }
}

和插入節點一樣,在刪除節點時我們也要先定位第 i-1個節點,不過和插入節點有一點不同的是,我們要先檢查第 i個節點是否存在,只有當第 i個節點存在時我們才執行刪除操作。

這裡我們為什麼要定位第 i-1個節點,而不是第 i個節點呢?

這是因為單鏈表只能單向訪問,第 i個節點時無法訪問第 i-1個節點的。所以如果我們定位到第 i個節點的話,就無法將第 i-1個節點指向後面一個節點了。

3、雙鏈表的基本運算實現

雙鏈表中有兩個指標域,一個指向前驅節點、另一個指向後繼節點。

typedef char ElemType;
typedef struct DListNode {
    ElemType data;
    DListNode* pre;
    DListNode* next;
} DLinkList;

和單鏈表類似,建立雙鏈表也有兩種方法:頭插法和尾插法。

(1)頭插法建立雙鏈表
void CreateDoubleLinkList(DLinkList *&L, ElemType data[], int n) {
    L = (DLinkList*)malloc(sizeof(DLinkList));
    L->pre = NULL;
    L->next = NULL;

    for (int i = 0; i < n; i++) {
        DLinkList *t = (DLinkList*)malloc(sizeof(DLinkList));

        t->next = L->next;
        t->pre = L;
        t->data = data[i];

        L->next = t;

        if (t->next != NULL) {
            t->next->pre = t;
        }
    }
}

在頭插法建立雙鏈表的演算法中,我們先為頭節點分配儲存空間並將頭節點的兩個指標域都賦值為 NULL。

在向雙鏈表中插入節點時,我們總是將待插入的節點插入到頭節點和開始節點之間。

插入節點時,我們先將待插入節點的 next指標指開始節點(也就是 L->next所指向的節點),再將待插入節點的 pre指標指向頭節點,這時我們已經建立了待插入節點與頭節點和開始節點之間的關係。

不過這時的關係還是單向的,我們還需要讓頭節點的 next指標指向待插入節點,這時頭節點和待插入節點之間的雙向關係就已經建立好了。

我們可以用同樣的方法將待插入節點和其後繼節點建立雙向連線,不過在建立連線之前我們需要檢查一下是否存在後繼節點,存在後繼節點才建立雙向連線。

(2)尾插法建立雙鏈表
void ECreateDoubleLinkList(DLinkList *&L, ElemType data[], int n) {
    L = (DLinkList*)malloc(sizeof(DLinkList));
    L->pre = NULL;
    L->next = NULL;

    //始終指向雙鏈表尾部的指標
    DLinkList *end = L;

    for (int i = 0; i < n; i++) {
        DLinkList* t = (DLinkList*)malloc(sizeof(DLinkList));

        t->pre = end;
        t->data = data[i];

        end->next = t;

        end = t;
    }
    end->next = NULL;
}

最後一次修改於 2018年 9月 13日。

轉載自:
資料結構教程(第二版)
李春葆 等 編著
清華大學出版社
ISBN:978-7-302-14229-4