1. 程式人生 > >線性表(順序表,連結串列的表示和實現)

線性表(順序表,連結串列的表示和實現)

本篇文章將從以下幾點進行講解:  1.線性表的型別定義  2.1線性表的順序表示和實現  2.2線性表的鏈式表示和實現 1.線性表的型別定義:
接下來介紹下什麼是線性表,學習每一樣東西都要從定義下手. 由n( n>= 0 )個數據特性相同的元素構成的有限序列稱為線性表//這個是官方給出的定義有點生僻 換句話說,同一線性表中的元素必定 具有相同特性 , 也就是說屬於同一資料物件(就是它們的結構型別是一樣的,有著同個爸媽生出來的),還有一個最明顯的特點就是 相鄰資料元素之間存在著 序偶關係;那麼問題又來了這裡 為什麼說 序偶 ,序偶 又是神馬呢?這裡先不解釋,到後面 講解完對比著去思考 線性表中的順序表 和 鏈式表 你就迎刃而解了. 接下來在對線性表的定義再囉嗦幾句- -,    線性表中元素的個數為n( n >= 0 ) ,將這個n定義為線性表的長度, 當 n = 0 時稱為空表;    那麼對於非空線性表或線性結構,有一下特點:           (1)存在唯一的一個被稱為 "第一個"的資料元素;           (2)存在唯一的一個被稱為"最後一個"的資料元素"           (3)除第一個元素外,結構中每個元素均只有一個"前驅"           (4)除最後一個素外,結構中每個元素均只有一個"後繼"        ps:這裡的前驅和後繼 你要是不理解的話對線性表的鏈式結構理解起來就有很大的問題了;線上性表的鏈式結構中,前驅和後繼就是存放鏈式結構的 指標域,相信很多的同學都知道 鏈式儲存可以的儲存記憶體分配方式可以是一整塊的記憶體 也可以是雜湊開的記憶體,相鄰的兩個鏈節點,前一結點的後繼指向後一結點的前驅.還是沒理解的小夥伴也不要太著急,在後面講解到鏈式結構的時候我會圖文進行講解鏈式結構的線性表的講解.   對於線性表的概念定義把上面的內容理解完就差不多了,接下來就是我們的一個重點內容了:對線性表的順序表現和實現的介紹,對上面概念每臺弄懂的同學,可以再結合下順序表的內容再一次加深  2.1線性表的順序表示和實現

      首先說說我對線性表為什麼分為順序表示和鏈式表示,都歸結記憶體分配結構,順序表的記憶體分配是有序的,鏈式線性表的記憶體分配可以是雜湊的分配,這個就是最本質的問題,所以也導致在後面出現將要引出的話題:順序表和鏈式表的操作和差異,由於在資料的儲存方式上的特點所以這兩者的(增刪改查)行為也是不一樣的的. 還是老規矩我們先來一個官方的定義下什麼是線性表的順序表示:      用一組地址連續的儲存單元一次儲存線性表的資料元素,通常把這種儲存結構的線性表稱為順序表,他具有的特點:邏輯上相鄰的資料元素,其物理次序也是相鄰的.      這裡頭舉一個栗子:假設線性表的每個元素佔 k 個儲存單元,那麼以第一個單元的儲存結構作為起始地址,第 i + 1 個元素的地址是多少呢?           Loc(a[i+1] ) = Loc(a[i]) + k        由於上面這個特點,那麼就可以引出順序表的一個最重要的特性: 只要確定了線性表的起始位置,那麼線性表中任意元素都可以隨機存取,說以業界 又稱順序表儲存結構是一種隨機存取的儲存結構. 2.1.2順序表中的基本操作的實現      介紹完順序表,那麼同學肯定很渴望知道順序表的具體操作是怎麼樣的(增刪改查,建立)

     1.順序表的初始化//建立

//1.為順序表動態的分配一個預定大小的陣列空間,將elem指向這個段空間的基地址
//2.將表當前長度設定為0
Status InitList_Sq(SqList &L){
    //構造一個空的順序表L
    L.elem = new ElemType[MaxSize];  //為順序表動態分配一個大小為MaxSize的陣列空間
    if(!L.elem) exit(Error);         //儲存分配失敗
    L.length = 0;                    //將表當前長度設定為0
    return OK;                       
}
PS:動態的分配線性表的儲存區域可以更有效的利用系統的資源,當不需要這個線性表的時候,可以用銷燬操作及時的釋放佔用的空間.      2.查詢     查詢分為兩種情況:一種是按序號查詢,另一種是按值查詢;     由於書序表是隨機存取的,所以按序號查詢的話很簡單,只需要將序號寫到順序表的角標裡頭就可以定位到要找的角標對應的那個元素了;     下面給給小夥伴們介紹另一種情況:給按照所給定的值進行查詢
//在順序表中查詢與給定值 e 相等的資料元素,如果找到返回其在表中的"位置序號",否則返回 0
    //1.從第一個元素起,一次和 e 進行比較
    //2.若比對成功,第 i 個元素的值與 e 值相同,則返回 i + 1
    //3.若比對失敗,返回 0
