資料結構—線性表

張俊紅發表於2018-07-20

前言

本篇開始,又會開始一個新的系列,資料結構,資料結構在演算法或者是程式設計中的重要性是不言而喻,所以學好資料結構還是很有必要的。本篇主要介紹資料結構的第一個結構——線性表,主要分為以下幾部分:
1.概念
2.儲存結構

  • 順序儲存
  • 鏈式儲存

3.儲存結構優缺點比較
4.表操作

  • 單連結串列操作
  • 雙連結串列操作

注:本系列語言會使用C語言進行,所以要看懂本系列,需要懂一些C語言基礎。

概念

線性表是零個或多個具有相同特性的資料元素組成的有限序列,該序列中所含元素的個數叫做線性表的長度,線性表有以下幾個特點:

  • 首先是一個序列
  • 其次是有限的
  • 可以是有序的也可以是無序的,你可以把線性表理解成一隊學生,可以讓這些學生根據身高從小到大排列,也可以隨機排成一列
  • 線性表的開始元素沒有前驅元素只有後繼元素,線性表的結束元素沒有後繼元素只有前驅元素,除了開頭元素和結尾元素以外,每個元素都有且只有一個前驅元素和後繼元素。

儲存結構

線性表的儲存結構有順序儲存結構和鏈式儲存結構兩種,前者稱為順序表,後者稱為連結串列。

順序儲存結構

順序表就是把線性表中的所有元素按照某種邏輯順序,依次儲存到從指定位置開始的一塊連續的儲存空間,重點是連續的儲存空間

陣列長度和線性表的長度區別:陣列長度是存放線性表的儲存空間的長度,儲存分配後這個量一般是不變的,線性表的長度是線性表中資料元素的個數,隨著線性表插入和刪除操作的進行,這個量是變化的。

順序表的結構體定義:

typedef struct
{

    int data[maxsize];     //建立存放int型別資料的一個陣列
    int lengeth;           //存放順序表的長度
}
複製程式碼

還有比較簡潔的寫法,如下:

int A[maxsize];
int n;
複製程式碼

線性表的順序儲存結構的優缺點

優點 缺點
無須為表示表中元素之間的邏輯關係而增加額外的儲存空間 插入和刪除操作需要移動大量元素
可以快速地存取表中任一位置的元素 插入和刪除操作需要移動大量元素
當線性表長度變化較大時,難以確定儲存空間的容量
造成儲存空間碎片

鏈式儲存結構

鏈式儲存結構是為了改善順序儲存結構的缺點,順序儲存結構最大的缺點就是插入和刪除某一元素時都需要移動大量的元素,這是很耗費時間的。為什麼會出現這種移動和刪除某一元素時都需要移動大量的元素,是因為相鄰兩元素的儲存位置也是具有相鄰關係,他們在記憶體中的位置也是挨著的,中間沒有空虛,不能直接進行插入,要想進行插入,需要先把其他元素進行挪動,同理,若刪除某個元素以後,就會流出空隙,也是需要移動其他元素進行補齊。綜上所述,造成順序儲存的主要問題是因為相鄰兩元素的儲存位置是相鄰的,在記憶體中的位置也是挨著的。

現在順序儲存問題的原因我們已經知道了,接下來只需要針對性去解決就可以了,讓元素之間的位置不必相鄰,記憶體中的位置也不必挨著即可,我們把這種儲存結構稱為鏈式儲存結構,線性表的鏈式儲存結構的特點是用一組任意的儲存單元儲存線性表的資料元素,這組儲存單元可以是連續的,也可以是不連續的,這就意味著這些資料元素可以存在記憶體未被佔用的任意位置。還有一點就是在順序儲存結構中,每個資料空間只需要儲存資料元素的資訊即可,但是在鏈式結構中,除了要儲存資料元素資訊外,還需要儲存他的後繼元素的儲存位置。我們把儲存資料元素資訊的域稱為資料域,把儲存直接後繼位置的域稱為指標域,指標域中儲存的資訊稱為指標或鏈,資料域和指標域組成資料元素的儲存映像,稱為結點。

1.單連結串列

