資料結構與演算法(一):線性表

古沐古沐諾發表於2020-04-03

線性表

線性表是零個或多個資料元素的有限序列。

線性表的順序儲存結構:

線性表的順序儲存是指在記憶體中用地址連續的一塊儲存空間順序存放線性表中的各資料元素,用這種儲存形式儲存的線性表稱為順序表。因為記憶體中的地址空間是線性的,所以用物理位置關係上的相鄰性實現資料元素之間的邏輯相鄰關係既簡單又自然。

順序表儲存結構

順序表的基本操作的實現

順序表結構設計

/* ElemType型別根據實際情況而定,這裡假設為int */
typedef int ElemType;
/* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef int Status;
typedef struct {
    ElemType *data;
    int length;
}Sqlist;
複製程式碼

1. 順序表的初始化

Status InitList(Sqlist *L){
    //為順序表分配一個大小為MAXSIZE 的陣列空間
    L->data =  malloc(sizeof(ElemType) * MAXSIZE);
    //儲存分配失敗退出
    if(!L->data) exit(ERROR);
    //空表長度為0
    L->length = 0;
    return OK;
}
複製程式碼

2. 順序表的插入

/*
 初始條件:順序線性表L已存在,1≤i≤ListLength(L);
 操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1
 */
Status ListInsert(Sqlist *L,int i,ElemType e){
    
    //i值不合法判斷
    if((i<1) || (i>L->length+1)) return ERROR;
    //儲存空間已滿
    if(L->length == MAXSIZE) return ERROR;
 
    //插入資料不在表尾,則先移動出空餘位置
    if(i <= L->length){
        for(int j = L->length-1; j>=i-1;j--){
       
            //插入位置以及之後的位置後移動1位
            L->data[j+1] = L->data[j];
        }
    }
    
    //將新元素e 放入第i個位置上
    L->data[i-1] = e;
    //長度+1;
    ++L->length;
    
    return OK;
    
}
複製程式碼

3. 順序表的取值

/*
 初始條件:順序線性表L已存在,1≤i≤ListLength(L)
 操作結果: 取出L的第i個資料元素,賦值給e
 */
Status GetElem(Sqlist L,int i, ElemType *e){
    //判斷i值是否合理, 若不合理,返回ERROR
    if(i<1 || i > L.length) return  ERROR;
    //data[i-1]單元儲存第i個資料元素.
    *e = L.data[i-1];
    
    return OK;
}
複製程式碼

4. 順序表刪除

/*
 初始條件:順序線性表L已存在,1≤i≤ListLength(L)
 操作結果: 刪除L的第i個資料元素,L的長度減1
 */
 Status ListDelete(Sqlist *L,int i){
    
    //線性表為空
    if(L->length == 0) return ERROR;
    
    //i值不合法判斷
    if((i<1) || (i>L->length+1)) return ERROR;
    
    for(int j = i; j < L->length;j++){
        //被刪除元素之後的元素向前移動
        L->data[j-1] = L->data[j];
    }
    //表長度-1;
    L->length --;
    
    return OK;
    
}
複製程式碼

線性表的鏈式儲存結構:

單連結串列

單連結串列是用一組任意的儲存單元存放線性表的元素,這組儲存單元可以連續也可以不連續,甚至可以零散分佈在記憶體中的任意位置。為了能正確表示元素之間的邏輯關係,每個儲存單元在儲存資料元素的同時,還必須儲存其後繼元素所在的地址資訊,這個地址資訊稱為指標(pointer),這兩部分組成了資料元素的儲存映像,稱為結點(node)。單連結串列正是通過每個結點的指標域將線性表的資料元素按其邏輯次序連結在一起的,由於每個結點只有一個指標域,故稱為單連結串列。

結點結構

單連結串列的結構:

