線性表的實現分順序存儲結構和鏈式存儲結構
上一節我們主要介紹了順序存儲結構,在最后我們還分別總結了順序存儲結構的優缺點,
對於順序結構的缺點,我們有沒有什么好的解決方法呢?
我們今天要介紹的線性表的鏈式存儲結構就可以很好的解決順序結構的缺點,一起來看。
順序結構最大的缺點就是在進行插入和刪除操作的時候,如果插入位置不理想,那么我們需要移動大量的元素,那產生這一問題的原因是什么呢?
仔細分析后,我們可以發現在順序存儲結構中,他們相鄰的元素的存儲位置也是相鄰的,我們在申請內存的的時候,是一次性申請一塊連續的內存,中間是沒有空隙的,這樣我們也就沒辦法進行快速的插入,如果進行刪除操作,就需要進行位置的填充。
鏈式存儲結構之所以能很好地解決這個問題,原因就在於它不考慮存儲位置的相鄰關系了,哪里有空位就存到哪里,我們只需要讓每個元素都知道下一個元素的位置在哪里就可以了。
這樣就是我們在定義鏈式存儲結構的時候,除了定義它本身需要存儲的信息之外,還需要存儲一個能夠指示它直接后繼的一個信息,這個信息我們可以用指針來表示。
這也就是我們課本上講的數據域和指針域。
我們將這種只帶有一個指針域的線性表稱為單鏈表。
鏈表中第一個結點的存儲位置叫做頭指針。
單鏈表的第一個結點錢附設一個結點,稱為頭結點。
上一節我們提到過,線性表的最后一個元素是沒有直接后繼的,所以在鏈式存儲中,我們將最后一個結點的指針域設置為null.
下面我們來看單鏈表的具體代碼實現

1 typedef struct LNode{ 2 ElemType data; //數據域 3 struct LNode *next; //指針域,用來指向本節點的直接后繼 4 }LNode,*LinkList; //定義節點,以及頭指針
很多同學分不清頭指針,頭結點,以及第一個結點之間的關系和區別,我們下面簡單地區分下。
頭指針:是指向鏈表的指針,如果鏈表有頭結點,它會指向頭結點
頭結點:第一個結點之前的一個輔助結點,其next指向第一個結點
第一個結點:就是一個結點,data變量存放第一個數據,next指針變量指向第二個結點
(畫圖渣渣,大家將就着看)
這里要注意的就是頭指針是一個鏈表的必要元素,而頭結點卻不是,那么頭結點存在的意義是什么呢?
個人的理解就是使第一個結點的插入操作和刪除操作和后面結點的操作一致,否則我們在修改第一個結點的時候,就需要修改頭指針。
如果沒有頭結點,頭指針直接指向第一個結點。
下面我們來看鏈式存儲結構的具體操作
在進行操作之前我們需要建立一個連式存儲結構的鏈表
下面看插入操作

1 void CreateList(LinkList *L,int n){ 2 //創建一個有n個結點的鏈表 3 LinkList p; //新結點 4 int i; //循環變量 5 L = (LinkList)malloc(sizeof(LNode)); 6 L->next = NULL; //構建一個帶頭結點的空鏈表 7 for(i=0;i<n;i++){ 8 p = (listLink1)malloc(sizeof(LNode)); //申請申結點的內存 9 scanf(&(p->data)); //賦值 10 p->next = L->next; 11 L->next = p; //插入的表頭 12 } 13 14 }
我覺得這個創建鏈表的方法可以叫插隊法,它就是無限在插隊,哈哈

1 int ListInsert(LinkList *L,int i,ElemType e){ 2 //向第i個位置之前插入節點,數據域為e 3 int j; //查找插入位置的輔助變量,用於循環 4 LinkList p,newNode; 5 p = *L; //將指針p指向鏈表頭結點 6 j = 1; //從1開始遍歷 7 while(p&&j<i){ 8 p = p->next; 9 j = j + 1; 10 } //尋找插入位置,第i-1個節點 11 //這里之所以不用for是因為我不知道這個鏈表的長度 12 if(!p||j>i-1){ 13 return 0; 14 } //找到位置后,我們需要對位置j的合理性進行驗證 15 16 //下面就是正式的插入節點了 17 newNode = (LinkList)malloc(sizeof(LNode)); 18 //申請新節點的內存位置 19 20 newNode->data = e; //將e的值存入新節點的數據域 21 newNode->next = p->next; //新節點的指針域指向第i個位置 22 p->next = newNode; //i-1位置的節點的指針域指向新節點 23 //但一定要注意上面三步的順序 24 25 return 1; //插入成功 26 27 }
寫完代碼之后,我們一起來整理下插入結點的思路:
- 聲明一個指針p指向鏈表的頭結點
- 循環遍歷,找到插入的位置,讓p的指針不斷向后移動,不斷指向下一個結點,計數變量j累加
- 判斷插入位置的合理性
- 生成新結點
- 將要插入的值賦給新結點的數據域
- 單鏈表插入的重點:newNode->next = p->next; p->next = newNode;
- 成功
接下來看鏈式存儲的刪除操作

1 int ListDelete(LinkList *L,int i,ElemType *e){ 2 //刪除第i個位置的結點,並用e返回其數據域 3 int j; //尋找刪除位置的輔助變量,用於循環 4 LinkList p,q; 5 p = *L; //將指針p指向鏈表頭結點 6 j = 1; //從1開始遍歷 7 8 while(p->next&&j<i){ 9 p = p->next; 10 j = j+1; 11 } 12 //尋找刪除的位置,注意這里是用p->next開始遍歷的 13 if(!(p->next)||j>i){ 14 return 0; 15 } //判斷刪除位置是否合理 16 //這里要給大家簡單解釋一下,我們這里要判斷的是p->next而不是p 17 //原因是,刪除的時候要將刪除位置的前后兩個結點連起來,通過前面的while循環,前結點一定是存在的 18 //那么重點就是要判斷一下是否存在后繼結點了 19 //這樣就會有另一個問題,最后一個元素怎么刪除,答案是尾結點 20 //這里我們默認是存在尾結點的,這個尾結點和頭結點的作用差不多,為了讓最以后一個結點的插入和刪除操作和別的結點是一樣的 21 22 23 q = p->next; //讓q指向要刪除位置的結點 24 p->next = q ->next; //讓要刪除結點的前驅的后繼指向要刪除點的后繼 25 e = q->data; //賦值 26 free(q); //釋放內存 27 28 return 1; //刪除成功 29 }
整理一下刪除結點的思路:
- 聲明指針p指向鏈表的頭結點
- 循環遍歷查找要刪除的位置,計算變量累加
- 判斷刪除位置的合理性,如果要刪除的結點的后繼為空,則位置不合理
- 將要刪除的結點p->next 賦值給q
- 單鏈表刪除語句 p->next = q->next
- 賦值
- 釋放內存
- 成功
寫完插入和刪除操作,我們便可以看出,鏈式存儲結構對於插入和刪除的優勢是明顯的,不需要進行大量的元素的移動。
增刪改查,增和刪給大家說完了,下面來說改和查,個人感覺這兩個其實差不多,改就是比查多了個修改元素,所以這里我只給大家貼一個查的代碼了

1 int GetElem(LinkList L,int i,ElemType &e){ 2 int j; //計數變量 3 LinkList p; 4 5 p = L->next; //p指向鏈表的第一個結點 6 j = 1; 7 8 while(p && j<i){ 9 p = p->next; //p指向下一個結點 10 j = j+1; 11 } 12 13 if(!p||j>i){ 14 return 0; 15 } //判斷位置合理性 16 17 e = p->next; //賦值 18 return 1; //成功 19 }
觀察查找操作,大家就會發現,鏈式存儲結構也是有其缺點的,其不便於進行查找和修改
這里再給大家說下另外兩種其他形式的鏈表
1、循環鏈表:
這個就是在單鏈表的基礎上頭尾相接了,將最后一個結點的指針指向了L->next;這里我們也不多做贅述,它的大部分操作和單鏈表是相似的。
還有一點要注意的就是判斷一個循環鏈表是否為空條件是看頭結點是否指向其本身。
2、雙向鏈表:
就是在單鏈表的基礎上多了一個指針域,用來指向其前驅,這個主要是解決了單鏈表只能順指針往后選差其他的結點的問題。

1 typedef struct DulNode 2 { 3 struct DulNode *prior; //前驅指針 4 struct DulNode *next; //后繼指針 5 ElemType e; //數據域 6 }DulNode,*DuLinkList;
然后簡單說說插入操作,我畫個簡圖幫助大家理解,然后寫下關鍵代碼

1 s->prior = p; //把p賦值給s的前驅,如圖中的1 2 s->next = p->next; //把p->next賦值給s的后繼,如圖中的2 3 p->next->prior = s; //把s賦值給p-next的前驅,如圖中的3 4 p->next = s; //把s賦值給p的后繼,如圖中的4
下面是刪除,老規矩,先上圖

1 P->prior->next = p->next; //把p->next的值賦給p->prior的后繼,如圖中的① 2 p->next->prior = P->prior;//把p=>prior賦值給p->next的前驅,如圖中的②
對於雙向離岸邊來說,它要稍微復雜些,因為了一個前驅指針,這樣,在插入和刪除的時候尤其小心指針變換的順序。
但是由於前后指針都有,這樣一定程度上給我們的操作帶來了方便。
到這里,我們的鏈式存儲就算是講完了,最后將順序存儲結構和鏈表存儲結構進行一下對比
我們主要從兩個方面進行對比,一個是時間,一個是空間
時間:
- 查找
順序存儲結構 O{1}
鏈式存儲結構O{n}
- 插入和刪除
順序存儲結構 O{n}
鏈式存儲結構O{1}
空間:
- 順序存儲結構:需要提前分配空間大小,分配打了,產生碎片,浪費,分配小了,容易導致溢出
- 連式存儲結構:不需要預分配,只要有就可以分配,並且數量不受變量限制
從以上比較不難看出
1、若線性表需要頻繁的查找,很少進行插入和刪除操作的時候,應該采用順序存儲。相反,則應采用鏈式存儲結構
2、當線性表不知道到底有多大的時候,建議采用鏈式存儲結構,如果我們已經提前知道其大小,可采用順序存儲結構
3、順序存儲結構和鏈式存儲結構各有各的優缺點,不能一概而論說就是哪種更好。我們需要根據實際情況來選擇我們需要的結構
線性表到這里就算是結束了,接下來會帶大家一起學習棧。