線性表(中)之鏈式存儲

一、線性表的鏈式存儲結構
前面所講的線性表的順序存儲結構是有缺點的,最大的缺點就是插入和刪除時需要移動大量的元素,這顯然就需要耗費大量時間。
仔細考慮一下產生該問題的原因,在於相鄰元素的存儲位置也具有鄰居關係,它們在內存中是緊挨着的,沒有空隙,自然也沒有空位進行介入,而刪除後留下的空隙自然也需要彌補。
爲了解決上述問題,我們打破常規,不再讓相鄰元素在內存中緊挨着,而是上一個元素留存下一個元素的“線索”,這樣我們找到第一個元素時,根據“線索”自然而然就找到下一個元素的位置;依次類推,通過遍歷的方法每一個元素的位置都可以通過遍歷找到。
1、線性表的鏈式存儲定義
定義:節點(或譯爲“結點”)(Node):爲了表示每個數據元素ai與其後續數據元素ai+1之間的邏輯關係,對數據ai來說,除了存儲本身的數據信息之外,還需要存儲一個指示其直接後繼信息(即直接後繼的存儲位置)。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱作指針或鏈。這兩部分信息組成數據元素ai的存儲映像,稱爲節點(Node)。
定義:單鏈表:n個節點(ai的存儲映像)鏈接成一個鏈表,即爲線性表的鏈式存儲結構。因爲此鏈表的每個節點中只包含一個指針域,所以叫單鏈表。
定義:頭指針:我們把鏈表中第一個節點的存儲位置叫做頭指針,整個單鏈表的存儲就必須從頭指針開始進行。
但是,單純使用頭指針無法區分一個單鏈表是否爲空 還是 一個單鏈表不存在。因爲二者從頭指針的角度來說,都是指針爲空,而單鏈表爲空和單鏈表不存在是兩種完全不同的概念。
爲了解決這個問題,我們引入頭結點head的概念。頭結點即單鏈表的第一個節點,該節點不存儲任何有效數據,實際鏈表的起點是頭結點的後繼節點。
當頭結點的後繼節點爲空,即
head->next==NULL
時,此時我們判定該鏈表爲空鏈表。而當頭結點head不存在時,此時我們判定該單鏈表不存在。

2、線性表的鏈式存儲結構代碼描述
單鏈表的存儲結構的C語言描述:
typedef struct Node
{
data_t data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
由代碼我們可以看出,節點是由存放數據元素的數據域和存放後繼節點的指針域組成的。假設指針p是指向第i個元素ai的指針,則p->data表示ai的數據域,p->next表示ai的指針域。p->next指向下一個元素,即ai+1,也就是說,p->data=ai,p->next->data=ai+1。
1)單鏈表的整表創建
在順序表中,順序表的創建其實就是一個數組的初始化過程。而單鏈表和順序表的存儲結構不同,它不能像順序表一樣整體集中地操作數據,而且單鏈表是一種動態結構。所以創建單鏈表的過程實際上是將一個“空表”動態依次建立各元素節點並逐步插入到鏈表中的過程。
單鏈表的整表創建的算法思路(頭插法):
⒈聲明指針p和計數器i
⒉初始化一個空鏈表頭結點head
⒊讓head的結點指針指向NULL,即建立一個帶頭結點的空單鏈表
⒋循環以下過程:
⒋⒈通過指針p生成新節點
⒋⒉新節點獲得數據,即p->data=數據
⒋⒊將p插入到頭結點與前一新節點之間
//代碼見附錄
以上代碼裏,我們讓新生成的節點始終處在第一個位置,我們把這種方法稱爲“頭插法”。
實際上,按照日常生活中“先來後到”的思想,新生成的節點應當插入到當前鏈表的尾部。若採用這種方法創建單鏈表,我們稱爲“尾插法”。
尾插法的算法思路基本等同於頭插法,只需將上文的頭插法的算法
⒋⒊將p插入到頭結點與前一新節點之間
改爲:
⒋⒊將p插入到當前鏈表的尾節點之後
即可。
//代碼見附錄
注意代碼中L與r的關係,L是指向整個單鏈表,而r是指向當前鏈表的尾節點。L不會隨着循環變換位置,而r會隨着循環實時變換位置。
2)單鏈表的整表刪除
當我們不打算使用一個鏈表時,我們應當對其進行銷燬,也就是在內存中釋放這個鏈表,以便留出空間供其他程序使用。
單鏈表的整表刪除的算法思路:
⒈聲明指針p和q;
⒉將鏈表的第一個節點賦值給p
⒊循環以下過程:
⒊⒈將p的下一個節點賦值給q
⒊⒉釋放p
⒊⒊將q賦值給p
//代碼見附錄
注意代碼中指針q的作用,q的作用是引導指針p,若無指針q的話,在執行free(p)語句之後,指針p就無法找到其下一個節點p->next的位置了,因爲該節點的指針域已經隨節點一併釋放了。

