第八篇、線性表中的鏈式存儲(鏈表)

順序表必須佔用一塊事先分配好的、大小固定的存儲空間,不便於存儲空間的管理,爲此有人提出可以實現存儲空間的動態管理,即鏈式存儲方式——鏈表。
本篇文章將學習下什麼是鏈表,以及鏈表的實現。

鏈表存儲的原理

和順序存儲不同,在鏈式存儲中,結點之間的存儲單元地址可能是不連續的。鏈式存儲中每個結點都包含了兩個部分:

存儲元素本身的數據域

存儲結點地址的指針域

我們在前邊講解連式存儲時,提到了一些鏈式存儲的原理,結點中的指針指向的是下一個結點,如果結點中只有指向後繼結點的指針,那麼這些結點組成的鏈表成爲單向鏈表。
還是來張圖複習一下撒

單向鏈表.png

單向鏈表.png

一般在鏈表中也會有一個頭結點來保存鏈表信息,然後有一個指針指向下一個結點,下一個結點又指向他後邊的一個結點,如果這個指針沒有後繼結點,那麼他就指向NULL。
在鏈表中,這些存儲單元可以是不連續的,因此他可以提高空間利用率。當需要存儲元素時,哪裏有空閒的空間就在哪裏分配,只要將分配的空間地址保存到上一個結點就可以。這樣通過訪問上一個元素就能找到後一個元素。

擋在鏈表中某一個位置插入元素時,從空閒空間中爲該元素分配一個存儲單元,然後將兩個結點之間的指針斷開,上一個結點的指針指向新分配的存儲單元,新分配的結點中指針指向下一個結點。這樣不需要移動原來元素的位置,效率比較高。同樣,當刪除鏈表中的某個元素時,就斷開它與前後兩個結點的指針,然後他的前後兩個結點連接起來,同樣也不需要移動原來元素的位置。與順序表相比,在插入、刪除元素方面,鏈表的效率要比順序表高許多。
但是,隨機查找元素時,由於鏈表沒有像順序表的索引標註,存儲單元的空間並不連續,如果要查找某一個元素,必須先得經過他的上一個結點中的地址才能找到他,因此不管遍歷哪一個元素,都必須要把他前面的那個元素都遍歷後才能找到他,效率就不如順序表的高了。

總結一句話,鏈表增刪元素的效率比較高,但是查找元素的效率就比順序表低了。

鏈式存儲的實現

鏈表的幾種操作與順序表差不多,也是增刪改查等操作,接下來我們實現一個鏈表。

1、創建鏈表(根據上圖的單向鏈表創建)

在創建鏈表時,頭結點中保存鏈表的信息,則需要創建一個結構體struct,在其中定義鏈表的信息與指向下一結點的指針。代碼如下:

    struct Header{ //頭結點
    int length;//記錄鏈表的長度
    struct Node * next;//指向第一個結點的指針 
    }

存儲元素結點包含兩部分內容:數據域和指針域,則也需要再定義一個struct,代碼如下:

struct Node{//結點
int data;//數據域
struct Node * next;//指向下一個結點的指針
}

這樣頭結點與數據結點均已定義,爲了使用方便,將兩個struct用typedef重新定義新的名稱,代碼如下

  typedef struct Node List;//把struct Node重命名爲List
  typedef struct Header pHead;//把struct Header 重命名爲pHead

創建鏈表要比創建順序表簡單一些。
順序表中需要先爲頭結點分配空間,其次爲數組分配一段連續空間,將這段連續空間地址保存在頭結點中,然後往其中存儲數據。
而創建鏈表時,只需要創建一個頭結點,每存儲一個元素就分配一個存儲單元,然後將存儲單元的地址保存在上一個結點的指針中即可,不需要再創建時把所有空間都分配好。
ok,我們把兩個結構體定義好之後,就可以創建鏈表了,創建鏈表的代碼如下:
 

  pHead * createList(){//pHead 是struct Header的別名,是頭結點類型
  pHead * ph=(pHead *)malloc(sizeof(pHead));//爲頭結點分配內存
  ph->lenght=0;//爲頭結點初始化
  ph->next=NULL;    
  return ph;//將頭結點地址返回
  }

2、獲取鏈表大小

鏈表大小等信息也保存在頭結點中,因此需要從頭結點中獲取即可,也是很簡單的:

int Size(pHead * ph){
      if(ph==NULL){
      printf(“參數傳入有誤”);
      return 0;
      }
      return ph->length;
}

3、插入元素

在鏈表中插入元素要比在順序表中快,只需要將插入位置前後指針斷開,然後讓前元素指針指向新元素,新元素指針指向後元素即可。
插入元素示意圖

 

插入元素步驟1.png

插入元素步驟1.png

插入元素步驟2.png

插入元素步驟2.png