int LocateElem_Sq(SqList L,ElemType e){
    for(int i=0;i <L.length; ++i)
          if(L.elem[i]==e) return i+1;
      return 0;
}  
 ps:當順序表中查詢一個元素的時候,查詢的時間主要花銷在比對上面,而比對的次數又取決於查詢元素的位置;所以時間的複雜度為在查詢成功時的期望,平均查詢長度,O(n)   3.插入    線性表要在第 i 的位置插入一個元素,即在 a[i - 1] 和a[i]間插入一個元素,由於順序表的資料元素不僅在邏輯上相鄰,在物理上也是相鄰的.那麼要在a[i-1]後面插入一個數據元素,那麼剩下的a[i]到a[n]都要依次往後挪動,騰出位置來給 那個值 e插進去,為什麼要依次挪動呢,因為 順序表的資料結構覺得的:物理上相鄰的,又由於順序表只能使用系統的一塊整的 連續的 儲存空間,所以你要想實現中間插入一個,那麼剩下的就得依次往後挪動一個位置
//假設在第i個元素之前插入一個元素,需要從最後一個元素n開始,依次向後移動一個位置,直至第i個元素
//期中啟動了 (n - i + 1)個元素
        //1.判斷插入位置i是否合法(i值的合法範圍 1 <= i <= n+1),判斷是否越界
        //2.判斷順序表的儲存空間是否已經滿,滿還強行插入會造成記憶體溢位
        //3.將第n個至第i個位置的元素依次向後移動一個位置,,空出第i個位置
        //4.將要插入的新元素 e放到第i個位置
       //表長度加1,插入成功返回OK
Status ListInsert_Sq(SqList &L,int i,ElemType e){
    if(i<1 || i>L.length + 1) return Error; // i 值不合法
    if(L.length == MaxSize) return Error; //當前儲存空間已滿
   for(j=L.length -1;j>= i-1;j--)
         L.elem[j+1] = L.elem[j];           //插入 i 到 n 的位置向後移動
    L.elem[i-1] = e;                        //將值 e 插入到 第i個位置
    ++L.length;                             //表長叫1
    return ok;
}
  Ps:為什麼要判斷是否已滿,在上面的程式碼中沒有做動態擴充,因此當表長達到預設的最大空間是,就不能再插入元素了     與查詢的演算法相似,插入一個元素時,時間主要消耗在移動原始上,而移動元素的個數又取決於插入元素的位置,時間複雜度也是為 O(n)     4.刪除    線上性表的順序結構中,由於在儲存結構上是連續的,所以在刪除 a[i]這個元素的時候,a[i-1]和a[i+1]是聯動跟著變化的,那麼在刪除第i個元素的時候,需要依次向前移動第i+1個到第n個元素的位置,共需要移動(n -i)個元素
    //1.首先是判斷刪除的位置i是否合法,合法值為(1<= i <=n),若超出這個範圍的話就返回error
    //2.將要刪除的數儲存在e中(這裡根據具體的需求,自由選擇這一步)
    //3.將第i+1到第n位置的元素依次向前移動一個位置
    //4.表長度減1,刪除成功返回ok
