線性表有兩種:一種是順序存儲的叫順序表,上節已經說過了,另外一種是鏈式存儲的叫鏈表,本節說的是單鏈表,即單向鏈表(每一個節點中只包含一個指針域)。node
本節知識點:
1.鏈表的好處:對於動態鏈表,能夠對未知數據量的數據進行存儲。插入和刪除比順序表方便的多,不用大量移動。
鏈表的缺點:除了數據信息,還需對額外的鏈表信息進行分配內存,佔用了額外的空間。訪問指定數據的元素須要順序訪問以前的元素。
2.鏈表的基本概念:
鏈表頭(表頭節點):鏈表中的第一個節點,包含指向第一個數據元素的指針以及鏈表自身的一些信息(即鏈表長度length)
數據節點:鏈表中的表明數據元素的節點,包含指向下一個數據元素的指針和數據元素的信息
尾節點:鏈表中的最後一個數據節點,其下一元素指針爲空,表示無後繼
3.
對於本節的可複用單鏈表的設計想法是這樣的:
a. 可複用的順序表中保存的是各個數據的地址,因此我最初想到的是在鏈表元素中也保存各個數據的地址:
使用這樣的結構,add是鏈表中保存的數據,其實就是想複用保存的各類類型的地址,add是一個unsigned int型,用來保存各類數據類型的地址,next是鏈表結構,用來指向鏈表元素的下一個鏈表元素的。
b.可是這樣的結構有一個問題,就是從使用的總空間(鏈表結構的空間+add中保存數據的空間)角度來看,add就是一個浪費空間的變量。由於在add中保存地址,爲何不強制類型成next的類型(此時next應該是鏈表第一個結構的類型),直接使用這個地址把各類你想要存儲的結構賦值給next,這樣存儲的各個結構就變成了,如圖。
c.可是把全部的類型都轉換成鏈表第一個元素的指針類型 再賦值給next 顯得程序很不規整,因此最好直接給鏈表一個結構,把這些結構類型都統一強制類型轉換成這個鏈表的類型,以下:
- typedef struct Str_LinkList LinkListNode;
- struct Str_LinkList
- {
- LinkListNode* next;
- };
把什麼鏈表頭啊,鏈表元素啊,想要鏈接進入這個鏈表的各類結構都強制類型成 LinkListNode* 可是要保證每一個結構中都有LinkListNode* next 成員。
d.最後一點,就有點接受不了,也是爲了代碼的整潔,提升可讀性,使鏈表結構都規整到LinkListNode這個結構中去,便於對鏈表進行管理,好比說雙向鏈表的前驅和後繼。把每一個類型中的LinkListNode* next 成員 變成LinkListNode node。
這裏面有一個很好的c語言技巧,就是這個LinkListNode node必需要放在每一個結構中(如 str)的第一個元素位置,即node的地址就是結構體str的地址,由於只有這樣了,在把str強制類型轉換成 n=(LinkListNode* )str的時候,訪問n->next纔是訪問str.node->next的值,由於二者地址相同,切記必定要放到第一個元素的位置!!!
4.對於鏈表這個數據結構,必定要注意一個問題,也是這節我犯的一個很難發現的錯誤:
就是已經在鏈表中的元素,千萬不要再一次往鏈表中進行插入,由於這樣會致使從它插入的地方開始鏈表的後繼就開始混亂了,把整個鏈表徹底弄亂,出現你想不到的問題。
本節代碼:
本節實現的是一個能夠複用的單鏈表:
LinkList.c:
- #ifndef __LinkList_H__
- #define __LinkList_H__
-
- typedef void LinkList;
- typedef struct Str_LinkList LinkListNode;
- struct Str_LinkList
- {
- LinkListNode* next;
- };
-
- LinkList* Creat_LinkListHead(void);
-
- int Destroy_LinkListHead(LinkList* head);
-
- int Get_Length(LinkList* head);
-
- int Clean_LinkListHead(LinkList* head);
-
- int Add_LinkList(LinkList* head, LinkListNode* Node, int pos);
-
- LinkListNode* Get_LinkListNode(LinkList* head, int pos);
-
- LinkListNode* Del_LinkListNode(LinkList* head, int pos);
-
- #endif
main.c:
- #include <stdio.h>
- #include <stdlib.h>
- #include <malloc.h>
- #include <string.h>
- #include "LinkList.h"
-
- typedef struct student
- {
-
- LinkListNode node;
- int num;
- char name[30];
- }str;
- int main(int argc, char *argv[])
- {
- str str1,str2,str3,str4,str5,str6,*strp;
- int i=0;
- LinkList* list_head;
- list_head = Creat_LinkListHead();
-
- str1.num = 1;
- strcpy(str1.name,"haohao");
-
- str2.num = 2;
- strcpy(str2.name,"ququ");
-
- str3.num = 3;
- strcpy(str3.name,"popo");
-
- str4.num = 4;
- strcpy(str4.name,"wowo");
-
- str5.num = 5;
- strcpy(str5.name,"tiantian");
-
- str6.num = 6;
- strcpy(str6.name,"cheche");
-
- Add_LinkList(list_head, (LinkListNode*)&str1, 0);
- Add_LinkList(list_head, (LinkListNode*)&str2, 0);
- Add_LinkList(list_head, (LinkListNode*)&str3, 0);
- Add_LinkList(list_head, (LinkListNode*)&str4, 0);
- Add_LinkList(list_head, (LinkListNode*)&str5, 0);
- strp = (str*)Del_LinkListNode(list_head, 5);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- printf("\n");
- for(i=1; i<= Get_Length(list_head); i++)
- {
- strp = (str*)Get_LinkListNode(list_head, i);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- }
- printf("\n");
- Add_LinkList(list_head, (LinkListNode*)&str6, 3);
- for(i=1; i<= Get_Length(list_head); i++)
- {
- strp = (str*)Get_LinkListNode(list_head, i);
- printf("%d\n",strp->num);
- printf("%s\n",strp->name);
- }
-
-
- Clean_LinkListHead(list_head);
- Destroy_LinkListHead(list_head);
- return 0;
- }
課後練習:
1.對於上節順序表中的unsigned int型保存數據地址,可不可用void*
這個問題已經獲得了唐老師的解答,其實使用void* 最好了,由於使用unsigned int 僅僅可以在32位機上面運行成功,在64位機上運行就會出錯的!!!
2.對於有頭鏈表和無頭鏈表的區別
所謂有頭鏈表,就是有頭結點的鏈表,頭結點是一個鏈表元素,但不存放數據。無頭鏈表就是沒有頭結點的鏈表。
相比之下有頭鏈表比無頭鏈表,方便不少,優勢也不少。
無頭鏈表就是在譚浩強老師的c語言書中的那個鏈表。就是沒有頭結點,只有一個指針指向鏈表第一個元素。這個指針被叫作鏈表頭。 我的建議使用有頭鏈表!!!
3.對順序表和單鏈表添加一個反轉操做
a.對於順序表 其實就是在不斷的交換
b.對於鏈表 我以爲仍是使用雙向鏈表吧,解決這個問題就方便了~~~
本節知識點:
1.靜態鏈表究竟是什麼:鏈表就是鏈式存儲的線性表,可是它分爲
動態和
靜態兩種,所謂動態就是長度不固定,能夠根據狀況自行擴展大小的,靜態鏈表就是長度大小固定的,鏈式存儲的線性表。
2.本節的靜態鏈表和順序表很像(其實和數組也很像),
準確的來講就是利用順序表實現的,只是這個順序表,不是順序排列的,是經過一個next變量,鏈接到下一個變量的。
如圖:
3.唐老師說靜態鏈表是在一些沒有指針的語言中使用的,來實現鏈表的功能,可是我以爲鏈表的最大優點就在於它的伸縮,用多少開闢多少。可是靜態鏈表就偏偏失去了這個優點。依我看,
學習靜態鏈表的目的是學習它這種相似內存管理的算法思想。
4.靜態鏈表中值得學習的思想:就是在初始化鏈表的時候,把因此空間都標記成爲可用-1,每次插入數據的時候,都在標記爲可用的空間內挑取,再把-1改爲next。當刪除變量的時候在把next改爲-1,標記爲空間可用。
5.其實仔細想一想 看看,靜態鏈表只是一個思想,爲何這麼說,首先在獲取index的時候,你是順序獲取的,這致使你的next也是連續的,因此他其實就變成了一個順序表。在這裏我想到了一個唐老師的問題,爲何node[0]就能夠看成頭節點,還要再定義一個head變量。唐老師的解答是:順序得到index的時候每次都要遍歷太浪費時間了,因此最好應該在同一塊空間再定義一個鏈表,來保存這些空閒空間,而後這樣就須要兩個鏈表的頭節點了,因此須要一個head。而後讓node[0]一會是空閒鏈表的鏈表頭節點,一會是真實保存數據的鏈表的頭節點。當插入的時候,只須要在那個空閒鏈表取空間就能夠了,提升了算法的效率。
PS1:對於node[0]我真的想說,其實讓node[0]看成頭節點的使用真的很方便,比head方便不少,僅僅是我的體會。
PS2:對於兩個鏈表的那個算法,我以爲若是仍是順序在鏈表中得到index,依然沒有解決這個index是有順序的且順序是固定的問題。這裏的順序是指的是那個空閒鏈表的順序。因此說這僅僅是一個思想。
6.
本節最重要的知識點也是最大的難點:對於柔性數組的描述。
對於柔性數組的結構以下:
- typedef struct _tag_StaticList
- {
- int capacity;
- StaticListNode head;
- StaticListNode node[];
- }StaticList;
而後:給柔性數組開闢空間
- StaticList* ret = NULL;
- ret = (StaticList*)malloc( sizeof(StaticList)*1 + sizeof(StaticListNode)*(capacity+1) );
其實柔性數組就是以數組的方式訪問內存。對於 StaticList ret這個結構體的大小是不包括StaticLIstNode node[]的,StaticLIstNode node[]是沒有大小的,StaticLIstNode node[0]訪問的內存是StaticList ret這個結構體後面的第一個內存,StaticLIstNode node[1]訪問的內存是StaticList ret這個結構體後面的第二個內存等等。
PS:StaticLIstNode node[]這個結構究竟是個什麼結構,很差說,不是數組,也不是指針。就把它看成爲了柔性數組而產生的結構吧!!!
本節代碼:
StaticList.c:
StaticList.h:
- #ifndef __STATICLIST_H__
- #define __STATICLIST_H__
-
- typedef void SList;
- typedef void SListNode;
-
- SList* Creat_StaticList(int capacity);
- void Destroy_StaticList(SList* Static_List);
- int Get_Lenth(SList* List);
- int Get_Capacity(SList* List);
- int Clear_StaticList(SList* List);
- int Add_StaticList(SList* List, SListNode* Node, int pos);
- SListNode* Get_StaticListNode(SList* List, int pos);
- SListNode* Del_StaticListNode(SList* List, int pos);
-
- #endif
main.c:
- #include <stdio.h>
- #include <malloc.h>
- #include <string.h>
- #include "StaticList.h"
-
- int main()
- {
- SList* list = Creat_StaticList(10);
- int *f = 0;
- int i = 0;
- int a = 1;
- int b = 2;
- int c = 3;
- int d = 4;
- int e = 5;
-
- Add_StaticList(list, &a, 0);
- Add_StaticList(list, &b, 0);
- Add_StaticList(list, &c, 0);
- Add_StaticList(list, &d, 0);
-
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
-
- Add_StaticList(list, &e, 2);
- printf("\n");
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
-
- printf("\n");
- f=(int* )Del_StaticListNode(list, 4);
- printf("del %d\n",*f);
- printf("\n");
- for(i=1; i<=Get_Lenth(list); i++)
- {
- f=(int* )Get_StaticListNode(list, i);
- printf("%d\n",*f);
- }
- Destroy_StaticList(list);
- return 0;
- }
本節知識點:
1.爲何選擇循環鏈表:由於有不少生活中結構是循環的,是單鏈表解決不了的,好比星期、月份、24小時,對於這些循環的數據,循環鏈表就體現出它的優點了。算法
2.循環鏈表的結構:數組
![](http://static.javashuo.com/static/loading.gif)
循環鏈表就是從頭結點後面開始,尾節點的next再也不是NULL了,而是頭結點後面的第一個鏈表元素,如上圖。安全
3.如何建立一個循環鏈表:數據結構
步驟一:dom
![](http://static.javashuo.com/static/loading.gif)
步驟二:ide
![](http://static.javashuo.com/static/loading.gif)
不管是頭插法,仍是尾插法都沒有關係,均可以建立完成這個循環鏈表。函數
4.如何將一個單向鏈表改寫成一個循環鏈表:學習
第一步 (改寫插入函數):
a.把插入位置pos的容許範圍改爲0~~~無窮大
- ret=( NULL != node) && ( NULL != Node) && (pos >= 0);
b.把兩種方式的頭插法狀況加入程序,第一種是pos值爲0和1的狀況,如圖:
![](http://static.javashuo.com/static/loading.gif)
這種狀況分爲兩部:先把node插入到head和第一個元素直接,而後再把鏈表尾指向node元素(node表示插入元素)。
代碼以下:
- if(node == (CircleListNode* )head)
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- Last->next = Node;
- }
頭插法的第二種狀況,是循環鏈表,循環了一圈回來了,與第一種不一樣的是此時插入的相對位置和第一種的相對位置不同。(其實這種方法跟普通插入是同樣的) 如圖:
![](http://static.javashuo.com/static/loading.gif)
第二步 (改寫刪除函數):
a.也是把pos值的取值範圍改爲0 到 無窮大,可是同時記得判斷length要大於0 ,要保證鏈表中有數據,否則刪什麼呀~~~~
- if(( NULL != lhead) && (pos > 0) && (lhead->length>0))
b.對於刪除第一個元素有兩種狀況 這裏是難點:首先要在刪除鏈表元素的 前面 判斷是否要刪除第一個元素(此時的狀況是pos爲1的狀況),而後刪除鏈表元素,再判斷是不是刪除第一個元素的第二種狀況(鏈表循環一圈後,到達鏈表第一個元素,此時元素的前一個鏈表再也不是head頭結點了)。如圖:
![](http://static.javashuo.com/static/loading.gif)
代碼以下:
- if(node == (CircleListNode* )head)
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- }
-
- ret = node->next;
- node->next = ret->next;
-
- if((first == ret) &&(NULL == Last))
- {
- Last =(CircleListNode* )Get_CircleListNode(lhead, lhead->length);
- }
-
- if( Last != NULL )
- {
- Last->next = ret->next;
- lhead->head.next = ret->next;
- }
圖中紅筆的代碼是:
- ret = node->next;
- node->next = ret->next;
圖中藍筆的代碼是:
- if( Last != NULL )
- {
- Last->next = ret->next;
- lhead->head.next = ret->next;
- }
c.當length爲0的是,即鏈表長度爲0的時候,記得給頭結點的next賦值爲NULL
第三步 (改寫得到鏈表元素函數)
a.記得把pos給成 0 到 無窮大,而後判斷length鏈表長度是否爲0 ,若是爲0 就不能獲取。
5.遊標的引入:
在循環鏈表中通常能夠定義一個遊標,對於這樣一個封裝好的可複用循環鏈表,定義一個遊標是十分方便的。例如:若是想依次得到鏈表中的每個元素,利用get函數,太太低效了O(n2),想一想利用這樣一個遊標去遍歷的話,複雜度僅僅是O(n)。還有就是在循環鏈表中,遊標能夠在鏈表中進行轉圈,例如:能夠解決約瑟夫環問題。
![](http://static.javashuo.com/static/loading.gif)
6.指定刪除鏈表中某一個元素的函數CircleListNode* CircleList_Del(CircleList* head,CircleListNode* node),其實也不是很高效,得到了當前遊標的值的時候,再去調用CircleList_Del函數,這個輪詢函數得到了pos,再去調用Del_CircleListNode而後又遍歷了一邊,把複雜的搞到了O(n2)。其實徹底能夠在找到pos的時候直接刪除掉這個鏈表元素,這樣的複雜度是O(n)。
7.我還以爲得到當前遊標得值的函數CircleList_Slider的返回值有些問題,我以爲若是返回的是當前遊標的上一個鏈表元素的值會更好,由於這個是一個單向鏈表,若是獲得了上一個鏈表元素的值,就能夠經過遊標實現,刪除啊,插入啊等高效的操做了。
本節代碼:
CricleList.c:
CircleList.h:
- #ifndef __CircleList_H__
- #define __CircleList_H__
-
- typedef void CircleList;
- typedef struct Str_CircleList CircleListNode;
- struct Str_CircleList
- {
- CircleListNode* next;
- };
-
- CircleList* Creat_CircleListHead(void);
-
- int Destroy_CircleListHead(CircleList* head);
-
- int Get_Length(CircleList* head);
-
- int Clean_CircleListHead(CircleList* head);
-
- int Add_CircleList(CircleList* head, CircleListNode* Node, int pos);
-
- CircleListNode* Get_CircleListNode(CircleList* head, int pos);
-
- CircleListNode* Del_CircleListNode(CircleList* head, int pos);
-
- CircleListNode* CircleList_Del(CircleList* head,CircleListNode* node);
-
- CircleListNode* CircleList_Next(CircleList* head);
-
- CircleListNode* CircleList_Reset(CircleList* head);
-
- CircleListNode* CircleList_Slider(CircleList* head);
-
- #endif
main.c:
- #include <stdio.h>
- #include <stdlib.h>
- #include "CircleList.h"
-
- typedef struct _tag_str
- {
- CircleListNode head;
- int i;
- }str;
- int main(int argc, char *argv[])
- {
- str str1,str2,str3,str4,str5,str6;
- str *strp;
- int i=0;
- str1.i=1;
- str2.i=2;
- str3.i=3;
- str4.i=4;
- str5.i=5;
- str6.i=6;
- CircleList* head;
- head = Creat_CircleListHead();
-
- Add_CircleList(head, (CircleListNode*)&str1, 0);
- Add_CircleList(head, (CircleListNode*)&str2, 0);
- Add_CircleList(head, (CircleListNode*)&str3, 0);
- Add_CircleList(head, (CircleListNode*)&str4, 0);
- Add_CircleList(head, (CircleListNode*)&str5, 5);
-
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
- printf("\n");
-
- printf("%d\n",Get_Length(head));
- strp = (str* )Del_CircleListNode(head, 6);
- printf("%d\n",strp->i);
-
- printf("%d\n",Get_Length(head));
- printf("\n");
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
-
- printf("\n");
- printf("%d\n",Get_Length(head));
- strp = (str* )Del_CircleListNode(head, 1);
- printf("%d\n",strp->i);
-
- printf("%d\n",Get_Length(head));
- printf("\n");
- for(i=1; i<=2*Get_Length(head); i++)
- {
- strp = (str* )Get_CircleListNode(head, i);
- printf("%d\n",strp->i);
- }
-
-
-
-
- printf("\n");
- for(i=1; i<=3; i++)
- {
- strp = (str* )Del_CircleListNode(head, 1);
- printf("%d\n",strp->i);
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Destroy_CircleListHead(head);
- return 0;
- }
本節知識點:
1.爲何選擇雙向鏈表:由於單向鏈表只能一直指向下一個鏈表元素,不能得到前一個元素,若是要進行逆序訪問操做是極其耗時的,因此引入雙向鏈表。
2.雙向鏈表的結構:在單向鏈表的基礎上增長了一個鏈表結構pre,如圖。
注意:鏈表第一個元素的前驅pre不是指向頭結點head,而是指向NULL,鏈表尾結點的後繼next指向NULL
3.如何將一個單向鏈表改爲雙向鏈表:
第一步 (改變鏈表的結構加入前驅):
- struct Str_DLinkList
- {
- DLinkListNode* next;
- DLinkListNode* pre;
- };
第二步 (改寫插入函數):
對於一個尾插法,如圖:
(1).正常的鏈表插入操做,代碼以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)
- {
- node = node->next;
- }
-
- Node -> next = node -> next;
- node -> next = Node;
(2).把剛剛插入的數據的前驅pre跟前一個數據元素相連,代碼以下:
對於一個正常插入,如圖:
(1).正常的鏈表插入操做,代碼以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)
- {
- node = node->next;
- }
-
- Node -> next = node -> next;
- node -> next = Node;
(2).先判斷是否是尾插法,若是是尾插法,就像上一個狀況同樣,就不進行這一步的操做了,代碼以下:
- if(NULL != Node->next)
- {
- Node->next->pre = Node;
- }
(3).把剛剛插入的數據的前驅pre跟前一個數據元素相連,代碼以下:
對於一個頭插法,如圖:
(1).正常的鏈表插入操做,代碼以下:
- for(i=1; ( (i<pos) && (node->next != NULL) ); i++)