n個結點鏈結成一個連結串列,即為線性表的鏈式儲存結構,因為此連結串列的每個結點中只包含一個指標域,所以叫做單連結串列,單連結串列是通過每個結點的指標域將線性表的資料元素按其邏輯次序連結在一起。

有的連結串列是帶有頭結點的,有的是不包含頭結點的,頭節點的資料域可以不儲存任何資訊,可以儲存線性表長度等附加資訊,頭節點的 指標域儲存指向第一個結點的指標。當連結串列是帶有頭結點的時候,就相當於火車頭一樣的存在,只是用來表面列車順序開始的方向,並不乘坐客人。(連結串列一般都是包含頭結點的)

帶頭結點的單連結串列中,頭指標head指向頭結點,頭結點的資料域不包含任何資訊,從頭結點的後繼結點開始儲存資料資訊。頭指標始終不等於NULL(指標是指指向下一個元素的的資訊,當為NULL時,即不指向任何元素),head->next等於NULL的時候,連結串列為空。

不帶頭結點的單連結串列中的頭指標head直接指向開始結點,當head等於NULL(head->=NULL)的時候,連結串列為空。

連結串列中整個連結串列的存取就必須從頭指標開始進行,之後的每個結點就是上一個結點的後繼指標指向的位置,最後一個結點(終端結點)的指標為空,通常用NULL或^表示。

單連結串列結點定義

typedef struct LNode
{

    int data;              //data中存放結點資料域
    struct LNode *next;    //指向後繼結點的指標
}LNode;                    //定義單連結串列結點型別
複製程式碼

2.靜態連結串列

前面的單連結串列是用的指標,但是有的程式語言是沒有指標這個功能的,那怎麼?聰明的人總是有,有人想出了用陣列來代替指標,來描述單連結串列,讓每個陣列的元素都由兩個資料域組成,陣列的每個下標都對應兩個資料域,一個用來存放資料元素,一個用來存放next指標。我們把這種用陣列描述的連結串列叫做靜態連結串列。

3.迴圈連結串列

將單連結串列中終端結點的指標端由空指標改為指向頭結點,就使整個單連結串列形成一個環,這種頭尾相接的單連結串列稱為單迴圈連結串列,簡稱迴圈連結串列。

4.雙向連結串列

在單連結串列的基礎上,再在每個結點中設定一個指向其前驅結點的指標域,這樣一個結點既可以指向它的前面又可以指向它的下一個,我們把這種連結串列稱為雙向連結串列。

雙連結串列結點定義

typedef struct DLLNode
{

    int data;                    //data中存放結點資料域(預設是int型別,也可以是其他)
    struct DLNode *prior;        //指向前驅結點的指標
    struct DLNode *next;         //指向後繼結點的指標
}DLNode;                         //定義雙連結串列結點型別
複製程式碼

結點是記憶體中一片由使用者分配的儲存空間,只有一個地址用來表示它的存在,沒有顯式的名稱,因此我們會在分配連結串列結點空間的時候,同時定義一個指標,來儲存這片空間的地址(這個過程通俗的講叫指標指向結點),並且常用這個指標的名稱來作為結點的名稱,比如下面這個:

LNode *A = (LNode*)malloc(sizeof(LNode));  //使用者分配(sizeof)了一片LNode空間,這時定義指標A來指向這個結點,同時我們也把A當作這個結點的名字。
複製程式碼

順序儲存和鏈式儲存比較

因為順序表的儲存地址是連續的,所以只需要知道第一個元素的位置,就可以通過起始位置的偏移去獲取順序表中的任何元素,我們把這種特徵稱為隨機訪問特性

順序表中的資料元素是存放在一段地址連續的空間中,且這個儲存空間(即存放位置)的分配必須預先進行,一旦分配好了,在對其進行操作的過程中是不會更改的。

順序表在插入刪除一個元素的時候需要移動大量元素。

因為連結串列的儲存結構是一個元素中包含下一個資料元素的位置資訊,下一個包含下下一個,也就是每個資料元素之間都是單線聯絡的,你要想知道最後一個元素在哪裡,你必須從頭走到尾才可以,所以連結串列是不支援隨機訪問的。

連結串列中的每一個結點需要劃分出一部分空間來儲存指向下一個結點的指標,所以連結串列中結點的儲存空間利用率比順序表要低。