Status ListDelete_Sq(SqList &L,int i,ElemType e){
    if(i<1 || i>L.length) return Error;  //i超出邊界
    e = L.elem[i-1];                     //儲存刪除的那個值   
    for(j=i;j<=L.length;++j)             //將第i-1 到第 n 個位置的元素依次向前移動一位 
            L.elem[j-1] = L.elem[j];
     --L.length;                         //長度減1  
     return ok;
}
 PS:由於刪除演算法可以看做是插入演算法的逆過程,所以也免不了要移動元素,時間的複雜度也是O(n)  2.2線性表的鏈式表示和實現
    首先要了解線性表的鏈式表示,那麼就不得不提線性表的鏈式結構的最典型的一種資料結構:單鏈表;學習都是從簡單到複雜,複雜的東西就是在簡單的基礎上稍作變化.    老規矩先介紹下什麼是線性表的鏈式儲存結構吧.線性表的鏈式儲存結構的特點:用一組任意的儲存單元儲存線性表的資料(這組儲存單元可以是連續的也可以是雜湊的);這樣的每一個數據成為 結點 ,結點包含兩個域: 資料域(儲存元素的資訊的) 指標域(儲存直接後繼儲存位置,也就是用來指向下一個結點的地址資訊,在c語言中有這麼一句話,"地址就是指標,指標就是地址"),n個這樣的節點就鏈結成一個連結串列.     那麼現在就可以引出  單鏈表(每個結點只含有一個指標域的連結串列);當然節點的指標域也可以是兩個 ,那就叫雙向連結串列咯;    連結串列的種類有很多 大體可以分為 線性表 和非線性表 : 他們的分類依據是->所含指標的個數/指標指向/指標連結方式;    市面上出現的連結串列可以大致分為 : 單鏈表,迴圈連結串列,雙向連結串列,二叉連結串列,十字連結串列,鄰接多連結串列;其中 單鏈表 迴圈連結串列 和雙向連結串列 用於實現 線性表的鏈式結構    繞了那麼大個圈,現在就 再圍繞本次的核心介紹 單鏈表 :首先要知道單鏈表的儲存結構,整個連結串列必須從 頭指標開始, 頭指標指示連結串列中的第一個結點的儲存位置(ps:為什麼從頭指標開始? 同學們沒忘記單鏈表我們剛剛把他歸結為 線性表的一種吧,所以單鏈表在邏輯結構上是連續,又由於單鏈表支援 連續空間和雜湊空間的儲存結構 ,所以 就需要從第一的 那個頭指標 一級一級的往下指,才能 索引出 連結串列中的所有結點 ). 同時 由於最後一個數據元素沒有直接後繼(在前面已經介紹了線性表的定義了 忘記的同學滑上去再看下),則單鏈表中最後一個結點的指標為 空(NULL).  下面就貼一下單鏈表的表示,順便圖文表示下指標域的實際作用,這樣對指標的理解就不會辣麼抽象了(Ps:圖畫的太醜不要嫌棄,主要看氣質)


Ps單鏈表的邏輯關係是由結點中的指標指向的 2.2.1單鏈表基本操作的實現     1.初始化           單鏈表的初始化操作就是構造一個空表

  //單鏈表的儲存結構
typedef struct LNode{
   ElemType data;             //結點的資料域
   struct LNode *next;        //結點的指標域
}LNode,*LinkList;             //LinkList 為指向結構體LNode的指標型別
Ps: LinkList 和 LNode* 同為結構體指標型別;在順序表中,由於邏輯上相鄰的兩個元素在物理位置上也是緊密相鄰的.然而在單鏈表中,兩個元素的儲存位置沒有固定的相鄰關係,但是每個元素的儲存位置都包含在其直接前驅結點的資訊中.假設p是連結串列指向第i個數據元素的指標,他的資料域為 a[i]; 那麼 p->next是指向 第i+1 個數據元素的指標.則有  p-> data = a[i];       p->next->data = a[i+1]; 又由於單鏈表是非隨機存取的儲存結構,要去第i個元素 必須從 頭指標出發順鏈進行查詢,順序"存取"的儲存結構,所以他的基本操作實現不同於順序表
   //1.生成一個新的結點作為頭結點,用頭指標L指向頭結點
   //2.頭結點的指標域置空
Status InitList_L(LinkList &L){
   L = new LNode;
   L->next = NULL;      //頭結點的指標域置空
   return ok 
}


2.按序號查詢 從連結串列的第一個結點順鏈掃描,用指標p指向當前掃描到的節點,p的初始值指向第一個結點(P = L-next) .用 j 做計數器,j的初始值設定為1,一直增加進去,知道 j 的值等於 我們要查詢的第i個結點
status GetEleme_L(LinkList L,int i,ElemType &e){
  p = L->next;j=1; //初始化,p指向第一個結點,j為計數器
  while(p&&j<i){     //迴圈的跳出條件是p為空 ,計數器增加到i時   
      p= p->next;
      ++j;
   }
  if(!p||j>i) return Error;  //第i個元素不存在
  e=p->data;                  //取出第i個元素的值
  return ok;
}
Ps:這個演算法的時間主要花銷在,比較j和i的值,並且移動p指標,所以時間取決於 i 的值, 時間複雜度為O(n) 3.按值查詢  這個操作和順序表的查詢操作相似,從第一個結點開始,一起和e相比較,如果查詢到該結點的資料域與e的值相等就跳出迴圈
LNode *LocateElem_L(LinkList L,ElemType e){
    p = L->next;
    while(p && p->data!=e)
           p=p->next;              //尋找滿足條件的指標,否則指向下一個結點
    return p;
}
Ps:和順序表的查詢相似,所以時間複雜度為 o(n); 4.單鏈表的 插入  還記得順序表的插入嗎?在第i個 位置插入一個數據元素,那麼需要從第i到第n個數據元素依次向後移動一位.由於這種開銷的巨大,所以才引進了連結串列結構,連結串列的一大亮點就是他的插入特點,只需要知道要插入的那個位置的指標域就可以了.   假設s為指向結點x的指標         s->next = p->next; p->next = s;

