線性表的定義
線性表(List):零個或多個數據元素的有限序列。
有幾個地方需要強調:
- 首先它是一個序列,也就是說元素之間是有順序的,若元素存在多個,則第一個元素無前驅,最後一個元素無後繼,其他每個元素都有且只有一個前驅和後繼。
- 然後線性表強調的是有限的。
- 最後線性表中的數據元素必須是相同類型。
如果用數據語言來定義,可如下(配合下圖理解):
數學語言定義: 若將線性表記爲(a1,…,ai,…,an),則表中ai-₁領先於ai,ai領先於ai+₁,稱ai-₁是ai的直接前驅元素,ai+₁是ai的直接後繼元素。當i=1,2,…,n-1時,ai有且僅有一個直接後繼,當i=2,…,n時,ai有且僅有一個直接前驅。
線性表元素的個數n(n≧0)定義爲線性表的長度,當n=0時,稱爲空表。
一個線性表中的數據元素ai,i稱爲數據元素ai在線性表中的位序。
線性表的抽象數據類型
線性表常見的操作如下:
- 創建和初始化
- 重置
- 根據位序獲取數據元素
- 查找某數據元素是否存在
- 獲得線性表長度
- 插入數據
- 刪除數據
根據以上的常見操作,線性表的抽象數據類型定義如下:
ADT 線性表(List)
Data
線性表的數據對象集合爲{a1,a2,......,an},每個元素的類型均爲DataType。其中除第一個元素a1外,每一個元素有且只有一個直接前驅元素,除了最後一個元素an外,每一個元素有且只有一個直接後繼元素。數據元素之間的關係是一對一的關係。
Operation
InitList(*L): 初始化操作,建立一個空的線性表L。
ListEmpty(L): 若線性表爲空,返回true,否則返回false。
ClearList(*L): 將線性表清空。
GetElem(L,i,*e): 將線性表L中的第i個位置的元素值返回給e。
LocateElem(L,e): 在線性表L中查找與給定值e相等的元素,如果查找成功,返回該元素在表中的序號表示成功;否則,返回0表示失敗。
ListInsert(*L,i,e): 在線性表L中的第i個位置插入新元素e。
ListDelete(*L,i,*e): 刪除線性表L中第i個位置元素,並用e返回其值
ListLength(L): 返回線性表L的元素個數
endADT
對於不同的應用,線性表的基本操作是不同的,上述操作時最基本的,對於實際問題中涉及的關於線性表的更復雜操作,我們可以用這些基本操作的組合來實現。
我們假設La表示集合A,Lb表示集合B,求兩個集合的並集:
//將所有的在線性表Lb中但不在La中的數據元素插入到La中
void unionL(List *La,List *Lb){
int La_len,Lb_len,i;
//聲明與La和Lb相同的數據元素e
ElemType e;
//求線性表的長度
La_len = ListLength(La);
Lb_len = ListLength(Lb);
for(i = 1; i <= Lb_len; i++){
//取Lb中第i個數據元素賦給e
GetElem(Lb,i,&e);
if(!LocateElem(*La,e)){
//插入
ListInsert(La,++La_len,e);
}
}
}
我們對於union操作,用到了前面線性表基本操作LocateElem,ListInsert,ListLength等,可見,對於複雜的個性化的操作,其實就是把基本操作組合起來實現的。
線性表的順序存儲結構
定義:線性表的順序存儲結構,指的是用一段地址連續的存儲單元依次存儲線性表的數據元素。
線性表(a1,a2,… ,an)的順序存儲示意圖如下:
在C語言中我們可以使用一維數組來實現順序存儲結構,即把第一個數據元素存到數組下標爲0的位置中,接着把線性表相鄰的元素存儲在數組中相鄰的位置。下面來看下線性表的順序存儲的結構代碼:
//存儲空間初始分配量
#define MAXSIZE 20
//ElemType類型根據實際情況而定,這裏假設爲int
typedef int ElemType;
typedef struct{
//數組存儲數據元素,最大值爲MAXSIZE
ElemType data[MAXSIZE];
//線性表當前長度
int length;
}SqList;
根據上面的定義,我們發現描述順序存儲結構需要三個屬性:
- 存儲空間的起始位置:數組data,它的存儲位置就是存儲空間的存儲位置。
- 線性表的最大存儲容量:數組長度MaxSize。
- 線性表的當前長度:length。
我們已知存儲空間的起始位置,即數組data的存儲位置,等同於數組中下標爲0的元素的存儲位置,所以已知下標爲0的數據元素的存儲位置 LOC(a0),假設每個數據元素所佔的空間爲c,那麼第i個數據元素的存儲位置LOC(ai)爲:
- *LOC(ai) = LOC(a0) +(i -1)c
通過以上公式,我們可以隨時算出線性表中任意位置的地址。此時我們對線性表中任意位置數據的讀取和更新對於計算機來說時間都是相等的,也就是一個常數,其時間複雜度爲常數階O(1),我們把具有這一特點的存儲結構稱爲隨機存取結構。
注意數組長度和線性表長度的區別:
- 數組長度是存放線性表的存儲空間的長度(即存儲容量),分配之後一般是不變的。不過在高級語言中,可以用編程手段實現動態分配數組,不過這會帶來性能上的損耗。
- 線性表的長度是線性表中數據元素的個數,隨着線性表插入和刪除操作的進行,這個量是變化的。
順序存儲結構的獲取、插入和刪除
-
數據元素的獲取
對於線性表的順序存儲結構來說,我們要實現GetElem操作,即將線性表L中的第i個位置元素值返回即可。代碼實現如下:
//順序存儲線性表獲取特定下標i的數據元素值。時間複雜度爲O(1)
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//初始條件:順序線性表L已存在,1<=i<=ListLength(L)
//操作結果:用e返回L中第i個數據元素的值
Status GetElem(SqList L,int i,ElemType *e){
if(L.length == 0 || i < 1 || i > L.length){
return Error;
}
*e = L.data[i - 1];
return OK;
}
-
插入操作
當我們要在線性表L中的第i個位置插入新元素e,即實現ListInsert(*L,i,e),該如何操作呢?當然是從數組的最後向前遍歷到位置i,將這些元素全部後移一個位置,然後在i位置插入新的數據元素。代碼實現如下:
//時間複雜度爲O(n) //初始條件:順序線性表L已存在,1<=i<=ListLength(L) //操作結果:在L中第i個位置之前插入新的數據元素e,L的長度加1 Status ListInsert(SqList *L,int i,ElemType e){ int k; //順序線性表已滿 if(L->length == MAXSIZE){ return ERROR; } //i不在範圍內 if(i > L->length+1 || i < 1){ return ERROR; } //若插入數據位置不在表尾 if(i <= L->length){ //將要插入位置後面的數據元素後移一位 for(k = L->length - 1; k >= i - 1;k--){ L->data[k+1]=L->data[k]; } } //將新元素插入 L->data[i-1] = e; L->length++; return OK; }
-
刪除操作
當我們要刪除線性表中第i個位置的數據元素,即實現ListDelete(*L,i,*e),該如何操作呢?當然是從刪除位置開始遍歷到最後一個元素位置,分別將它們都向前移動一個位置。代碼實現如下:
//時間複雜度爲O(n) //初始條件:順序線性表L已存在,1<=i<=ListLength(L) //操作結果:刪除L中第i個位置的數據元素,並用e返回其值,L的長度減1 Status ListDelete(SqList *L,int i,ElemType e){ int k; //空表 if(L->length == 0){ return ERROR; } //i不在範圍內 if(i > L->length || i < 1){ return ERROR; } *e = L->data[i-1]; //若刪除位置不在表尾 if(i < L->length){ //將刪除位置的後繼元素前移 for(k = i; k < L->length; k++){ L->data[k-1]=L->data[k]; } } L->length--; return OK; }
線性表順序存儲結構的優缺點
優點:
- 無須爲表示表中元素之間的邏輯關係而增加額外的存儲空間。
- 可以快速地存取表中任一位置的元素(存取時間複雜度爲O(1))。
缺點:
- 插入和刪除操作需要移動大量元素(以最壞情況來分析,插入和刪除時間複雜度爲O(n))。
- 當線性表長度變化較大時,難以確定存儲空間的容量。
- 造成存儲空間的“碎片”。即浪費,無法全部利用。
線性表的鏈式存儲結構
線性表的順序存儲結構最大的缺點就是插入和刪除時需要移動大量的元素,這顯然就需要耗費大量的時間。那麼爲什麼當插入和刪除時,就要移動大量的元素?根據線性表的順序存儲結構的定義,其中的數據元素是存放在一段地址連續的存儲空間中的,所以相鄰兩元素的存儲位置也是相鄰的,簡單的說就是它們都是挨着的,我們要插入必須要移動大量元素。因爲線性表的順序存儲結構的這個缺點,線性表的鏈式存儲結構就出現了,下面我們看下它的定義:
鏈式存儲結構是把數據元素存放在任意的存儲單元裏,這組存儲單元可以是連續的,也可以是不連續的。數據元素的存儲關係不能反映其邏輯關係,因此需要一個指針存放數據元素的地址,通過這個地址就可以找到相關聯數據元素的位置。
爲了表示每個數據元素ai與其直接後繼數據元素ai+₁之間的邏輯關係,對數據元素ai來說,除了存儲其本身的信息之外,還需要存儲一個指示其直接後繼的信息(即直接後繼的存儲位置)。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱作指針或鏈,這兩部分信息組成數據元素ai的存儲映像,稱爲結點。
n個結點鏈接成一個鏈表,即爲線性表的鏈式存儲結構,因爲此鏈表的每個結點中只包含一個指針域,所以叫做單鏈表。
我們把鏈表中第一個結點的存儲位置叫做頭指針,整個鏈表的存取必須是從頭指針開始進行。同時我們規定,線性表的最後一個結點指針爲空(通常用NULL或“^”符號表示)。
有時,我們爲了更加方便地對鏈表進行操作,會在單鏈表的第一個結點前附設一個結點,稱爲頭結點。頭結點的數據域可以不存儲任何信息,也可以存儲如線性表的長度等附加信息,頭結點的指針域存儲指向第一個結點的指針。以下是頭指針和頭結點的異同:
頭指針 | 頭結點 |
---|---|
頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針 | 頭結點是爲了操作的統一和方便而設立的,放在第一元素的結點之前,其數據域一般無意義(也可存放鏈表長度) |
頭指針具有標識作用,所以常用頭指針冠以鏈表的名字 | 有了頭結點,對在第一元素結點前插入節點和刪除第一結點,其操作與其它結點的操作就統一了 |
無論鏈表是否爲空,頭指針均不爲空。頭指針是鏈表的必要元素 | 頭結點不一定是鏈表必要要素 |
單鏈表中,我們在C語言中可用結構指針來描述。
//線性表的單鏈表存儲結構
typedef struct Node{
//數據域
ElemType data;
//指針域,指向直接後繼數據元素地址
struct Node *next;
}Node;
//定義LinkList
typedef struct Node *LinkList;
單鏈表的讀取、插入和刪除
-
單鏈表中數據元素的讀取
由於單鏈表並不是使用連續的地址來存儲數據元素,因此我們無法像線性表的順序存儲結構那樣方便的獲取到任意一個元素的存儲位置。在單鏈表中要獲得鏈表第i個數據的算法思路:
(1)聲明一個指針p指向鏈表第一個結點,初始化j從1開始。
(2)當j < i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加。
(3)若到鏈表末尾p爲空,則說明第i個結點不存在。
(4)否則查找成功,返回結點p的數據。實現代碼算法如下:
//初始條件:單鏈表L已存在,1<=i<=ListLength(L) //操作結果:用e返回L中第i個數據元素的值 Status GetElem(LinkList L,int i,ElemType *e){ int j; //聲明一指針p LinkList p; //讓p指向鏈表L的第一個結點 p = L->next; //j爲計數器 j = 1; //p不爲空且計數器j還沒有等於i時,循環繼續 while(p && j < i){ //讓p指向下一個結點 p = p->next; ++j; } if(!p || j > i){ //第i個結點不存在 return ERROR; } //取第i個結點的數據。結果就保存在e中 *e = p->data; return OK; }
-
單鏈表的插入
單鏈表第i個數據插入結點的算法思路:
(1)聲明一指針p指向鏈表頭結點,初始化j從1開始
(2)當j < i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1
(3)若鏈表末尾p爲空,則說明第i個結點不存在
(4)否則查找成功,在系統中生成一個空結點s
(5)將數據元素e賦值給s->data
(6)單鏈表的插入標準語句s->next=p->next; p->next = s;
(7)返回成功實現代碼算法如下:
//時間複雜度爲O(n) //初始條件:單鏈表L已存在,1<=i<=ListLength(L) //操作結果:在L中第i個結點位置之前插入新的數據元素e,L的長度加1 Status ListInsert(LinkList *L,int i,ElemType e){ int j; LinkList p,s; p = *L; j = 1; //尋找第i-1個結點 while(p && j < i){ p = p->next; ++j; } if(!p || j > i){ //第i個結點不存在 return ERROR; } //生成新結點 s = (LinkList)malloc(sizeof(Node)); //將p的後置結點的地址賦值給s的指針域 s->next=p->next; //將s的地址賦給p的指針域 p->next = s; return OK; }
-
單鏈表的刪除
單鏈表第i個數據刪除結點的算法思路:
(1)聲明一指針p指向鏈表頭指針,初始化j從1開始
(2)當j < i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1
(3)若鏈表末尾p爲空,則說明第i個結點不存在
(4)否則查找成功,將欲刪除的結點p->next賦值給q
(5)單鏈表的刪除標準語句p->next=q->next;
(6)將q節點中的數據賦值給e,作爲返回。
(7)釋放q結點
(8)返回成功實現代碼算法如下:
//時間複雜度爲O(n)
//初始條件:單鏈表L已存在,1<=i<=ListLength(L)
//操作結果:刪除L中第i個結點,並用e返回其值,L的長度減1
Status ListDelete(LinkList *L,int i,ElemType *e){
int j;
LinkList p,q;
p = *L;
j = 1;
//尋找第i-1個結點
while(p->next && j < i){
p = p->next;
++j;
}
if(!(p->next) || j > i){
//第i個結點不存在
return ERROR;
}
q = p->next;
//將q的後繼賦值給p的後繼
p->next=q->next;
//將q結點中的數據給e
*e = q->data;
//讓系統回收此結點,釋放內存
free(q);
return OK;
}
從整個算法來看,如果在我們不知道第i個結點的指針位置,單鏈表數據結構在插入和刪除操作上,與線性表的順序存儲結構是沒有太大優勢的。但如果,我們希望從第i個位置,插入10個結點,對於順序存儲結構意味着,每一次插入都需要移動n-i個結點,每次都是O(n)。而單鏈表,我們只需要在第一次時,找到第i個位置的指針,此時爲O(n),接下來只是簡單地通過賦值移動指針而已,時間複雜度爲O(1)。顯然,對於插入和刪除數據越頻繁的操作,單鏈表的效率優勢就越是明顯。
單鏈表的整表創建
順序存儲結構的創建,其實就是一個數組的初始化,即聲明一個類型和大小的數組並賦值的過程。而單鏈表和順序存儲結構就不一樣,它不像順序存儲結構這麼集中,它可以很散,是一種動態結構。對於每個鏈表來說,它所佔用空間的大小和位置是不需要預先分配劃定的,可以根據系統的情況和實際的需求即時生成。所以創建單鏈表的過程就是一個動態生成鏈表的過程,即從空表的初始狀態起,依次建立各元素結點,並逐個插入鏈表。
單鏈表整表創建的算法思路:
- 聲明一指針p和計數器變量i
- 初始化一空鏈表L
- 讓L的頭結點的指針指向NULL,即建立一個帶頭結點的單鏈表
- 循環:
- 生成一新結點賦值給p
- 隨機生成一數字賦值給p的數據域p->data
- 將p插入到頭結點與前一新結點之間(頭插法)
實現代碼算法如下:
//隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(頭插法--始終讓新結點在第一的位置)
void CreateListHead(LinkList *L,int n){
LinkList p;
int i;
//初始化隨機數種子
srand(time(0));
//先建立一個帶頭結點的單鏈表
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for(i = 0; i < n; i++){
//生成新結點
p = (LinkList)malloc(sizeof(Node));
//隨機生成100以內的數字
p->data = rand()%100+1;
//將p的指針域置爲NULL
p->next = (*L)->next;
//插入到表頭,即頭結點指向新結點
(*L)->next = p;
}
}
當然除了頭插法,還有尾插法,尾插法的實現代碼如下:
//隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(尾插法--把每次新結點都插在終端結點的後面)
void CreateListHead(LinkList *L,int n){
LinkList p,r;
int i;
//初始化隨機數種子
srand(time(0));
//新建單鏈表
*L = (LinkList)malloc(sizeof(Node));
//r爲指向尾部的結點
r = *L;
for(i = 0; i < n; i++){
//生成新結點
p = (Node*)malloc(sizeof(Node));
//隨機生成100以內的數字
p->data = rand()%100+1;
//將表尾終端結點的指針指向新結點
r->next = p;
//將當前新結點定義爲表尾終端結點
r = p;
}
//表示當前鏈表結束
r->next = NULL;
}
單鏈表的整表刪除
當我們不打算使用這個鏈表時,我們需要把它銷燬,其實也就是在內存中將它釋放掉。單鏈表整表刪除的算法思路如下:
- 聲明結點p和q
- 將第一個結點賦值給p
- 循環:
- 將下一結點賦值給q
- 釋放p
- 將q賦值給p
實現代碼算法如下:
//初始條件:順序線性表L已存在,操作結果:將L重置爲空表
Status ClearList(LinkList *L){
LinkList p,q;
p = (*L)->next;
while(p){
q = p->next;
free(p);
p = q;
}
//頭結點指針域爲空
(*L)->next = NULL;
return OK;
}
單鏈表結構與順序存儲結構優缺點
存儲分配方式 | 時間性能 | 空間性能 |
---|---|---|
順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素 ; 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素 | 1.查找時間複雜度:順序存儲結構O(1); 單鏈表O(n) 2.插入和刪除:順序存儲結構需要平均移動表長一半的元素,時間複雜度爲O(n); 單鏈表在得出某位置的指針後,插入和刪除時間僅爲O(1) | 順序存儲結構需要預分配存儲空間,分大了,浪費,分小了易發生上溢; 單鏈表不需要預分配存儲空間,只要有就可以分配,元素個數也不受限制 |
通過上面的對比,我們可以得出一些結論:
- 若線性表需要頻繁查找,很少進行插入和刪除操作時,宜採用順序存儲結構。若需要頻繁插入和刪除時,宜採用單鏈表結構。
- 當線性表中的元素個數變化較大或者根本不知道多大時,最好用單鏈表結構,這樣就可以不需要考慮存儲空間的大小問題。而如果事先知道線性表的大致長度,比如一年12個月,一週就是週一至週日共七天,這種用順序存儲結構效率會高很多
靜態鏈表(遊標實現法–不具備指針語言的實現)
C語言具有指針能力,使得它可以非常容易地操作內存中的地址和數據,那對於那些沒有指針的高級語言要如何實現單鏈表呢?答案是數組。用數組來代替指針,來描述單鏈表,這就是靜態鏈表。具體是如何實現的呢?
首先我們讓數組的元素都是由兩個數據域組成,data和cur。數據域data用來存放數據元素,也就是通常我們要處理的數據;cur相當於單鏈表中的next指針,存放該元素的後繼在數組中的下標,簡稱遊標。
線性表的靜態鏈表存儲結構代碼實現:
//假設鏈表的最大長度爲1000.爲了方便插入數據,我們通常會把數組建立的大一些,以便有一些空閒空間可以便於插入時不至於溢出
#define MAXSIZE 1000
typedef struct{
ElemType data;
//遊標,爲0時表示無指向
int cur;
}Component,StaticLinkList[MAXSIZE];
另外我們對數組第一個和最後一個元素作爲特殊元素處理,不存數據。我們通常把未被使用的數組元素稱爲備用鏈表。而數組第一個元素,即下標爲0的元素的cur就存放備用鏈表的第一個結點的下標(即第一個未被使用的數組元素的下標);而數組的最後一個元素的cur則存放第一個有數值的元素的下標,相當於單鏈表的頭結點的作用,當整個鏈表爲空時,則爲0。
下面是初始化數組的狀態的代碼:
//將一維數組space中各分量鏈成一備用鏈表(備用鏈表:未被使用的數組元素)
//space[0].cur爲頭指針,“0”表示空指針
Status InitList(StaticLinkList space){
int i;
for(i = 0 ; i < MAXSIZE - 1 ; i++){
//初始化狀態,每一個元素的cur指向下一個座標(第一個元素和最後一個元素除外)
space[i].cur = i+1;
}
//目前靜態鏈表爲空,最後一個元素的cur爲0
space[MAXSIZE-1].cur = 0;
return OK;
}
-
靜態鏈表的插入操作
爲了辨明數組中哪些分量未被使用,解決的辦法是將所有未被使用過的及已被刪除的分量用遊標鏈成一個備用鏈表(上一面初始化的時候其實已經完成),每當進行插入時,便可以從備用鏈表上取得第一個結點作爲待插入的新結點。獲取插入結點的下標的實現代碼算法如下:
//若備用空間鏈表非空,則返回分配的結點下標,否則返回0 int Malloc_SLL(StaticLinkList space){ //當前數組第一個元素的cur存的值就是要返回的第一個備用空閒的下標 int i = space[0].cur; //非0爲true if(space[0].cur){ //由於要拿出一個分量來使用,所以我們就得把它的下一個分量拿來備用 space[0] = space[i].cur; } return i; }
靜態鏈表插入數據元素的算法思路如下:
(1)判斷插入的位序是否符合規範,不符合返回ERROR
(2)獲得空閒分量的下標,如果空閒分量下標爲0,說明已滿無法繼續插入,返回ERROR
(3)若空閒分量下標不爲0,將插入數據賦值給此分量的data
(4)找到插入位序i之前的位置,比如插入位序爲3,則之前的位置爲2
(5)把第i個元素之前的cur賦值給新元素(插入數據)的cur,並把新元素(插入數據)的下標賦值給第i個元素之前元素的cur。在上一步我們獲取到之前的位置爲2,其cur爲3,假設我們要在靜態鏈表位序爲3(注意,這裏是靜態鏈表意義上的3,在數組中其下標不一定爲3)的位置插入數據,此時我們就設置空閒分量處的cur爲3,然後原來2上面的cur設置爲空閒分量的下標
(6)返回成功下面是插入數據的代碼算法實現:
//在L中第i個元素之前插入新的數據元素 Status LinkInsert(StaticLinkList L,int i,ElemType e){ int j,k,l; //注意k首先是最後一個元素的下標 k = MAXSIZE - 1; //如果插入位置異常,直接返回ERROR if(i < 1 || i > ListLength(L) + 1){ return ERROR; } //獲得空閒分量的下標 j = Malloc_SSL(L); if(j){ //將數據賦值給此分量data L[j].data = e; //找到第i個元素之前的位置 for(l = 1; l <= i - 1; l++ ){ k = L[k].cur; } //把第i個元素之前的cur賦值給新的元素的cur L[j].cur = L[k].cur; //把新元素的下標賦值給第i個元素之前元素的cur L[k].cur = j; return OK; } return ERROR; }
-
靜態鏈表的刪除操作
靜態鏈表刪除數據元素的算法思路如下:
(1)判斷刪除的位序是否符合規範,不符合返回ERROR
(2)找到第i個元素之前的位置
(3)獲取到要刪除元素所在位置的下標
(4)將要刪除要素的cur賦值給第i個元素之前元素的cur
(5)釋放空間
(6)返回成功下面是刪除數據的代碼算法實現:
//刪除在L中第i個數據元素e Status ListDelete(StaticLinkList L,int i){ int j,k; //如果刪除位置異常,直接返回ERROR if(i < 1 || i > ListLength(L)){ return ERROR; } //注意k首先是最後一個元素的下標 k = MAXSIZE - 1; //找到第i個元素之前的位置 for(j = 1; j <= i - 1; j++ ){ k = L[k].cur; } //獲取到要刪除元素所在位置的下標 j = L[k].cur; //將要刪除要素的cur賦值給第i個元素之前元素的cur L[k].cur = L[j].cur; //釋放空間 Free_SSL(L,j) return OK; }
下面是釋放空間,即回收結點到備用鏈表的代碼實現:
void Free_SSL(StaicLinkList space,int k){
//把第一個元素cur值賦值給要刪除的分量cur
space[k].cur = space[0].cur;
//把要刪除的分量下標賦值給第一個元素的cur
space[0].cur = k;
}
當然靜態鏈表也有相應的其他操作的相關實現。可以根據自己需求去實現。
靜態鏈表的優缺點
優點:
- 在插入和刪除操作時,只需要修改遊標,不需要移動元素,從而改進了在順序存儲結構中插入和刪除操作需要移動大量元素的缺點
缺點:
- 沒有解決連續存儲分配帶來的表長難以確定的問題
- 失去了順序存儲結構隨機存取的特性,即隨機存取的時間複雜度不再是O(1)。因爲當前讀取一個數據元素,需要通過遍歷來讀取,而不是之前的直接通過計算就可以
循環鏈表
當我們需要從單鏈表中間的某個結點出發,訪問到鏈表的全部結點,該如何操作呢?答案是循環鏈表。
循環鏈表:將單鏈表中終端結點的指針端由空指針改爲指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表就稱爲單循環鏈表,簡稱循環鏈表。
在單鏈表中,我們有了頭結點時,我們可以用O(1)的時間訪問第一個結點,但對於要訪問到最後一個結點,卻需要O(n),因爲我們需要將單鏈表全部掃描一遍。但是如果我們採用尾指針(指向終端結點的指針爲尾指針),就可以實現用O(1)的時間訪問到最後一個結點。
雙向鏈表
我們在單鏈表中,有了next指針,這就使得我們要查找下一結點的時間複雜度爲O(1)。可如果我們要查找的是上一結點的話,那最壞的時間複雜度就是O(n)了,因爲我們每次都要從頭開始遍歷查找。
爲了克服單向性這一缺點,提出了雙向鏈表的概念:**雙向鏈表是在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。**所以在雙向鏈表中的結點都有兩個指針域,一個指向直接後繼,另一個指向直接前驅。下面是雙向鏈表的結構:
typedef struct DulNode{
ElemType data;
//直接前驅指針
struct DulNode *prior;
//直接後繼指針
struct DulNode *next;
}DulNode,*DuLinkList;