連結串列支援儲存空間動態分配。

連結串列在插入和刪除一個元素時,不需要移動大量元素,只需要更改插入位置的指標指向就可以。

表的操作

表的操作其實主要分為幾種:查詢、插入、刪除

順序表操作:

1.按元素值的查詢演算法,
int findElem (Sqlist L,int e)
{
    int i;
    for (i=0,i<L.length,++i)   //遍歷L長度中的每個位置
        if(e == L.data[i])          //獲取每個位置對應的值和e值進行判斷,這裡的等於可以是大於、小於
            return i;                    //如果找到與e值相等的值,則返回該值對應的位置
    return -1;                        //如果找不到,則返回-1
}
複製程式碼
2.插入資料元素演算法

在順序表L的第p個(0<p<length)個位置上插入新的元素e,如果p的輸入不正確,則返回0,代表插入失敗;如果p的輸入正確,則將順序表第p個元素及以後元素右移一個位置,騰出一個空位置插入新元素,順序表長度增加1,插入操作成功,返回1。

int insertElem(Sqlist &L,int p,int e) //L是順序表的長度,要發生變化,所以用引用型
{

    int i
    if (p<0 || p>L.length || L.length==maxsize) //如果插入e的位置p小於0,或者是大於L的長度,或者是L的長度已經等於了順序表最大儲存空間
        return 0
;
    for (i=L.length-1;i>=p;--i)    //從L中的最後一個元素開始遍歷L中位置大於p的每個位置
        L.data[i+1]=L.data[i];    //依次將第i個位置的值賦值給i+1
    L.data[p]=e;                  //將p位置插入e
    ++(L.length);                 //L的長度加1
    return 1;                     //插入成功,返回1

}
複製程式碼
3.刪除資料元素演算法

將順序表的第p個位置的元素e進行刪除,如果p的輸入不正確,則返回0,代表刪除失敗;如果p的輸入正確,則將順序表中位置p後面的元素依次往前傳遞,把位置p的元素覆蓋掉即可。

int deleteElem (Sqlist &L,int p,int &e)    //需要改變的變數用引用型
{
    int i;
    if(p<0 || p>L.length-1)    //對位置p進行判斷,如果位置不對,則返回0,表示刪除失敗
        return 0;
    e=L.data[p];               //將要刪除的值賦值給e
    for(i=p;i<L.length-1;++i)  //從位置p開始,將其後邊的元素逐個向前覆蓋
        L.data[i]=L.data[i+1]; 
    --(L.length)               //將表的長度減1
    return 1;                  //刪除成功,返回1
}
複製程式碼

單連結串列操作

1.單連結串列的歸併操作

A和B是兩個單連結串列,其中元素遞增有序,設計一個演算法,將A和B歸併成一個按元素值非遞減有序的連結串列C,C由A、B組成。

分析:已知A、B中的元素遞增有序,要使歸併後的C中的元素依然有序,可以從A、B中挑出最小的元素插入C的尾部,這樣當A、B中所有元素都插入C中時,C一定是遞增有序的。

void merge(LNode *A,LNode *B,LNode *&C)
    
{
        LNode *p = A->next;                 //用指標p來追蹤連結串列A的後繼指標
        LNode *p = B->next;                 //用指標p來追蹤連結串列B的後繼指標
        Lnode *r;                           //r始終指向C的終端結點
        C = A;                              //用A的頭結點來做C的頭結點
        C-> = NULL;                         //
        free(B);
        r = C;
        while(p!=NULL&&q!=NULL)             //當p和q都不為空時,選取p與q中所指結點中較小的值插入C的尾部
        {
            if(p->data<=q->data)            //如果p結點的值小於等於q結點的值,則將p的結點指向r,即C,p的下一個結點繼續指向p
            {
                r->next = p;p = p->next;
                r=r->next;

            }
            else
            {
                r->next=q;q=q-next;
                r=r->next;
            }
        }
        r->next = NULL;
        if(p!=NULL)r->next=p;
        if(q!=NULL)r->next=q;  
    }
複製程式碼
2.單連結串列的尾插法