3)單鏈表的讀取
在線性表的順序存儲結構中,我們要通過任意位置讀取元素的值是十分方便容易的,但在單鏈表中,對於第i個元素具體在哪無法一開始就得知,必須從頭指針開始尋找。因此對於單鏈表的讀取第i個元素的操作在算法上相對要麻煩很多。
獲得單鏈表第i個元素的算法:
⒈定義一個指針指向鏈表的第一個節點。初始化循環變量j
⒉當j<i時,不斷讓指針p向後移動
⒊若到鏈表結尾p爲空,則說明第i個節點不存在,返回錯誤
⒋當p移動到i位置成功時,返回節點p的數據
//代碼見附錄
因爲該鏈表的時間複雜度取決於位置i,因此該算法的時間複雜度爲O(n)。
因爲單鏈表結構定義時沒有定義表長,所以無法事先獲知循環次數,因此不推薦使用for循環。該算法的主要核心是“當前工作指針”,這其實也是多數關於鏈表算法的核心思想。
4)單鏈表的插入與刪除
單鏈表的插入與刪除操作是單鏈表的優勢之一。插入和刪除操作無需像線性表的順序存儲結構一樣,插入或刪除一個節點需要影響到衆多其他節點。
假設在單鏈表中,待插入節點的指針爲s,s節點的前驅節點爲p,則插入操作只需2步即可:
s->next=p->next;p->next=s;
但是注意,這兩句順序不可交換。
如果先讓p->next=s;那麼下一句s->next=p->next就相當於s->next=s,這樣新加入節點s就無法接入它的後繼節點了,“臨場掉鏈子”。
單鏈表的第i個數據位置插入節點的算法思路:
⒈聲明一個指針p指向鏈表頭結點,初始化j從1開始
⒉當j<i時,遍歷鏈表,即指針p不斷向後移動
⒊若到鏈表末尾p爲空,則位置i不存在
⒋p找到第i個位置,生成待插入空結點s
⒌將數據元素e賦值給s->data
⒍執行單鏈表的插入語句:s->next=p->next;p->next=s;
⒎返回成功
//代碼見附錄

接下來我們來看單鏈表的刪除。假設第i個位置節點爲q,它的前驅節點是p,現在要刪除節點q,其實只需將q的前驅節點p繞過q節點指向q的後繼節點即可:
q=p->next;p->next=q->next;
單鏈表的第i個數據位置刪除節點的算法思路:
⒈聲明一個指針p指向鏈表頭結點,初始化j從1開始
⒉當j<i時,遍歷鏈表,即指針p不斷向後移動
⒊若到鏈表末尾p爲空,則說明第i個位置的節點不存在
⒋p指向q的前驅節點,即q==p->next
⒌執行單鏈表的刪除語句:p->next=q->next;
⒍將q節點中數據取出作爲結果
⒎釋放q節點
⒏返回成功
//代碼見附錄
分析單鏈表的插入和刪除代碼,我們可以發現,它們的算法其實都是由兩部分組成:第一部分是遍歷查找第i個節點,第二部分是對它進行相應的操作。而且我們可以看出,對於第i個節點的操作不會影響到其他位置的節點,這也是單鏈表比順序表優勢的地方。顯然,對於插入/刪除比較頻繁的操作,單鏈表的效率要明顯高於順序表。

3、順序表與單鏈表的優缺點
//見附圖2
通過對比,我們可以得出一些結論:
⒈若該線性表需要頻繁進行查找操作,而很少進行插入/刪除操作時,我們推薦採用順序表存儲。而若該線性表需要頻繁進行插入/刪除操作時,我們推薦使用單鏈表。例如在一款遊戲中,玩家個人信息除註冊時涉及到插入數據外,一般不會發生大的改變,因此我們使用順序表存儲。而玩家的裝備列表則會隨着玩家的遊戲而發生改變,即隨時會發生插入/刪除操作,這時使用順序表就不太合適,而應採用單鏈表。
⒉當線性表中的元素數量變化較大,或無法事先預製數目時,我們推薦使用單鏈表,這樣可以無需考慮存儲空間大小分配的問題。反之,若我們已事先知道了數據規模(例如1年有12月,1星期有7天等情況),則我們推薦使用順序表,這樣存儲效率會高很多。
總之,順序表與單鏈表各有其優缺點,需要在實際情況中權衡需要使用哪種方式。
練習1:單鏈表反序
已有單鏈表L,編寫函數使得單鏈表的元素反序存儲。
提示:函數原型:int ListReverse(LinkList L)
//代碼見附錄
練習2:已有單鏈表L存放的數據類型爲整型,其頭結點爲head,編寫函數,求單鏈表中相鄰兩點數據data之和爲最大的一對節點的第一節點的指針。
提示:函數原型:LinkList Adjmax(LinkList h)
//代碼見附錄

