鏈表的簡單介紹
為什么需要線性鏈表
當然是為了克服順序表的缺點,在順序表中,做插入和刪除操作時,需要大量的移動元素,導致效率下降。
線性鏈表的分類
- 按照鏈接方式:單鏈表、循環鏈表、雙鏈表
- 按照實現角度:靜態鏈表、動態鏈表
線性鏈表的創建和簡單遍歷
算法思想
創建一個鏈表,並對鏈表的數據進行簡單的遍歷輸出。
算法實現
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;//數據域
struct Node * pNext;//指針域 ,通過指針域 可以指下一個節點 “整體”,而不是一部分;指針指向的是和他本身數據類型一模一樣的數據,從結構體的層面上說,也就是說單個指向整體,(這里這是通俗的說法,實施情況並非是這樣的)下面用代碼進行說明。
}NODE,*PNODE; //NODE == struct Node;PNODE ==struct Node *
PNODE create_list(void)//對於在鏈表,確定一個鏈表我們只需要找到“頭指針”的地址就好,然后就可以確認鏈表,所以我們直接讓他返回頭指針的地址
{
int len;//存放有效節點的個數
int i;
int val; //用來臨時存放用書輸入的節點的的值
PNODE pHead = (PNODE)malloc(sizeof(NODE)); //請求系統分配一個NODE大小的空間
if (NULL == pHead)//如果指針指向為空,則動態內存分配失敗,因為在一個鏈表中首節點和尾節點后面都是NULL,沒有其他元素
{
printf("分配內存失敗,程序終止");
exit(-1);
}
PNODE pTail = pHead;//聲明一個尾指針,並進行初始化指向頭節點
pTail->data = NULL;//把尾指針的數據域清空,畢竟和是個結點(清空的話更符合指針的的邏輯,但是不清空也沒有問題)
printf("請您輸入要生成鏈表節點的個數:len =");
scanf("%d",&len);
for (i=0;i < len;i++)
{
printf("請輸入第%d個節點的值",i+1);
scanf("%d",&val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));//創建新節點,使之指針都指向每一個節點(循環了len次)
if(NULL == pNew)//如果指針指向為空,則動態內存分配失敗,pNew 的數據類型是PNODE類型,也就是指針類型,指針指向的就是地址,如果地址指向的 //的 地址為空,換句話說,相當於只有頭指針,或者是只有尾指針,尾指針應該是不能的,因為一開始的鏈表是只有一個 //頭指針的,所以說,如果pNew指向為空的話,說明,內存並沒有進行分配,這個鏈表仍然是只有一個頭節點的空鏈表。
{
printf("內存分配失敗,程序終止運行!\n");
exit(-1);
}
pNew->data = val; //把有效數據存入pNEW
pTail->pNext = pNew; //把pNew 掛在pTail的后面(也就是pTail指針域指向,依次串起來)
pNew->pNext = NULL;//把pNew的指針域清空
pTail = pNew; //在把pNew賦值給pTai,這樣就能循環,實現依次連接(而我們想的是只是把第一個節點掛在頭節點上,后面的依次進行,即把第二個
//節點掛在第一個節點的指針域上),這個地方也是前面說的,要給pHead 一個“別名的原因”
/*
如果不是這樣的話,代碼是這樣寫的:
pNew->data = val;//一個臨時的節點
pHead->pNext = pNew;//把pNew掛到pHead上
pNew->pNext=NULL; //這個臨時的節點最末尾是空
注釋掉的這行代碼是有問題的,上面注釋掉的代碼的含義是分別把頭節點后面的節點都掛在頭節點上,
導致頭節點后面的節點的指針域丟失(不存在指向),而我們想的是只是把第一個節點掛在頭節點上,后面的依次進行,即把第二個
節點掛在第一個節點的指針域上,依次類推,很明顯上面所注釋掉的代碼是實現不了這個功能的,pTail 在這里的做用就相當於一個中轉站的作用,類似於兩個數交換算法中的那個中間變量的作用,在一個鏈表中pHead 是頭節點,這個在一個鏈表中是只有一個的,但是如果把這個節點所具備的屬性賦值給另外的一個變量(pTail)這樣的話,pTail 就相當於另外的一個頭指針,然后當然也是可以循環。
*/
}
return pHead;//返回頭節點的地址
}
void traverse_list(PNODE pHead)//怎樣遍歷,是不能像以前一樣用數組的,以為數組是連續的,這里不連續
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;//等價於 struct Node * pHead = NULL;把首節點的地址賦值給pHead(在一個鏈表中首節點和尾節點后面都是NULL,沒有其他元素)
//PNODE 等價於struct Node *
pHead = create_list();
traverse_list(pHead);
return 0;
}
運行演示
算法小結
這只是一個簡單的示例,其中用到的插入節點的算法就是尾插法,下面有具體的算法。
線性鏈表頭插法實現
算法思想
從一個空表開始,每次讀入數據,生成新結點,將讀入數據存放到新結點的數據域中,然后將新結點插入到當前表的表頭結點之后。
算法實現
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
}NODE,*PNODE;
//遍歷
void traverse_list(PNODE pHead)//怎樣遍歷,是不能像以前一樣用數組的,以為數組是連續的,這里不連續
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的長度\n");
int n;
int val;
scanf("%d",&n);
for (int i = n;i > 0;i--)
{
printf("請輸入的第%d個數據",i);
PNODE p = (PNODE)malloc(sizeof(NODE));//建立新的結點p
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!\n");
exit(-1);
}
scanf("%d",&val);
p->data = val;
p->pNext = pHead->pNext;//將p結點插入到表頭,這里把頭節點的指針賦給了p結點
//此時,可以理解為已經把p節點和頭節點連起來了,頭指針指向,也就變成了
//p節點的指針指向了(此時的p節點相當於首節點了)
pHead->pNext = p;
}
return pHead;
}
int main(void)
{
PNODE pHead = NULL;
pHead = create_list();
traverse_list(pHead);
return 0;
}
運行演示
算法小結
采用頭插法得到的單鏈表的邏輯順序與輸入元素順序相反,所以也稱頭插法為逆序建表法。為什么是逆序的呢,因為在開始建表的時候,所謂頭插法,就是新建一個結點,然后鏈接在頭節點的后面,也就是說,最晚插入的結點,離頭節點的距離也就是越近!這個算法的關鍵是 p->data = val;p->pNext = pHead->pNext; pHead->pNext = p;
。用圖來表示的話可能更加清晰一些。
線性鏈表尾插法實現
算法思想
頭插法建立鏈表雖然算法簡單,但生成的鏈表中節點的次序和輸入順序相反,如果希望二者的順序一致,可以采用尾插法,為此需要增加一個尾指針r,使之指向單鏈表的表的表尾。
算法實現
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的長度:\n");
int n;
int val;
PNODE r = pHead;//r 指針動態指向鏈表的當前表尾,以便於做尾插入,其初始值指向頭節點,
//這里可以總結出一個很重要的知識點,如果都是指針類型的數據,“=”可以以理解為指向。
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("請輸入的第%d個數據",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!");
exit(-1);
}
scanf("%d",&val);
p->data = val; //給新節點p的數據域賦值
r->pNext = p;//因為一開始尾指針r是指向頭節點的, 這里又是尾指針指向s
// 所以,節點p已經鏈接在了頭節點的后面了
p->pNext = NULL; //把新節點的指針域清空 ,先清空可以保證最后一個的節點的指針域為空
r = p; // r始終指向單鏈表的表尾,這樣就實現了一個接一個的插入
}
return pHead;
}
//遍歷
void traverse_list(PNODE pHead)//怎樣遍歷,是不能像以前一樣用數組的,以為數組是連續的,這里不連續
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
//刪除
void DelList(PNODE pHead,int i,int * e)
//在帶頭節點的單鏈表L中刪除第i個元素,並將刪除的元素保存到變量 *e 中
{
NODE * pre;
NODE * r;
int k = 0;
pre = pHead;
while (pre->pNext!=NULL && k<i-1)
//尋找被刪除結點i的前驅節點i-1,使p指向它
{
pre = pre->pNext;
k = k+1;
}
if(pre->pNext == NULL)
{
printf("刪除位置i不合理!");
exit(-1);
}
r = pre->pNext;
pre->pNext = r->pNext;//修改指針,刪除結點
*e = r->data;
printf("您要刪除的結點%d已經被刪除!",*e);
free(r);//注意順序,是最后才把rfree掉!
}
int main(void)
{
int val;
PNODE pHead = NULL;
pHead = create_list();
traverse_list(pHead);
DelList(pHead,2,&val);
traverse_list(pHead);
return 0;
}
運行演示

