C語言實現鏈表(鏈式存儲結構)

鏈表(鏈式存儲結構)及創建

鏈表,別名鏈式存儲結構單鏈表,用於存儲邏輯關係爲 “一對一” 的數據。與順序表不同,鏈表不限制數據的物理存儲狀態,換句話說,使用鏈表存儲的數據元素,其物理存儲位置是隨機的。

例如,使用鏈表存儲 {1,2,3},數據的物理存儲狀態如下圖所示:
在這裏插入圖片描述
我們看到,上圖 根本無法體現出各數據之間的邏輯關係。對此,鏈表的解決方案是,每個數據元素在存儲時都配備一個指針,用於指向自己的直接後繼元素。如下圖所示:
在這裏插入圖片描述
像上圖這樣,數據元素隨機存儲,並通過指針表示數據之間邏輯關係的存儲結構就是鏈式存儲結構。

鏈表的節點

從圖 2 可以看到,鏈表中每個數據的存儲都由以下兩部分組成:

  1. 數據元素本身,其所在的區域稱爲數據域
  2. 指向直接後繼元素的指針,所在的區域稱爲指針域

即鏈表中存儲各數據元素的結構如下圖 所示:
在這裏插入圖片描述
上圖 所示的結構在鏈表中稱爲節點。也就是說,鏈表實際存儲的是一個一個的節點,真正的數據元素包含在這些節點中,如下圖 所示:
在這裏插入圖片描述
因此,鏈表中每個節點的具體實現,需要使用 C 語言中的結構體,具體實現代碼爲:

typedef struct Linklist{
	int  elem;//代表數據域
	struct Linklist *next;//代表指針域,指向直接後繼元素
}Linklist; //link爲節點名,每個節點都是一個 link 結構體

提示:由於指針域中的指針要指向的也是一個節點,因此要聲明爲 Link 類型(這裏要寫成 struct Link* 的形式)。

頭節點,頭指針和首元節點

其實,圖 4 所示的鏈表結構並不完整。一個完整的鏈表需要由以下幾部分構成:

  1. 頭指針:一個普通的指針,它的特點是永遠指向鏈表第一個節點的位置。很明顯,頭指針用於指明鏈表的位置,便於後期找到鏈表並使用表中的數據;
  2. 節點:鏈表中的節點又細分爲頭節點、首元節點和其他節點:
    頭節點:其實就是一個不存任何數據的空節點,通常作爲鏈表的第一個節點。對於鏈表來說,頭節點不是必須的,它的作用只是爲了方便解決某些實際問題;
    首元節點:由於頭節點(也就是空節點)的緣故,鏈表中稱第一個存有數據的節點爲首元節點。首元節點只是對鏈表中第一個存有數據節點的一個稱謂,沒有實際意義;
    其他節點:鏈表中其他的節點;

因此,一個存儲{1,2,3} 的完整鏈表結構如下圖 所示:
在這裏插入圖片描述

注意:鏈表中有頭節點時,頭指針指向頭節點;反之,若鏈表中沒有頭節點,則頭指針指向首元節點。

明白了鏈表的基本結構,下面我們來了解如何創建一個鏈表。

鏈表的創建(初始化)

創建一個鏈表需要做如下工作:

  1. 聲明一個頭指針(如果有必要,可以聲明一個頭節點);
  2. 創建多個存儲數據的節點,在創建的過程中,要隨時與其前驅節點建立邏輯關係;

例如,創建一個存儲{1,2,3,4 }且無頭節點的鏈表,C 語言實現代碼如下:

linklist * initlinklist(){
	linklist * p=NULL;//創建頭指針
	linklist * temp = (linklist*)malloc(sizeof(linklist));//創建首元節點
	//首元節點先初始化
	temp->elem = 1;
	temp->next = NULL;
	p = temp;//頭指針指向首元節點
	//從第二個節點開始創建
	for (int i=2; i<5; i++) {
	 //創建一個新節點並初始化
		linklist *a=(linklist*)malloc(sizeof(linklist));
		a->elem=i;
		a->next=NULL;
		//將temp節點與新建立的a節點建立邏輯關係
		temp->next=a;
		//指針temp每次都指向新鏈表的最後一個節點,其實就是 a節點,這裏寫temp=a也對
		temp=temp->next;
	}
	//返回建立的節點,只返回頭指針 p即可,通過頭指針即可找到整個鏈表
	return p;
}