二、循環鏈表
若將單鏈表的尾節點的指針由空改成指向頭結點,則將整個單鏈表形成了一個環,這種頭尾相接的單鏈表稱爲單循環鏈表,簡稱循環鏈表(Circular linked list)。
循環鏈表解決了一個單鏈表存在的很麻煩的問題:如何從鏈表的任意節點出發,訪問到鏈表的全部節點。
同單鏈表一樣,爲了解決單循環鏈表的空表與非空表操作一致問題,我們通常會設置一個頭結點,該頭結點裏不存任何數據(或只存其他無關數據)。這樣對於循環的判斷結束條件就從p->next是否爲空,變成了p->next是否爲頭結點。
//代碼見附錄
練習:使用單項循環鏈表求解約瑟夫環問題,其中人數n爲33人,每逢m=7人槍斃一人,起始位置爲第k=1個人
/***********約瑟夫環問題描述******************/
約瑟夫入獄,監獄內共有n=33個犯人。某日33名犯人圍成一圈,從第k=1個犯人開始報數,報到數字m=7的犯人出列,被槍斃,下一名犯人重新從1開始報數。依次類推,直至剩下最後1名犯人可被赦免。聰明的約瑟夫在心裏稍加計算,算出了最後槍斃的位置,他站在這個位置,最終避免了自己被槍斃,逃出昇天。
問:約瑟夫算出的是哪個位置?
/***********約瑟夫環問題描述end***************/
//代碼見附錄

三、雙向鏈表
我們在單鏈表中,有了next指針,它使得我們要查找某節點的下一個節點的時間複雜度爲O(1)。可是若要查找某節點的上一個節點,那時間複雜度就是O(n)了。因爲我們每次要查找某節點的上一個節點,必須從頭指針開始遍歷查找。
爲了克服單向性這一缺陷,我們設計出了雙向鏈表。雙向鏈表(double linked list)是在單鏈表的每個節點中,再設置一個指向其前驅節點的指針域。所以在雙向鏈表的每個節點都有兩個指針域,一個指向直接後繼,另一個指向直接前驅
/*雙向鏈表的存儲結構*/
typedef struct DulNode
{
data_t data;
struct DulNode *prior;
struct DulNode *next;
}DulNode,*DuLinkList;
由於雙向鏈表的節點指針有兩個,那麼對於某節點p,它的後繼的前驅是其本身,它的前驅的後繼也是其本身,即:
p->next->prior = p = p->prior->next
因爲雙向鏈表是單鏈表擴展出來的結構,因此它的很多操作與單鏈表是基本相同的。比如獲得位置i的節點的數據、遍歷打印整個鏈表、求表長等操作,這些操作都只涉及到一個方向的指針(prior或next),另一個方向的指針基本無用,因此操作與單鏈表本身並無區別。
對於雙向鏈表的插入/刪除操作,需要更改兩個指針變量,因此雙向鏈表的插入/刪除操作需要注意兩個指針變量的操作順序。
雙向鏈表的插入操作:向節點p與p->next之間插入節點s
//見附圖3
注意4步的順序不能錯:
①s->prior = p;
②s->next = p->next;
③p->next->prior = s;
④p->next = s;
由於第二步與第三步都涉及到p->next,如果先行執行第四步的話,會使得p->next節點提前變成s,使得後續的工作無法完成。所以,實現雙向鏈表的插入操作的順序是:先解決s的前驅和後繼,再解決後節點的前驅,最後解決前節點的後繼。
//代碼見附錄
雙向鏈表的刪除操作比較簡單,若要刪除節點p,只需兩個步驟即可:
//見附圖3
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
//代碼見附錄
對於單鏈表來說,雙向鏈表要複雜一些,對於插入和刪除操作要注意其操作順序。另外每個節點都使用了額外的存儲空間來存儲前驅節點。但是雙向鏈表有良好的對稱性,而且對某節點的前驅節點操作要方便許多。
線性表之鏈式存儲(單鏈表)
//注意:該文件操作的單鏈表爲帶頭結點單鏈表,頭結點數據無效
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define OK 1
#define ERROR 0