頭節點、頭指標和首元節點

  • 頭指標:一個普通的指標,它的特點是永遠指向連結串列第一個節點的位置。很明顯,頭指標用於指明連結串列的位置,便於後期找到連結串列並使用表中的資料;

    注意:連結串列中有頭節點時,頭指標指向頭節點;反之,若連結串列中沒有頭節點,則頭指標指向首元節點。

  • 節點:連結串列中的節點又細分為頭節點、首元節點和其他節點:

  • 頭節點:其實就是一個不存任何資料的空節點,通常作為連結串列的第一個節點。對於連結串列來說,頭節點不是必須的,它的作用只是為了方便解決某些實際問題;

  • 首元節點:由於頭節點(也就是空節點)的緣故,連結串列中稱第一個存有資料的節點為首元節點。首元節點只是對連結串列中第一個存有資料節點的一個稱謂,沒有實際意義;

  • 其他節點:連結串列中其他的節點;

完整連結串列結構

單連結串列的基本操作

1.定義結點

typedef int Status;/* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef int ElemType;/* ElemType型別根據實際情況而定,這裡假設為int */

//定義結點
typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node * LinkList;
複製程式碼

2.初始化單連結串列

Status InitList(LinkList *L){
    
    //產生頭結點,並使用L指向此頭結點
    *L = (LinkList)malloc(sizeof(Node));
    //儲存空間分配失敗
    if(*L == NULL) return ERROR;
    //將頭結點的指標域置空
    (*L)->next = NULL;
    
    return OK;
}

複製程式碼

3.單連結串列插入

資料結構與演算法(一):線性表

/*
 初始條件:單連結串列L已存在;
 操作結果:在L中第i個位置之後插入新的資料元素e;
 */
Status ListInsert(LinkList *L,int i,ElemType e){
 
    int j;
    LinkList p,s;
    p = *L;
    j = 1;
    
    //尋找第i-1個結點
    while (p && j<i) {
       p = p->next;
        ++j;
    }
    
    //第i個元素不存在
    if(!p || j>i) return ERROR;
    
    //生成新結點s
    s = (LinkList)malloc(sizeof(Node));
    //將e賦值給s的數值域
    s->data = e;
    //將p的後繼結點賦值給s的後繼
    s->next = p->next;
    //將s賦值給p的後繼
    p->next = s;
    
    return OK;
}

複製程式碼

4.單連結串列取值

Status ListGetElem(LinkList L,int i,ElemType *e){
    //j: 計數.
    int j;
    //宣告結點p;
    LinkList p;
    //將結點p 指向連結串列L的第一個結點;
    p = L->next;
    //j計算=1;
    j = 1;
    //p不為空,且計算j不等於i,則迴圈繼續
    while (p && j<i) {
        
        //p指向下一個結點
        p = p->next;
        ++j;
    }
    //如果p為空或者j>i,則返回error
    if(!p || j > i) return ERROR;

    //e = p所指的結點的data
    *e = p->data;
    return OK;
    
}
複製程式碼

5.單連結串列刪除元素

Status ListDelete(LinkList *L,int i,ElemType *e){
    int j;
    LinkList p,q;
    p = (*L)->next;
    j = 1;
    //查詢第i-1個結點,p指向該結點
    while (p->next && j<(i-1)) {
        p = p->next;
        ++j;
    }
    //當i>n 或者 i<1 時,刪除位置不合理
    if (!(p->next) || (j>i-1)) return  ERROR;
    //q指向要刪除的結點
    q = p->next;
    //將q的後繼賦值給p的後繼
    p->next = q->next;
    //將q結點中的資料給e
    *e = q->data;
    //讓系統回收此結點,釋放記憶體;
    free(q);
    
    return OK;
}
複製程式碼

連結串列結構與順序儲存結構對⽐

儲存分配⽅式

順序儲存結構⽤⽤⼀段連續的儲存單元依次儲存線性表的資料元素; 
單連結串列採⽤鏈式儲存結構,⽤⼀組任意的儲存單元存放線性表的元素;
複製程式碼

時間效能

  • 查詢:
順序儲存 O(1);
單連結串列O(n);
複製程式碼
  • 插⼊和刪除:
儲存結構需要平均移動⼀個表⻓⼀半的元素,時間O(n); 
單連結串列查詢某位置後的指標後,插⼊和刪除為O(1);
複製程式碼