插入元素步驟3.png

插入元素步驟3.png

插入元素bingo.png

插入元素bingo.png

代碼如下:

    int insert(pHead *ph,int pos,int val){
  if(ph==NULL||pos<0||pos>ph->length){
      printf("參數傳入有誤");
      return 0;
      }
  //在向鏈表中插入元素時,先要找到這個位置
    //先分配一塊內存給要插入的數據
    List * pval=(List *)malloc(sizeof(List));
    pval->data=val;
   //當前指針指向頭結點後的第一個節點
    List * pCur=ph->next;  
   //如果要插入的位置是0
    if(pos==0){
    ph->next=pval;
    pval->next=pCur;
    }
    else{
         //通過for循環找到要插入的位置
        for(int i=1;i<pos;i++){
          pCur=pCur->next;
      }
        //指針重指
        pval->next=pCur->next;
        pCur->next=pval;
    }
  //由於增加了一個元素,所以長度加一
  ph->length++;
  return 1;
  }

4、查找某個元素

查找鏈表中的某個元素,其效率沒有順序表高,因爲不管查找的元素在哪個位置,都需要將她前邊的那個元素都完全遍歷才能找到。查找元素的代碼如下:

  List * find(pHead * ph,int val){
        if(ph==NULL){
          printf("傳入參數有誤\n");
          return NULL;
        }
        //遍歷鏈表來查找元素,從第一個元素開始遍歷
        LIst * pTmp=ph->next;
        do{
                if(pTmp->data==val){
                      //一旦發現有符合條件的值,直接返回
                     return pTmp;
                 }
                //讓循環動起來
                pTmp=pTmp->next;
            }
        //如果到最後都沒找到符合條件的結點,說明沒有
        while(pTmp->next!=NULL);
        printf("沒有職位%d的元素“,val);
        return NULL;
  }

5、刪除元素

再刪除元素時,首相將被刪除元素與上下結點之間的連接斷開,然後將這兩個上下結點重新連接,這樣元素就從鏈表中成功刪除了。示意圖如下

 

刪除元素步驟1.png

刪除元素步驟1.png

刪除元素步驟2.png

刪除元素步驟2.png

刪除元素bingo.png

刪除元素bingo.png

我們看到,從鏈表中刪除元素,也不需要移動其他元素,效率也比較高,我們看下代碼吧

    LIst * Delete(pHead * ph,int val){
          if(ph==NULL){
              printf("鏈表傳入錯誤!");
              return NULL;  
          }
          //找到val值所在的結點,這裏調用了上方查找的方法
          List * pval=find(ph,val);
          if(pval==NULL){
              printf("沒有值爲%d的元素!",val);
              return NULL;  
          }
          //遍歷鏈表找到要刪除的結點,並找出其前驅和後繼結點
          List  * pRe=ph-next;
          List  * pCur=NULL;
          //判斷特殊情況:如果要刪除的元素是第一個結點
          if(pRe->data==val){
              ph->next=pRe->next;
              ph->length--;
              return pRe;
          }  
          //排查特殊情況之後
          else{
                for(int i=0;i<ph->length;i++){
                    pCur=pRe->next;
                    if(pCur->data==val){
                        //執行上方圖例操作
                        pRe->next=pCur->next;
                        ph->length--;
                        return pCur;
                     }  
                      //延續循環遍歷
                      pRe=pRe->next;
                    }
                 }
    }

6、銷燬鏈表

銷燬鏈表時,將鏈表的每個結點元素釋放。頭結點可以釋放,也可以保留,並將其置爲初始化狀態。代碼如下:

     void Destory(pHead * ph){
          List * pCur=ph->next;
          List * pTmp;
          if(ph==NULL){
            printf("參數傳遞有誤!”):
          }
          while(pCur->next!=NULL){
           pTmp=pCur->next;
          //將結點釋放
           free(pCur);
           pCur=pTmp;
        }
        //將頭結點置爲初始化狀態
        ph->length=0;
        ph->next=NULL;
     }

在本例中,沒有釋放頭結點,只是將頭結點中的信息置爲初始化狀態了。

7、遍歷打印鏈表

實現出鏈表的遍歷打印函數,代碼如下:

  void print(pHead * ph){
  if(ph==NULL){
    printf(“參數傳遞有誤!”);
  }  
  List * pTmp=ph->next;
  while(pTmp!=NULL){
     printf("%d",pTmp->data); 
     pTmp=pTmp->next;
    }
  printf("\n");
  }

至此,鏈表基本的操作都已實現。
現在我們對於線性表的一些相關知識:原理及實現方式,應該有了一定的認識了,其實只要瞭解了其中的存儲原理,思路清晰,代碼實現並不難。

掌握了這兩個最基本的線性表,對接下來我們學習其他數據結構會有很大幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章