typedef int data_t;
typedef struct Node
{
data_t data;
struct Node *next;
}Node;
typedef struct Node *LinkList;

int GetElem(LinkList L,int i,data_t *data)//讀取單鏈表的第i個元素
{
int j;
LinkList p;//工作指針
p = L->next;
j = 1;
while(p && j<i)
{
p = p->next;//讓p指向下一個節點
j++;
}
if(!p)
{
printf("%d position is error\n",i);
return ERROR;
}
*data = p->data;
return OK;
}

int ListInsert(LinkList L,int i,data_t e)//插入新節點,使其成爲第i個節點
{
int j;
LinkList p,s;
p=L;
j=1;
while(p && j<i)//尋找i的位置
{
p=p->next;
j++;
}
if(!p)//說明p爲NULL,即沒有第i個節點,位置無效
{
printf("%d position is error\n",i);
return ERROR;
}
//若if沒有執行則證明位置有效,可以插入數據
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}

int ListDelete(LinkList L,int i,data_t *e)//刪除第i個位置節點,數據由e獲得
{
int j;
LinkList p,q;
if(L->next==NULL)
{
printf("LinkList is Empty!\n");
return ERROR;
}
p=L;
j=1;
while(p->next && j<i)
{
p=p->next;
j++;
}
if(!(p->next))
{
printf("%d position is error\n",i);
return ERROR;
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}

LinkList CreateEmptyLinklist()//創建一個空表,空表只有頭結點
{
LinkList p;
p = (LinkList)malloc(sizeof(Node));
if(p==NULL)
{
perror("CreateEmptyLinkList error");
exit(0);
}
p->data=-255;//表示無效數據
p->next=NULL;
return p;
}

LinkList CreateListHead(LinkList L,int n)//創建鏈表(頭插法)
{
LinkList p;
int i;
srand(time(NULL));//初始化隨機數種子
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
p->next = L->next;
L->next = p;
}
return L;
}

LinkList CreateListTail(LinkList L,int n)//創建鏈表(尾插法)
{
LinkList p,r;
int i;
srand(time(NULL));
r = L;
for(i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;//鏈表封尾
return L;
}

int ClearList(LinkList L)//清空鏈表
{
LinkList p,q;
p=L->next;
while(p)
{
q=p->next;
free(p);
p=q;
}
L->next=NULL;
return OK;
}
int PrintList(LinkList L)//遍歷打印整個鏈表
{
LinkList p=L->next;
while(p)
{
printf("%d\t",p->data);
p=p->next;
}
printf("\n");
return OK;
}

int ListReverse(LinkList L)//練習1:單鏈表反序
{
if(!L)
{
printf("LinkList is not exist\n");
return ERROR;
}
LinkList p,q;
p=L->next;
L->next=NULL;
while(p!=NULL)
{
q=p;
p=p->next;
q->next=L->next;
L->next=q;
}
return OK;
}

LinkList Adjmax(LinkList h)//練習2:尋找最大元素對
{
LinkList p, p1, q;
int m0, m1;
p = h->next;
p1 = p;
if(p1 == NULL)
return p1; //表空返回
q = p->next;
if(q == NULL)
return p1; //表長=1時的返回
m0 = p->data + q->data; //相鄰兩結點data值之和
while (q->next != NULL)
{
p = q;
q = q->next; //取下一對相鄰結點的指針
m1 = p->data + q->data;
if(m1 > m0)
{
p1 = p;
m0 = m1;
}
}//取和爲最大的第一結點指針
return p1;
}
int main()
{
/*
LinkList head1,head2;
int i;
data_t data=12;
head1=CreateEmptyLinklist();
head2=CreateEmptyLinklist();
printf("head1\n");
head1=CreateListHead(head1,15);
PrintList(head1);
printf("head2\n");
head2=CreateListTail(head2,15);
PrintList(head2);
scanf("%d",&i);
printf("Insert head1 %d position, data is %d\n",i,data);
ListInsert(head1,i,data);
PrintList(head1);
scanf("%d",&i);
ListDelete(head1,i,&data);
printf("Delete head1 %d position, data is %d\n",i,data);
PrintList(head1);
LinkList adjmax = Adjmax(head1);
printf("Adjmax data is %d, Adjmax data next data is %d\n",adjmax->data,adjmax->next->data);
ListReverse(head1);
printf("Reserve head1:\n");
PrintList(head1);
if(ClearList(head1)==OK)
{
printf("head1 Clear success\n");
}
if(ClearList(head2)==OK)
{
printf("head2 Clear success\n");
}
*/
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章