0x00 什么是鏈表
鏈表可以說是一種最為基礎的數據結構了,而單向鏈表更是基礎中的基礎。鏈表是由一組元素以特定的順序組合或鏈接在一起的,不同元素之間在邏輯上相鄰,但是在物理上並不一定相鄰。在維護一組數據集合時,就可以使用鏈表,這一點和數組很相似。但是,鏈表有着數組所不具有的優勢。一方面,鏈表在執行插入刪除操作時擁有更高的效率;另一方面,鏈表是在堆區動態的開辟存儲空間,而大多數的數據在編譯時大小並不能確定,因此這種動態開辟空間的特性也可以說是鏈表的一個優點。
0x01 鏈表的應用
- 多項式計算
- 滾動列表
- 郵件列表
- 文件的鏈式分配
- 內存管理
……
0x02 單向鏈表初見
就像圖像所示,單向鏈表各個元素之間通過一個指針先后鏈接起來。每個元素包含兩個部分,分別是數據域和指針域。前一個元素通過next指針指向后一個元素,鏈表的開始的元素為鏈表頭,即head指針所指,鏈表結束的元素為鏈表尾,尾部元素的next指針指向NULL。可見,單向鏈表為線性結構。
若想訪問鏈表中的一個元素,我們只能從鏈表的頭部開始,順着指針指向逐查找。如果從鏈表頭移動到指定的元素,而這時候我們又想訪問當前元素之前的某個元素,這時候只能從頭在次遍歷鏈表。這相比數組能夠通過下標直接訪問要麻煩的多,不過我們應該根據不同的應用場景選擇數組還是鏈表,它們只有在對的地方才能發揮出巨大的威力。
0x03 單向鏈表的操作
0x00 鏈表結構
typedef struct node//鏈表元素的結構
{
void *data;//節點中的數據域,設置為無類型指針,數據類型大小由使用者定義
struct node *next;//指向下一節點的指針
}Node;
typedef struct list//鏈表的結構
{
int size;//鏈表中節點個數
void (*destroy)(void *data);//由於鏈表節點中的數據是用戶自定義的,故需要調用者提供釋放空間的函數
void (*print_data)(const void *data);//同,由用戶自定義打印數據的函數
int (*match)(const void *key1, const void *key2);//同,由用戶自定義數據的比較方式
Node *head;//記錄鏈表的頭部位置
Node *tail;//記錄鏈表的尾部位置
}List;
示意圖如下:
0x01 接口
下面是鏈表操作函數的接口,以及簡單介紹:
extern void list_init(List *list, void (*destroy)(void *data), void (*print_data)(const void *data), \
int (*match)(const void *key1, const void *key2));//初始化一個鏈表
extern int list_ins_head(List *list, const void *data);//鏈表的插入,將節點從頭部插入
extern int list_ins_tail(List *list, const void *data);//鏈表的插入,將節點從尾部插入
extern int list_ins_sort(List *list, const void *data);//鏈表的插入,插入后鏈表是一個有序的鏈表
extern void* list_search(List *list, const void *data);//在鏈表中查找指定數據,若找到返回數據的地址
extern void* list_remove(List *list, const void *data);//在鏈表中刪除指定數據,若找到刪除節點並將數據地址返回
extern void list_reverse(List *list);//將鏈表逆置
extern void list_sort(List *list);//將鏈表按照一定方式排序
extern void print_list(List *list);//打印鏈表
extern void list_destroy(List *list);//刪除整個鏈表
#define list_size(list) (list->size) //返回鏈表節點個數
0x02 list_init
使用list_init函數初始化一個鏈表,以便鏈表的其他操作。
void list_init(List *list, void (*destroy)(void *data), void (*print_data)(const void *data), \
int (*match)(const void *key1, const void *key2))
{
list->size = 0;//初始時,鏈表沒有節點,設置為0
list->head = NULL;//頭和尾置空
list->tail = NULL;
list->match = match;//初始化鏈表的成員函數
list->destroy = destroy;
list->print_data = print_data;
return;
}
0x03 list_ins_head
使用list_ins_head函數,在鏈表的頭部插入數據。示意圖如下:
從示意圖可以看出,單向鏈表的部插入邏輯非常簡單。仔細觀察,標綠的部分代碼有重復,可以優化
/*在鏈表的頭部插入數據*/
int list_ins_head(List *list, const void *data)
{
Node *new_node = (Node *)calloc(1, sizeof (Node)); //創建插入的節點
if(new_node == NULL)
return -1;
new_node->data = (void *)data;//關聯節點與數據
/*
if(list_size(list) == 0)//鏈表為空時,插入節點
{
list->tail = new_node;
new_node->next = NULL;
list->head = new_node;
}
else //鏈表非空時將節點插入頭部
{
new_node->next = list->head;
list->head = new_node;
}
*/
if(list_size(list) == 0)//鏈表為空時,插入節點
list->tail = new_node;
new_node->next = list->head;
list->head = new_node;
list->size ++;//維護鏈表size屬性
return 0;
}
0x04 list_ins_tail
使用list_ins_tail函數,在鏈表的尾部插入數據,示意圖如下:
/*在鏈表的尾部插入數據*/
int list_ins_tail(List *list, const void *data)
{
Node *new_node = (Node *)calloc(1, sizeof (Node)); //創建插入的節點
if(new_node == NULL)
return -1;
new_node->data = (void *)data;//關聯節點與數據
if(list_size(list) == 0)
list->head = new_node;
else
list->tail->next = new_node;
list->tail = new_node;
new_node->next = NULL;
list->size ++;
return 0;
}
0x05 list_ins_sort
使用list_ins_sort函數,進行鏈表的有序插入。
鏈表的有序插入大致可以分為兩種情況:
其一,鏈表為空時直接插入;
其二,鏈表非空時,在此時又分為三種小情況;
- 在鏈表頭部插入
- 在鏈表中部插入
- 在鏈表尾部插入
鏈表為空時,操作方法和頭尾部插入類似。鏈表非空時,我們需要先尋找到插入位置,然后在將數據插入鏈表。
在此之前我們已經了解了如何在鏈表的頭部和尾部插入元素,那么,現在唯一需要處理的便是 在鏈表中部插入節點 ,這是鏈表插入操作的核心。
注意:在鏈表中部插入節點時,必須得到前一節點的位置,即圖中指向藍色節點的指針p_pre.
插入節點的邏輯了解后,處理在非空鏈表情況下插入節點就清晰多了。
/*在鏈表的有序插入數據*/
int list_ins_sort(List *list, const void *data)
{
Node *new_node = (Node *)calloc(1, sizeof (Node)); //創建插入的節點
if(new_node == NULL)
return -1;
new_node->data = (void *)data;//關聯節點與數據
if(list_size(list) == 0)//鏈表為空時,插入節點
{
list->tail = new_node;
new_node->next = NULL;
list->head = new_node;
}
else//鏈表非空
{
Node *p_cur = list->head;
Node *p_pre = list->head;
while(p_cur != NULL && list->match(new_node->data, p_cur->data) > 0)//查找鏈表的插入位置
{
p_pre = p_cur;
p_cur = p_cur->next;
}
if(p_cur != NULL)//插入位置在頭部和中間時
{
if(p_cur == list->head)//插入位置在頭部
{
new_node->next = list->head;
list->head = new_node;
}
else//位置在鏈表中間
{
new_node->next = p_pre->next;
p_pre->next = new_node;
}
}
else//插入位置在鏈表尾部
{
list->tail->next = new_node;
list->tail = new_node;
new_node = NULL;
}
}
list->size ++;
return 0;
}
0x06 list_search
使用list_search函數,查找鏈表中與數據匹配的節點,並返回節點指針。
此處查找邏輯與list_ins_sort中的查找邏輯基本類似,不做贅述。
/*查找鏈表中與數據匹配的節點,並返回節點指針*/
void* list_search(List *list, const void *data)
{
if(list_size(list) == 0)
{
printf("list is empty\n");
return NULL;
}
else
{
Node *p_cur = list->head;
while(p_cur != NULL && list->match(p_cur->data, data) != 0)//查找數據在鏈表中的位置
p_cur = p_cur->next;
if(p_cur != NULL)//找到返回數據地址,否則返回NULL
return p_cur->data;
else
return NULL;
}
}
0x07 list_remove
使用list_remove函數,刪除節點,並將節點中的數據返回,交由用戶處理。
此處查找邏輯與list_ins_sort中的查找邏輯基本類似,不做贅述。
和插入節點中分為頭部、中部尾部類似,刪除也分為頭中尾部。
刪除頭部節點
不過刪除頭部節點時需要注意一點,就是當鏈表僅有一個節點時,我們需要維護一下tail指針。
刪除中部節點
刪除尾部節點
注意:代碼中將中部與尾部的刪除進行了合並。
/*刪除指定數據的節點*/
void* list_remove(List *list, const void *data)
{
void *old_data = NULL;
Node *p_cur = list->head;
Node *p_pre = list->head;
while (p_cur != NULL && list->match(p_cur->data, data) !=0)
{
p_pre = p_cur;
p_cur = p_cur->next;
}
if(p_cur != NULL && list->match(p_cur->data, data) ==0)//刪除位置在頭部和中間時
{
if(p_cur == list->head)//刪除位置在頭部
{
list->head = p_cur->next;
if(p_cur->next == NULL)
list->tail = NULL;
}
else//中部時或尾部
{
p_pre->next = p_cur->next;
if(p_cur->next == NULL)//判斷是否為尾部
list->tail = p_pre;
}
old_data = p_cur->data;
free(p_cur);
list->size --;
}
return old_data;
}
0x08 list_reverse
使用list_reverse函數將鏈表逆置。
逆置過程:
觀察可以發現,逆置的過程本質上就是將原來的鏈表逐個摘下頭節點,插入逆置后的鏈表的頭部
void list_reverse(List *list)
{
if(list_size(list) != 0)
{
Node *p_pre = list->head;
Node *p_cur = list->head->next;
list->head->next = NULL;
list->tail = list->head;
while(p_cur!= NULL)
{
p_pre = p_cur;
p_cur = p_cur->next;
p_pre->next = list->head;
list->head = p_pre;
}
}
return;
}
0x09 list_sort
使用list_sort函數對鏈表進行排序,此處使用選擇排序法。
void list_sort(List *list)
{
if(list_size(list) != 0)
{
Node *p_i = list->head;
while(p_i->next != NULL)
{
Node *p_min = p_i;
Node *p_j = p_min->next;
while (p_j != NULL)
{
if(list->match(p_min->data, p_j->data) > 0)
p_min = p_j;
p_j = p_j->next;
}
if(p_min != p_i)
{
void *data = p_i->data;
p_i->data = p_min->data;
p_min->data = data;
}
p_i = p_i->next;
}
}
return;
}
0x0a print_list和list_destroy
void print_list(List *list)
{
if(list->head == NULL)//鏈表為空
{
printf("list is empty\n");
}
else //鏈表非空
{
Node * p_cur = list->head;
while (p_cur)
{
list->print_data(p_cur->data);
p_cur = p_cur->next;
}
}
return;
}
void list_destroy(List *list)
{
Node *p_cur = list->head;
while (p_cur != NULL)
{
list->head = list->head->next;
list->destroy(p_cur->data);//釋放節點中的數據
free(p_cur);//釋放節點
p_cur = list->head;
}
memset(list, 0, sizeof (List));
return;
}