數據結構6: 靜態鏈表及C語言實現


 

本節繼續介紹線性表的另外一種鏈式表示——靜態鏈表。(前面介紹的鏈表稱為 動態鏈表 )。

邏輯結構上相鄰的數據元素,存儲在指定的一塊內存空間中,數據元素只允許在這塊內存空間中隨機存放,這樣的存儲結構生成的鏈表稱為靜態鏈表。
靜態鏈表和動態鏈表的區別:靜態鏈表限制了數據元素存放的位置范圍;動態鏈表是整個內存空間。
圖1 靜態鏈表的存儲結構

靜態鏈表的構建方法

靜態鏈表使用數組這一數據類型預先申請足夠大的內存空間。

由於各數據元素在數組申請的內存空間內隨機存放,為了體現邏輯上的相鄰,為每一個數據元素配備一個具有指針作用的整形變量,用於記錄下一元素在數組中的位置。

在數組申請的存儲空間中,各數據元素雖隨機存儲,每一個元素都記錄着下一元素在數組中的位置,通過前一個元素,可以找到下一個元素,構成了一條鏈表,這條被局限在特定內存空間的鏈表就是靜態鏈表。

靜態鏈表中結點的構成

靜態鏈表中每個結點既有自己的數據部分,還需要存儲下一個結點的位置,所以靜態鏈表的存儲實現使用的是結構體數組,包含兩部分: 數據域 和 游標(存放的是下一個結點在數組中的位置下標)。

