邏輯結構上一個挨一個的數據,在實際存儲時,並沒有像順序表那樣也相互緊挨着。恰恰相反,數據隨機分布在內存中的各個位置,這種存儲結構稱為線性表的鏈式存儲。
由於分散存儲,為了能夠體現出數據元素之間的邏輯關系,每個數據元素在存儲的同時,要配備一個指針,用於指向它的直接后繼元素,即每一個數據元素都指向下一個數據元素(最后一個指向NULL(空))。

圖1 鏈式存儲存放數據
如圖1所示,當每一個數據元素都和它下一個數據元素用指針鏈接在一起時,就形成了一個鏈,這個鏈子的頭就位於第一個數據元素,這樣的存儲方式就是鏈式存儲。
線性表的鏈式存儲結構生成的表,稱作“鏈表”。
鏈表中數據元素的構成
每個元素本身由兩部分組成:
- 本身的信息,稱為“數據域”;
- 指向直接后繼的指針,稱為“指針域”。

圖2 結點的構成
這兩部分信息組成數據元素的存儲結構,稱之為“結點”。n個結點通過指針域相互鏈接,組成一個鏈表。

圖3 含有n個結點的鏈表
圖 3 中,由於每個結點中只包含一個指針域,生成的鏈表又被稱為 線性鏈表 或 單鏈表。
鏈表中存放的不是基本數據類型,需要用結構體實現自定義:
typedef struct Link
{ char elem; //代表數據域 struct Link * next; //代表指針域,指向直接后繼元素 }link;
頭結點、頭指針和首元結點
頭結點:有時,在鏈表的第一個結點之前會額外增設一個結點,結點的數據域一般不存放數據(有些情況下也可以存放鏈表的長度等信息),此結點被稱為頭結點。
若頭結點的指針域為空(NULL),表明鏈表是空表。頭結點對於鏈表來說,不是必須的,在處理某些問題時,給鏈表添加頭結點會使問題變得簡單。
首元結點:鏈表中第一個元素所在的結點,它是頭結點后邊的第一個結點。
頭指針:永遠指向鏈表中第一個結點的位置(如果鏈表有頭結點,頭指針指向頭結點;否則,頭指針指向首元結點)。

單鏈表中可以沒有頭結點,但是不能沒有頭指針!
鏈表的創建和遍歷
萬事開頭難,初始化鏈表首先要做的就是創建鏈表的頭結點或者首元結點。創建的同時,要保證有一個指針永遠指向的是鏈表的表頭,這樣做不至於丟失鏈表。
例如創建一個鏈表(1,2,3,4):
link * initLink()
{ link * p = (link*)malloc(sizeof(link)); //創建一個頭結點 link * temp = p; //聲明一個指針指向頭結點,用於遍歷鏈表 //生成鏈表 for (int i=1; i<5; i++)
{ link *a = (link*)malloc(sizeof(link)); a->elem = i; a->next = NULL; temp->next = a; temp = temp->next; } return p; }
鏈表中查找某結點
一般情況下,鏈表只能通過頭結點或者頭指針進行訪問,所以實現查找某結點最常用的方法就是對鏈表中的結點進行逐個遍歷。
實現代碼:
int selectElem(link * p, int elem)
{ link *t = p; int i = 1; while (t->next)
{ t = t->next; if (t->elem == elem)
{ return i; } i++; } return -1; }
鏈表中更改某結點的數據域
鏈表中修改結點的數據域,通過遍歷的方法找到該結點,然后直接更改數據域的值。
實現代碼:
//更新函數,其中,add 表示更改結點在鏈表中的位置,newElem 為新的數據域的值 link *amendElem(link * p, int add, int newElem)
{ link * temp = p; temp = temp->next; //在遍歷之前,temp指向首元結點 //遍歷到被刪除結點 for (int i=1; i<add; i++)
{ temp = temp->next; } temp->elem = newElem; return p; }
向鏈表中插入結點
- 插入到鏈表的首部,也就是頭結點和首元結點中間;
- 插入到鏈表中間的某個位置;
- 插入到鏈表最末端;

圖 5 鏈表中插入結點5
雖然插入位置有區別,都使用相同的插入手法。分為兩步,如圖 5 所示:
- 將新結點的next指針指向插入位置后的結點;
- 將插入位置前的結點的next指針指向插入結點;
提示:在做插入操作時,首先要找到插入位置的上一個結點,圖4中,也就是找到結點 1,相應的結點 2 可通過結點 1 的 next 指針表示,這樣,先進行步驟 1,后進行步驟 2,實現過程中不需要添加其他輔助指針。
實現代碼:
link * insertElem(link * p, int elem, int add)
{ link * temp = p; //創建臨時結點temp //首先找到要插入位置的上一個結點 for (int i=1; i<add; i++)
{ if (temp == NULL)
{ printf("插入位置無效\n"); return p; } temp = temp->next; } //創建插入結點c link *c = (link*)malloc(sizeof(link)); c->elem = elem; //向鏈表中插入結點 c->next = temp->next; temp->next = c; return p; }
注意:首先要保證插入位置的可行性,例如圖 5 中,原本只有 5 個結點,插入位置可選擇的范圍為:1-6,如果超過6,本身不具備任何意義,程序提示插入位置無效。
從鏈表中刪除節點
當需要從鏈表中刪除某個結點時,需要進行兩步操作:
- 將結點從鏈表中摘下來;
- 手動釋放掉結點,回收被結點占用的內存空間;
實現代碼:
link * delElem(link * p,int add)
{ link * temp = p; //temp指向被刪除結點的上一個結點 for (int i=1; i<add; i++)
{ temp = temp->next; } link * del = temp->next;//單獨設置一個指針指向被刪除結點,以防丟失 temp->next = temp->next->next;//刪除某個結點的方法就是更改前一個結點的指針域 free(del);//手動釋放該結點,防止內存泄漏 return p; }
完整代碼
#include <stdio.h>
#include <stdlib.h>
typedef struct Link
{
int elem;
struct Link *next;
}link;
link * initLink(); //鏈表插入的函數,p是鏈表,elem是插入的結點的數據域,add是插入的位置
link * insertElem(link * p,int elem,int add); //刪除結點的函數,p代表操作鏈表,add代表刪除節點的位置
link * delElem(link * p,int add); //查找結點的函數,elem為目標結點的數據域的值
int selectElem(link * p,int elem); //更新結點的函數,newElem為新的數據域的值
link *amendElem(link * p, int add, int newElem);
void display(link *p);
int main()
{
//初始化鏈表(1,2,3,4)
printf("初始化鏈表為:\n");
link *p = initLink();
display(p);
printf("在第4的位置插入元素5:\n");
p = insertElem(p, 5, 4);
display(p);
printf("刪除元素3:\n");
p = delElem(p, 3);
display(p);
printf("查找元素2的位置為:\n");
int address = selectElem(p, 2);
if (address == -1)
printf("沒有該元素");
else
printf("元素2的位置為:%d\n",address);
printf("更改第3的位置的數據為7:\n");
p = amendElem(p, 3, 7);
display(p);
return 0;
}
link * initLink()
{
link * p = (link*)malloc(sizeof(link));//創建一個頭結點
link * temp = p;//聲明一個指針指向頭結點,用於遍歷鏈表
//生成鏈表
for (int i=1; i<5; i++)
{
link *a = (link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}
link *insertElem(link * p, int elem, int add)
{
link * temp = p; //創建臨時結點temp
//首先找到要插入位置的上一個結點
for (int i=1; i<add; i++)
{
if (temp == NULL)
{
printf("插入位置無效\n");
return p;
}
temp = temp->next;
}
//創建插入結點c
link * c = (link*)malloc(sizeof(link));
c->elem = elem; //向鏈表中插入結點
c->next = temp->next;
temp->next = c;
return p;
}
link * delElem(link * p, int add)
{
link * temp = p; //遍歷到被刪除結點的上一個結點
for (int i=1; i<add; i++)
{
temp = temp->next;
}
link * del = temp->next; //單獨設置一個指針指向被刪除結點,以防丟失
temp->next = temp->next->next; //刪除某個結點的方法就是更改前一個結點的指針域
free(del); //手動釋放該結點,防止內存泄漏
return p;
}
int selectElem(link * p, int elem)
{
link *t = p;
int i = 1;
while (t->next)
{
t = t->next;
if (t->elem == elem)
return i;
i++;
}
return -1;
}
link *amendElem(link * p, int add, int newElem)
{
link * temp = p;
temp = temp->next; //tamp指向首元結點
//temp指向被刪除結點
for (int i=1; i<add; i++)
{
temp = temp->next;
}
temp->elem = newElem;
return p;
}
void display(link *p)
{
link* temp = p;//將temp指針重新指向頭結點
//只要temp指針指向的結點的next不是Null,就執行輸出語句。
while (temp->next)
{
temp = temp->next;
printf("%d", temp->elem);
}
printf("\n");
}
運行結果:
初始化鏈表為: 1234
在第4的位置插入元素5: 12354
刪除元素3: 1254
查找元素2的位置為:
元素2的位置為:2
更改第3的位置的數據為7: 1274
總結
線性表的鏈式存儲相比於順序存儲,有兩大優勢:
- 鏈式存儲的數據元素在物理結構沒有限制,當內存空間中沒有足夠大的連續的內存空間供順序表使用時,可能使用鏈表能解決問題。(鏈表每次申請的都是單個數據元素的存儲空間,可以利用上一些內存碎片)
- 鏈表中結點之間采用指針進行鏈接,當對鏈表中的數據元素實行插入或者刪除操作時,只需要改變指針的指向,無需像順序表那樣移動插入或刪除位置的后續元素,簡單快捷。
鏈表和順序表相比,不足之處在於,當做遍歷操作時,由於鏈表中結點的物理位置不相鄰,使得計算機查找起來相比較順序表,速度要慢。