//將值為e作為一個新的節點插入到表的第a[i-1] 和 a[i]的位置之間
    //1.找到結點a[i-1],並由指標p指向該結點
    //2.生成一個新的節點*s
    //3.將新結點*s的資料域置為e
    //4.將新結點的指標域指向結點a
    //5.令結點a[i-1]的指標域指向新結點*s
status ListInsert_L(LinkList &L,int i,ElemType e){
    p = L; j =0;
    while(p&&j<i-1){            //尋找第i-1個結點
           p=p->next;++j;
    }
    if(!p||j>i-1)
          return Error;
    s = new LNode;    //生成一個新的節點
     s->data = e;      //將該結點的資料域置為e
    
     s->next = p->next;  //這兩步是核心演算法,你要將s插進入時,你要先把 p的後事先解決了再插
     p->next = s;
}
Ps:因為第i個結點在插入之前,你不需要先從第一個結點開始索引找到第i-1個結點,所以還是要一個索引的過程,那麼時間複雜度為O(n) 5.單鏈表的刪除  跟插入資料元素的演算法差不多,你要刪除之前必須先找到要刪除的位置

  //1.找到結點a[i-1],並由指標p指向該結點
    //2.臨時儲存刪除的結點a[i]在q中,以備釋放(Ps:不釋放的話,容易導致記憶體洩露)
    //3.令p->next指向a[i]的直接後繼結點
    //4.將待刪除結點的值儲存在e中 (這裡具體看需求,可以不用這行程式碼)
    //5.釋放a[i]控制元件
status ListDelete_L(LinkList &L,int i,ElemType &e){
    p=L;j=0;
     while(p->next&&j<i-1){
         p=p->next;++j;      // 索引到第i-1個結點
     }
     if(!(p->next)|| (j>i-1))
            return Error;
     
     q=p->next;           //存放待刪除的結點
     p->next = q->next;   
     e = q->data;
     delete q;
     return ok;
}


6.1前插法建立單鏈表     前插法是通過將新結點逐一插入連結串列的頭部.首先要建立一個只有頭結點 空連結串列,沒讀入一個數據元素就申請一個新結點,並將新結點插入到頭結點之後
void CreateList_F(LinkList &L,int n){
  //逆位序輸入n個元素的值
  L= new LNode;
  L->next=NULL;
  for(i=n;i>0;--i){
       p=new LNode;              //建立新結點
       cin>>p->data;             //輸入資料元素的值
       p->next=L->next;L->next=p;   //插入到頭
  }
}

6.2後插法建立單鏈表
   將新結點逐個插入到連結串列的尾部

void CreateList_L(LinkList &L,int n){
   //正位序插入元素
   L=new LNode;
   L->next=NULL;    
   r = L;            //尾指標r指向頭結點
   for(i=0;i<n;++i){
         p=new LNode;  //生成新結點
         cin>>p->data;  //輸入元素值
         p->next = NULL;r->next=p;  //r指向新的尾結點
         r=p;
   }
 
}

總結 先感謝下那些耐心的讀者能 讀到這裡,希望幫助同學對線性表有一個較為全面的理解

順序表

鏈     表

空間

儲存空間

預先分配,會導致記憶體閒置和溢位現象

動態分配,不會出現記憶體閒置和溢位現象

儲存密度

密度為1

需要藉助指標來體現元素間的邏輯關係,儲存密度小於1

時間

存取元素

隨機存取,時間複雜度O(1)

順序存取,時間複雜度O(n)

插入\刪除

平均移動約表中一般元素,時間複雜度O(n)

不需要移動元素,確定插入刪除位置後,時間複雜度O(1)

適用情況

1.       表長變化不大,且事先確定變化的範圍

2.       很少進行插入刪除,經常按照元素序號訪問資料元素

1.       長度變化不大

2.       頻繁進行插入刪除操作

儲存密度:一個結點資料本身所佔的儲存空間和整個結點所佔儲存空間的比值