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中结果如图:
拙作一篇,望诸位同道不吝斧正。