已知有n個元素儲存在陣列a中,用尾插法(即從尾部插入)建立連結串列C

void createlistR(LNode *&C,int a[],int n)        //需要不斷變化的值用引用型
{
    LNode *s,*r;                                 //s用來指向新申請的結點,r始終指向C的終端結點
    int i;
    C = (LNode * )malloc(sizeof(LNode));         //申請一個頭結點空間
    C -> next = NULL                             //初始化一個空連結串列
    r = C;                                       //r為指標,指向頭結點C,此時的頭結點也是終端結點
    for(i=0;i<n;++i):
    {
        s = (LNode*)malloc(sizeof(LNode));       //新申請一個結點,指標s指向這個結點
        s -> data = a[i]                         //將陣列元素a[i]賦值給指標s指向結點的值域
                                                 //此時,結點值域和指標都有了,一個完整的結點就建好了,要想把這個結點插進連結串列C中
                                                 //只需要將頭結點指標指向這個結點就行
        r -> next = s;                           //頭結點指標指向結點s
        r = r -> next;                           //更新r指標目前的指向

    }
    r -> next = NULL;                            //直到終端結點為NULL,表示插入成功
}
複製程式碼
3.單連結串列的頭插法

頭插法和尾插法是相對應的一種方法,頭插法是從連結串列的頭部開始插入,保持終端結點不變;尾插法是從連結串列的尾部開始插入,保持頭結點不變。

void createlistF(LNode *&C,int a[],int n)        //需要不斷變化的值用引用型
{
    LNode *s;                                 
    int i;
    C = (LNode * )malloc(sizeof(LNode));         //申請C的結點空間
    C -> next = NULL                             //該節點指向為空
    for(i=0;i<n;++i):
    {
        s = (LNode*)malloc(sizeof(LNode));       //新申請一個結點,指標s指向這個結點
        s -> data = a[i]                         //將陣列元素a[i]賦值給指標s指向結點的值域
                                                 //此時,結點值域和指標都有了,一個完整的結點就建好了,要想把這個結點插進連結串列C中
                                                 //只需要讓這個結點的指標指向連結串列C的開始結點即可
        s -> next = C -> next;                           //結點s指向C指標的開始結點
        C -> next = s;                           //更新r指標目前的指向

    }
}
複製程式碼

雙連結串列操作

1.採用尾插法建立雙連結串列
void createFlistR(DLNode *&L,int a[],int n)
{
    DLNode *s,*r;
    int i;
    L = (DLNode*)malloc(sizeof(DLNode)); //新建一個結點L
    L -> prior = NULL;
    L -> next = NULL;
    r = L;                               //r指標指向結點L的終端結點,開始頭結點也是尾結點
    for(i=0;i<n;++i)
    {        s = (DLNode*)malloc(sizeof(DLNode));  //建立一個新節點s
        s -> data = a[i]                      //結點s的值域為a[i]
        r -> next = s;                        //r指標的後繼指標指向s結點
        s ->prior = r;                        //s的前結點指向r
        r = s;                                //更新指標r的指向
    }    
    r -> next = NULL;                         //直到r指標指向為NULL
}
複製程式碼
2.查詢結點的演算法

在雙連結串列中查詢值為x的結點,如果找到,則返回該結點的指標,否則返回NULL值。

DLNode* findNode(DLNode *C,int x)
{
    DLNode *p = C -> next;
    while(p != NULL)
    {
        if(p -> data == x)
            break;
        p = p -> next;
    }
    return p;
}
複製程式碼
3.插入結點的演算法

在雙連結串列中p所指的結點之後插入一個結點s,核心思想就是將p的指向賦值給s,即讓s指向p所指,s的前結點就是p,p的後結點就是s,具體程式碼如下:

s -> next = p -> next;
s -> prior = p;
p -> next = s;
s -> next -> prior = s;
複製程式碼
4.刪除結點的演算法

要刪除雙連結串列中p結點的後繼結點,核心思想就是先將p的後繼結點給到q,然後讓p指向q的後繼結點,q的後繼結點的前結點就是p,然後把q釋放掉,具體程式碼如下:

q = p -> next;
p -> = q -> next;
q -> next -> prior = p;
free(q);
複製程式碼

相關文章