线性结构--离散存储 链表讲解
1. 線性結構.
2. 非線性結構( 樹,圖)
1. 什么是線性結構
?????? 大概上可以這樣定義: 加入所有的節點可以用一條直線連接起來. 就是線性結構...
2. 線性機構也可以分成兩種:
?????? 1) 連續存儲 (數組)
???????????? 也就是指每1個節點在物理內存上是相連的.
?????? 2) 離散存儲(鏈表)
????????????? 節點在物理內存上并不一定相連, 而是利用指針來關聯.
?????? 這篇文章主要講的第二種.
??????????
3. 鏈表的定義
??????? 1.多個節點離散分配.
??????? 2.彼此通過指針相連.
???????3.每個節點只有1個前驅節點, 1個后續節點.
???????4.首節點沒有前驅節點, 尾節點沒有后續節點.
???????其中第1 2點也適用于樹和圖,?? 后面3 4點就用于區別樹和圖了.
4.一些專業用語解析:
??????1) 首節點:
???????????????????第一個存放有效數據的節點.
??????
??????2)尾節點:
???????????????????最后一個存放有效數據的節點.尾節點的尾部指針為空
?????? 3) 頭節點:???????
???????????????????第1個有效節點之前的那個節點.
??????????????????? 頭節點并不存放那個有效數據,? 但是頭節點跟后面每個節點的數據類型是一樣的.
???????????????????加頭節點的目地主要是為了方便對鏈表的操作.
????????總之, 要注意頭節點并不是首節點,? 而是首節點前面的1個不存放有效數據的節點, 用于方便鏈表操作.
??????????????????指向頭節點的指針, 也是頭節點的地址
?????
??????????????????指向尾節點的指針, 也是尾節點的地址
????????????????????具體可以參考下圖所
?
5.確定1個鏈表所必須的幾個參數
??????? 首先: 如果是確定1個數組就必須知道數組的頭指針 和 數組的長度.
??????? 但是鏈表是可以根據每個節點的尾部指針一直遍歷下去, 如果遇到1個節點的尾部指針是空, 就認為它是尾節點.
??????? 如果希望通過1個函數來對數組進行處理,例如打印數組的函數,或者數組排序的函數,就要接受數組的頭部指針,和數組的長度.
???????但是如果希望通過1個函數來對鏈表進行處理,
???????則只需要1個參數:
???????????????? 就是鏈表的頭部指針啦,? 因為鏈表的長度等其他信息都可以推算出來的.
6. 如何用代碼表示1個節點.
????????首先, 要明確兩點:
??????????? 1) 1個鏈表中每個節點的數據類型都是一樣的.
??????????? 2) 每個節點的數據都分兩部分(域), 1是數據部分,? 2是尾部指針.
???????????? 所以結構體的數據類型不能是常規的數據類型(int /long / char 等) , 必須是結構體, 因為結構體才能可能有多個成員嘛.
?????????????
???????????? 而每個節點的尾部指針指向的是下1個相同類型的節點, 所以尾部指針的類型和結構體的類型是必須一樣的.
struct person{int id;char name[16];struct person * pnext;};typedef struct person PERSON;?????????????? 如上面代碼,這樣就定義了1個 簡單的PERSON 類型的節點.
7.鏈表的分類:
???????7.1 單鏈表和雙鏈表
?????????????????? 單鏈表中每1個只有1個指針域, 例如上面代碼定義的就是單鏈表的結點
?????????????????? 雙鏈表中每個指針有2個指針域,?? 其中1個指針域指向下1個節點,? 另1個指向前1個結點.
??????????
???????7.2?非循環鏈表和 循環鏈表
???????????????????? 這個容易理解,? 如果尾節點的尾部指針指向首節點, 那么它就是1個循環鏈表.
8.鏈表的一些算法:
?????? 所謂算法就是對鏈表的一些操作了.
?????? 包括和很多種, 當然我只會實現幾種最基本常用的算法:
?????????????? 新建1個鏈表
?????????????? 添加1個節點到尾部
?????????????? 插入節點
?????????????? 刪除節點
? ? ? ? ? ? ?? 遍歷
?????????????? 查找某個節點
?????????????? 清空
?????????????? 銷毀
? ? ? ? ? ? ?? 求長度
? ? ? ? ? ? ?? 排序
? ? ? ? ? ? ?
?????? 下面會對這些算法進行講解和代碼實現
9. 1個單鏈表(非循環)容器的簡單c語言實現例子
9.1. 編寫頭文件.
??????因為c語言并不是面向對象語言, 所以我們不能將編寫好的鏈表容器作為1個類庫, 所以要寫個頭文件,? 那么別的文文件引用了這個頭文件, 就可以使用這個鏈表容器了.
?????頭文件代碼如下:
?????linklist1.h?
//注意bool_me.h 這是一個簡單定義布爾類型 宏的頭文件.
#include "bool_me.h" #ifndef __LINKLIST1_H_H #define __LINKLIST1_H_Hstruct person{int id;char name[16];struct person * pnext;};typedef struct person PERSON;struct Link_person{ //just a struct for linklist, it is not a node of linklistPERSON * phead; //address of the Headnode of linklistPERSON * ptail; // address of the tailnode, yes it's not a neccssity, but it can make the operation easilyint len; //numbers of the nodes which contains the userful data.it's not a neccssity, but it can make the operation easilyBOOL is_inited; // judge whether the linklist is inited};typedef struct Link_person LINKPERSON;//init a new block()PERSON * person_new(int id,char *pname);//init a LinklistLINKPERSON * link_create(int len);//judge whether the linklist is emptyBOOL link_is_empty(LINKPERSON * pLink);//add a node to the tail of LinklistBOOL link_add(LINKPERSON * plink, PERSON * pnode);//traverse the linklist to print all of the node of linklist;void link_traverse(LINKPERSON * pLink);//insert a node behind another nodeBOOL link_insert(LINKPERSON * pLink, PERSON * pBefore, PERSON * pnode);//insert a node behind another nodeBOOL link_insertbyindex(LINKPERSON * pLink, int index, PERSON * pnode);//remove a node from the linklistBOOL link_remove(LINKPERSON * pLink, PERSON * pnode);//delete a node from the linklist, and free the memory space of the nodeBOOL link_delete(LINKPERSON * pLink, int index);//get a the index of a node, if not existed, return -1int link_getindex(LINKPERSON * pLink, PERSON * pnode);//get the length of Linklistint link_getlength(LINKPERSON * pLink); //clear a LinklistBOOL link_clear(LINKPERSON * pLink);//destroy a LinklistBOOL link_free(LINKPERSON * pLink);//sort by idvoid link_sort(LINKPERSON * pLink);//get a node from linklist by indexPERSON * link_getnode(LINKPERSON * pLink, int index);#endif
????????????? 講解下,? 這里我定義了兩個結構體, 其中第1個結構體PERSON就是鏈表的節點類型
????????????? 而第2個結構體LINKPERSON是1個鏈表本身的類型, 里面包含了鏈表的第1個(頭節點)結構體的地址.? 通常這個頭節點是不存放有效數據的, 上面提過了.
? ? ? ? ? ? ? 第2個結構體里面存放了一些鏈表的關鍵信息, 雖然這些關鍵信息例如, 長度, 尾節點地址等都可以由頭節點地址推算出來, 但是畢竟遍歷推算是1個很浪費cpu時間的行為, 犧牲一些內存空間來方便運算啦
? ? ? ? ? ? ? 下面定義了若干提供給其他程序是使用的函數,? 當然我這個例子是1個極其基本簡單的例子, 也只會實現一些基本的功能的函數啦. ????????????
9.2.?處理錯誤函數voidlink_error();
?????????恩, 雖然只是1個簡單的實現例子, 但是國際慣例,我還是盡量寫的專業一些吧..
????????代碼如下:
linklist1.c?????????? //后面的函數都寫在這個文件里
9.3.?創建1個新的節點函數 PERSON* person_new(int id,char* pname)
????????為什么要專門寫1個函數來創建node??
????????直接 PERSONnode1; 這樣不就創建出1個結構體節點了嗎?
???????? 沒錯, 但是這樣創建的結構體變量是靜態變量, 也就代表它所占的內存不能重復使用,? 而且當這個節點被移除時, 它所占的內存也不能被清空啊.
????????所以我會單獨寫1個函數來創建1個新的節點
????????邏輯:
???????? 1.定義1個結構體指針
???????? 2. 為這個指針分配1塊足夠的內存
?????????? 4. 給結構體的各成員賦值, 尾部指針為空指針.
????????3. 返回這個指針所指向的地址.
????????代碼如下:
PERSON * person_new(int id, char * pname){PERSON * pnode;pnode = (PERSON *)malloc(sizeof(PERSON));if (NULL == pnode){link_error("fail to assign memory to PERSON");}pnode->id = id;strcpy(pnode->name, pname);pnode->pnext = NULL;return pnode; }
9.4.初始化(新建)1個鏈表函數 LINKPERSON *link_create(int len)
?????????初始化1個鏈表,? 其實這個邏輯不難理解, ?? 至于為什么有1個參數len? ? 其實就是方便用戶新建1個鏈表給這個鏈表分配數量為len的有效節點, 當然len 可以設成0啦, 這樣這個鏈表就只有1個頭節點了.
?????????無非分如下若干步:
?????????1.新建1個鏈表類型(自定義 LINKPERSON)指針
?????????2.動態給這個指針分配1個內存.
????????? 3. 動態分配1個頭節點, 地址賦給?上面的鏈表類型的第1個成員plist,? 這個成員就指向頭節點的地址. 頭節點不存放有效數據, 但是尾部指針設為空
? ? ? ? ? 4.根據len參數動態分配若干個 有效節點, 掛到頭節點后面.
???????????5. 給鏈表類型的其他成員賦值
?????????6. 最后返回這個鏈表類型的地址.
????????? 還是有點復雜啊..
代碼如下:
???????
LINKPERSON * link_create(int len){LINKPERSON * pLink = (LINKPERSON *)malloc(sizeof(LINKPERSON));if (NULL == pLink){link_error("fail to assign memory to LINKPERSON");}PERSON * phead = (PERSON *)malloc(sizeof(PERSON));if (NULL == phead){link_error("fail to assign memory to headnode");}phead->pnext = NULL;pLink->phead = phead;pLink->ptail = phead;pLink->len = 0;pLink->is_inited = TRUE;if (len==0){return pLink;}int i;char * name[16];PERSON * pnode;for(i=0;i<len;i++){char name[16];sprintf(name,"node%d",i+1);pnode = person_new(i+1,name);if (FALSE == link_add(pLink,pnode)){link_error("fail to add nodes!");}}return pLink; }???????從代碼可以見到, 無論初始長度len是否為0 我都會有定義1個頭節點,? 然后根據len的長度循環將存放數據的有效節點掛到鏈表的尾部.
???????而新建1個節點用的是 person_new() 函數, 這個函數上面寫過了.
???????而將1個節點掛到鏈表尾部我用的是link_add() 函數, 這個函數頭文件也提到的, 下面就會講解如下實現這個函數.
9.5.將1個節點添加到鏈表尾部末尾函數 BOOL link_add(LINKPERSON * pLink, PERSON * pnode)
??????首先, 這個要添加的節點最好是動態分配的, 也就是說用我上面的person_new() 生成的. 否則當移除這個節點就無法釋放它的內存了.
??????而添加1個節點到鏈表尾部邏輯上是很簡單的..
???????1. 鏈表的尾節點的尾部指針指向這個要添加的節點
??????? 2.這個節點的尾部指針設為NULL.
???????3. 鏈表成員len+1
??????? 但是實際上還是有另外需要注意的問題.
???????a. 就是如何獲取尾節點???
???????? ? ? ?? 這個當然可以用頭節點逐個推算出來了.. ? 但是我頭文件提過, 為了方便操作,?我會把尾節點的地址也保存在鏈表類型的結構體成員中..
???????b.判斷 鏈表內是否已經有這個節點.?
??????????????? 這個就跟數組不同了. 數組是可以存放相同的數據的,例如Arr_add(10),這個函數可以重復執行.
??????????????? 鏈表呢, 因為鏈表是由節點組成的, 而唯一標識節點的是節點的地址.
???????????????假如鏈表添加1個節點, 但是這個節點的地址已經在這個鏈表中的話就出現下圖中的問題了:
???????????????????? 如下圖:
??????????所以這個函數會判斷下這個節點是否在鏈表中, 還是要遍歷一次啊..??有更好方法的可以告訴我..
代碼如下:
BOOL link_add(LINKPERSON * pLink, PERSON * pnode){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (NULL == pnode){printf("pnode is empty!\n");return -1;}//judge whether pnode is existed in the linklist alreadyif (link_getindex(pLink,pnode) > -1){printf("the node is existed in linklist already!\n");return FALSE;}pLink->ptail->pnext = pnode;pnode->pnext = NULL;pLink->ptail = pnode;pLink->len++;return TRUE; }
可以見到, 我并沒有每次都遍歷求出 尾節點的地址, 而是把尾節點地址放到 鏈表類型的1個成員中, 方便操作啊~
上面只所以說遍歷了1次, 似乎因為執行力獲取節點序號函數? link_getindex(),如果這個節點不存在, 則返回0. 下面就是這個函數的寫法.
9.6.獲取1個節點在鏈表中的位置.??? intlink_getindex(LINKPERSON * pLink,PERSON * pnode)
???????注意這個函數也用與判斷節點是否存于鏈表中,? 如果存在就返回位置index, 不存在就返回-1.
???????邏輯上也很清楚,? 沖首節點起(不是頭節點), 逐個判斷, 直到到達尾節點, 這個過程不需要關心鏈表的長度.
???????而且這個遍歷不包括頭節點,所以如果把頭節點作為參數, 一樣會返回 -1,因為頭節點不是存放數據的有效節點.
??????? 代碼如下:
int link_getindex(LINKPERSON * pLink, PERSON * pnode){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (NULL == pnode){printf("pnode is empty!\n");return -1;}PERSON * pn = pLink->phead;int i=0;while (NULL != pn->pnext){if (pn == pnode){return i;}i++;pn = pn->pnext;}return -1; }
?
9.7. 判斷鏈表是否阿為空 BOOL link_is_empty(LINKPERSON * pLink).
????????這個就毫無難度啦,只需判斷鏈表內是否只有頭節點就ok了.
?????????當然, 為了方便操作, 也可以判斷 鏈表成員len 是否等于0
BOOL link_is_empty(LINKPERSON * pLink){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (NULL == pLink->phead->pnext){return TRUE;}return FALSE; }
9.8.?遍歷輸出鏈表函數. void link_traverse(LINKPERSON * pLink)
????????這個要注意的是, 實際上就是遍歷鏈表, 然后逐個輸出節點.??
????????? 只需定義1個指針,首先指向首節點(頭節點的下1個), 然后輸出.
????????? 再把這個指針指向被輸出的指針的后1個, 繼續輸出,直到尾部指針為空(尾節點).
?????????所以這個過程根本不關心鏈表的長度的.
?????????
9.9.?打印1個節點的函數. void?person_print(PERSON * pnode)
???就是上面用的person_print(pnode)啦, 太簡單不講解了
9.10.?根據序號獲取1個節點函數 PERSON * link_get(LINKPERSON * pLink, int index)
? ? ? ?相對來講,這個函數就跟簡單了。
? ? ? ?首先, 判斷參數 index 的范圍, 如果少于0 , 或者大于鏈表當前個數-1 , 則返回1個空指針。
? ? ? ?鏈表的個數怎么求? ?這時我鏈表類型的len成員就發揮作用了..雖然作用意義不是很大.
? ? ? ?然后根據index的值 ?遍歷若干次就得到想找的節點地址啦。
? ? ? ?代碼如下:
PERSON * link_getnode(LINKPERSON * pLink, int index){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if ( index < 0 || index > (pLink->len -1)){printf("index is over the limit!\n");return NULL; }int i;PERSON * pnode = pLink->phead; for( i=0;i <= index ;i++){pnode = pnode->pnext;} return pnode; }
9.11.插入1個節點到另1個節點之后的函數 link_insert(LINKPERSON *pLink, PERSON * pBefore, PERSON * pnode);
?????通常來講,? 鏈表是整個數據結構的重點,? 而插入節點的操作就是鏈表的重點..
????? 講下原理,???假如要將參數中的 pnode 插入到 pBefore的后面.
?????假如原來的pBefore 的后面是 pAfter.
?????那么pBefore->pnext == pAfter?? 這個很簡單.??
?????? 而現在要把pnode 放到 pBefore 和pAfter 之間.? 則pnode 在 pAfter前面
??????? 所以要執行:
????????????????
pnode->pnext = pBefore->pnext //相當與 pnode->pnext = pAfter??????????
???????? 然后pBefore 的后面是pnode, 所以再執行
pBefore->pnext = pnode
??????? 就完成插入了.
???????? 那么對于這個函數, 邏輯上包括如下幾點:
??????? 0. 判斷pBefore 和 pnode 不是空指針
??????? 1. 判斷 pBefore 是否存在于鏈表中, 如果不存在, 返回false.
??????? 2. 判斷 pnode 是否已經存在于鏈表中, 如果是, 返回false.
??????? 3. 把pnode 插到pBefore 后面
??????? 4. 如果pnode 是最后1個節點.? 則pLink的成員 ptail = pnode.
??????? 5. pLink->len++
??????? 6. return true.
代碼如下
BOOL link_insert(LINKPERSON * pLink, PERSON * pBefore, PERSON * pnode){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (NULL == pBefore || NULL == pnode){printf("pBefore or pnode is empty!\n");return FALSE;}if (0 > link_getindex(pLink,pBefore)){printf("pBefore is not existed in linklist!\n");return FALSE;}if (-1 < link_getindex(pLink,pnode)){printf("pnode is existed in linklist already!\n");return FALSE;}pnode->pnext = pBefore->pnext;pBefore->pnext = pnode;if (NULL == pnode->pnext){pLink->ptail = pnode;}pLink->len++;return TRUE; }9.12. 插入1個節點到制定鏈表的位置link_insertbyindex(LINKPERSON *pLink, int index, PERSON * pnode);
????????這個函數是把1個節點插入到鏈表的第 index? 個節點后面(index 由0(首節點開始));
????????這個需要做的事情如下:
????????就是調用上面的函數啦?
???????? link_insert(pLink, link_getnode(index), pnode);???????? ...
???????但是假如? index 不是一個有效的數據, 比如超出了鏈表范圍??
???????那么 link_getnode(index) 就會返回空指針,? 而 link_insert 函數接收到空指針參素就會返回FALSE啊~
???????? 代碼如下:
BOOL link_insertbyindex(LINKPERSON * pLink, int index, PERSON * pnode){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}return link_insert(pLink, link_getnode(pLink, index), pnode); }
9.13.將1個節點移除出鏈表BOOL link_remove(LINKPERSON * pLink, PERSON *pnode);
??????見到我的頭文件中又有remove又 有delete? 其實他們作用并不相同,
?????? 首先,? remove 函數只會將節點pnode 移出鏈表, 不會釋放這個節點的內存(free), 因為用戶很可能還會將這個放到鏈表的其他地方或者放入另1條鏈表.
?????? 而delete 函數直接會將 具體位置的節點移除鏈表后,并銷毀 , 釋放出內存.
?????? 至于怎樣移除出1個節點pnode?
???????? 1. 如果節點不再鏈表中,? 返回FALSE
??????2.pnode 前1個節點指向 pnode->pnext
?????? 3.如果前1個節點的pnext 是空, 則帶代表pnode 是1個尾節點,?把尾節點移除后,? pLink的成員ptail 的值改成pnode的前1個節點
??????問題來了, 怎樣找到pnode 的前1個節點 pBefore, 如果是雙鏈表, 直接可以由指針找到, 但是這個是單鏈表..
??????當然, 可以先用link_getindex(pnode) 獲得 pnode 的 index,? 再用link_getnode(index-1) 獲得 pBefore,但是這樣就執行兩次遍歷了, 為了性能著想, 還是直接遍歷吧..
?????? 代碼如下:
BOOL link_remove(LINKPERSON * pLink, PERSON * pnode){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (NULL == pnode){printf("pnode is empty!\n");return -1;}PERSON * pBefore = pLink->phead;while (NULL != pBefore->pnext ){if (pBefore->pnext == pnode){pBefore->pnext = pnode->pnext;if (NULL == pBefore->pnext){ // pnode is ptail;pLink->ptail = pBefore;pLink->len--;return TRUE;}}pBefore = pBefore->pnext;}return FALSE; //pnode is not existed in the linklist before. }
9.14將1個節點移除出鏈表并釋放內存BOOL link_delete(LINKPERSON *pLink,int index);
??????好吧, 這個函數實際上就是上面那個remove 函數, 然后再手動釋放內存..
?????? 邏輯如下:
?????? 1. 利用link_getnode() 函數獲得要remove 的節點
??????2. 利用link_remove 移除這個節點.
?????? 3. 手動釋放內存.
?????? 代碼如下:
BOOL link_delete(LINKPERSON * pLink, int index){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}PERSON * pnode = link_getnode(pLink, index);if (NULL == pnode){return FALSE;}if (TRUE == link_remove(pLink, pnode)){free(pnode);return TRUE;}return FALSE;}
9.15? 清空1張鏈表 BOOL link_clear(LINKPERSON * pLink);
???清空鏈表的意識就是把鏈表所有的節點移除?
??? 那么可以循環執行 link_remove嗎??
???執行一次link_remove 就遍歷1次啊...
??? 其實清空1張鏈表 根本不需要遍歷:
??? 直接將頭節點的尾部指針設為NULL 就ok啦!
代碼如下:
BOOL link_clear(LINKPERSON * pLink){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}pLink->phead-> pnext = NULL;pLink->ptail = pLink->phead;pLink->len=0;return TRUE; }
9.16?銷毀1張鏈表, BOOL link_clear(LINKPERSON * pLink);
??這個函數跟上面完全不同啊, 沒那么簡單~
?? 首先這個函數分兩步,? 首先釋放所有節點的內存,
??然后釋放這個鏈表類型(結構體)指針本身.
???第2步很簡單? 直接free(pLink) 就ok了,? 問題如何執行第一步呢?
??? 循環去link_delete嗎?? 問題每執行一次link_delete 就執行1此 link_remove里面每次都遍歷1次啊..
???為了避免渣性能, 還是手動循環去釋放節點吧..
???? 這里就有個問題了, 到底是從首節點開始釋放內存呢?? 還是由尾節點開始釋放?
???? 如果從尾節點開始釋放,? 你把尾節點釋放后, 怎么找前1個節點? 除了遍歷還真沒有辦法..
????? 從首節點釋放后, 怎么找下1個節點?? 這個就簡單啊, 釋放首節點前保存它的尾部節點指針就是了.
????? 所以我們應該從首節點釋放
???? 代碼如下:
BOOL link_free(LINKPERSON * pLink){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}PERSON * pnode = pLink->phead;PERSON * pAfter =pnode->pnext;//printf("free pnode which id is %d\n",pnode->id);free(pnode); //free pheadwhile(NULL != pAfter){pnode=pAfter;//printf("free pnode which id is %d\n",pnode->id);pAfter = pnode->pnext;free(pnode);}free(pLink);return TRUE;}
9.17鏈表排序算法(根據id成員的值) void link_sort(LINKPERSON * pLink)
????當然這里還是講解下最簡單的冒泡排序法了,??
???? 冒泡排序法在對于數組來講是十分容易實現的, 基本上都能背出來了~
???? 代碼如下:
int i,j,m;for (i=0; i<len - 1; i++){for(j=i+1;j<len; j++){if (a[i] > a[j]){m = a[i];a[i]=a[j];a[j]=m;}} }原理就是循環求出數組最小的值的元素, 放在最左邊, 然后求出第二小的元素, 放在數組第2位.....
這里關鍵就是每比較一次, 如果左邊的值大于右邊的值
這個兩個元素就會互相交互它們的值.
對于鏈表來講, 實現原理也差不多的..
1.
數組中用i, j兩個變量來存儲要比較的元素的位置.
那么在鏈表中, 我們可以定義2個指針pPre, pAfter來存儲要比較節點的位置
2.
數組中i的初始是第1個元素, 所以i=0 開始
對于i的每1個值.?? j初始話是i后面1個元素. 所以j從 j=i+1 開始
那么對于鏈表來講. pPre就從首節點開始,? 所以pPre= phead->pnext? (phead 是頭節點)
pAfter 從第pPre的后1個節點開始, 所以pAfter= pPre->pnext 開始.
3.
數組中每執行1次循環, i 或j的值加1, 表示執行她們的下1個元素
鏈表中, 每執行1次循環. pPre 或 pAfter的地址指向下1個節點. 所以p=p->pnext
代碼如下:
void link_sort(LINKPERSON * pLink){if (TRUE != pLink->is_inited){link_error("the linklist is not inited yet");}if (pLink->len < 2){return;}PERSON * pPre; //pheadPERSON * pAfter;PERSON * pPB; //used to save the node address which before the pPrePERSON * pAB; //used to save the node address which before the pAfterPERSON * m;for (pPB=pLink->phead,pPre=pLink->phead->pnext; NULL != pPre->pnext; pPB=pPre,pPre=pPre->pnext){for (pAB=pPre,pAfter=pPre->pnext; NULL != pAfter; pAB=pAfter,pAfter=pAfter->pnext){if (pPre->id > pAfter->id){link_exchange(pPB,pAB);m = pPre;pPre = pAfter;pAfter = m;};}} }注意上面, 我還定義多兩個指針, 專門指向, pPre 和 pAfter 的前1個節點地址,
就是pPB->pnext == pPre??????? pAB->pnext ==pAfter
為什么這樣做, 是因為單鏈表中無法由1個節點求出上1個節點的地址(只能根據尾部指針得到下1個節點的地址)
在數組中,?
如果比較一次? a[i] > a[j]? 之后在就會交換它們的值,
鏈表比較 pPre->id >pAfter->id 后, ? 當然也可以互相交換他們的id值, ??
例如
但是對于1個節點來講, 地址沒有變化, 但是成員值變化了.
實際應用中? , 大部分希望1個節點里面的成員值不會產生變化, 而是節點本身在鏈表中的位置變化
而且當1個節點的成員非常多時,? 交換全部成員的值的成本往往大于交換他們在鏈表中位置的成本.
我用了
link_exchange(pPB,pAB);這個函數來交換pPre, 和pAfter的地址,但是注意傳的參數是他們的上1個節點, 而是他們本身.
為什么呢, 下面會講解這個函數.
假如 pPre指向的是鏈表第2個節點,? pAfter 指向的是第4個節點
那么交換節點后,? pPre 就指向鏈表中第4個節點了,? 而pAfter 指向第2個節點了.? 因為他們指向的節點在鏈表中的位置換了嘛...
所以根據冒泡排序法的原理, pPre 要指向前面的節點, 而pAfter 要指向后面的節點.
所以我們還要把 pPre 和 pAfter所指向的地址互換.? 讓pPre 指回第2個節點. 而pAfter 指回第4個節點.
所以要執行
9.18鏈表交換兩個節點的下1個節點函數BOOL link_exchange(LINKPERSON*pPB, LINKPERSON * pAB)
?????當然, 可以先執行link_remove 再 link_insert到適當的位置達到目地, 但是就執行了若干次無謂的遍歷動作, 不推薦!
?????注意的是, 這個函數交換的不是pPB 和 pAB的位置,? 是交換 pPB->pnext 和 pAB->pnext 的位置.
?????為什么要這樣呢, 看到后面就明白了.
首先圖解交換兩個節點位置要做的操作:
假如我要交換下圖中節點2 和 節點4 的位置.
1.首先節點2的前1個節點1的尾部指針指向節點4
2.然后節點4的原本的前1個節點3的尾部指針指向節點2
'
3.然后節點2的尾部指針指向節點4的尾部指針地址(節點5)
4.然后節點4的尾部指針指向節點2原本的尾部指針地址(節點3)
經過總結, 可以發現上面4步實際上可以總結成為2步:
1. 交換節點2和節點4的前1個節點,???? 也就是節點2 和 節點4 的前1個節點的尾部指針的值互換
2. 交換節點2和節點4的后1個節點 ,??? 也就是節點2 和 節點4 的尾部地址的值互換
第2步很簡單?
無非就是? p2->pnext? 和 p4->pnext 互換
問題來了,
第1步怎么實現呢,?? 因為根據p2 和 p4 無法知道 p1 和 p3的地址啊,?? 單鏈表只能單向求1下1個節點地址,? 而不能往前退的.
所以!
'我們在這個函數中, 參數不要定為 p2 和 p4 ,? 而是把他們的前1個節點? p1 和? p3 作為參數傳入來
那么
p2==p1->pnext
p4==p3->pnext?
那么第1步?? 把 p1->pnext 和 p3->pnext 的值互換就ok啦
第二步呢,?? 因為p1->pnext 和? p3->pnext 的值互換過了.
所以p1->pnext 就是p4啊??? p3->pnext 就是p2啊?? ,? 而我們上面見過第2步是 p2->pnext 的值和 p4->pnext的值互換
所以就是 p3->pnext->pnext 的值 和 p1->pnext->pnext 的值互換啊!
代碼如下:
static void link_exchange(PERSON * pPB, PERSON * pAB){//this function will exchange the place of the next nodes of pPB and pAB//if pPB->pnext == pPre ; pAB->pnext == pAfter//after exchange, pPre and pAfter will exchange their index in the linklistPERSON * m;//first , exchange their Pre node;m = pPB->pnext;pPB->pnext = pAB->pnext;pAB->pnext = m;//then . exchange their next node;m=pPB->pnext->pnext;pPB->pnext->pnext = pAB->pnext->pnext;pAB->pnext->pnext = m; }
9.19寫個小程序來測試一下這個鏈表容器
其實就是寫1個c文件, 引用這個鏈表容器的頭文件,調用我上面寫的函數,就是可以測試了
代碼如下啦:
輸出:
10,一些總結,
數組的優點:
????????? 存取速度快, 因為根據頭部指針就可以根據下標定位到對應的元素.
????????? 缺點:
????????? 實現必須知道數組的長度
????????? 需要連續的內存空間
????????? 插入和刪除速度慢. 一旦插入或刪除1個元素, 意味這大量元素要向前或向后移動1位.
鏈表的優點:
????????? 空間幾乎不用限制, 而數組必須要有連續的內存空間
????????? 插入和刪除元素的速度很快, 只需要修改部分指針.
鏈表缺點:
????????? 存儲速度慢. 因為要找1個元素就必須一直遍歷下去..
??? 本文的函數就寫到這里了,? 其實還有很多功能沒有實現, 例如求鏈表有效長度(這個太簡單),?? 兩個鏈表的對接(這個...也不難啦)等..? 怎么說呢, 雖然偽算法不難看懂, 但是真正用代碼來實現還是有點復9雜啊...
??? 關鍵就是對于指針的理解了, 對于數據結構, 指針機會就是一切啊....
總結
以上是生活随笔為你收集整理的线性结构--离散存储 链表讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle 实例崩溃恢复原理剖析 -
- 下一篇: 数据结构 线性存储 -- 栈 讲