空間效能

順序儲存結構需要預先分配儲存空間,分太⼤,浪費空間;分⼩了,發⽣溢位; 
單連結串列不需要分配儲存空間,只要有就可以分配, 元素個數也不受限制;
複製程式碼

單向迴圈連結串列

將單連結串列的兩頭連線,使其成為了一個環狀連結串列,就成為了單向迴圈連結串列。

資料結構與演算法(一):線性表

迴圈連結串列實現約瑟夫環

約瑟夫環問題,是一個經典的迴圈連結串列問題,題意是:已知 n 個人(分別用編號 1,2,3,…,n 表示)圍坐在一張圓桌周圍,從編號為 k 的人開始順時針報數,數到 m 的那個人出列;他的下一個人又從 1 開始,還是順時針開始報數,數到 m 的那個人又出列;依次重複下去,直到圓桌上剩餘一個人。 單向迴圈連結串列的基本操作和單向連結串列差別不大,所以我們來實現一個約瑟夫環。

#include <stdio.h>
#include "stdlib.h"

typedef struct Node{
    int number;//編號
    struct Node *next;
}Person;

//建立一個n人的連結串列 n>0
Person * initLinkList(int n){
    Person * head = (Person *)malloc(sizeof(Person));
    head->next = NULL;
    head->number = 1;
    Person * tail = head;
    for (int i= 2; i <= n;i++){
        Person *temp = (Person *)malloc(sizeof(Person));
        temp->number = i;
        temp->next = NULL;
        tail->next = temp;
        tail = temp;
    }
    //首尾相連
    tail->next = head;
  
    return head;
}
//從編號為 k 的人開始順時針報數,數到 m 的那個人出列
void findAndKill(int k, int m, Person **head) {
    Person *tail = *head;
    //找到連結串列第一個結點的上一個結點,為刪除操作做準備
    while (tail->next!=*head) {
        tail=tail->next;
    }
    //找到編號為k的人  tail要指向p的上一個人以便刪除
    Person *p = *head;
    while (p->number != k) {
        tail = p;
        p = p->next;
    }
    //當p->next = p 時 說明連結串列中只有p結點了
    while (p->next != p) {
        //數m次
        for (int i = 1; i < m; i++) {
            tail = p;//記錄p的上一個
            p = p->next;
        }
        //移除p
        tail->next = p->next;
        printf("出列的人編號為:%d\n", p->number);
        free(p);
        //p指向下一個人,繼續數
        p = tail->next;
    }
    //head指向的人可能已經釋放了 指向最後剩下的人
    *head = p;
    printf("最後剩下的人編號為:%d\n", p->number);
}

int main(int argc, const char * argv[]) {
    
    Person *head =  initLinkList(15);
    printf("列印head :%d\n", head->number);
    findAndKill(8, 1, &head);
    printf("列印head :%d\n", head->number);
    return 0;
}
複製程式碼

輸出結果

列印head :1
出列的人編號為:8
出列的人編號為:9
出列的人編號為:10
出列的人編號為:11
出列的人編號為:12
出列的人編號為:13
出列的人編號為:14
出列的人編號為:15
出列的人編號為:1
出列的人編號為:2
出列的人編號為:3
出列的人編號為:4
出列的人編號為:5
出列的人編號為:6
最後剩下的人編號為:7
列印head :7
複製程式碼

雙向連結串列

如果希望快速確定單連結串列中任一結點的前驅結點,可以在單連結串列的每個結點中再設定一個指向其前驅結點的指標域,這樣就形成了雙向連結串列。

雙向連結串列的結構

  • 雙向連結串列的結點構成:

資料結構與演算法(一):線性表

  • 雙向連結串列結構

資料結構與演算法(一):線性表

雙向連結串列的基本操作

雙向迴圈連結串列

和單向迴圈連結串列一樣,將雙向連結串列的尾結點部與頭結點相連線,就成了雙向迴圈連結串列。

資料結構與演算法(一):線性表

相關文章