如果想創建一個存儲{1,2,3,4}且含頭節點的鏈表,則 C 語言實現代碼爲:

linklist * initlinklist(){
	linklist * p=(linklist*)malloc(sizeof(linklist));//創建一個頭結點
	linklist * temp=p;//聲明一個指針指向頭結點,
	//生成鏈表
	for (int i=1; i<5; i++) {
		linklist *a=(linklist*)malloc(sizeof(linklist));
		a->elem=i;
		a->next=NULL;
		temp->next=a;
		temp=temp->next;
	}
	return p;
}

我們只需在主函數中調用 initLink 函數,即可輕鬆創建一個存儲 {1,2,3,4} 的鏈表,C 語言完整代碼如下:

#include <stdio.h>
#include <stdlib.h>
//鏈表中節點的結構
typedef struct Linklist{
	int  elem;
	struct Linklist *next;
}linklist;
//初始化鏈表的函數
linklist * initlinklist();
//用於輸出鏈表的函數
void display(linklist *p);
int main() {
	//初始化鏈表(1,2,3,4)
	printf("初始化鏈表爲:\n");
	linklist *p=initlinklist();
	display(p);
	return 0;
}
linklist * initlinklist(){
	linklist * p=NULL;//創建頭指針
	linklist * temp = (linklist*)malloc(sizeof(linklist));//創建首元節點
	//首元節點先初始化
	temp->elem = 1;
	temp->next = NULL;
	p = temp;//頭指針指向首元節點
	for (int i=2; i<5; i++) {
		linklist *a=(linklist*)malloc(sizeof(linklist));
		a->elem=i;
		a->next=NULL;
		temp->next=a;
		temp=temp->next;
	}
	return p;
}
void display(linklist *p){
	linklist* temp=p;//將temp指針重新指向頭結點
	//只要temp指針指向的結點的next不是Null,就執行輸出語句。
	while (temp) {
		printf("%d ",temp->elem);
		temp=temp->next;
	}
	printf("\n");
}

程序運行結果爲:

初始化鏈表爲:
1 2 3 4
注意:如果使用帶有頭節點創建鏈表的方式,則輸出鏈表的 display 函數需要做適當地修改:

void display(linklist *p){
	linklist* temp=p;//將temp指針重新指向頭結點
	//只要temp指針指向的結點的next不是Null,就執行輸出語句。
	while (temp->next) {
		temp=temp->next;
		printf("%d",temp->elem);
	}
	printf("\n");
}

鏈表插入元素

同順序表一樣,向鏈表中增添元素,根據添加位置不同,可分爲以下 3 種情況:

  • 插入到鏈表的頭部(頭節點之後),作爲首元節點;
  • 插入到鏈表中間的某個位置;
  • 插入到鏈表的最末端,作爲鏈表中最後一個數據元素;

雖然新元素的插入位置不固定,但是鏈表插入元素的思想是固定的,只需做以下兩步操作,即可將新元素插入到指定的位置:

  1. 將新結點的 next 指針指向插入位置後的結點;
  2. 將插入位置前結點的 next 指針指向插入結點;

例如,我們在鏈表{1,2,3,4}的基礎上分別實現在頭部、中間部位、尾部插入新元素 5,其實現過程如下圖 所示:
在這裏插入圖片描述
從圖中可以看出,雖然新元素的插入位置不同,但實現插入操作的方法是一致的,都是先執行步驟 1 ,再執行步驟 2。

注意:鏈表插入元素的操作必須是先步驟 1,再步驟 2;反之,若先執行步驟 2,會導致插入位置後續的部分鏈表丟失,無法再實現步驟 1。

通過以上的講解,我們可以嘗試編寫 C 語言代碼來實現鏈表插入元素的操作:

