第二章:線性表(鏈式表示)
學習數據結構–第二章:線性表(順序存儲、插入、刪除) 這篇文章講到線性表的順序表示也就是順序表,順序表雖然可以隨機存儲,但是在初始化的時候需要申請一大塊連續的存儲空間,且在執行插入和刪除操作時,也需要大量的移動元素,時間複雜度比較高,下面講線性表的另一種存儲結構:
鏈式存儲
1.單鏈表的定義
線性表的鏈式存儲又稱:單鏈表
,通過一組任意
的存儲單元來存儲線性表中的數據元素。
數據元素存儲在任意位置,不一定連續,通過指針實現線性邏輯關係。
我們把單鏈表中這樣 數據加地址的組合
叫做單鏈表的一個結點,一個結點
存儲數據元素的數據域
和下一個結點(數據元素)的地址的指針域
組成。
單鏈表有兩種創建方式
- 無頭結點的單鏈表
- 有頭節點的單鏈表
有頭節點的單鏈表,它的頭節點的數據域一般不存儲數據,它的指針域存儲第一個結點的地址。
優點:
- 鏈表的第一個位置和其他位置的操作統一(比如插入操作:無頭節點的鏈表,在表中插入結點的時候兩邊都有結點,而在表頭插入結點的時候左邊是沒有結點的,而有頭節點就都是一樣的。)
- 空表和非空表的操作統一
2.單鏈表的基本操作
2.1頭插法建立
//頭插法
LinkList List_HeadInsert (LinkList &L){
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
scanf("%d",&x);
while(x!=9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
時間複雜度:O(n)
2.2尾插法建立
LinkList List_TailInsert (LinkList &L){
int x;
L=(LinkList)malloc(sizeof(LNode));
LNode *s,*r=L; //注意這裏重新定義一個指針r,作爲尾指針,同時初始化爲了頭節點
scanf("%d",&x);
while(x!=999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //修改尾指針,指向新插入的結點
scanf("%d",&x);
}
r->next=NULL;
return L;
}
時間複雜度:O(n)
2.3按序號查找&按值查找
按照和按序號查找都要遍歷單鏈表。
按序號查找
LNode *GetElem(LinkList L,int i){
int j=1; //標識當前結點的序號
LNode *p=L->next; //當前所查找的結點,初始化爲頭節點的下一個結點,因爲頭節點不保存數據
if(i==0){ //序號不合法
return L;
}
if(i<1){ //序號不合法
return NULL;
}
while(p&&j<i){ //當結點不爲空,且序號小於j的時候,繼續循環
p=p->next;
j++;
}
return p;
}
時間複雜度:O(n)
按序值查找
LNode *LocateElem(LinkList L,ElemType e){
//初始化一個指針指向頭節點的下一個結點
LNode *p=L->next;
//判斷結點不爲空,且數據不爲e
while(p!=NULL&&p->data!=e){
//如果結點不爲空,且值不爲e,則指針繼續向下移動
p=p->next;
}
return p;
}
時間複雜度:O(n)
2.4插入結點
插入有兩種方式,前插法和後插入法,比如插入位置爲 i ,前插法就是在 i 的位置之前插入, 後插法就是在 i 的位置之後插入,所以前插法要找 i-1 位置,而後插法要找 i 的位置。所以如果 i 的位置是已知的,這樣兩種方法就會產生區別,前插法仍然需要遍歷鏈表O(n),而後插法直接使用這個位置即可O(1)。後插法是可以實現前插法的,插入之後交換兩個結點的位置即可。下面演示前插法:
插入結點首先要知道插入的位置,假如插入位置爲 i
,則需要知道 i-1
結點的位置。接着修改新結點的指針指向 i-1
結點的下一個位置,然後修改i-1
結點的指針指向新插入結點。
注意
下面的代碼,不能交換位置。
s->next=p->next;
p->next=s;
why???
這個順序是不能交換的,如果交換會出現i
結點地址丟失的問題。
p->next=s;
這時已經將p
結點中存儲的i
結點的地址給覆蓋了,成了新結點的地址,接着再s->next=p->next;
這相當於講s
結點的指針指向了他自己。這樣就把後面的鏈表給丟棄了。
2.5刪除結點
結點的位置未知
假如要刪除鏈表中第 i
號結點的位置,修改第 i-1
號結點的指針,讓其指向第 i+1
號結點的位置。這是要注意要使用一個指針指向第i
個結點,因爲修改之後我們會失去第 i
個結點的位置,這樣後面就無法釋放i
結點的空間。
結點的位置已知*p
這時可以先交換p
結點和後一個
結點的數據,然後刪除後一個結點即可,注意要有一個指針指向後一個結點
,方便之後釋放空間。
2.6求表長
有頭節點和無頭節點的鏈表判斷是不一樣的。
3.特殊鏈表
3.1雙鏈表
在使用單鏈表的時候,我們知道當前結點i
的指針,在執行插入,刪除等需要知道它的前驅結點的操作,我們需要通過按序號查找的方式查找到他的前驅結點。這樣時間複雜度是O(n)。
如果節點中有一個直接指向它的前驅結點的指針,那麼我們就可以直接找到它的前驅節點了。所以這樣就出現了雙鏈表。
3.1雙鏈表插入操作
前插法和後插法的時間複雜度都是O(1)。這個插入順序是可以調的,但是第一個和第二步,必須在第四步之前,因爲第二步我們需要i+1
結點的位置。
在單鏈表中,在表頭、表中和表尾的插入步驟相同。但是注意在雙鏈表中,在表頭和表中跟在表尾插入是不一樣的步驟。在雙鏈表的表尾進行插入的時候要注意表尾後面沒有下一個結點,所以不能修改下一個結點指向前驅的指針,否則會出現錯誤。
3.1雙鏈表刪除操作
首先找到要刪除的結點(q
)的前驅結點的指針,設爲p
,接着直接修改指針,釋放空間即可。時間複雜度爲O(1)。同樣在表尾進行刪除的時候也是不一樣的。
3.2循環鏈表
3.2.1循環單鏈表
假設我們使用單鏈表,我們只知道尾指針,但是需要知道頭指針,這時候是無法知道的。
這是如果將單鏈表的最後一個結點的指針指向頭節,這樣就可以找到的頭指針,這樣的鏈表形成一個環,叫做循環單鏈表。
這樣就只設置一個尾指針就行了,而且效率更高:因爲如果只有頭指針,我們想找到尾指針,需要遍歷單鏈表,但是如果有一個尾指針,我們找頭指針直接就可以找到。
在循環單鏈表插入和刪除操作,在每一個位置都是一樣。
3.2.1循環雙鏈表
我們需把鏈表最後一個結點的指針修改爲頭節點,且需要修改頭節點的前驅指針指向最後一個結點。這時每一個位置的插入和刪除操作都是一樣。
3.2.3循環鏈表判空
我們發現在循環鏈表中,我們利用了每一個結點的指針,也就是說在循環鏈表中,沒有空指針了,這時該怎麼判空呢??請看下圖:
3.3靜態鏈表
靜態鏈表:就是使用數組來實現的鏈式存儲結構的鏈表
。
單鏈表:
靜態鏈表:
靜態鏈表中每個結點既有自己的數據部分,還需要存儲下一個結點的位置,所以靜態鏈表的存儲實現使用的是結構體數組,包含兩部分: 數據域
和 遊標
(存放的是下一個結點在數組中的位置下標)。
#define MaxSize 50
typedef struct DNode{
ElemType data;
int next;
}SLinkList[MaxSize];