順序表必須佔用一塊事先分配好的、大小固定的存儲空間,不便於存儲空間的管理,爲此有人提出可以實現存儲空間的動態管理,即鏈式存儲方式——鏈表。
本篇文章將學習下什麼是鏈表,以及鏈表的實現。
鏈表存儲的原理
和順序存儲不同,在鏈式存儲中,結點之間的存儲單元地址可能是不連續的。鏈式存儲中每個結點都包含了兩個部分:
存儲元素本身的數據域
存儲結點地址的指針域
我們在前邊講解連式存儲時,提到了一些鏈式存儲的原理,結點中的指針指向的是下一個結點,如果結點中只有指向後繼結點的指針,那麼這些結點組成的鏈表成爲單向鏈表。
還是來張圖複習一下撒
單向鏈表.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
插入元素步驟2.png
插入元素步驟3.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
刪除元素步驟2.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");
}
至此,鏈表基本的操作都已實現。
現在我們對於線性表的一些相關知識:原理及實現方式,應該有了一定的認識了,其實只要瞭解了其中的存儲原理,思路清晰,代碼實現並不難。
掌握了這兩個最基本的線性表,對接下來我們學習其他數據結構會有很大幫助。