數據結構-線性表的鏈式存儲相關算法(一)(C語言實現)
鏈表的簡單介紹
為什麽需要線性鏈表
當然是為了克服順序表的缺點,在順序表中,做插入和刪除操作時,需要大量的移動元素,導致效率下降。
線性鏈表的分類
- 按照鏈接方式:
- 按照實現角度:
線性鏈表的創建和簡單遍歷
算法思想
創建一個鏈表,並對鏈表的數據進行簡單的遍歷輸出。
算法實現
# 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");
}
int main(void)
{
PNODE pHead = NULL;
pHead = create_list();
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,它有兩個作用,第一個作用是記錄節點的序號,第二個作用是限制指針指向的範圍,防止出現指針指向別的地方。第二個按值查找,當然也可以用相同的方法來限制範圍,防止指針指向別的位置。或者和上面寫的那樣,加一個判斷,如果到了表尾,為空了,就退出循環。
線性鏈表的優缺點
參考文獻
- 數據結構-用C語言描述(第二版)[耿國華]
- 數據結構(C語言版)[嚴蔚敏,吳偉民]
數據結構-線性表的鏈式存儲相關算法(一)(C語言實現)