1. 鏈表的定義
線性表的鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)的特點(diǎn)是用一組任意的存儲(chǔ)單元存儲(chǔ)線性表的數(shù)據(jù)元素,這組存儲(chǔ)單元可以存在內(nèi)存中未被占用的任意位置。
(比起順序存儲(chǔ)結(jié)構(gòu)每個(gè)數(shù)據(jù)元素只需要存儲(chǔ)一個(gè)位置就可以了。現(xiàn)在鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)中,除了要存儲(chǔ)數(shù)據(jù)元素信息外,還要存儲(chǔ)它的后繼元素的存儲(chǔ)地址(指針)。也就是說除了存儲(chǔ)其本身的信息外,還需存儲(chǔ)一個(gè)指示其直接后繼的存儲(chǔ)位置的信息。)
我們把存儲(chǔ)數(shù)據(jù)元素信息的域稱為數(shù)據(jù)域,把存儲(chǔ)直接后繼位置的域稱為指針域。指針域中存儲(chǔ)的信息稱為指針或鏈。這兩部分信息組成數(shù)據(jù)元素稱為存儲(chǔ)映像,稱為結(jié)點(diǎn) (Node) 。
n 個(gè)結(jié)點(diǎn)鏈接成一個(gè)鏈表,即為線性表 (a1, a2,a3, ..., an) 的鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)。因?yàn)榇随湵淼拿總€(gè)結(jié)點(diǎn)中只包含一個(gè)指針域,所以叫做單鏈表。見如下圖:
2. 頭指針和頭結(jié)點(diǎn)
2.1 頭指針:
1)頭指針是指鏈表指向第一個(gè)結(jié)點(diǎn)的指針,若鏈表有頭結(jié)點(diǎn),則是指向頭結(jié)點(diǎn)的指針。
2)頭指針具有標(biāo)識(shí)作用,所以常用頭指針冠以鏈表的名字(指針變量的名字)。
3)無論鏈表是否為空,頭指針均不為空。
4)頭指針是鏈表的必要元素。
2.2 頭結(jié)點(diǎn)
1)頭結(jié)點(diǎn)是為了操作的統(tǒng)一和方便而設(shè)立的,放在第一個(gè)元素的結(jié)點(diǎn)之前,其數(shù)據(jù)域一般無意義(但也可以用來存放鏈表的長度)。
2)有了頭結(jié)點(diǎn),對(duì)在第一元素結(jié)點(diǎn)前插入結(jié)點(diǎn)和刪除第一結(jié)點(diǎn)起操作與其它結(jié)點(diǎn)的操作就統(tǒng)一了。
3)頭結(jié)點(diǎn)不一定是鏈表的必須要素。
3. 單鏈表的存儲(chǔ)結(jié)構(gòu)
3.1我們?cè)?C 語言中可以用結(jié)構(gòu)指針來描述單鏈表。見例1.
//例1
typedef struct Node
{
ElemType data; // 數(shù)據(jù)域
struct Node* Next; // 指針域
} Node;
typedef struct Node* LinkList;
(注:我們看到結(jié)點(diǎn)由存放數(shù)據(jù)元素的數(shù)據(jù)域和存放后繼結(jié)點(diǎn)地址的指針域組成。)
假設(shè) p 是指向線性表第 i 個(gè)元素的指針,則該結(jié)點(diǎn) ai 的數(shù)據(jù)域我們可以用 p->data 的值是一個(gè)數(shù)據(jù)元素,結(jié)點(diǎn) ai 的指針域可以用 p->next 來表示, p->next 的值是一個(gè)指針。那么 p->next 指向 ai+1 的指針。
4. 單鏈表的讀取
獲得鏈表第 i 個(gè)數(shù)據(jù)的算法思路:
1)聲明一個(gè)結(jié)點(diǎn) p 指向鏈表第一個(gè)結(jié)點(diǎn),初始化 j 從1 開始;
2)當(dāng) j<i 時(shí),就遍歷鏈表,讓 p 的指針向后移動(dòng),不斷指向一下結(jié)點(diǎn), j+1 ;
3)若到鏈表末尾 p 為空,則說明第 i 個(gè)元素不存在;
4)否則查找成功,返回結(jié)點(diǎn) p 的數(shù)據(jù)。
(注:由于這個(gè)算法的時(shí)間復(fù)雜度取決于 i 的位置,當(dāng)i=1 時(shí),則不需要遍歷,而 i=n 時(shí)則遍歷 n-1 次才可以。因此最壞情況的時(shí)間復(fù)雜度為 O(n) 。見例2)
//例2.
/* 初始條件:順序線性表L已存在,1<=i<=ListLength(L) /
/ 操作結(jié)果:用e返回L中第i個(gè)數(shù)據(jù)元素的值 */
Status GetElem( LinkList L, int i, ElemType *e )
{
int j;
LinkList p;
p = L->next;
j = 1;
while( p && j<i )
{
p = p->next;
++j;
}
if( !p || j>i )
{
return ERROR;
}
*e = p->data;
return OK;
}
5. 單鏈表的插入
單鏈表第 i 個(gè)數(shù)據(jù)插入結(jié)點(diǎn)的算法思路:(見例3.)
1)聲明一結(jié)點(diǎn) p 指向鏈表頭結(jié)點(diǎn),初始化 j 從 1 開始;
2)當(dāng) j<1 時(shí),就遍歷鏈表,讓 p 的指針向后移動(dòng),不斷指向下一結(jié)點(diǎn), j 累加 1 ;
3)若到鏈表末尾 p 為空,則說明第 i 個(gè)元素不存在;
4)否則查找成功,在系統(tǒng)中生成一個(gè)空結(jié)點(diǎn) s ;
5)將數(shù)據(jù)元素 e 賦值給 s->data ;
6)單鏈表的插入剛才兩個(gè)標(biāo)準(zhǔn)語句;
7)返回成功。
//例3.
/* 初始條件:順序線性表L已存在,1<=i<=ListLength(L) /
/ 操作結(jié)果:在L中第i個(gè)位置之前插入新的數(shù)據(jù)元素e,L的長度加1 /
Status ListInsert(LinkList * L, int i, ElemType e) /注:這里的LinkList表示的是“ struct Node* LinkList”*/
{
int j;
LinkList p, s;
p = *L;
j = 1;
while( p && j<i ) // 用于尋找第i個(gè)結(jié)點(diǎn)
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
(注意:必須先“s->next = p->next;”, 再“p->next = s;”。 因?yàn)槿绻葓?zhí)行“ p->next = s”的話“ p->next ”會(huì)先被覆蓋為 “s” 的地址,那么” s->next = p->next “其實(shí)就等于 “s->next = s” 了。)
6. 單鏈表的刪除
單鏈表第 i 個(gè)數(shù)據(jù)刪除結(jié)點(diǎn)的算法思路:(見例4)
1)聲明結(jié)點(diǎn) p 指向鏈表第一個(gè)結(jié)點(diǎn),初始化 j=1 ;
2)當(dāng) j<1 時(shí),就遍歷鏈表,讓 P 的指針向后移動(dòng),不斷指向下一個(gè)結(jié)點(diǎn), j 累加1 ;
3)若到鏈表末尾 p 為空,則說明第 i 個(gè)元素不存在;
4)否則查找成功,將欲刪除結(jié)點(diǎn) p->next 賦值給 q ;
5)單鏈表的刪除標(biāo)準(zhǔn)語句 p->next = q->next ;
6)將 q 結(jié)點(diǎn)中的數(shù)據(jù)賦值給 e ,作為返回;
7)釋放 q 結(jié)點(diǎn)。
//例4
/* 初始條件:順序線性表L已存在,1<=i<=ListLength(L) /
/ 操作結(jié)果:刪除L的第i個(gè)數(shù)據(jù)元素,并用e返回其值,L的長度-1 */
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while( p->next && j<i )
{
p = p->next;
++j;
}
if( !(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
7. 單鏈表的整表創(chuàng)建
7.1 概念:鏈表的各個(gè)元素分布在在內(nèi)存各個(gè)角落的,他的增長也是動(dòng)態(tài)的。對(duì)于鏈表來說,它所占用空間的大小和位置是不需要預(yù)先分配劃定的,可以根據(jù)系統(tǒng)的情況和實(shí)際的需求即時(shí)生成。
創(chuàng)建單鏈表的過程是一個(gè)動(dòng)態(tài)生成鏈表的過程,從“空表”的初始狀態(tài)起,依次建立各元素結(jié)點(diǎn)并逐個(gè)插入鏈表。所以單鏈表整表創(chuàng)建的算法思路如下:
1)聲明一結(jié)點(diǎn) p 和計(jì)數(shù)器變量 i ;
2)初始化一空鏈表 L ;
3)讓 L 的頭結(jié)點(diǎn)的指針指向 NULL ,即建立一個(gè)帶頭結(jié)點(diǎn)的單鏈表;
4)循環(huán)實(shí)現(xiàn)后繼結(jié)點(diǎn)的賦值和插入。
7.2 單鏈表創(chuàng)建
單鏈表整表創(chuàng)建有兩種方式:1)頭插法;2)尾插法
1)頭插法
頭插法從一個(gè)空表開始,生成新結(jié)點(diǎn),讀取數(shù)據(jù)存放到新結(jié)點(diǎn)的數(shù)據(jù)域中,然后將新結(jié)點(diǎn)插入到當(dāng)前鏈表的表頭上,直到結(jié)束為止。簡單來說,就是把新加進(jìn)的元素放在表頭后的第一個(gè)位置:(見例5)
(1)先讓新節(jié)點(diǎn)的 next 指向頭節(jié)點(diǎn)的next;(例5還提供一個(gè)版本是沒有頭結(jié)點(diǎn),即第一個(gè)節(jié)點(diǎn)即為數(shù)據(jù)節(jié)點(diǎn))
(2)然后讓頭節(jié)點(diǎn)的 next 指向新節(jié)點(diǎn)。(沒有頭結(jié)點(diǎn)的話,頭指針直接指向最后一個(gè)插入的數(shù)據(jù)節(jié)點(diǎn)。)
//例5
/#include<stdio.h>
/#include<stdlib.h>
typedef void Status;
typedef int Elemtype;
typedef struct node
{
Elemtype data;
struct node *next;
}Node;
Status Visit(Node *node)
{
if(node)
{
printf("%d", node->data);
}
}
Status Traversal(Node **node)
{
Node *target;
target = *node;
while(target)
{
Visit(target);
target = target->next;
}
}
Status InitLinkList(Node * * node)
{
*node = (Node )malloc(sizeof(Node));
( * node)->next = NULL;
}
Status CreateLinkList(Node * * node, int i) //有頭結(jié)點(diǎn)版本
{
Node * temp;
int j;
( * node)->data = i; //頭結(jié)點(diǎn)存儲(chǔ)鏈表長度
for(j = 1; j <= i; j++)
{
(temp) = (Node * )malloc(sizeof(Node));
(temp)->data = j;
(temp)->next = (node)->next;
( * node)->next = (temp);
}
}
Status CreateLinkList2(Node **node, int i) //無頭結(jié)點(diǎn)版本
{
Node temp;
int j;
if(i >= 1)
{
( * node)->data = 1;
for(j = 2; j <= i; j++)
{
temp = (Node * )malloc(sizeof(Node));
temp->data = j;
temp->next = (node);
( * node) = temp;
}
}
else
{
printf("The LinkList is empty!");
}
}
Status Clear(Node *node)
{
Node * target, temp;
target = ( * node)->next;
while(target)
{
//temp = target->next;
//free(target);
//target = temp; //上下兩種都可以
temp = target;
target = target->next;
free(temp);
}
( * node)->next = NULL; //保留頭結(jié)點(diǎn)
}
void main()
{
Node * node;
InitLinkList(&node);
CreateLinkList(&node, 10);
Traversal(&node);
printf("\n");
//InitLinkList(&node); //可以采用初始化鏈表方式重新創(chuàng)建鏈表node
Clear(&node); //也可以采用清空原鏈表方式
CreateLinkList2(&node, 11); /無頭結(jié)點(diǎn)版本即將頭結(jié)點(diǎn)數(shù)據(jù)域直接覆蓋為首節(jié)點(diǎn)的數(shù)據(jù)。/
Traversal(&node);
}
//輸出:
10 10 9 8 7 6 5 4 3 2 1 //有頭結(jié)點(diǎn)
11 10 9 8 7 6 5 4 3 2 1 //無頭結(jié)點(diǎn)
(注意:空鏈表必須有頭結(jié)點(diǎn),因?yàn)橛泄?jié)點(diǎn)才為鏈表;而非空鏈表可以有頭結(jié)點(diǎn),也可以沒有頭結(jié)點(diǎn)。)
2)尾插法
尾插法就是將新元素按照先來后到的順序依次插入鏈表的尾部,見例6.
//例6
/#include<stdio.h>
/#include<stdlib.h>
typedef void Status;
typedef int Elemtype;
typedef struct node
{
Elemtype data;
struct node *next;
}Node;
Status Visit(Node *node)
{
if(node)
{
printf("%d ", node->data);
}
}
Status Traversal(Node **node)
{
Node *target;
target = *node;
while(target)
{
Visit(target);
target = target->next;
}
}
Status InitLinkList(Node **node)
{
*node = (Node *)malloc(sizeof(Node));
( * node)->next = NULL;
}
Status CreateLinklist(Node **node, int i) //有頭結(jié)點(diǎn)
{
Node *target, *temp;
int j;
target = *node;
target->data = i;
for(j = 1; j <= i; j++)
{
temp = (Node *)malloc(sizeof(Node));
temp->data = j;
target->next = temp;
target = target->next;
}
}
Status CreateLinklist2(Node **node, int i) //無頭結(jié)點(diǎn)
{
Node *target, *temp;
int j;
target = *node;
for(j = 1; j <= i; j++)
{
if(j == 1)
{
target->data = j; //頭結(jié)點(diǎn)直接被覆蓋為首節(jié)點(diǎn)
}
else
{
temp = (Node *)malloc(sizeof(Node));
temp->data = j;
target->next = temp;
target = target->next;
}
}
}
void main()
{
Node *node;
InitLinkList(&node);
CreateLinklist(&node, 10);
Traversal(&node);
printf("\n");
InitLinkList(&node);
CreateLinklist2(&node, 11);
Traversal(&node);
}
//輸出:
10 1 2 3 4 5 6 7 8 9 10 //有頭結(jié)點(diǎn)
1 2 3 4 5 6 7 8 9 10 11 //無頭結(jié)點(diǎn)
7. 單鏈表的整表刪除
單鏈表整表刪除的算法思路如下:(見例7.)
1)聲明結(jié)點(diǎn) p 和 q ;
2)將第一個(gè)結(jié)點(diǎn)賦值給 p ,下一結(jié)點(diǎn)賦值給 q ;
3)循環(huán)執(zhí)行釋放 p 和將 q 賦值給 p 的操作。
//例7
Status Clear(Node **node)
{
Node * p, * q;
p = ( * node)->next;
while(p)
{
q= p->next;
free(p);
p = q;
}
( * node)->next = NULL; //保留頭結(jié)點(diǎn)
}
(可見實(shí)例5中的Clear函數(shù))
8. 單鏈表結(jié)構(gòu)與順序存儲(chǔ)結(jié)構(gòu)優(yōu)缺點(diǎn)
分別從存儲(chǔ)分配方式、時(shí)間性能、空間性能三方面來做對(duì)比。
8.1 存儲(chǔ)分配方式:
1)順序存儲(chǔ)結(jié)構(gòu)用一段連續(xù)的存儲(chǔ)單元依次存儲(chǔ)線性表的數(shù)據(jù)元素。
)2)單鏈表采用鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu),用一組任意的存儲(chǔ)單元存放線性表的元素。
8.2 時(shí)間性能:
1)查找
? 順序存儲(chǔ)結(jié)構(gòu) O(1)
? 單鏈表 O(n)
2)插入和刪除
? 順序存儲(chǔ)結(jié)構(gòu)需要平均移動(dòng)表長一半的元素,時(shí)間復(fù)雜度為 O(n)
? 單鏈表在計(jì)算出某位置的指針后,插入和刪除時(shí)間僅為 O(1)
8.3 空間性能:
1)順序存儲(chǔ)結(jié)構(gòu)需要預(yù)分配存儲(chǔ)空間,分大了,容易造成空間浪費(fèi),分小了,容易發(fā)生溢出。
2)單鏈表不需要分配存儲(chǔ)空間,只要有就可以分配,元素個(gè)數(shù)也不受限制。
8.4 綜上所述對(duì)比,我們得出一些經(jīng)驗(yàn)性的結(jié)論:
1)若線性表需要頻繁查找,很少進(jìn)行插入和刪除操作時(shí),宜采用順序存儲(chǔ)結(jié)構(gòu)。
2)若需要頻繁插入和刪除時(shí),宜采用單鏈表結(jié)構(gòu)。