//p爲原鏈表,elem表示新數據元素,add表示新元素要插入的位置
linklist * insertElem(linklist * p,int elem,int add){
	linklist * temp=p;//創建臨時結點temp
	//首先找到要插入位置的上一個結點
	for (int i=1; i<add; i++) {
		if (temp==NULL) {
			printf("插入位置無效\n");
			return p;
		}
		temp=temp->next;
	}   
	//創建插入結點c
	linklist * c=(linklist*)malloc(sizeof(linklist));
	c->elem=elem;
	//向鏈表中插入結點
	c->next=temp->next;
	temp->next=c;
	return  p;
}

提示:insertElem 函數中加入一個 if 語句,用於判斷用戶輸入的插入位置是否有效。例如,在已存儲 {1,2,3} 的鏈表中,用戶要求在鏈表中第 100 個數據元素所在的位置插入新元素,顯然用戶操作無效,此時就會觸發 if 語句。

鏈表刪除元素

從鏈表中刪除指定數據元素時,實則就是將存有該數據元素的節點從鏈表中摘除,但作爲一名合格的程序員,要對存儲空間負責,對不再利用的存儲空間要及時釋放。因此,從鏈表中刪除數據元素需要進行以下 2 步操作:

  1. 將結點從鏈表中摘下來;
  2. 手動釋放掉結點,回收被結點佔用的存儲空間;

其中,從鏈表上摘除某節點的實現非常簡單,只需找到該節點的直接前驅節點 temp,執行一行程序:

temp->next=temp->next->next;

例如,從存有{1,2,3,4}的鏈表中刪除元素 3,則此代碼的執行效果如下圖 所示:
在這裏插入圖片描述
因此,鏈表刪除元素的 C 語言實現如下所示:

//p爲原鏈表,add爲要刪除元素的值
linklist * delElem(linklist * p,int add){
	linklist * temp=p;
	//temp指向被刪除結點的上一個結點
	for (int i=1; i<add; i++) {
		temp=temp->next;
	}
	linklist * del=temp->next;//單獨設置一個指針指向被刪除結點,以防丟失
	temp->next=temp->next->next;//刪除某個結點的方法就是更改前一個結點的指針域
	free(del);//手動釋放該結點,防止內存泄漏
	return p;
}

我們可以看到,從鏈表上摘下的節點 del 最終通過 free 函數進行了手動釋放。

鏈表查找元素

在鏈表中查找指定數據元素,最常用的方法是:從表頭依次遍歷表中節點,用被查找元素與各節點數據域中存儲的數據元素進行比對,直至比對成功或遍歷至鏈表最末端的NULL(比對失敗的標誌)。

因此,鏈表中查找特定數據元素的 C 語言實現代碼爲:

//p爲原鏈表,elem表示被查找元素、
int selectElem(linklist * p,int elem){
//新建一個指針t,初始化爲頭指針 p
	linklist * t=p;
	int i=1;
	//由於頭節點的存在,因此while中的判斷爲t->next
	while (t->next) {
		t=t->next;
		if (t->elem==elem) {
			return i;
		}
		i++;
	}
	//程序執行至此處,表示查找失敗
	return -1;
}

注意:遍歷有頭節點的鏈表時,需避免頭節點對測試數據的影響,因此在遍歷鏈表時,建立使用上面代碼中的遍歷方法,直接越過頭節點對鏈表進行有效遍歷。

鏈表更新元素

更新鏈表中的元素,只需通過遍歷找到存儲此元素的節點,對節點中的數據域做更改操作即可。

直接給出鏈表中更新數據元素的 C 語言實現代碼:

//更新函數,其中,add 表示更改結點在鏈表中的位置,newElem 爲新的數據域的值
linklist *amendElem(linklist * p,int add,int newElem){
	linklist * temp=p;
	temp=temp->next;//在遍歷之前,temp指向首元結點
	//遍歷到被刪除結點
	for (int i=1; i<add; i++) {
		temp=temp->next;
	}
	temp->elem=newElem;
	return p;
}

以上內容詳細介紹了對鏈表中數據元素做"增刪查改"的實現過程及 C 語言代碼相關完整代碼已經push到GitHub,需要的小夥伴自行clone,如果覺得還不錯的話,歡迎Star!這裏是傳送門鏈式存儲結構,除此之外,想要了解更多的C,C++,Java,Python等相關知識的童鞋,歡迎來我的博客(相逢的博客),我們一起討論!接下來我會持續更新其他算法,敬請期待!

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