第二講---線性表的鏈式表示
-
複習
上次我們講到了線性表的順序表示,這裏我們做一個簡單的複習:
線性表的順序表示有哪些優點?
- 訪問方面
- 存儲方面
- 不同操作的複雜度方面
- 元素個數咋樣,元素之間咋樣,每個元素大小咋樣,元素之間的順序和元素的內容有沒有關係。
- 線性表的結構體創建方式
- 線性表有幾種表示方式
- 線性表的順序表示是順序存取還是隨機存取
- LineInsert、LineDelete分別有哪些操作過程
- 什麼是邏輯結構,什麼是存儲結構
- 如果我們要刪除一個線性表中,所有數據元素是“4”的元素,應該怎麼辦?
一、線性表的鏈式表示
由於線性表的順序表示中,對於插入和刪除需要移動大量元素。所以我們引入了線性表的鏈式表示。鏈式存儲線性表示後,不需要使用地址連續的存儲單元,即它不要求在邏輯上相鄰的兩個元素在物理上也相鄰。對鏈表的插入和刪除只需要修改指針!
舉個栗子
上次我們講到唐僧五人成立了一個公司,公司給分配集體房屋。但是,由於這幫人沒有經驗,公司賠的就差賣金箍棒了。唐僧想了想,最近P2P(傳銷)好像挺賺錢的,他打算重新成立家傳銷公司。
但是呢,大家都懂的,這玩意不能公開透明的去搞,而且呢還得發展下線,爲了防止亂套,唐僧定了如下規矩:
1.一個人只能發展一個下線,通過和下線的電話號碼聯繫。
2.下線不能知道上線是誰,因爲怕出事以後,把上線供出來,連鎖反應後大家都得進去。
OK~公司制度定下來了,唐僧是公司的頭頭,公司就叫大唐實業(註冊號爲999)吧(大氣~),所以我們現在有這樣一個記錄:
唐僧作爲公司的第一個人加入,其電話號碼是666,所以有:
剛開始只有唐僧,他還沒有發展下線,所以下線電話號佔時是空(NULL)。他在某個公用電話亭上面給猴哥打了個電話(之前有猴哥的電話是222)的方式鼓動猴哥去當他的下線(不當就念緊箍咒~),所以現在公司成了這個樣子:
然後猴哥發展了八戒(電話號111),八戒發展了沙僧(電話號777),沙僧發展了小白龍(電話號000)。一個龐大的制度清晰的傳銷帝國的雛形就成立了:
可以看到:
1.大唐實業的註冊號位999,工商局以及外人想查找這個公司,需要通過999這個號碼來查詢
(通常用頭指針來標識一個單鏈表)
2.唐僧是公司的第一人,但是大唐實業是第一個結點,大唐實業是整個傳銷的標識,工商局通過大唐實業來聯繫到整個公司。爲什麼不採取通過唐僧來聯繫整個公司呢,因爲這樣如果唐僧離開了公司,這樣工商局就找不到這個公司了。所以我們會在之前加一個結點。
(爲了操作方便,在單鏈表的第一個結點之前附加一個結點)
3.頭結點和頭指針的區分:頭指針是指的聯繫到這個公司的號碼,可以是999,也可以是666。但是999這樣的方式會更加的方便。頭結點指的是第一個結點。也就是上面鏈表中的第一個元素。
4.引入頭結點的兩個優點:
a)講究!在查找時候,所有人都是下線(唐僧可以理解爲是公司的下線),方便一點。
b)這樣就算公司人都跑了,公司還存在,這樣方便以後再加人。如果不用頭結點,都跑光以後,公司就不曉得哪裏去 了。
二、單鏈表上基本操作實現
1.與前一講一樣,我們先定義一下單鏈表的結構體
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
這裏面LNode是結構體名字,*LinkList是結構體指針,至於爲什麼這麼創建......(也不會考,睜一隻眼閉一隻眼就過去了,細究反而會浪費很多時間)。
下面會多次用到 LNode 和LinkList,其實LNode * = LinkList,我們一般用LNode * 去表示鏈表中的一個結點,用LinkList去表示一個鏈表,二者互換了也不會報錯,但是最好這麼寫。
2.採用頭插法建立單鏈表
//頭插法--每一次插入一個人,這個人插在公司這個頭結點後面,相當於每次發展一個總負責人
//之前的總負責人做他的下線
void CreatListHead(LinkList &L){
LinkList s; //創建待發展的新人
int x; //待發展新人的名字
L = (LinkList)malloc(sizeof(LNode)); //創建公司999
L->next = NULL; //公司總負責人(第一個下線)置爲空,還沒加呢
scanf("%d",&x); //創建新人的名字(第一個下線)
while(x!=-1){ //如果一個新人的名字是-1的話,意思就是不再發展了,結束了
//1.申請空間---手機號
s = (LinkList)malloc(sizeof(LNode)); //給新人創建個手機號,這裏s的地址相當於手機號
//也就是這個人新人的聯繫方式
//2.初始化名字
s->data = x; //加入新人的名字
//3.處理一下他的下線
s->next = L->next; //把之前的總負責人當做這個新人的下線
//4.上鍊
L->next = s;//新人成功上位爲總負責人
scanf("%d",&x);//再讀入下一個新人
}
}
頭插法相當於每次插入一個新人,這個新人當做公司的第一人(總負責人),也就是唐僧的位置,之前的唐僧將會變成新人的下線。邏輯上面不好理解,是我們正常發展下線的過程的逆過程,最後的結果是 公司->小白龍->沙僧->八戒->猴哥->唐僧
3.採用尾插法建立單鏈表
void CreatListTail(LinkList &L){
LinkList s; //創建待發展的新人
int x; //待發展新人的名字
L = (LinkList)malloc(sizeof(LNode)); //創建公司
L->next = NULL; //公司總負責人(第一個下線)置爲空,還沒加呢
//爲什麼會單獨創建tail,而不是用L去操作
LinkList tail;
tail = L;
scanf("%d",&x); //創建新人的名字(第一個下線)
while(x!=-1){ //如果一個新人的名字是-1的話,意思就是不再發展了,結束了
//1.申請空間---手機號
s = (LinkList)malloc(sizeof(LNode)); //給新人創建個手機號,這裏s的地址相當於手機號
//也就是這個人新人的聯繫方式
//2.初始化名字
s->data = x; //加入新人的名字
//3.處理一下他的下線--注意這裏和頭插法的區別
//之所以有這樣的差別,是應爲他們發展下線的方式不同,所以不要死記代碼
//要記住爲什麼這麼做
s->next = NULL;
//4.上鍊
tail->next = s;
//5.更新尾部
tail = s;
scanf("%d",&x);//再讀入下一個新人
}
}
尾插法就是咱正常發展公司下線的方法,這裏面有幾個注意的點:
a).爲什麼申請一個tail?
因爲每次都需要在尾部插入一個人,所以我們需要一個變量去保存當前的尾部,這樣才能把新人的手機號給這個當前的尾部。
b).爲什麼需要更新尾部?
因爲我們採用的是尾插法,意思就是在當前鏈表的最後一個元素後面再插入一個新人,如果一個新人插入後,尾部應該就是這個新人了,不是原來的尾部了。所以我們要在插入一個新人以後更新尾部。
c).爲什麼不用L代替tail?
我們在頭插法中,一直是在L的後面插元素。如果尾插法,每次把尾部更新爲L,最後我們得到的L地址就是最後一個元素的地址,前面的完全丟了。所以我們需要最先把頭結點的地址給L,然後申請一個tail讓他當尾部。這樣不管怎麼加人,L始終指向的是頭結點。
4.遍歷鏈表中所有結點操作
void PrintList(LinkList L){
/*我們一般不直接對L進行操作,雖然這裏L傳入的不是地址,裏面操作了無所謂
但是有些操作需要傳入L的地址,如果直接讓L=L->next的話,會破壞整個列表*/
LNode *p = L->next; //我們只打印出人,最開始是頭結點,先跳過
while(p!=NULL){
printf("%d ",p->data);
p = p->next;
}
printf("\n");
}
5.按序號查找結點值
LNode* GetElem(LinkList L,int i){
//同樣,我們先檢查下i的範圍對不對,由於鏈表中,我們不能知道當前鏈表一共有多少人
//所以只能檢查一下i的下界,上界的話等遍歷時候再檢查
if(i<=0)
return NULL; //之前錯誤一般返回false,但是這裏返回的類型是結構體,所以錯誤是NULL
LNode *p = L->next; //我們只打印出人,最開始是頭結點,先跳過
int j = 1; //表示現在我們p指向的是第一個人
//你也可以定義j = 0 代表第一個下標,都可以,但是後面需要i-1
//所以就看怎麼定義了,只要最後的操作對就行
//如果p爲空了但是j還是小於i,那麼說明i超出了上界,我們最後也會返回一個NULL
while(p!=NULL){
if(j == i){
return p; //找到了
}
p = p->next;
j++;
}
return NULL;
}
6.按值查找表結點
LNode* LocateElem(LinkList L,int e){
/*我們一般不直接對L進行操作,雖然這裏L傳入的不是地址,裏面操作了無所謂
但是有些操作需要傳入L的地址,如果直接讓L=L->next的話,會破壞整個列表*/
LNode *p = L->next; //我們只打印出人,最開始是頭結點,先跳過
while(p!=NULL){
if(p->data == e){
return p;
}
p = p->next;
}
return NULL;
}
7.插入節點操作(在單鏈表的第i個位置上插入新的結點)
(找到第i個人的上司,讓第i個人變成新人的下線,讓新人變成上司的下線,順序不能變)
bool ListInsert(LinkList &L,int i,int e){
//同樣,先檢查下下界
if(i<=0){
return false; //爲什麼這裏是false了
//因爲我不需要返回一個結構體,只需要返回是否成功,所以返回值類型是bool
}
LNode *p = L; //同樣,我們一般不直接對L進行處理
int j = 0;
//這裏,我們需要考慮一下,爲什麼j是從0開始的,並且p = L而不是P = L->next
//因爲我們執行的是插入操作,那麼就有可能插入到第一個位子,也就是i = 1
//如果P = L->next,j =1 那麼相當於我們跳過了第一位,這樣i=1這樣合理的訴求就滿足不了了
//而之前查找時候,爲什麼P = L->next呢?
//因爲查找嘛!
//我們在插入時候,需要找到i前面那個人
//前面那個人原來的下線是第i個人,現在我們讓他的下線變爲待插入的新人
//然後讓原來第i個人成爲新人的下線
//p!=NULL可以輔助我們檢測i是否超過上界
while(p!=NULL){
if(j==i-1){ //找到i前面那個人
LNode *newGuy = (LinkList)malloc(sizeof(LNode));//給新人開闢一個空間
newGuy->data = e;
newGuy->next = p->next;
p->next = newGuy;
//這倆順序一定不能變,如果先p->next = newGuy
//那麼原來第i個人的手機號就被這個新人覆蓋掉了
//接下來新人是需要把原來第i個人當成下線的,這時候,誰告訴他第i個人的手機號?
//可以想想p->next = newGuy,newGuy->next = p->next的後果有多麼美麗(恐怖)
return true;
}
p = p->next;
j++;
}
return false;
}
8.刪除結點的操作(找到待刪除結點的上司,讓他的下線變成上司的下線,這樣他就被刪掉了)
bool ListDelete(LinkList &L,int i){
//同樣,先檢查下下界
if(i<=0){
return false; //爲什麼這裏是false了
//因爲我不需要返回一個結構體,只需要返回是否成功,所以返回值類型是bool
}
LNode *p = L; //同樣,我們一般不直接對L進行處理
int j = 0;
//與插入相同,注意上面p和j的初始值
//我們在刪除時候,需要找到i前面那個人
//前面那個人原來的下線是第i個人,現在我們讓他的下線變爲待插入的新人
//然後讓第i個人就地爆炸
//p!=NULL可以輔助我們檢測i是否超過上界
while(p!=NULL){
if(j==i-1){ //找到i前面那個人
LNode *d = p->next;//待刪除的那個人
p->next = d->next;//讓待刪除的那個人的下線變成他原來上司的下線
//如果你想騷一點的話可以寫成:
//p->next = p->next->next;
free(d);//你也可以不執行這一步,當然這樣的習慣會不好
//如果這裏面有密碼什麼的,你也沒有free掉,它就一直在裏面,會被別人利用
return true;
}
p = p->next;
j++;
}
return false;
}
三、雙鏈表
公安局覺得唐僧這個只有上司知道下線,下線不知道上司的理念太牛B了。他們不知道如何把這個公司一鍋端,所以他們強制唐僧修改公司理念(一旦找不到解決問題的方法,就解決掉了製造問題的人)。
當然,這是含有頭結點的雙鏈表,雙鏈表中主要考的是插入和刪除操作,先看下雙鏈表的結構體定義吧:
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode,*DLinkList;
這裏面多了一個*prior,我們可以把*prior和*next理解爲一個人的左右手,並且整個鏈表中的人都在懸崖上面一個吊着一個,一旦一個人右手(*next)鬆開了,那麼下面的人都沒了,除非在他鬆開前,有另外一個人把他下面那個人拽住。如果能理解這一點,插入刪除就很好處理了。
首先我們看插入
舉個栗子:
唐僧和猴哥現在處於相互拉着的狀態,唐僧的右手(*next)拉着猴哥,猴哥的左手(*prior)拉着唐僧,現在丘比特想插進來。有如下操作
1.唐僧右手拉丘比特
2.丘比特左手拉唐僧
3.丘比特右手拉猴哥
4.猴哥左手拉丘比特
我們可以想一想,這第一步和第三步應該先做哪一步。如果先執行第一步,那麼猴哥就死掉了。如果先執行第三步,這時候猴哥的左手被兩個人拉着,然後執行第一步,唐僧鬆開右手,這時候有人會問,丘比特的左手是空的,那不丘比特和猴哥一起掉下去了(因爲丘比特會飛..我盡力舉一個看似恰當的例子了.......在計算機中,是因爲我們有變量去保存帶插入結點(丘比特)的地址,所以即使唐僧鬆開,我們還是有辦法找到丘比特和猴哥)。然後,唐僧右手把丘比特左手一拉,就插進來了。
qiuBt->next = houGe; //丘比特的右手拉着猴哥
houGe->prior = qiuBt; //猴哥左手拉着丘比特
qiuBt->prior = TangSeng; //丘比特左手拉着唐僧
TangSeng->next = qiuBt; //唐僧右手拉着丘比特
當然,我們可以先讓丘比特左手拉唐僧,順序有很多種,我們只需要保證:
1.猴哥不掉下去
2.最後大家左右手都有人拉
就OK!
然後我們看刪除:
舉個栗子:
現在是唐僧,丘比特,猴哥這三個人手拉着手,如果丘比特要出去的話,唐僧最後右手抓猴哥,猴哥左手抓唐僧,我們想一下一下幾個順序:
1.唐僧的右手抓猴哥
2.猴哥的左手抓唐僧
我們可以發現,兩個順序都不會導致出問題。所以這裏先後順序無所謂。
四、循環鏈表
循環鏈表可以分爲循環單鏈表和循環雙鏈表,其特性就是,最後一個結點的*next指針指的是頭結點,每一次在插入和刪除時候需要額外考慮一下,也沒什麼難度,只要理解了就行。
五、課後習題
6.在一個單鏈表中,一直q所知節點是p所知結點的前驅結點,若在q和p之間插入節點s,需要執行:
7.給定有n個元素的一維數組,建立一個有序的單鏈表的最低時間複雜度是
這個題中,有序的話我們得先排序,最好的排序算法的時間複雜度是O(nlog_2n),而建立一個單鏈表的複雜度是O(n
),我們知道複雜度主要看的是最複雜的那一項,所以是O(nlog_2n)
14.在雙鏈表中向p所指的結點之前插入一個結點q的操作爲
六、總結
在做鏈表的題時候,其本身比較抽象,做的時候一定要在紙上畫呀畫。線性表的順序表示和鏈式表示有一定的區別,這裏也比較愛考,我做一下總結:
內容 |
順序表 |
鏈表 |
存取方式 |
隨機存取/順序存取 |
順序存取 |
邏輯結構與物理結構 |
邏輯結構相鄰的兩個元素其對應存儲位置(物理結構)也相鄰(門牌號的關係) |
邏輯結構相鄰的兩個元素其對應存儲位置(物理結構)不一定相鄰(電話號的關係) |
按序查找 |
O(1) |
O(n) |
插入、刪除 |
O(n) |
O(1) |
優點 |
方便查找 |
方便插入刪除 |