實現代碼:
typedef struct 
{   
int data;//數據域   int cur;//游標 }component;

 

例如:使用靜態鏈表存儲(1,2,3,4,5),創建數組a[7]:
圖2 靜態鏈表

圖2 中,鏈表頭指針指向 a[0] ,表示為第一個結點,數據域存放的是 1,通過游標確定,下一個結點的位置在 a[3] ,數據域存放的是 2 ,依次類推。若游標為 0,表示此結點為鏈表的最后一個結點。

靜態鏈表的空間重復利用

由於靜態鏈表提前申請了有限的內存空間,在使用的過程中,極有可能會出現申請的內存空間不足,需要使用之前被遺棄的內存空間。
被遺棄的意思是:之前已經使用,但是后期對該結點做了摘除操作,該內存空間中存放的是已經不用的垃圾數據。
所以,在整個過程中,需要自己動手把兩者區分開,也就是需要自己實現 malloc 和 free 兩個函數的作用。

解決的辦法是:提前將所有未被使用的結點鏈成一個備用鏈表。需要對鏈表做插入操作時,從備用鏈表上摘下一個結點使用;刪除鏈表中的結點時,刪除的同時鏈接到備用鏈表上,以備下次使用。


圖3 備用鏈表和數據鏈表
圖3 分析:

第一步:備用鏈表:(0,1)(1,2)(2,3)(3,4)(4,5)(5,6)(6,0)
              數據鏈表中還沒有數據

第二步:向數據鏈表中插入一個數據,將備用鏈表上的(1,2)摘下下,提供給數據元素使用,備用鏈表的(0,1)游標直接變成2就可以了:
              備用鏈表:(0,2)(2,3)(3,4)(4,5)(5,6)(6,0)
              數據鏈表:(1,0)

第三步:繼續向數據鏈表中插入一個數據,備用鏈表把(2,3)摘下來,備用鏈表中的(0,1)直接變成3就可以了:
              備用鏈表:(0,3)(3,4)(4,5)(5,6)(6,0)
              數據鏈表:(1,2)(2,0)

以此類推。以上為插入結點的過程,在刪除結點的反方向操作過程中,只需要將被刪除結點從數據鏈表上摘除,並添加到備用鏈表中即可(也就是只改變相關結點的游標的值)。

創建並初始化鏈表

建立靜態鏈表 S,存儲線性表(a,b,c,d):
創建結構體數組,例如名為 array,存儲空間足夠大;
先將 array 數組中的分量全部鏈接到備用鏈表上;(使用 reserveArr 函數實現)
從備用鏈表上申請一個分量作為鏈表 S 的頭結點,每次從備用鏈表上申請分量鏈接到 S 鏈表中,依次類推;
( mallocArr 函數用於每次向備用鏈表申請一個結點的空間,initArr 函數用於初始化靜態鏈表) 當存儲到最后一個結點時,游標設置為 0。  

代碼實現:
//創建備用鏈表
void reserveArr(component *array)
{   
for (int i=0; i<maxSize; i++)
  {     array[i].cur
= i+1;//將每個數組分量鏈接到一起   }   array[maxSize - 1].cur = 0;//鏈表最后一個結點的游標值為0 }
//提取分配空間 int mallocArr(component * array)
{   
//若備用鏈表非空,則返回分配的結點下標,否則返回0(當分配最后一個結點時,該結點的游標值為0)   int i = array[0].cur;   if (array[0].cur)
  {     array[
0].cur = array[i].cur;   }   return i; }
//初始化靜態鏈表 int initArr(component *array)
{   reserveArr(array);  
//鏈接備用鏈表   //從備用鏈表中拿出一個分量作為鏈表頭結點,返回的是這個分量的下標   int body = mallocArr(array);   //聲明一個變量,把它當指針使,指向鏈表的最后的一個結點,因為鏈表為空,所以和頭結點重合   int tempBody = body;   for (int i=1; i<5; i++)
  {     
int j = mallocArr(array); //從備用鏈表中拿出空閑的分量     array[tempBody].cur = j;     //將申請的空線分量鏈接在鏈表的最后一個結點后面     array[j].data = 'a' + i - 1; //給新申請的分量的數據域初始化     tempBody = j;            //將指向鏈表最后一個結點的指針后移   }   array[tempBody].cur = 0;       //新的鏈表最后一個結點的指針設置為0   return body; }

 

程序最終效果圖:

注:array[0]用作備用鏈表的頭結點,array[1]用作存放數據的鏈表的頭結點,所以array[0]和array[6]為備用鏈表上的結點。

靜態鏈表中查找數據

一般情況下,訪問靜態鏈表只能通過頭結點(頭結點在數組中的位置下標是知道的),所以查找數據通過遍歷鏈表的方式實現。

實現代碼:
//在以body作為頭結點的鏈表中查找數據域為elem的結點在數組中的位置
int selectElem(component * array, int body, char elem)
{   
int tempBody = body;   //當游標值為0時,表示鏈表結束   while (array[tempBody].cur != 0)
  {     
if (array[tempBody].data == elem)
    {       
return tempBody;     }     tempBody = array[tempBody].cur;   }
  
return -1;//返回-1,表示在鏈表中沒有找到該元素 }

靜態鏈表中更改數據

更改鏈表中某結點的數據,只需要通過查找算法找到要更改結點的位置,然后直接更改該結點的數據域即可。

實現代碼:
//在以body作為頭結點的鏈表中將數據域為oldElem的結點,數據域改為newElem
void amendElem(component * array, int body, char oldElem, char newElem)
{   
int add=selectElem(array, body, oldElem);   if (add == -1)
  {     printf(
"無更改元素");     return;   }   array[add].data = newElem; }

 

靜態鏈表中插入結點

繼續上邊的例子,插入一個結點,例如該結點的數據域為 e,插入到第 3 的位置:
  1. 首先從備用鏈表中申請空間存儲數據元素 e;
  2. 由於要將 e 結點插入到第 3 的位置上,所以要找到 b 結點,將 b 結點的游標賦值給 e 結點;
  3. 最后將 e 結點所在位置的下標給 b 結點的游標;

實現代碼(在理解靜態鏈表的存儲結構的基礎上):
//向鏈表中插入數據,body表示鏈表的頭結點在數組中的位置,add表示插入元素的位置,a表示要插入的數據
void insertArr(component *array, int body, int add, char a)
{   
int tempBody = body;  //tempBody做遍歷結構體數組使用   //找到要插入位置的上一個結點在數組中的位置   for (int i=1; i<add; i++)
  {     tempBody
= array[tempBody].cur;   }   int insert = mallocArr(array);  //申請空間,准備插入   array[insert].cur = array[tempBody].cur;  //首先要插入結點的游標等於要插入位置的上一個結點的游標   array[insert].data = a;   array[tempBody].cur = insert;  //然后讓上一結點的游標等於插入結點所在數組中的位置的下標 }

 

代碼運行效果:

靜態鏈表做刪除操作

靜態鏈表中刪除結點,要實現兩步操作:從鏈表上摘下結點后,將該結點鏈接到備用鏈表上,以備下次使用。
注:被摘除結點中的數據不需要手動刪除,待下次使用時,會被新的數據域將舊數據覆蓋點。
例如,在(a,b,c,d,e)鏈表中,刪除數據域為 ‘a’ 的結點:

實現代碼:
//刪除結點函數,a 表示被刪除結點中數據域存放的數據
void deletArr(component *array, int body, char a)
{   
int tempBody = body;   //找到被刪除結點的位置   while (array[tempBody].data != a)
  {     tempBody
= array[tempBody].cur;     //當tempBody為0時,表示鏈表遍歷結束,說明鏈表中沒有存儲該數據的結點     if (tempBody == 0)
    {       printf(
"鏈表中沒有此數據");       return;     }   }   //運行到此,證明有該結點   int del = tempBody;   tempBody = body;   //找到該結點的上一個結點,做刪除操作   while (array[tempBody].cur != del)
  {     tempBody
= array[tempBody].cur;   }   //將被刪除結點的游標直接給被刪除結點的上一個結點   array[tempBody].cur = array[del].cur;   freeArr(array, del); }

 

在該函數中,調用了一個freeArr函數,它的作用是回收被刪除結點所占用的空間,將此空間鏈接到備用鏈表中,以備下次分配使用。(自己實現的free函數)

freeArr函數實現代碼:
void freeArr(component *array, int k)
{   array[k].cur
= array[0].cur;   array[0].cur = k; }

 
刪除數據域為 ’a’ 結點的運行效果圖:

完整實現代碼

#include <stdio.h>
#define maxSize 7

typedef struct
{   
char data;   int cur; }component;
//將結構體數組中所有分量鏈接到備用鏈表中 void reserveArr(component *array); //初始化靜態鏈表 int initArr(component *array); //向鏈表中插入數據,body表示鏈表的頭結點在數組中的位置,add表示插入元素的位置,a表示要插入的數據 void insertArr(component * array,int body,int add,char a); //刪除鏈表中含有字符a的結點 void deletArr(component * array,int body,char a); //查找存儲有字符elem的結點在數組的位置 int selectElem(component * array,int body,char elem); //將鏈表中的字符oldElem改為newElem void amendElem(component * array,int body,char oldElem,char newElem); //輸出函數 void displayArr(component * array,int body); //自己需要實現的malloc和free函數 int mallocArr(component * array); void freeArr(component * array,int k);
int main()
{   component array[maxSize];   
int body = initArr(array);   printf("靜態鏈表為:\n");   displayArr(array, body);   printf("在第3的位置上插入結點‘e’:\n");   insertArr(array, body, 3,'e');   displayArr(array, body);   printf("刪除數據域為‘a’的結點:\n");   deletArr(array, body, 'a');   displayArr(array, body);   printf("查找數據域為‘e’的結點的位置:\n");   int selectAdd = selectElem(array, body, 'e');   printf("%d\n", selectAdd);   printf("將結點數據域為‘e’改為‘h’:\n");   amendElem(array, body, 'e', 'h');   displayArr(array, body);   return 0; }
//創建備用鏈表 void reserveArr(component *array)
{   
for (int i=0; i<maxSize; i++)
  {     array[i].cur
= i + 1;//將每個數組分量鏈接到一起   }   array[maxSize - 1].cur = 0;//鏈表最后一個結點的游標值為0 }
//提取分配空間 int mallocArr(component * array)
{   
//若備用鏈表非空,則返回分配的結點下標,否則返回0(當分配最后一個結點時,該結點的游標值為0)   int i = array[0].cur;   if (array[0].cur)
  {     array[
0].cur = array[i].cur;   }
  
return i; }
//初始化靜態鏈表 int initArr(component *array)
{   reserveArr(array);   
int body = mallocArr(array);   //聲明一個變量,把它當指針使,指向鏈表的最后的一個結點,因為鏈表為空,所以和頭結點重合   int tempBody = body;   for (int i=1; i<5; i++)
  {     
int j = mallocArr(array);//從備用鏈表中拿出空閑的分量     array[tempBody].cur = j;//將申請的空線分量鏈接在鏈表的最后一個結點后面     array[j].data = 'a' + i - 1;//給新申請的分量的數據域初始化     tempBody = j;//將指向鏈表最后一個結點的指針后移   }   array[tempBody].cur = 0;//新的鏈表最后一個結點的指針設置為0   return body; }
void insertArr(component *array, int body, int add, char a)
{   
int tempBody = body;   for (int i=1; i<add; i++)
  {     tempBody
= array[tempBody].cur;   }   int insert = mallocArr(array);   array[insert].cur = array[tempBody].cur;   array[insert].data = a;   array[tempBody].cur = insert; }
void deletArr(component *array, int body, char a)
{   
int tempBody = body;   //找到被刪除結點的位置   while (array[tempBody].data != a)
  {     tempBody
= array[tempBody].cur;     //當tempBody為0時,表示鏈表遍歷結束,說明鏈表中沒有存儲該數據的結點     if (tempBody == 0)
    {       printf(
"鏈表中沒有此數據");       return;     }   }   //運行到此,證明有該結點   int del = tempBody;   tempBody = body;   //找到該結點的上一個結點,做刪除操作   while (array[tempBody].cur != del)
  {     tempBody
= array[tempBody].cur;   }   //將被刪除結點的游標直接給被刪除結點的上一個結點   array[tempBody].cur = array[del].cur;   freeArr(array, del); }
int selectElem(component * array, int body, char elem)
{   
int tempBody = body;   //當游標值為0時,表示鏈表結束   while (array[tempBody].cur != 0)
  {     
if (array[tempBody].data == elem)
    {       
return tempBody;     }     tempBody = array[tempBody].cur;   }   return -1;//返回-1,表示在鏈表中沒有找到該元素 }
void amendElem(component *array, int body, char oldElem, char newElem)
{   
int add = selectElem(array, body, oldElem);   if (add == -1)
  {     printf(
"無更改元素");     return;   }   array[add].data = newElem; }

void displayArr(component *array, int body)
{   
int tempBody = body;  //tempBody准備做遍歷使用   while (array[tempBody].cur)
  {     printf(
"%c,%d ", array[tempBody].data, array[tempBody].cur);     tempBody = array[tempBody].cur;   }   printf("%c,%d\n", array[tempBody].data, array[tempBody].cur); }
void freeArr(component *array, int k)
{   array[k].cur
= array[0].cur;   array[0].cur = k; }
輸出結果:
靜態鏈表為: ,
2 a,3 b,4 c,5 d,0 在第3的位置上插入結點‘e’: ,2 a,3 b,6 e,4 c,5 d,0 刪除數據域為‘a’的結點: ,3 b,6 e,4 查找數據域為‘e’的結點的位置: 6 將結點數據域為‘e’改為‘h’: ,3 b,6 h,4 c,5 d,0

總結

靜態鏈表綜合了順序表和動態鏈表的優點:使用數組存儲數據元素,便於做查找遍歷操作;同時,在數組中借鑒了動態鏈表的特點,在鏈表中插入或者刪除結點時只需更改相關結點的游標,不需要移動大量元素。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM