資料結構:單鏈表
最近學習了資料結構中的連結串列。
關於連結串列,個人整理筆記如下:
什麼是連結串列?
連結串列是物理儲存單元上非連續、非順序的儲存結構。與我們之前學習過的陣列同為儲存結構,區別是陣列是連續的、順序的儲存結構。
在連結串列這種非連續、非順序的儲存結構中,每個元素以結點的形式儲存。而每個結點都由資料域和指標域構成,如下圖所示。
關於頭結點:
連結串列可以有頭結點也可以沒有,區別在於連結串列有頭結點雖 浪費空間,但易理解,邊界好處理,不易出錯,程式碼簡單,相反 無頭結頭節省空間,難理解,邊界不易處理,程式碼稍複雜。
有頭結點的引入是為了對連結串列刪除、逆向、建立的時候操作更統一,不用專門對第一個元素或最後一個元素進行單獨處理。
可能講述得不是那麼清楚,更多詳情請參閱此博文 http://blog.csdn.net/21aspnet/article/details/160019
為什麼要使用連結串列呢?
首先,陣列是實體記憶體上連續的、順序的空間,那麼要是陣列大小太大或沒有連續大小的空間時就尬尷了,此時就需要用到連結串列了。
其次,我們如果我們對一組資料進行刪除操作時,用陣列的話,我們就需要將要刪除的那個資料所在的位置後邊的所有資料都得向前移動一個位置,而當我們新增資料時,同樣也是這樣的操作。
那麼使用連結串列的話,就是把那個結點的指標域所指向地址指向下一個結點的地址就 ok 了。而新增資料呢,也是類似的指標操作。
當然陣列仍然存在肯定是還有連結串列不能替代的地方,使用連結串列的時候呢,我們要查詢某個資料時就必需得從第一個結點開始依次的查詢,
還有當我們要打印出連結串列中最後一個數據時,用陣列直接使用下標地址就可以打印出來了,然而連結串列還在一個接一個地跟著指標往下傳,
同樣的我們用陣列列印某個指定序列內的資料時直接用標法可以解決,而連結串列就比較麻煩了。
那麼綜上所述,可以知道為什麼要用到連結串列,陣列和連結串列存在的意義了。
那什麼又是單向連結串列?什麼又是迴圈連結串列?就用一個結構圖來表明它們的關係吧。
線性表:一種最簡單、常用的資料結構,資料元素之間是一對一的關係。
那麼,怎樣建立一個單鏈表呢?
首先定義單鏈表的儲存結構
// 為 int 型別建立 ElemType 別名 typedef int ElemType; // 定義連結串列儲結構 typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList;
此處定義單鏈表每個結點的儲存結構,包括 儲存結點的資料域 data 和 儲存後繼結點位置的指標域 next 。
為了方便閱讀使用別名 ElemType 表示 int 型,同時也為了提高程式可讀性,對同一結構體指標型別命了兩個名 LinkList 、LNode * ,兩者本質上是等價的。
通常習慣用 LinkList 定義單鏈表,表示某單鏈表的頭指標;用LNode *定義指向單鏈表中任意結點的指標變數。
如: 定義LinkList L ,則L為單鏈表的頭指標; 定義LNode *p ,則p為指向單鏈表中某個結點的指標,用 *p 表示該結點。
單鏈表由表頭指標唯一確定,因此單鏈表可以用頭指標的名字來命名,如頭指標名為 L ,則簡稱該連結串列為為表 L。
注意區分指標變數和結點變數兩個不同概念,若定義 LinkList p 或 LNode *p ,則p為指向某結點的指標變數,表示該結點的地址;而*p為對應結點的變數,表示該結點的名稱。
此處為了 便於首元結點的處理 便於空表和非空表的統一處理 為連結串列增加頭結點。
初始化單鏈表
1.生成一個新結點作為頭結點 ,用頭指標 L 指向頭結點。
2.頭結點的指標域置空。
此處: 可以使用關鍵字 new 為結點分配記憶體,也可以用標頭檔案 stdlib.h 中的 malloc 分配記憶體。
void InitList(LinkList &L) { L = new LNode; // 分配記憶體 //L = (structLNode *)malloc(sizeof(struct LNode)); // 呼叫標頭檔案 stdlib.h 中的 malloc 分配記憶體 L->next = NULL; }
建立單鏈表
初始化操作是建立一個只有頭結點的空連結串列,那麼如何建立包括若干結點的連結串列呢?
建立連結串列時根據結點插入位置不同,連結串列的建立方法可分為前插法和後插法。
- 前插法
前插法通過將新結點逐個插入連結串列的頭部(頭結點之後)來建立連結串列,每次申請一個新結點,讀入相應的資料元素值,然後將新結點插入到頭結點之後 。
1.建立一個只有頭結點的空連結串列。
2.根據建立連結串列結點元素個數 n ,迴圈n次執行以下操作:
(1)生成一個新結點 *p ;
(2)將輸入元素賦新結點 *p 的資料域中;
(3)將新結點 *p 插入到頭結點之後。
// 前插法插入 void CreateList_H(LinkList &L, int n) { LNode *p; for(int i = 0; i < n; i++) { p = new LNode; cin>>p->data; p->next = L->next; L->next = p; } }
- 後插法
後插法通過將新結點逐個插入到連結串列的尾部來建立連結串列。同前插法,每次申請一個新結點,讀入相應的資料元素值。不同的是,為了使新結點能夠插入到單鏈表尾部,需要增加一個尾指標 tailNode 指向連結串列的尾結點。
1.建立一個只有頭結點的空連結串列。
2.尾指標 tailNode 初始化,指向頭結點。
3.根據建立連結串列結點元素個數 n,迴圈n次執行以下操作:
(1)生成一個新結點 *p ;
(2)輸入元素值賦給新結點 *p 的資料域;
(3)將新結點 *p 插入到 *tailNode 之後;
(4)尾指標tailNode指向新的尾結點 *p。
// 尾插法插入 void CreateList_R(LinkList &L, int n) { LNode *p, *tailNode; tailNode = L; for(int i = 0; i < n; i++) { p = new LNode; cin>>p->data; p->next = NULL; tailNode->next = p; // 將新結點*p插入尾結點*tailNode後的資料域 tailNode = p; // tailNode 指向新的尾結點 *p } }
單鏈表的插入
建立了含若干個結點的單鏈表,就可能根據需求對連結串列進行新結點的插入操作。
將資料域為 e 的新結點插入到單鏈表的第 i 個結點位置上,即插入到結點 N i-1 和 N i 之間。
1.查詢結點 N i-1 ,並由指標 p 指向該結點;
2.生成一個新的結點 *newNode ;
3.將新結點 *newNode 資料域置為 e,指標域指向結點 N i ;
4.將結點 *p 的指標域指向新結點 *newNode。
// 單鏈表插入新結點 int InsertLinkList(LinkList &L, int i, ElemType e) { LNode *p = L, *newNode; int j = 0; while(p &&j < i-1) // 查詢第 i-1 個結點 p指向該結點 { p = p->next; j++; } if(!p || j > i-1) return -1; // 類似前插法 newNode = new LNode; // 生成新結點newNode newNode->data = e; newNode->next = p->next; p->next = newNode; return 1; }
單鏈表的刪除
單鏈表對其中某個結點的刪除也是基本操作,同單鏈表插入元素一樣,將某個結點上從連結串列中刪除,首先查詢結點 N i-1 然後刪除該位置結點。
1.查詢結點 N i-1 位置,並由指標 p 指向該結點;
2.臨時儲存待刪除結點 N i 地址在 tempNode 中,以備釋放結點空間;
3.將結點 *p 指標域指向結點 N i 的的後繼結點;
4.釋放結點 N i 的空間。
// 刪除單鏈表元素 int LinkListDelete(LinkList &L, int i) { int j = 0; LNode *p, *tempNode; p = L; while((p->next) && (j < i-1)) // 查詢第 i-1 個元素 p指向該結點 { p = p->next; j++; } if(!(p->next) || j > i-1) // 判斷插入位置是否合理 return -1; tempNode = p->next; // 臨時儲存被刪除結點的地址以備釋放 p->next = tempNode->next; // 改變刪除結點前驅結點的指標域 delete tempNode; // 釋放刪除結點空間 return 1; }
單鏈表列印輸出
如果要檢視單鏈表中元素,怎麼做呢?
1.生成新結點 *p,其指單鏈表首元結點;
2從首元結點開始依次順著連結串列指標域 next 向下訪問,只要當前結點的指標 p 不為空(NULL)則執行以下操作:
(1)列印輸出當前結點資料域值;
(2)p 指向下一個結點;
// 列印輸出單鏈表元素 void PrintLinkList(LinkList &L) { LNode *p; p = L->next; while(p != NULL) { cout<<p->data<<" "; p = p->next; } cout<<endl; }
單鏈表的取值
和順序表不同,連結串列中邏輯相鄰的結點並沒有儲存在物理相鄰的單元中,這樣根據給定結點位置序號 i,在連結串列中獲取該結點資料域的值時不能像順序表那樣隨機訪問,只能從連結串列的首元結點出發,順著連結串列指標域next 逐個結點向下訪問。
1.用指標 p 指向首元結點, 用 j 做計數器初值賦為 1;
2.從首元結點開始依次順著連結串列指標域 next 向下訪問,只要指向當前結點的指標 p 不為空(NULL) ,並且沒有到達序號為 i 的結點,則迴圈執行以下操作:
(1)p 指向下一個結點 ;
(2)計數器 j 相應加 1;
3.退出迴圈時,如果指標 p 為空,或者計數器 j 大於 i,則說明指定的序號 i 值不合法(i 大於表長 n 或 i 小於等於 0),取值失敗提示資訊 返回 -1 ;否則取值成功,此時 j = i 時,p 所指的結點就是要找的第 i 個結點,可以輸出此結點資料域中元素值,返回 1。
// 單鏈表取值 int getElem(LinkList &L, int i) { LNode *p; int j = 1; // 計數器 j 初值賦為 1 p = L->next; // 初始化 p 指向首元結點 while(p && j < i) // 順連結串列指標域向後掃描 直到 p 為空或指向第 i 個元素 { p = p->next; j++; } if(!p || j > i) // i 值不合法 i > n 或 i <= 0 { cout<<"No the Node"<<endl; return -1; } cout<<p->data; return 1; }
單鏈表的查詢
連結串列的查詢過程和順序表類似,從連結串列的首元結點出發,依次將結點資料域中的值與給定值 e 進行比較,返回查詢結果。
1.用指標 p 指向首元結點 ;
2.從首元結點開始依次順著連結串列指標域向下查詢,只要指向當前結點的指標 p 不為空,並且 p 所指結點的資料域與給定值 e 不等時,則執行以下操作:
(1) p 指向下一個結點。
3.判斷 p 是否為空(NULL),如為空,則連結串列中沒有結點資料域值為所給定的 e ,給出提示資訊;如不為空,則列印輸出其所在位置。
// 單鏈表的查詢 void LocationElem(LinkList L, ElemType e) { LNode *p; int j = 1; p = L->next; // 初始化 p 指向首元結點 while(p && p->data != e) // 順著連結串列指標域向下掃描 直到 p 為空或所指結點的資料域等於 e { j++; p = p->next; } if(p != NULL) // 判斷是否查詢到結點資料域為 e 的結點 cout<<"The element of "<<e<<" local "<<j<<endl; else cout<<"No the element"<<endl; }
最後,完整程式碼如下 :
1 #include <iostream> 2 3 using namespace std; 4 5 // 為 int 型別建立 ElemType 別名 6 typedef int ElemType; 7 8 // 定義連結串列儲結構 9 typedef struct LNode 10 { 11ElemType data; 12struct LNode *next; 13 }LNode, *LinkList; 14 15 16 // 初始化連結串列 17 void InitList(LinkList &L) 18 { 19L = new LNode; // 分配記憶體 20//L = (structLNode *)malloc(sizeof(struct LNode)); // 呼叫標頭檔案 stdlib.h 中的 malloc 分配記憶體 21L->next = NULL; 22 } 23 24 // 單鏈表插入新結點 25 int InsertLinkList(LinkList &L, int i, ElemType e) 26 { 27LNode *p = L, *newNode; 28int j = 0; 29while(p &&j < i-1) // 查詢第 i-1 個結點 p指向該結點 30{ 31p = p->next; 32j++; 33} 34if(!p || j > i-1) 35return -1; 36// 類似前插法 37newNode = new LNode; // 生成新結點newNode 38newNode->data = e; 39newNode->next = p->next; 40p->next = newNode; 41return 1; 42 } 43 44 45 // 列印輸出連結串列 46 void PrintList(LinkList &L) 47 { 48LNode* p; 49p = L->next; 50while(p != NULL) 51{ 52cout<<p->data<<" "; 53p = p->next; 54} 55cout<<endl; 56 } 57 58 // 單鏈表取值 59 int getElem(LinkList &L, int i) 60 { 61LNode *p; 62int j = 1; // 計數器 j 初值賦為 1 63p = L->next; // 初始化 p 指向首元結點 64while(p && j < i) // 順連結串列指標域向後掃描 直到 p 為空或指向第 i 個元素 65{ 66p = p->next; 67j++; 68} 69if(!p || j > i) // i 值不合理 i > n 或 i <= 0 70{ 71cout<<"所給定 i 值不合理"<<endl; 72return -1; 73} 74cout<<p->data; 75return 1; 76 } 77 78 // 單鏈表的查詢 79 void LocationElem(LinkList L, ElemType e) 80 { 81LNode *p; 82int j = 1; 83p = L->next; // 初始化 p 指向首元結點 84while(p && p->data != e) // 順著連結串列指標域向下掃描 直到 p 為空或所指結點的資料域等於 e 85{ 86j++; 87p = p->next; 88} 89if(p != NULL) // 判斷是否查詢到結點資料域為 e 的結點 90cout<<"連結串列中與 "<<e<<" 等值的資料元素所在結點位於 "<<j<<endl; 91else 92cout<<"連結串列中沒有資料元素和 "<<e<<" 等值 "<<endl; 93 } 94 95 // 刪除單鏈表元素 96 int LinkListDelete(LinkList &L, int i) 97 { 98int j = 0; 99LNode *p, *tempNode; 100p = L; 101while((p->next) && (j < i-1)) // 查詢第 i-1 個元素 p指向該結點 102{ 103p = p->next; 104j++; 105} 106if(!(p->next) || j > i-1) // 判斷插入位置是否合理 107return -1; 108tempNode = p->next; // 臨時儲存被刪除結點的地址以備釋放 109p->next = tempNode->next; // 改變刪除結點前驅結點的指標域 110delete tempNode; // 釋放刪除結點空間 111return 1; 112 } 113 114 // 前插法插入 115 void CreateList_H(LinkList &L, int n) 116 { 117LNode *p; 118// LinkList p; 119for(int i = 0; i < n; i++) 120{ 121p = new LNode; 122cin>>p->data; 123p->next = L->next; 124L->next = p; 125} 126 } 127 128 // 尾插法插入 129 void CreateList_R(LinkList &L, int n) 130 { 131LNode *p, *tailNode; 132tailNode = L; 133for(int i = 0; i < n; i++) 134{ 135p = new LNode; 136cin>>p->data; 137p->next = NULL; 138tailNode->next = p; // 將新結點*p插入尾結點*tailNode後的資料域 139tailNode = p; // tailNode 指向新的尾結點 *p 140} 141 } 142 143 int main() 144 { 145// 定義連結串列 L 146LinkList L; 147// 初始化連結串列 148InitList(L); 149 150cout<<"前插法建立有5個元素的單鏈表"<<endl; 151CreateList_H(L, 5); 152cout<<"列印連結串列資料"<<endl; 153PrintList(L); 154 155cout<<"後插法建立有5個元素的單鏈表"<<endl; 156CreateList_R(L, 5); 157cout<<"列印連結串列資料"<<endl; 158PrintList(L); 159 160cout<<"在連結串列第三個位置插入數值為 100 的結點"<<endl; 161InsertLinkList(L, 3, 100); 162cout<<"列印插入元素後的連結串列資料"<<endl; 163PrintList(L); 164 165cout<<"刪除第六個結點"<<endl; 166LinkListDelete(L, 6); 167cout<<"列印刪除結點後連結串列資料"<<endl; 168PrintList(L); 169 170cout<<"查詢元素資料為 100 所在連結串列中位置"<<endl; 171LocationElem(L, 100); 172 173 }
本文參考:
《資料結構》(C語言版|第2版) 嚴蔚梅 李冬梅 吳偉民 編著