日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

一口气搞懂「链表」,就靠这20+张图了

發布時間:2023/12/19 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一口气搞懂「链表」,就靠这20+张图了 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

? 順序存儲和鏈式存儲

數組—順序存儲

數組作為一個順序儲存方式的數據結構,可是有大作為的,它的靈活使用為我們的程序設計帶來了大量的便利;

但是,但是,數組最大的缺點就是我們的插入和刪除時需要移動大量的元素,所以呢,大量的消耗時間,以及冗余度難以接受了。

以C語言數組插入一個元素為例,當我們需要在一個數組{1,2,3,4}的第1個元素后的位置插入一個’A’時,我們需要做的有:

  • 將第1個元素后的整體元素后移,形成新的數組{1,2,2,3,4}

  • 再將第2個元素位置的元素替換為我們所需要的元素’A’

  • 最終形成我們的預期,這需要很多的操作喔。

  • 上圖可以看出,使用數組都有這兩大缺點:

  • 插入刪除操作所需要移動的元素很多,浪費算力。

  • 必須為數組開足夠的空間,否則有溢出風險。

  • 鏈表—鏈式存儲

    由于數組的這些缺點,自然而然的就產生鏈表的思想了。

    鏈表通過不連續的儲存方式,自適應內存大小,以及指針的靈活使用,巧妙的簡化了上述的內容。

    鏈表的基本思維是,利用結構體的設置,額外開辟出一份內存空間去作指針,它總是指向下一個結點,一個個結點通過NEXT指針相互串聯,就形成了鏈表。

    其中DATA為自定義的數據類型,NEXT為指向下一個鏈表結點的指針,通過訪問NEXT,可以引導我們去訪問鏈表的下一個結點。

    對于一連串的結點而言,就形成了鏈表如下圖:

    上文所說的插入刪除操作只需要修改指針所指向的區域就可以了,不需要進行大量的數據移動操作。如下圖:

    相比起數組,鏈表解決了數組不方便移動,插入,刪除元素的弊端,但相應的,鏈表付出了更加大的內存犧牲換來的這些功能的實現。

    鏈表概述

    包含單鏈表,雙鏈表,循環單鏈表,實際應用中的功能不同,但實現方式都差不多。

    • 單鏈表就像是美國男籃,一代一代往下傳;

    • 雙鏈表像是中國男足,你傳球給我,我傳球給你,最終傳給了守門員;

    • 循環鏈表就像是中國男籃,火炬從姚明傳給王治郅,王治郅傳給易建聯,現在易建聯傷了,又傳給了姚明

    單鏈表

    單鏈表概念和簡單的設計

    單鏈表是一種鏈式存取的數據結構,鏈表中的數據是以結點來表示的,每個結點由元素和指針構成。

    元素表示數據元素的映象,就是存儲數據的存儲單元;指針指示出后繼元素存儲位置,就是連接每個結點的地址數據。

    以結點的序列表示的線性表稱作線性鏈表,也就是單鏈表,單鏈表是鏈式存取的結構。

    對于鏈表的每一個結點,我們使用結構體進行設計,其主要內容有:

    其中,DATA數據元素,可以為你想要儲存的任何數據格式,可以是數組,可以是int,甚至可以是結構體(這就是傳說中的結構體套結構體)

    NEXT為一個指針,其代表了一個可以指向的區域,通常是用來指向下一個結點,鏈表的尾部NEXT指向NULL(空),因為尾部沒有任何可以指向的空間了

    故,對于一個單鏈表的結點定義,可以代碼描述成:

    //定義結點類型 typedef?struct?Node?{int?data;???????//數據類型,你可以把int型的data換成任意數據類型,包括結構體struct等復合類型struct?Node?*next;??????????//單鏈表的指針域 }?Node,*LinkedList;?? //Node表示結點的類型,LinkedList表示指向Node結點類型的指針類型

    鏈表的初始化

    初始化主要完成以下工作:創建一個單鏈表的前置節點并向后逐步添加節點,一般指的是申請結點的空間,同時對一個結點賦空值(NULL),其代碼可以表示為:

    LinkedList?listinit(){Node?*L;L=(Node*)malloc(sizeof(Node));??????//開辟空間?if(L==NULL){?????????????????????//判斷是否開辟空間失敗,這一步很有必要printf("申請空間失敗");//exit(0);??????????????????//開辟空間失敗可以考慮直接結束程序}L->next=NULL;???????//指針指向空 }

    注意:一定要判斷是否開辟空間失敗,否則生產中由于未知的情況造成空間開辟失敗,仍然在繼續執行代碼,后果將不堪設想啦,因此養成這樣的判斷是很有必要的。

    頭插入法創建單鏈表

    利用指針指向下一個結點元素的方式進行逐個創建,使用頭插入法最終得到的結果是逆序的。如圖所示:

    從一個空表開始,生成新結點,并將讀取到的數據存放到新結點的數據域中,然后將新結點插入到當前鏈表的表頭,即頭結點之后。

    //頭插法建立單鏈表 LinkedList?LinkedListCreatH()?{Node?*L;L?=?(Node?*)malloc(sizeof(Node));???//申請頭結點空間L->next?=?NULL;??????????????????????//初始化一個空鏈表int?x;?????????????????????????//x為鏈表數據域中的數據while(scanf("%d",&x)?!=?EOF)?{Node?*p;p?=?(Node?*)malloc(sizeof(Node));???//申請新的結點p->data?=?x;?????????????????????//結點數據域賦值p->next?=?L->next;?????//將結點插入到表頭L-->|2|-->|1|-->NULLL->next?=?p;}return?L; }

    尾插入法創建單鏈表

    如圖所示為尾插入法的創建過程。

    頭插法生成的鏈表中,結點的次序和輸入數據的順序不一致。若希望兩者次序一致,則需要尾插法。

    該方法是將新結點逐個插入到當前鏈表的表尾上,為此必須增加一個尾指針r, 使其始終指向當前鏈表的尾結點,代碼如下:

    //尾插法建立單鏈表LinkedList?LinkedListCreatT()?{Node?*L;L?=?(Node?*)malloc(sizeof(Node));???//申請頭結點空間L->next?=?NULL;??????????????????//初始化一個空鏈表Node?*r;r?=?L;??????????????????????????//r始終指向終端結點,開始時指向頭結點int?x;?????????????????????????//x為鏈表數據域中的數據while(scanf("%d",&x)?!=?EOF)?{Node?*p;p?=?(Node?*)malloc(sizeof(Node));???//申請新的結點p->data?=?x;?????????????????????//結點數據域賦值r->next?=?p;????????????//將結點插入到表頭L-->|1|-->|2|-->NULLr?=?p;}r->next?=?NULL;return?L; }

    遍歷單鏈表如打印、修改

    從鏈表的頭開始,逐步向后進行每一個元素的訪問,稱為遍歷。

    對于遍歷操作,我們可以衍生出很多常用的數據操作,比如查詢元素,修改元素,獲取元素個數,打印整個鏈表數據等等。

    進行遍歷的思路極其簡單,只需要建立一個指向鏈表L的結點,然后沿著鏈表L逐個向后搜索即可,代碼如下:

    //便利輸出單鏈表 void?printList(LinkedList?L){Node?*p=L->next;int?i=0;while(p){printf("第%d個元素的值為:%d\n",++i,p->data);p=p->next;} }

    對于元素修改操作,以下是代碼實現:

    //鏈表內容的修改,在鏈表中修改值為x的元素變為為k。 LinkedList?LinkedListReplace(LinkedList?L,int?x,int?k)?{Node?*p=L->next;int?i=0;while(p){if(p->data==x){p->data=k;}p=p->next;}return?L; }

    簡單的遍歷設計的函數只需要void無參即可,而當涉及到元素操作時,可以設計一個LinkedList類型的函數,使其返回一個操作后的新鏈表。

    插入操作

    鏈表的插入操作主要分為查找到第i個位置,將該位置的next指針修改為指向我們新插入的結點,而新插入的結點next指針指向我們i+1個位置的結點。

    其操作方式可以設置一個前驅結點,利用循環找到第i個位置,再進行插入。

    如圖,在DATA1和DATA2數據結點之中插入一個NEW_DATA數據結點:

    從原來的鏈表狀態

    到新的鏈表狀態:

    代碼實現如下:

    //單鏈表的插入,在鏈表的第i個位置插入x的元素LinkedList?LinkedListInsert(LinkedList?L,int?i,int?x)?{Node?*pre;??????????????????????//pre為前驅結點pre?=?L;int?tempi?=?0;for?(tempi?=?1;?tempi?<?i;?tempi++)?{pre?=?pre->next;?????????????????//查找第i個位置的前驅結點}Node?*p;????????????????????????????????//插入的結點為pp?=?(Node?*)malloc(sizeof(Node));p->data?=?x;p->next?=?pre->next;pre->next?=?p;return?L; }

    刪除操作

    刪除元素要建立一個前驅結點和一個當前結點,當找到了我們需要刪除的數據時,直接使用前驅結點跳過要刪除的結點指向要刪除結點的后一個結點,再將原有的結點通過free函數釋放掉。如圖所示:

    代碼如下:

    //單鏈表的刪除,在鏈表中刪除值為x的元素LinkedList?LinkedListDelete(LinkedList?L,int?x)?{Node *p,*pre;???????????????????//pre為前驅結點,p為查找的結點。p?=?L->next;while(p->data?!=?x)?{??????????????//查找值為x的元素pre?=?p;p?=?p->next;}pre->next = p->next;??????????//刪除操作,將其前驅next指向其后繼。free(p);return?L; }

    雙向鏈表

    雙向鏈表的簡介以及概念

    單鏈表是指結點中只有一個指向其后繼的指針,具有單向性,但是有時需要搜索大量數據的時候,就需要多次進行從頭開始的遍歷,這樣的搜索就不是很高效了。

    在單鏈表的基礎上,對于每一個結點設計一個前驅結點,前驅結點與前一個結點相互連接,構成一個鏈表,就產生了雙向鏈表的概念了。

    雙向鏈表可以簡稱為雙鏈表,是鏈表的一種,它的每個數據結點中都有兩個指針,分別指向直接后繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和后繼結點。

    雙向鏈表示意圖

    一個完整的雙向鏈表應該是頭結點的pre指針指為空,尾結點的next指針指向空,其余結點前后相鏈。

    雙向鏈表的結點設計

    對于每一個結點而言,有:

    其中,DATA表示數據,其可以是簡單的類型也可以是復雜的結構體;

    pre代表的是前驅指針,它總是指向當前結點的前一個結點,如果當前結點是頭結點,則pre指針為空;

    next代表的是后繼指針,它總是指向當前結點的下一個結點,如果當前結點是尾結點,則next指針為空

    其代碼設計如下:

    typedef?struct?line{int?data;???????????//datastruct?line?*pre;???//pre?nodestruct?line?*next;??//next?node }line,*a; //分別表示該結點的前驅(pre),后繼(next),以及當前數據(data)
  • 雙鏈表的創建

  • 創建雙向鏈表需要先創建頭結點,然后逐步的進行添加雙向鏈表的頭結點是有數據元素的,也就是頭結點的data域中是存有數據的,這與一般的單鏈表是不同的。

    對于逐步添加數據,先開辟一段新的內存空間作為新的結點,為這個結點進行的data進行賦值,然后將已成鏈表的上一個結點的next指針指向自身,自身的pre指針指向上一個結點。

    其代碼可以設計為:

    //創建雙鏈表 line*?initLine(line?*?head){int?number,pos=1,input_data;//三個變量分別代表結點數量,當前位置,輸入的數據printf("請輸入創建結點的大小\n");scanf("%d",&number);if(number<1){return?NULL;}?//輸入非法直接結束//頭結點創建///head=(line*)malloc(sizeof(line));head->pre=NULL;head->next=NULL;printf("輸入第%d個數據\n",pos++);scanf("%d",&input_data);head->data=input_data;line?*?list=head;while?(pos<=number)?{line?*?body=(line*)malloc(sizeof(line));body->pre=NULL;body->next=NULL;printf("輸入第%d個數據\n",pos++);scanf("%d",&input_data);body->data=input_data;list->next=body;body->pre=list;list=list->next;}return?head; }

    雙向鏈表創建的過程可以分為:創建頭結點->創建一個新的結點->將頭結點和新結點相互鏈接->再度創建新結點,這樣會有助于理解。

    雙向鏈表的插入操作

    如圖所示:

    對于每一次的雙向鏈表的插入操作,首先需要創建一個獨立的結點,并通過malloc操作開辟相應的空間;

    其次我們選中這個新創建的獨立節點,將其的pre指針指向所需插入位置的前一個結點;

    同時,其所需插入的前一個結點的next指針修改指向為該新的結點,該新的結點的next指針將會指向一個原本的下一個結點,而修改下一個結點的pre指針為指向新結點自身,這樣的一個操作我們稱之為雙向鏈表的插入操作。

    其代碼可以表示為:

    //插入數據 line?*?insertLine(line?*?head,int?data,int?add){//三個參數分別為:進行此操作的雙鏈表,插入的數據,插入的位置//新建數據域為data的結點line?*?temp=(line*)malloc(sizeof(line));temp->data=data;temp->pre=NULL;temp->next=NULL;//插入到鏈表頭,要特殊考慮if?(add==1)?{temp->next=head;head->pre=temp;head=temp;}else{line?*?body=head;//找到要插入位置的前一個結點for?(int?i=1;?i<add-1;?i++)?{body=body->next;}//判斷條件為真,說明插入位置為鏈表尾if?(body->next==NULL)?{body->next=temp;temp->pre=body;}else{body->next->pre=temp;temp->next=body->next;body->next=temp;temp->pre=body;}}return?head; }

    雙向鏈表的刪除操作

    如圖:

    刪除操作的過程是:選擇需要刪除的結點->選中這個結點的前一個結點->將前一個結點的next指針指向自己的下一個結點->選中該節點的下一個結點->將下一個結點的pre指針修改指向為自己的上一個結點。

    在進行遍歷的時候直接將這一個結點給跳過了,之后,我們釋放刪除結點,歸還空間給內存,這樣的操作我們稱之為雙鏈表的刪除操作。

    其代碼可以表示為:

    //刪除元素 line?*?deleteLine(line?*?head,int?data){//輸入的參數分別為進行此操作的雙鏈表,需要刪除的數據line?*?list=head;//遍歷鏈表while?(list)?{//判斷是否與此元素相等//刪除該點方法為將該結點前一結點的next指向該節點后一結點//同時將該結點的后一結點的pre指向該節點的前一結點if?(list->data==data)?{list->pre->next=list->next;list->next->pre=list->pre;free(list);printf("--刪除成功--\n");return?head;}list=list->next;}printf("Error:沒有找到該元素,沒有產生刪除\n");return?head; }

    雙向鏈表的遍歷

    雙向鏈表的遍歷利用next指針逐步向后進行索引即可。

    注意,在判斷這里,我們既可以用while(list)的操作直接判斷是否鏈表為空,也可以使用while(list->next)的操作判斷該鏈表是否為空,其下一節點為空和本結點是否為空的判斷條件是一樣的效果。

    其簡單的代碼可以表示為:

    //遍歷雙鏈表,同時打印元素數據 void?printLine(line?*head){line?*list?=?head;int?pos=1;while(list){printf("第%d個數據是:%d\n",pos++,list->data);list=list->next;} }

    循環鏈表

    循環鏈表概念

    對于單鏈表以及雙向鏈表,就像一個小巷,無論怎么走最終都能從一端走到另一端,顧名思義,循環鏈表則像一個有傳送門的小巷,當你以為你走到結尾的時候,其實這就是開頭。

    循環鏈表和非循環鏈表其實創建的過程唯一不同的是,非循環鏈表的尾結點指向空(NULL),而循環鏈表的尾指針指向的是鏈表的開頭。

    通過將單鏈表的尾結點指向頭結點的鏈表稱之為循環單鏈表(Circular linkedlist)

    一個完整的循環單鏈表如圖:

    循環鏈表結點設計(以單循環鏈表為例)

    對于循環單鏈表的結點,可以完全參照于單鏈表的結點設計,如圖:

    單向循環鏈表結點

    data表示數據;

    next表示指針,它總是指向自身的下一個結點,對于只有一個結點的存在,這個next指針則永遠指向自身,對于一個鏈表的尾部結點,next永遠指向開頭。

    其代碼如下:

    typedef?struct?list{int?data;struct?list?*next; }list; //data為存儲的數據,next指針為指向下一個結點

    循環單鏈表初始化

    先創建一個頭結點并且給其開辟內存空間,在開辟內存空間成功之后,將頭結點的next指向head自身,創建一個init函數來完成;

    為了重復創建和插入,我們可以在init函數重新創建的結點next指向空,而在主函數調用創建之后,將head頭結點的next指針指向自身。

    這樣的操作方式可以方便過后的創建單鏈表,直接利用多次調用的插入函數即可完成整體創建。

    其代碼如下:

    //初始結點 list?*initlist(){list?*head=(list*)malloc(sizeof(list));if(head==NULL){printf("創建失敗,退出程序");exit(0);}else{head->next=NULL;return?head;} }

    在主函數重調用可以是這樣

    ????//初始化頭結點//list?*head=initlist();head->next=head;

    循環鏈表的創建操作

    如圖所示:

    單向循環鏈表的創建

    通過逐步的插入操作,創建一個新的節點,將原有鏈表尾結點的next指針修改指向到新的結點,新的結點的next指針再重新指向頭部結點,然后逐步進行這樣的插入操作,最終完成整個單項循環鏈表的創建。

    其代碼如下:

    //創建——插入數據 int?insert_list(list?*head){int?data;???//插入的數據類型printf("請輸入要插入的元素:");scanf("%d",&data);list?*node=initlist();node->data=data;//初始化一個新的結點,準備進行鏈接if(head!=NULL){list?*p=head;//找到最后一個數據while(p->next!=head){p=p->next;}p->next=node;node->next=head;return?1;}else{printf("頭結點已無元素\n");return?0;}}

    循環單鏈表的插入操作

    如圖,對于插入數據的操作,可以創建一個獨立的結點,通過將需要插入的結點的上一個結點的next指針指向該節點,再由需要插入的結點的next指針指向下一個結點的方式完成插入操作。

    其代碼如下:

    //插入元素 list?*insert_list(list?*head,int?pos,int?data){//三個參數分別是鏈表,位置,參數list?*node=initlist();??//新建結點list?*p=head;???????//p表示新的鏈表list?*t;t=p;node->data=data;if(head!=NULL){for(int?i=1;i<pos;i++){t=t->next;??//走到需要插入的位置處}node->next=t->next;t->next=node;return?p;}return?p; }

    循環單鏈表的刪除操作

    如下圖所示,循環單鏈表的刪除操作是先找到需要刪除的結點,將其前一個結點的next指針直接指向刪除結點的下一個結點即可。

    需要注意的是尾結點,因為刪除尾節點后,尾節點前一個結點就成了新的尾節點,這個新的尾節點需要指向的是頭結點而不是空。

    其代碼如下:

    //刪除元素 int?delete_list(list?*head)?{if(head?==?NULL)?{printf("鏈表為空!\n");return?0;}//建立臨時結點存儲頭結點信息(目的為了找到退出點)//如果不這么建立的化需要使用一個數據進行計數標記,計數達到鏈表長度時自動退出//循環鏈表當找到最后一個元素的時候會自動指向頭元素,這是我們不想讓他發生的list?*temp?=?head;??????????list?*ptr?=?head->next;int?del;printf("請輸入你要刪除的元素:");scanf("%d",&del);while(ptr?!=?head)?{if(ptr->data?==?del)?{if(ptr->next?==?head)?{?temp->next?=?head;free(ptr);return?1;}temp->next?=?ptr->next;????//核心刪除操作代碼free(ptr);//printf("元素刪除成功!\n");return?1;}temp?=?temp->next;ptr?=?ptr->next;}printf("沒有找到要刪除的元素\n");return?0; }

    循環單鏈表的遍歷

    與普通的單鏈表和雙向鏈表的遍歷不同,循環鏈表需要進行結點的特殊判斷。

    先找到尾節點的位置,由于尾節點的next指針是指向頭結點的,所以不能使用鏈表本身是否為空(NULL)的方法進行簡單的循環判斷,我們需要通過判斷結點的next指針是否等于頭結點的方式進行是否完成循環的判斷。

    此外還有一種計數的方法,即建立一個計數器count=0,每一次next指針指向下一個結點時計數器+1,當count數字與鏈表的節點數相同的時候即完成循環;

    但是這樣做會有一個問題,就是獲取到鏈表的節點數同時,也需要完成一次遍歷才可以達成目標。

    其代碼如下:

    //遍歷元素 int?display(list?*head)?{if(head?!=?NULL)?{list?*p??=?head;//遍歷頭節點到,最后一個數據while(p->next?!=?head?)?{printf("%d???",p->next->data);p?=?p->next;}printf("\n");???//換行//把最后一個節點賦新的節點過去return?1;}?else?{printf("頭結點為空!\n");return?0;} }

    進階概念——雙向循環鏈表

    循環單鏈表也有一個孿生兄弟——雙向循環鏈表,其設計思路與單鏈表和雙向鏈表的設計思路一樣,就是在原有的雙向鏈表的基礎上,將尾部結點和頭部結點進行互相連接。交給大家了。

    關于鏈表的總結

    在順序表中做插入刪除操作時,平均移動大約表中一半的元素,因此對n較大的順序表效率低。并且需要預先分配足夠大的存儲空間,而鏈表恰恰是其中運用的精華。

    基于存儲,運算,環境這幾方面考慮,可以讓我們更好的在項目中使用鏈表。

    今天鏈表基礎就講到這里,下一期,我們再見!

    -END-

    最近不少同學問有沒有C相關的課程,今天推薦1個《C++/服務器開發4天實戰特訓營》,用4天時間帶你深入C++核心編程,完成1個google測試框架,并教你如何撰寫簡歷、應對大廠面試!

    掃描二維碼了解課程詳情:

    -END-

    往期好文合集

    在華為鴻蒙OS上嘗鮮,我的第一個“hello world”

    厲害了!華為繼鴻蒙OS后,即將發布國產編程語言,打破國外壟斷!

    嵌入式工程師是青春飯嗎?越老越吃香嗎?

    ??最 后??

    ?

    若覺得文章不錯,轉發分享,也是我們繼續更新的動力。

    5T資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,PCB、FPGA、DSP、labview、單片機、等等

    在公眾號內回復「更多資源」,即可免費獲取,期待你的關注~

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的一口气搞懂「链表」,就靠这20+张图了的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。