C語言:詳解單鏈表與例題
1.單鏈表的實現
2.例題:移除鏈表元素
1.單鏈表的實現
鏈表根據帶頭或不帶頭、單向或雙向、循環或不循環分類為8種,最常用的是單鏈表和雙向鏈表,單鏈表是 不帶頭單向不循環 鏈表。
鏈表由節點組成,節點分為兩部分,數據和指向下一節點的指針。
鏈表示意圖如下:
創建頭文件SList.h,包含stdio.h、stdlib.h、assert.h,在文件中定義節點結構,聲明函數打印鏈表、尾/頭插、尾/頭刪、查找、定位前插入、定位后插入、刪除pos節點、刪除pos后節點、銷毀鏈表。
//SList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode //定義節點結構
{ SLTDataType data; //數據struct SListNode* next;//指向下一節點的指針
}SLTNode;
void SLTPrint(SLTNode* phead);//打印鏈表void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插void SLTPushFront(SLTNode** pphead, SLTDataType x);//頭插void SLTPopBack(SLTNode** pphead);//尾刪void SLTPopFront(SLTNode** pphead);//頭刪SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//定位前插入void SLTInsertAfter(SLTNode* pos, SLTDataType x);//定位后插入void SLTErase(SLTNode** pphead, SLTNode* pos);//刪除pos節點void SLTEraseAfter(SLTNode* pos);//刪除pos后一個節點void SListDesTroy(SLTNode** pphead);//銷毀鏈表
創建SList.c文件,在文件中編寫函數。
打印鏈表:定義一個指針pcur指向鏈表首節點,遍歷并打印。
void SLTPrint(SLTNode* phead)//打印
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
創建節點:接下來的函數在使用時常常需要創建節點,所以另寫一個函數用于創建節點。用malloc函數動態開辟空間,定義指針newnode指向這塊空間,把所需的數據和指向下一個節點指針賦給data和next。
SLTNode* SLTBuyNode(SLTDataType x)//創建節點
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
尾插:尾插的關鍵是找到最后一個節點,讓這個尾節點的指向下一節點的指針next不再指向NULL,而是指向新節點newnode,此時newnode成為新的尾節點,所以最后還要讓newnode指向NULL。
void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{ //*pphead為指向第一個節點的指針assert(pphead);SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL)//若為空鏈表*pphead = newnode;else //若為非空鏈表{SLTNode* ptail = *pphead;while (ptail->next) //找尾ptail = ptail->next;ptail->next = newnode;}
}
尾插函數SLTPushBack的一個參數是二級指針,這個參數表示我們傳過去的是指向第一個節點的指針的地址。假設用的是一級指針phead,指向鏈表第一個節點的指針為plist,plist是實參,phead為形參,當為空鏈表時,phead被賦值為newnode,出了函數后形參phead被銷毀,實參plist仍指向NULL。
所以,要使形參的改變影響實參,就要傳地址。
頭插:先讓創建的新節點newnode的next指針指向原鏈表的第一個節點,再讓指向原鏈表第一個節點指針pphead指向newnode即可。這兩步的先后順序不可調換,若先讓pphead指向newnode,就無法找到原鏈表的第一個節點。
void SLTPushFront(SLTNode** pphead, SLTDataType x)//頭插
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
尾刪:定義指針ptail,把*pphead賦值ptail,用ptail遍歷鏈表,使ptail指向最后一個節點,然后用free釋放ptail指向的節點。這時還需要一個指針prev指向ptail指向節點的上一個節點,在尾節點被釋放后,prev指向的節點的next置為NULL。
void SLTPopBack(SLTNode** pphead)//尾刪
{assert(pphead && *pphead); //鏈表不為空if ((*pphead)->next == NULL) //只有一個節點{free(*pphead);*pphead = NULL;}else //不止一個節點{SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next) //使ptail指向最后一個節點{prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}
}
頭刪:定義指針next保存鏈表的第二個節點,釋放第一個節點后再把next賦值給*pphead。
void SLTPopFront(SLTNode** pphead)//頭刪
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
查找節點:假設要找到data為x的節點,讓指針pcur=phead,遍歷鏈表,直到pcur->data等于x,找到了返回指向該節點的指針,沒找到返回NULL。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x)return pcur; //找到了pcur = pcur->next;}return NULL;
}
定位前插入:給一個指向某個節點指針pos,若pos指向的是第一個節點,直接調用頭插函數SLTPushFront;若pos指向的不是第一個節點,則定義指針prev,遍歷鏈表,使prev指向pos指向節點的上一節點,然后插入新節點。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
//定位前插入,即pos之前插入,通過SLTFind得到pos
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTBuyNode(x);if (pos == *pphead)SLTPushFront(pphead, x);//頭插else{SLTNode* prev = *pphead;while (prev->next != pos)//找到pos,pos的前一個節點為prevprev = prev->next;newnode->next = pos;prev->next = newnode;}
}
定位后插入:給一個指向某個節點指針pos,在pos指向的節點和pos->next指向的節點之間插入新節點newnode即可。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//定位后插入,即pos后插入
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}
刪除pos節點:給一個指向某個節點指針pos,定義指針prev,遍歷鏈表,使prev指向pos指向節點的上一節點,再讓prev指向pos后的節點,最后free釋放pos。
void SLTErase(SLTNode** pphead, SLTNode* pos)//刪pos節點
{assert(pphead && *pphead);assert(pos);if (pos == *pphead) //pos為頭節點SLTPopFront(pphead);//頭刪else //pos不為頭節點{SLTNode* prev = *pphead;while (prev->next != pos)prev = prev->next;prev->next = pos->next;//prev pos pos->nextfree(pos);pos = NULL;}
}
刪除pos之后節點:先定義指針del指向pos之后節點,在用free釋放del之前,先pos->next指向del->next指向的節點,最后即可釋放del。
void SLTEraseAfter(SLTNode* pos)//刪pos之后的節點
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;//pos del del->nextfree(del);del = NULL;
}
銷毀鏈表:循環遍歷鏈表,循環一次,銷毀一次。
void SListDesTroy(SLTNode** pphead)//銷毀鏈表
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur) //循環一次,銷毀一個節點{SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
在SList.c文件中代碼如下:
//SList.c
#include"SList.h"
void SLTPrint(SLTNode* phead)//打印
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
SLTNode* SLTBuyNode(SLTDataType x)//創建節點
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{ //*pphead為指向第一個節點的指針assert(pphead);SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL)//若為空鏈表*pphead = newnode;else //若為非空鏈表{SLTNode* ptail = *pphead;while (ptail->next) //找尾ptail = ptail->next;ptail->next = newnode;}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)//頭插
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
void SLTPopBack(SLTNode** pphead)//尾刪
{assert(pphead && *pphead); //鏈表不為空if ((*pphead)->next == NULL) //只有一個節點{free(*pphead);*pphead = NULL;}else //不止一個節點{SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next) //使ptail指向最后一個節點{prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}
}
void SLTPopFront(SLTNode** pphead)//頭刪
{assert(pphead && *pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x)return pcur; //找到了pcur = pcur->next;}return NULL;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//定位前插入,即pos之前插入,通過SLTFind得到pos
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTBuyNode(x);if (pos == *pphead)SLTPushFront(pphead, x);//頭插else{SLTNode* prev = *pphead;while (prev->next != pos)//找到pos,pos的前一個節點為prevprev = prev->next;newnode->next = pos;prev->next = newnode;}
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//定位后插入,即pos后插入
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)//刪pos節點
{assert(pphead && *pphead);assert(pos);if (pos == *pphead) //pos為頭節點SLTPopFront(pphead);//頭刪else //pos不為頭節點{SLTNode* prev = *pphead;while (prev->next != pos)prev = prev->next;prev->next = pos->next;//prev pos pos->nextfree(pos);pos = NULL;}
}
void SLTEraseAfter(SLTNode* pos)//刪pos之后的節點
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;//pos del del->nextfree(del);del = NULL;
}
void SListDesTroy(SLTNode** pphead)//銷毀鏈表
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur) //循環一次,銷毀一個節點{SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
創建test.c文件進行測試。
例如:測試頭插和尾插
#include"SList.h"
void test1()
{SLTNode* phead = NULL;SLTPushBack(&phead, 2);SLTPushFront(&phead, 1);SLTPrint(phead);SListDesTroy(&phead);
}
int main()
{test1()return 0;
}
2.例題:移除鏈表元素
給一個鏈表的頭節點head和一個整數val,刪除鏈表中所有滿足Node.data==val的節點,返回新的頭節點,節點數目在[ 0, 10^4 ]內。
例如:
- 輸入 head=[1,2,6,3,4,5,6],val=6
- 輸出 [ 1,2,3,4,5 ]
方法:找值不為val的節點,尾插到新鏈表中。
#include<stdio.h>
typedef struct ListNode //節點結構體
{int data;struct ListNode* next;
}ListNode;
ListNode* removeElements(ListNode* head,int val)
{ListNode* newHead;ListNode* newTail; //定義兩個指向節點的指針newHead=newTail=NULL;ListNode* pcur=head; //遍歷原鏈表while(pcur) //pcur指向尾節點時跳出循環{if(pcur->data!=val){if(newHead==NULL)newHead=newTail=pcur;else{newTail->next=pcur;newTail=newTail->next;}}pcur=pcur->next;}if(newTail)newTail->next=NULL;return newHead;
}
以head=[1,2,6,3,4,5,6],val=6為例分析:
此時pcur->data==6,第三次循環后newTail不移動,pcur->data為3。
第七次循環后,pcur為NULL,newTail不移動,跳出循環,newTail->next賦為NULL。
構建的新鏈表如下:
輸入head=[1,2,6,3,4,5,6],val=6,vs2022中結果如圖:
拙作一篇,望諸位同道不吝斧正。