資料結構- -線性表- -單鏈表
順便提一下,之前說的順序儲存結構由於其兩個元素在物理位置上相鄰,故它的優點是很方便存取,但與此帶來的缺點是如果進行插入或者刪除操作的話,需要移動大量的元素,在工程量大的時候,很耗時間。鏈式儲存結構不要求元素的物理位置相鄰,不存在順序儲存的弱點,但也同時失去順序表可隨機存取的優點。
對於連結串列的資料元素,它除了要儲存本身的資訊外,還要儲存一個能夠指示其直接後繼的資訊(即指向後繼的儲存位置),邏輯上稱這部分資料元素為結點,結點包括兩個域:資料域,主要儲存本身的資料元素資訊。指標域,是用來儲存其直接後繼的儲存位置,這部分資訊稱作指標。連結串列一般分為:單鏈表(結點只有一個指標域的連結串列)、雙鏈表(有兩個指標域的連結串列)、多連結串列(有多個指標域的連結串列)、迴圈連結串列(首尾相接的連結串列),一般都是說的單鏈表。
單鏈表的特點:
- 資料元素的邏輯順序和物理順序不一定相同。
- 在查詢資料元素時,必須從頭指標開始依次查詢,表尾的指標域指向NULL。
- 在特定的資料元素之後插入或刪除元素,不需要移動資料元素,因此時間複雜度為 O(1)。
- 儲存空間不連續,資料元素之間使用指標相連,每個資料元素只能訪問周圍的一個元素。
- 長度不固定,可以任意增刪。
單鏈表的存取必須從頭指標開始進行,頭指標表示指向連結串列中第一個結點(頭結點或者首元結點)的指標,是一個具體的地址,同時,由於最後一個數據元素沒有直接後繼,連結串列最後一個結點的指標應設為“空”(NULL),其長度變化較大,主要用於插入和刪除操作。
為了增加可讀性,一般我們在單鏈表的第一個結點前附設一個結點,稱作頭結點,頭結點的資料域可以不存任何資訊,也可以儲存如線性表的長度等類的附加資訊,頭結點的指標域儲存指向第一個結點的指標(即第一個資料元素的儲存位置)。首元結點是指連結串列中儲存單鏈表第一個資料元素的結點。
其連結串列的儲存結構為:
typedef int Status;
typedef int ElemType;
typedef struct LNode{
ElemType data ;
struct LNode *next ;
}LNode,*LinkList;
初始化,為頭結點構造一個空間。
Status InitList(LinkList &L){
L = (LinkList)malloc(sizeof(LNode));
if(!L)
exit(OVERFLOW) ;
L->next=NULL ;
}
清空連結串列,前提是連結串列已存在(後續操作均在此基礎上進行)。
Status ClearList(LinkList &L){
if(L==NULL){
cout<<"該連結串列不存在。"<<endl;
return TRUE;
}
LinkList p ,q ;
p=L->next;
while(p!=NULL){
q=p ;
p=p->next;
free(q);
}
L->next=NULL;
//cout<<"連結串列已被清空。"<<endl;
return TRUE ;
}
銷燬連結串列,和清空不同的是注意要把頭結點一塊銷燬。
Status DestroyList(LinkList &L){
LinkList p,q ;
p=L;
while(p){
q=p->next;
free(p);
p=q;
}
L=NULL ;
return TRUE ;
}
判斷連結串列是否為空,是則TRUE,否便FALSE
Status ListEmpty(LinkList L){
if(L->next==NULL){
cout<<"連結串列是空的。"<<endl;
return TRUE ;
}
else{
cout<<"連結串列中尚有資料元素,不是空表。"<<endl;
return FALSE ;
}
}
獲取長度:由於連結串列中兩個相鄰元素在物理位置上不相鄰,在獲取連結串列的長度時需要從頭指標開始出發(包括後邊的獲取元素位置、插入和刪除)
int ListLength(LinkList L){
if(L->next==NULL){
// cout<<"連結串列是空表。"<<endl;
return FALSE ;
}
LinkList p ;
int i=0 ;
p=L->next;
while(p!=NULL){
i++;
p=p->next ;
}
return i ;
}
按位置查詢後返回其值
Status GetElem(LinkList L,int i,ElemType &e){
LinkList p ,q;
int j=1 ;
p=L->next;
while((p->next!=NULL)&&(j<i)){
p=p->next;
j++;
}
if(!p || j>i ){
cout<<"查詢不符合要求"<<endl;
return FALSE ;
}
e = p->data ;
return TRUE ;
}
此處寫的是返回連結串列中第一個和元素e相等元素的位置。完整的應該同之前順序表中的那個LocateElem函式功能一樣,形參多設有一比較引數,判斷連結串列中第一個和所傳入形參滿足比較引數關係的資料元素的位置。
int LocateElem(LinkList L,ElemType e){
LinkList p ;
int i=1 ;
p=L->next ;
while((p->next!=NULL)&&(p->data!=e)){
p=p->next;
i++;
}
if(p->next==NULL){
cout<<"連結串列中未找到所查詢的元素。"<<endl;
return FALSE;
}
return i;
}
找所查元素的前驅
Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e){
LinkList p ,q;
p=L->next;
q=NULL;
while(p&&(p->data!=cur_e)){
q=p;
p=p->next;
}
if(q==NULL){
cout<<cur_e<<"的前驅不存在。"<<endl;
return FALSE ;
}
pre_e=q->data ;
cout<<"其前驅是: "<<pre_e <<endl;
return TRUE;
}
找所查元素的後繼
Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e){
LinkList p ;
p=L->next;
while(p&&(p->data!=cur_e))
p=p->next;
if(p->next==NULL){
cout<<cur_e<<"的後繼不存在。"<<endl;
return FALSE ;
}
next_e=p->next->data ;
cout<<"其後繼是: "<<next_e <<endl;
return TRUE ;
}
資料元素的插入
Status ListInsert(LinkList &L,int i,ElemType e){
LinkList p ;
LinkList s ;
int j=0;
p=L; //如果在連結串列第一個位置插入元素的話,p應該表示頭結點。
while(p&&j<i-1){
p=p->next;
j++;
}
// q->next->data=e;
// q->next->next=p;
if(!p || j > i-1){
cout<<"連結串列中找不到所求的元素。"<<endl;
return FALSE;
}
s=(LinkList)malloc(sizeof(LNode));
if(!s)
exit(OVERFLOW);
s->data=e;
s->next=p->next;
p->next=s ;
return TRUE ;
}
資料元素的刪除
Status ListDelete(LinkList L,int i,ElemType &e){
LinkList p ,q ;
int j=0;
p=L;
while(p&&j<i){
j++;
q=p;
p=p->next;
}
if(q->next==NULL||j>i){
cout<<"表中不存在。"<<endl;
return FALSE;
}
q->next=p->next;
e=p->data;
free(p);
return TRUE ;
}
在主函式中的實現,一般來說,寫完一段程式後,編譯沒毛病不表示寫的就是對的,或許是你的演算法出了問題,所以在主函式中將一些具體值實現是一個非常有必要的過程,我寫這個連結串列的時候就是一下全把功能寫完了,然後中間某些演算法出了問題,改起來得一步一步再看,演算法出錯需要重新再考慮自己之前想的情況是不是漏了什麼,這個程式設計思想是很重要的。我有個朋友,他是喜歡寫一小部分就在主函式中執行看結果,雖然很慢,但這樣不容易出錯,或者就算出錯也方便修改,這也是一種比較好的習慣,但如果對自己程式設計能力自信的話,那按照自己習慣來。下面是我的一些實現。
int main(){
int i ,l ,j,k;
ElemType e ,cur_e , pre_e , next_e ;
LinkList L ;
InitList(L);
cout<<"當前長度值:"<<ListLength(L)<<endl;
ListEmpty(L);
cout<<"設定連結串列的長度值:";
cin>>i;
cout<<"依次輸入需要的連結串列值:"<<endl;
// 1 2 3 4 5
for(int j=1;j<=i;j++){
cin>>l;
ListInsert(L,j,l);
}
// ListInsert(L,1,1);
// ListInsert(L,2,2);
// ListInsert(L,3,3);
// ListInsert(L,4,4);
// ListInsert(L,5,5);
cout<<"將連結串列輸出顯示: ";
ListTraverse(L);cout<<endl;
cout<<"當前長度值:"<<ListLength(L)<<endl;
// ClearList(L);
// ListEmpty(L);
cout<<"輸入一個查詢元素: ";
cin>>cur_e ;
PriorElem(L,cur_e,pre_e);
cout<<"輸入一個查詢元素: ";
cin>>cur_e ;
NextElem(L,cur_e,next_e);
//cout<<"其後繼是: "<<next_e <<endl;
//在寫的過程中發現這一處加錯位置了,在實現的結果中是有問題的 ,上邊的前驅同樣
cout<<"輸入一個查詢位置i:";
cin>>i ;
cout<<"獲取連結串列中第i個位置的資料元素,用e將其輸出: ";
GetElem(L,i,e);
cout<<"e=" << e <<endl;
cout<<"輸入一個插入位置i:";
cin>>i ;
cout<<"對連結串列這個位置進行插入一個元素:";
cin>>j;
ListInsert(L,i,j);
cout<<"將連結串列再次輸出顯示: ";
ListTraverse(L);cout<<endl;
cout<<"輸入一個刪除位置i:";
cin>>i ;
ListDelete(L,i,k);
cout<<"將連結串列再次輸出顯示: ";
ListTraverse(L);cout<<endl;
cout<<"輸出連結串列中和3相等元素的位置:"<<LocateElem(L,3)<<endl;
}
一般來說,這幾個功能比較常用,也易容易實現,學習資料結構的話最好能自己基本上完全寫出來,當然有實在不懂的可以去看一下別人是怎麼寫的,畢竟每個人的思維都不一樣,多一種思路也就多一點見識,挺好。
至於下面有另外兩個關於連結串列的功能函式,逆位序輸入和連結串列的合併。
逆位序輸入是一個從表尾到表頭逆向建立單鏈表的演算法。
void CreateList(LinkList &L,int n){ //逆位序輸入。
LinkList p;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
for(int i=n ; i>0 ; i--){
p = (LinkList)malloc(sizeof(LNode)) ; //新建一個結點。
cin>>p->data ;
p->next=L->next ;
L->next=p; // 插入到表頭
}
}
兩個連結串列的合併。需注意的前提是這兩個連結串列內的元素都按照元素值非遞減的順序排列。對於合併後的表Lc也是按照值得非遞減順序排列。
void MergerList(LinkList &La,LinkList &Lb,LinkList &Lc){
LinkList pa , pb , pc ;
pa=La->next;
pb=Lb->next;
pc=Lc; // 這一處可以用兩個表中任一頭結點作為表Lc的頭結點 Lc=pc=La
while(pa&&pb){
if(pa->data<=pb->data){
pc->next=pa;
pc=pa; //pc永遠指向Lc連結串列當前最後一個結點。
pa=pa->next;
}
else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
if(pa)
pc->next=pa;
else if(pb)
pc->next=pb;
free(La);
free(Lb);
//有的教材上可以這樣表示:
//pc->next = pa?pb:pb ; 用來插入剩餘段。
}
時間效率分析:
- 查詢:查詢時要從頭指標找起,查詢的時間複雜度為 O(n)。
- 插入和刪除 因線性連結串列不需要移動元素,只要修改指標,一般情況下時間複雜度為 O(1)。但是,如果要在單鏈表中進行前插或刪除操作,由要從頭查詢前驅結點,所耗時間複雜度為 O(n)。
空間效率分析:
連結串列中每個結點都要增加一個指標空間,相當於總共增加了n 個整型變數,空間複雜度為 O(n)。