【内核链表】数据结构——深入理解内核链表的概念和操作笔记
內(nèi)核鏈表
- 一、內(nèi)核鏈表的前置概念
- 1、容器
- 2、通用解決方案
- 二、通用型鏈表節(jié)點的設(shè)計
- 1、初始化
- 2、增刪操作
- 3、查找節(jié)點
- 4、遍歷鏈表
- 5、示例代碼
- 三、內(nèi)核鏈表
- 1、普通鏈表弊端
- 2、內(nèi)核鏈表
- 2.1內(nèi)核鏈表結(jié)構(gòu)
- 2.2內(nèi)核鏈表的節(jié)點設(shè)計
- 2.3內(nèi)核鏈表的相關(guān)函數(shù)
- 1)內(nèi)核鏈表的初始化—— INIT_LIST_HEAD
- 2)插入節(jié)點
- 3)內(nèi)核鏈表的遍歷——list_for_each_entry(宏函數(shù) 就是一個for循環(huán))
- 4)內(nèi)核鏈表節(jié)點刪除——list_del
- 5)內(nèi)核鏈表的銷毀——先把除了頭結(jié)點的所有地址是否,最后釋放頭結(jié)點
- 6)示例代碼
一、內(nèi)核鏈表的前置概念
到目前為止,我們的順序表或者鏈表都以存放整型數(shù)據(jù)為例,但在實際工作應(yīng)用中,處理的對象不一定是一個整數(shù),而是任意的數(shù)據(jù)類型,這就要求我們對順序表和鏈表的設(shè)計要做一個更加深入的理解。
1、容器
首先要理解,所有的數(shù)據(jù)結(jié)構(gòu)本質(zhì)上是一種容器,包括已經(jīng)學(xué)習(xí)了的順序表、鏈表,以及后續(xù)將會學(xué)習(xí)的棧、隊列、二叉樹等,所謂容器,指的是只關(guān)心其內(nèi)部數(shù)據(jù)之間的邏輯關(guān)系,并提供這種邏輯關(guān)系相對應(yīng)的操作的集合。容器不關(guān)心數(shù)據(jù)本身的類型,因為對于容器而言,不管是存儲一個整數(shù)還是存儲一個進程,還是一個學(xué)生、一本圖書,他們都被稱為一個數(shù)據(jù)節(jié)點。
如圖:
2、通用解決方案
容器提供的是數(shù)據(jù)處理的通用解決方案,即:提供一套可以處理任意數(shù)據(jù)類型的通用API,不管是什么數(shù)據(jù),都可以統(tǒng)一處理。比如鏈表,不管處理什么數(shù)據(jù),對它們的操作都是統(tǒng)一的:初始化、插入、刪除、遍歷、銷毀等。
目前,有兩種常見的方式來獲得通用性:
- 創(chuàng)建容器時,讓用戶提供數(shù)據(jù)的類型。典型的應(yīng)用案例是STL(一個C++的類庫)。
- 將數(shù)據(jù)從容器中剝離出去,讓容器只提供邏輯,典型應(yīng)用案例是Linux內(nèi)核鏈表。
由于C語言沒有類,也不支持重載,受語言本身特性的限制,一般不使用第一種辦法來設(shè)計通用容器,但在一些小型程序中,C語言也是可以實現(xiàn)通用性的,關(guān)鍵在于:讓用戶提供數(shù)據(jù)的類型。而容器本身只處理跟數(shù)據(jù)邏輯結(jié)構(gòu)相關(guān)的操作,凡是涉及具體數(shù)據(jù)的操作,一律要讓用戶來提供。
3、下面以雙向鏈表為例,使用上述第一種方法,將其改造成通用的容器。
核心:
①數(shù)據(jù)域不再固定——鏈表的節(jié)點設(shè)計不再固定
②借助工程的模塊化和static關(guān)鍵字(限制作用域)
③頭文件特性——頭文件會在預(yù)處理時被展開
二、通用型鏈表節(jié)點的設(shè)計
1、初始化
// list.h #ifndef DATATYPE //此條件決定了下面通用結(jié)構(gòu)體的具體類型 #define DATATYPE int #endiftypedef DATATYPE datatype;// 此處的節(jié)點是通用的 // 原理是將具體數(shù)據(jù)的類型讓渡給用戶自己去定義 typedef struct node { //數(shù)據(jù)域datatype data; //指針域struct node *prev;struct node *next; }listnode, *linklist;以上代碼有幾處需要著重解釋:
- 上述代碼必須寫在*.h頭文件中,而不是*.c源文件中
- 用戶使用該容器的時候,定義DATATYPE為其所需要的數(shù)據(jù)類型
許多人比較困惑的地方在于,既然用戶需要提供數(shù)據(jù),那為什么不直接讓用戶定義datatype,而要去定義宏 DATATYPE 呢?原因是 typedef 無法跟宏一樣,給用戶提供一個默認(rèn)的數(shù)據(jù)類型。
接下來,對于跟用戶數(shù)據(jù)無關(guān)的操作,無需任何修改,直接就是通用的,比如初始化、判斷是否為空:
// 注意以下內(nèi)容必須放在頭文件 list.h 中// 初始化空鏈表,與用戶實際數(shù)據(jù)無關(guān) static node * initList() {node * head = (node *)malloc(sizeof(node));if(head != 0){head->prev = head;head->next = head;}return head; }// 判斷鏈表是否為空,與用戶實際數(shù)據(jù)無關(guān) static bool isEmpty(node *head) {return head->next == head; }注意:
通用型算法一律都只能寫到頭文件 list.h 中,因為編譯的時候 datatype 必須結(jié)合用戶提供的 *.c 源文件才能確定切確的類型,如果單獨編輯 list.c,那么在編譯產(chǎn)生 list.o 的過程中就無法使用用戶所指定的類型。
注意:
通用型算法代碼 list.h 的使用方法,就是直接作為頭文件放在用戶程序中即可,如果用戶需要使用鏈表容器處理其特定的數(shù)據(jù),那么就自定義宏 DATATYPE,如:
注意:
為防止頭文件被多個C文件包含而造成函數(shù)沖突,頭文件中的所有函數(shù)必須被定義為靜態(tài)存儲類型。
2、增刪操作
由于增刪操作都涉及用戶具體的數(shù)據(jù),因此需要對之前的操作作出修改。以增刪鏈表首部第一個節(jié)點為例,參考代碼如下:
// 根據(jù)用戶提供的數(shù)據(jù),產(chǎn)生一個新節(jié)點 static linklist __newNode(datatype *newData) {linklist new = malloc(sizeof(listnode));if(new != NULL){new->data = *newData;new->prev = new;new->next = new;}return new; }// 將新節(jié)點new插入到鏈表的首部 void listAdd(linklist head, datatype *newdata) {linklist new = __newNode(newdata);new->prev = head;new->next = head->next;head->next->prev = new;head->next = new; }// 將新節(jié)點new插入到鏈表的尾部 void listAddTail(linklist head, datatype *newdata) {linklist new = __newNode(newdata);new->prev = head->prev;new->next = head;head->prev->next = new;head->prev = new; }// 將指定節(jié)點從鏈表中剔除出去 bool listDel(linklist p) {if(p==NULL || isEmpty(p))return false;// 將原鏈表首節(jié)點剔除出鏈表p->prev->next = p->next;p->next->prev = p->prev;p->prev = p;p->next = p;return true; }提醒:
不對外的函數(shù)接口,一般使用下劃線開頭,比如 __newNode()
補充:
回調(diào)函數(shù):
這類函數(shù)是作為參數(shù)被其他函數(shù)調(diào)用
數(shù)組名作為參數(shù)傳遞進去本質(zhì)上是數(shù)組首元素地址
函數(shù)名作為參數(shù)傳遞進去本質(zhì)上是函數(shù)的地址
關(guān)于遍歷通用型鏈表,可以用戶自己設(shè)計一個適合當(dāng)前自定義節(jié)點的回調(diào)函數(shù),專門用來遍歷打印使用
3、查找節(jié)點
在鏈表中查找某個節(jié)點也是一種常規(guī)操作,但查找操作與上述的增刪操作有個很大的不同,節(jié)點的比對是跟節(jié)點本身數(shù)據(jù)密切相關(guān)的,比如整型數(shù)據(jù)可以直接使用等號來判斷是否一致,而字符串則需要通過特定的函數(shù)才能判斷,至于結(jié)構(gòu)體,則無法使用任何現(xiàn)成的方式去判定,只能由用戶根據(jù)其實際數(shù)據(jù)去判定。
因此,查找節(jié)點時,節(jié)點的判定接口必須由用戶提供,鏈表只提供回調(diào)接口。具體代碼如下:
4、遍歷鏈表
與上述查找算法類似,容器只應(yīng)提供跟通用性相關(guān)的操作,任何涉及用戶數(shù)據(jù)的操作都是不能寫的,否則就是去了通用性。之前對鏈表的遍歷,就是將節(jié)點中的數(shù)據(jù)打印出來,這是一種特定的針對整型數(shù)據(jù)的操作,是不具備通用性的。
注意:
1)在實際應(yīng)用中,遍歷鏈表時對每個節(jié)點的訪問操作不一定是將節(jié)點內(nèi)部數(shù)據(jù)打印出來。
2)對節(jié)點的訪問方式,應(yīng)該交給用戶去處理,只有用戶才知道怎么處理。
容器本身必須且只能提供“挨個訪問”每個節(jié)點的路徑操作,而不能涉及任何數(shù)據(jù)本身。
3)對節(jié)點的操作,需將用戶提供的特定操作函數(shù) handle 以參數(shù)的方式傳入給遍歷函數(shù),比如:
// 遍歷鏈表,并使用用戶提供的鉤子函數(shù) handle 處理節(jié)點
5、示例代碼
double_list.h:
#ifndef DATATYPE #define DATATYPE int #endif typedef DATATYPE datatype;typedef struct node {//數(shù)據(jù)域datatype data;//指針域struct node *next; //后繼指針,指向下一個與當(dāng)前類型一致的成員struct node *prev; //前驅(qū)指針 } listnode, *linklist;//通用型鏈表初始化 static linklist List_Init() {linklist Head = malloc(sizeof(listnode));Head->next = Head;Head->prev = Head;return Head; }//通用型鏈表頭插操作 static void HeadInsert(linklist Head, datatype info) {linklist Newnode = malloc(sizeof(listnode));//數(shù)據(jù)域Newnode->data = info;//指針域Newnode->next = Head->next;Head->next = Newnode;Newnode->next->prev = Newnode;Newnode->prev = Head; } //通用型鏈表尾插操作 static void TailInsert(linklist Head, datatype info) {linklist Newnode = malloc(sizeof(listnode));//數(shù)據(jù)域Newnode->data = info;//指針域Newnode->next = Head;Head->prev->next = Newnode;Newnode->prev = Head->prev;Head->prev = Newnode; }//通用型鏈表遍歷操作 static void List_brow(linklist Head, void (*pfunction)(datatype)) {linklist temp = Head->prev;while (temp != Head){pfunction(temp->data);temp = temp->prev;} }//通用型鏈表刪除節(jié)點操作 static void List_Nulldelete(linklist Node) {Node->next->prev = Node->prev;Node->prev->next = Node->next;free(Node); }//通用型鏈表按條件刪除節(jié)點操作 static void List_Havedelete(linklist Head, linklist (*fun)(linklist)) {linklist temp = fun(Head);if (temp != NULL){temp->prev->next = temp->next;temp->next->prev = temp->prev;free(temp);printf("刪除成功!\n");}elseprintf("刪除失敗!\n"); }//通用型鏈表查找節(jié)點操作 static void List_Search(linklist Head, linklist (*fun)(linklist)) {linklist temp = fun(Head);if (temp != NULL){printf("查找成功!\n");}elseprintf("查找失敗!\n"); }//通用型鏈表銷毀操作 static void List_Destroy(linklist Head) {linklist p = Head;linklist q = p->next;int i = 0;while (q != Head){p = q;free(p);i++;q = q->next;}free(Head);printf("成功釋放%d個節(jié)點\n", i); }double_list.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> struct book {char bookname[64];char Author[64];float price; }; #define DATATYPE struct book#include "double_list.h"//打印節(jié)點內(nèi)容 void pridata(datatype binfo) {printf("%s\t%s\t%.1f\n", (binfo).bookname, (binfo).Author, (binfo).price); }//按書名查找節(jié)點 linklist Search(linklist Head) {//查找char buf[32] = {0};printf("輸入你想要查找(刪除)的書名:");scanf("%s", buf);linklist temp = Head->next;int flag = 0;while (temp != Head){if (strcmp(temp->data.bookname, buf) == 0){return temp;flag = 1;break;}temp = temp->next;}if (flag == 0)return NULL; } int main() {//創(chuàng)建空表struct node *head = List_Init();datatype binfo;//尾插for (int i = 0; i < 3; i++){scanf("%s %s %f", binfo.bookname, binfo.Author, &binfo.price);while (getchar() != '\n'); //清空\nTailInsert(head, binfo);}//按條件刪除List_Havedelete(head, Search);//刪除指定節(jié)點//List_Nulldelete(head->prev);//修改節(jié)點//List_Revise(head->prev, Revise);//使用通用型的鏈表遍歷函數(shù),實現(xiàn)每找到一個節(jié)點 調(diào)用一次回調(diào)函數(shù),處理找到的當(dāng)前節(jié)點List_brow(head, pridata);//查找結(jié)點List_Search(head, Search);//銷毀節(jié)點List_Destroy(head);return 0; }三、內(nèi)核鏈表
1、普通鏈表弊端
普通鏈表概念簡單,操作方便,但存在有致命的缺陷,即:每一條鏈表都是特殊的,不具有通用性。因為對每一種不同的數(shù)據(jù),所構(gòu)建出來的鏈表都是跟這些數(shù)據(jù)相關(guān)的,所有的操作函數(shù)也都是數(shù)據(jù)密切相關(guān)的,換一種數(shù)據(jù)節(jié)點,則所有的操作函數(shù)都需要一一重新編寫,這種缺陷對于一個具有成千上萬種數(shù)據(jù)節(jié)點的工程來說是災(zāi)難性的。
2、內(nèi)核鏈表
如前所述,內(nèi)核鏈表解決通用性問題,大概分兩步:
①設(shè)計標(biāo)準(zhǔn)節(jié)點
②針對標(biāo)準(zhǔn)節(jié)點,設(shè)計由標(biāo)準(zhǔn)節(jié)點構(gòu)成的標(biāo)準(zhǔn)鏈表的所有操作
內(nèi)核鏈表的標(biāo)準(zhǔn)節(jié)點及其所有操作,都被封裝在內(nèi)核源碼中,具體來講都被封裝在一個名為 list.h 的文件中,該文件在內(nèi)核中的位置是:
kernel/linux/include/list.h
內(nèi)核中的源碼文件 list.h 實際上包含了兩部分內(nèi)容,一是內(nèi)核鏈表,二是哈希鏈表。經(jīng)過整理的、僅包含內(nèi)核鏈表的文件:kernel_list.h
百度網(wǎng)盤下載kernel_list.h文件:
鏈接:https://pan.baidu.com/s/1EIHrpbUgx-FphHDCX-jwDQ
提取碼:i7qs
2.1內(nèi)核鏈表結(jié)構(gòu)
內(nèi)核鏈表分為兩個結(jié)構(gòu)體
1)大結(jié)構(gòu)體——包含數(shù)據(jù)域與指針域(用戶自己來設(shè)計)
2)小結(jié)構(gòu)體——地址結(jié)構(gòu)體(內(nèi)核鏈表定義的類型)
內(nèi)核鏈表的雙向循環(huán)結(jié)構(gòu)其實就是地址結(jié)構(gòu)體形成的雙向循環(huán)結(jié)構(gòu)。
2.2內(nèi)核鏈表的節(jié)點設(shè)計
- 小結(jié)構(gòu)體
- 大結(jié)構(gòu)體
2.3內(nèi)核鏈表的相關(guān)函數(shù)
1)內(nèi)核鏈表的初始化—— INIT_LIST_HEAD
#define INIT_LIST_HEAD(ptr) do { \(ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0)參數(shù):ptr ——鏈表頭結(jié)點中地址結(jié)構(gòu)體的地址
2)插入節(jié)點
頭插法:
static inline void list_add(struct list_head *new, struct list_head *head) {__list_add(new, head, head->next); }尾插法:
static inline void list_add_tail(struct list_head *new, struct list_head *head) {__list_add(new, head->prev, head); }new : 新插入的節(jié)點的地址結(jié)構(gòu)體的地址
head : 鏈表頭結(jié)點的地址結(jié)構(gòu)體的地址
3)內(nèi)核鏈表的遍歷——list_for_each_entry(宏函數(shù) 就是一個for循環(huán))
#define list_for_each_entry(pos, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member); \ &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member))pos: 遍歷鏈表每一個節(jié)點的的指針p (p是大結(jié)構(gòu)體的類型)
head: 頭結(jié)點里面地址結(jié)構(gòu)體的地址 (小結(jié)構(gòu)體的類型)
member : 地址結(jié)構(gòu)體的名字 --》ptr
4)內(nèi)核鏈表節(jié)點刪除——list_del
static inline void list_del(struct list_head *entry) {__list_del(entry->prev, entry->next);entry->next = (void *) 0;entry->prev = (void *) 0; }entry : 需要被刪除的節(jié)點的地址結(jié)構(gòu)體的地址
5)內(nèi)核鏈表的銷毀——先把除了頭結(jié)點的所有地址是否,最后釋放頭結(jié)點
#define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member), \ n = list_entry(pos->member.next, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.next, typeof(*n), member))pos : 指向大的鏈表節(jié)點的指針 (大的結(jié)構(gòu)體類型)
n : 大的鏈表節(jié)點的指針,防止鏈表斷裂 (大的結(jié)構(gòu)體類型)
head : 鏈表頭結(jié)點里面地址結(jié)構(gòu)體的地址
member :地址結(jié)構(gòu)體的名字
6)示例代碼
#include <stdio.h> #include <stdlib.h> #include "kernel_list.h"//數(shù)據(jù)域結(jié)構(gòu)體 struct book {char bookname[64];char Author[64];float price; };#define DATATYPE struct book typedef DATATYPE datatype;//構(gòu)建節(jié)點----大結(jié)構(gòu)體 typedef struct node {//數(shù)據(jù)域struct book data;//指針域----小結(jié)構(gòu)體struct list_head list; }Node, *pNode;//借助內(nèi)核鏈表宏定義完成雙向循環(huán)鏈表空表初始化 pNode List_Init(void) {pNode Head = (pNode)malloc(sizeof(Node));if(Head == NULL){perror("malloc faild!");return NULL;}//做好大結(jié)構(gòu)體中小結(jié)構(gòu)體的鏈?zhǔn)疥P(guān)系INIT_LIST_HEAD(&Head->list);return Head; }//使用內(nèi)核鏈表完成頭插 int Head_Insert(pNode Head, datatype info) {//新節(jié)點空間分配pNode Newnode = (pNode)malloc(sizeof(Node));if(Newnode == NULL){perror("malloc faild!");return -1; //插入失敗}//數(shù)據(jù)域Newnode->data = info;//指針域---使用內(nèi)核鏈表頭插list_add(&(Newnode->list), &(Head->list)); }//使用內(nèi)核鏈表實現(xiàn)遍歷 void pridata(datatype binfo) {printf("%s\t%s\t%.1f\n",(binfo).bookname,(binfo).Author,(binfo).price); } void List_BrowRight(pNode Head, void (*pfunction)(datatype)) {pNode p;struct list_head *pos; //pos指向每一個大結(jié)構(gòu)體中的小結(jié)構(gòu)體list_for_each(pos, &(Head->list)) //循環(huán)遍歷{//以上循環(huán)只提供小結(jié)構(gòu)體指針的遍歷,但是遍歷找的是大結(jié)構(gòu)體的數(shù)據(jù)域,根據(jù)每個pos得到大結(jié)構(gòu)體地址p = list_entry(pos, Node, list);pfunction(p->data); //打印} }//使用內(nèi)核鏈表的刪除 void List_Delete(pNode Node) {list_del(&(Node->list));free(Node); }int main() {pNode head = List_Init();datatype tempinfo;//頭插for(int i=0;i<3;i++){scanf("%s %s %f", tempinfo.bookname, tempinfo.Author, &tempinfo.price);while(getchar()!='\n');Head_Insert(head, tempinfo);}//刪除節(jié)點pNode p; struct list_head *pos = (&(head->list))->next; //在小結(jié)構(gòu)體中找到你想刪除的節(jié)點p = list_entry(pos, Node, list); //獲取該節(jié)點的大結(jié)構(gòu)體的地址List_Delete(p); //刪除該節(jié)點//遍歷List_BrowRight(head, pridata); }總結(jié)
以上是生活随笔為你收集整理的【内核链表】数据结构——深入理解内核链表的概念和操作笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超好用的gif录制软件
- 下一篇: 网站后台 英文用户名不区分大小写都可以登