算法小結
通過尾插法的學習,進一步加深了對鏈表的理解,“=”可以理解為賦值號,也可以理解為“指向”,兩者靈活運用,可以更好的理解鏈表中的相關內容。
還有,這個尾差法其實就是這篇文章中的一開始那個小例子中使用的方法。兩者可以比較學習。
查找第i個節點(找到后返回此個節點的指針)
按序號查找
算法思想
在單鏈表中,由於每個結點 的存儲位置都放在其前一個節點的next域中,所以即使知道被訪問的節點的序號,也不能想順序表中那樣直接按照序號訪問一維數組中的相應元素,實現隨機存取,而只能從鏈表的頭指針觸發,順鏈域next,逐個結點往下搜索,直到搜索到第i個結點為止。
要查找帶頭節點的單鏈表中第i個節點,則需要從**單鏈表的頭指針L出發,從頭節點(pHead->next)開始順着鏈表掃描,用指針p指向當前掃面到的節點,初始值指向頭節點,用j做計數器,累計當前掃描過的節點數(初始值為0).當i==j時,指針p所指向的節點就是要找的節點。
代碼實現
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的長度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("請輸入的第%d個數據",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找第i個節點
NODE * getID(PNODE pHead,int i)//找到后返還該節點的地址,只需要需要頭節點和要找的節點的序號
{
int j; //計數,掃描的次數
NODE * p;
if(i<=0)
return 0;
p = pHead;
j = 0;
while ((p->pNext!=NULL)&&(j<i))
{
p = p->pNext;
j++;
}
if(i==j)//找到了第i個節點
return p;
else
return 0;
}
//遍歷
void traverse_list(PNODE pHead)//怎樣遍歷,是不能像以前一樣用數組的,以為數組是連續的,這里不連續
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;
int n;
NODE * flag;
pHead = create_list();
traverse_list(pHead);
printf("請輸入你要查找的結點的序列:");
scanf("%d",&n);
flag = getID(pHead,n);
if(flag != 0)
printf("找到了!");
else
printf("沒找到!") ;
return 0;
}
運行演示
按值查找
算法思想
按值查找是指在單鏈表中查找是否有值等於val的結點,在查找的過程中從單鏈表的的頭指針指向的頭節點開始出發,順着鏈逐個將結點的值和給定的val做比較,返回結果。
代碼實現
# include <stdio.h>
# include <stdlib.h>
#include <cstdlib> //為了總是出現null未定義的錯誤提示
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的長度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("請輸入的第%d個數據",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找按照數值
NODE * getKey(PNODE pHead,int key)
{
NODE * p;
p = pHead->pNext;
while(p!=NULL)
{
if(p->data != key)
{
p = p->pNext;//這個地方要處理一下,要不然找不到的話就指向了系統的的別的地方了emmm
if(p->pNext == NULL)
{
printf("對不起,沒要找到你要查詢的節點的數據!");
return p;//這樣的話,如果找不到的話就可以退出循環了,而不是一直去指。。。。造成指向了系統內存emmm
}
}
else
break;
}
printf("您找的%d找到了!",p->data) ;
return p;
}
//遍歷
void traverse_list(PNODE pHead)//怎樣遍歷,是不能像以前一樣用數組的,以為數組是連續的,這里不連續
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;
int val;
pHead = create_list();
traverse_list(pHead);
printf("請輸入你要查找的結點的值:");
scanf("%d",&val);
getKey(pHead,val);
return 0;
}
運行演示
算法小結
兩個算法都是差不多的,第一個按序號查找,定義了一個計數變量j,它有兩個作用,第一個作用是記錄節點的序號,第二個作用是限制指針指向的范圍,防止出現指針指向別的地方。第二個按值查找,當然也可以用相同的方法來限制范圍,防止指針指向別的位置。或者和上面寫的那樣,加一個判斷,如果到了表尾,為空了,就退出循環。
求鏈表的長度
算法思想
采用“數”結點的東方法求出帶頭結點單鏈表的長度。即從“頭”開始“數”(p=L->next),用指針p依次指向各個節點。並設計計數器j,一直疏導最后一個節點(p->next == NUll),從而得到單鏈表的長度。
算法實現
# include <stdio.h>
# include <stdlib.h>
#include <cstdlib> //為了總是出現null未定義的錯誤提示
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的節點(輸入0結束):\n");
int val=1;//賦一個初始值,防止 因為垃圾值而報錯,下面都會被scanf函數給覆蓋掉
PNODE r = pHead;
while(val != 0)
{
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//計算鏈表的長度
int ListLength(PNODE pHead)
{
NODE * p;
int j;//計數
p = pHead->pNext;
j = 0;
while(p!=NULL)
{
p = p->pNext;
j++;
}
return j;
}
int main(void)
{
PNODE pHead = NULL;
pHead = create_list();
int len = ListLength(pHead);
printf("%d",len);
return 0;
}
運行演示
算法小結
在順序表中,線性表的長度是它的屬性,數組定義時就已經確定,在單鏈表中,整個鏈表由“頭指針”來表示,單鏈表的長度在頭到尾遍歷的過程中統計計數,得到長度值未顯示保存。
求單鏈表中的最大值以及實現就地址逆置鏈表
算法思想(求單鏈表的最大值)
通過兩個指針就可以很簡單實現這個問題,值得注意的是,要主要模擬比較的過程,通過指針指向的兩個節點的數據域進行比較,把數據域大的節點的地址返回,如果找不到一直找。
算法實現
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("請輸入要生成的鏈表的長度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("請輸入的第%d個數據",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("內存分配失敗,程序終止運行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找單鏈表中的最大值
NODE * SearchMAx(PNODE pHead)
{
NODE * p1;//定義兩個指針,依次比較兩次的數據域大小
NODE * p2;
p1 = pHead->pNext;
p2 = p1->pNext;
while(p2 != NULL)
{
if(p2->data > p1->data) //注意這里和上面的賦值的順序
{
p1 = p2;//把p1定義為最大結點的指針
p2 = p2->pNext;//繼續走鏈表
}
else
{
p2 = p2->pNext; //如果一直沒有找到比p1大的數據,繼續找
}
}
return p1;//把最大的節點的地址返回
}
int main(void)
{
PNODE pHead = NULL;
NODE * p;
pHead = create_list();
p = SearchMAx(pHead);
printf("鏈表中的最大值為%d",p->data);
return 0;
}
運行演示
算法思想(就地址逆置換)
算法思想:逆置鏈表初始為空,表中節點從原鏈表中依次“刪除”,再逐個插入逆置鏈表的表頭(即“頭插”到逆置鏈表中),使它成為逆置鏈表的“新”的第一個結點,如此循環,直至原鏈表為空。利用頭插法。
算法實現
void converse(LinkList *head)
{
LinkList *p,*q;
p=head->next;
head->next=NULL;
while(p)
{
/*向后挪動一個位置*/
q=p;
p=p->next;
/*頭插*/
q->next=head->next;
head->next=q;
}
}
參考文獻
- 數據結構-用C語言描述(第二版)[耿國華]
- 數據結構(C語言版)[嚴蔚敏,吳偉民]