跳到主要内容单链表应用:经典算法题与通讯录实现 | 极客日志C算法
单链表应用:经典算法题与通讯录实现
单链表的经典算法题目及通讯录项目的实现。内容包括移除链表元素、反转链表、合并有序链表、查找中间节点、环形链表约瑟夫问题以及分割链表等算法题的解题思路与代码实现。此外,基于单链表重新实现了通讯录功能,涵盖联系人信息的增删改查及文件持久化,并与顺序表版本进行了对比分析。通过理论结合实践,深入理解单链表在实际开发中的应用场景及优缺点。
草莓泡芙1 浏览 第一部分:单链表经典算法 OJ 题
1.1 移除链表元素
解题思路:
- 在原链表上进行修改,创建 pcur 和 prev 两个变量,遍历原链表,最后返回,但是需要注意头节点也要被释放的情况。
- 创建一个带哨兵位的新链表,将符合条件的节点接到新链表后面,然后返回哨兵位的 next 节点,注意尾节点的 next 需要置为 NULL。
易错点:
如果原链表是空链表,那么直接返回空链表。
代码展示:
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
if(head==NULL) {
return NULL;
}
ListNode* newhead,* newtail;
newhead=newtail=(ListNode*)malloc(sizeof(ListNode));
ListNode* pcur=head;
while(pcur) {
if(pcur->val!=val) {
newtail->next=pcur;
newtail=newtail->next;
}
pcur=pcur->next;
}
newtail->next=NULL;
ListNode* ret=newhead->next;
free(newhead);
return ret;
}
1.2 反转链表
题目描述:给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
解题思路:
- 使用三个指针:prev(前一个节点)、curr(当前节点)、next(下一个节点)
- 遍历过程中,将当前节点的 next 指向前一个节点,然后整体后移
代码实现:
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
ListNode* n1,*n2,*n3;
(head==) head;
n1=,n2=head,n3=head->next;
(n2) {
n2->next=n1;
n1=n2;
n2=n3;
(n3) n3=n3->next;
}
n1;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
if
NULL
return
NULL
while
if
return
1.3 合并两个有序链表
题目描述:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路:
创建一个新的链表,通过两个指针遍历两个链表,并进行比较大小,把小的节点接到新链表后面。
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
ListNode* l1 = list1;
ListNode* l2 = list2;
ListNode* newhead = NULL;
ListNode* newtail = NULL;
if (l1 == NULL) return l2;
if (l2 == NULL) return l1;
if (l1->val < l2->val) {
newhead = newtail = l1;
l1 = l1->next;
} else {
newhead = newtail = l2;
l2 = l2->next;
}
while (l1 && l2) {
if (l1->val < l2->val) {
newtail->next = l1;
l1 = l1->next;
} else {
newtail->next = l2;
l2 = l2->next;
}
newtail = newtail->next;
}
if (l1) newtail->next = l1;
if (l2) newtail->next = l2;
return newhead;
}
1.4 链表的中间节点
题目描述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
- 使用快慢指针:快指针一次走两步,慢指针一次走一步
- 当快指针到达末尾时,慢指针恰好指向中间节点
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode* slow,*fast;
slow=fast=head;
while(fast&&fast->next) {
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
1.5 环形链表的约瑟夫问题
问题描述:编号为 1 到 n 的 n 个人围成一圈。从第 1 个人开始报数,数到 m 的人出列,然后从下一个人继续报数,直到所有人都出列。请输出出列顺序。
背景:这是著名的约瑟夫斯问题(Josephus problem)。
- 可以用循环链表模拟这个过程
- 每次数到 m 的人移除,从下一个人继续
- 直到链表为空
typedef struct ListNode ListNode;
ListNode* buynode(int num) {
ListNode* node=(ListNode*)malloc(sizeof(ListNode));
if(node==NULL) {
exit(1);
}
node->val=num;
node->next=NULL;
return node;
}
ListNode* CreatecirList(int n) {
ListNode* head=buynode(1);
ListNode* ptail=head;
for(int i=2;i<=n;i++) {
ptail->next=buynode(i);
ptail=ptail->next;
}
ptail->next=head;
return ptail;
}
int ysf(int n, int m) {
ListNode* prev=CreatecirList(n);
ListNode* pcur=prev->next;
int count=1;
while(pcur->next!=pcur) {
if(count==m) {
prev->next=pcur->next;
free(pcur);
pcur=prev->next;
count=1;
} else {
pcur=pcur->next;
prev=prev->next;
count++;
}
}
return pcur->val;
}
1.6 分割链表
题目描述:给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分隔,使得所有小于 x 的节点都出现在大于或等于 x 的节点之前。你应当保留两个分区中每个节点的初始相对位置。
- 创建两个虚拟头节点,分别存放小于 x 和大于等于 x 的节点
- 遍历原链表,将节点按条件接入两个新链表
- 最后将两个链表连接起来
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x) {
if(head==NULL) {
return NULL;
}
ListNode* lesshead,* lesstail;
ListNode* greathead,* greattail;
lesshead=lesstail=(ListNode*)malloc(sizeof(ListNode));
greathead=greattail=(ListNode*)malloc(sizeof(ListNode));
ListNode* pcur=head;
while(pcur) {
if((pcur->val)<x) {
lesstail->next=pcur;
lesstail=lesstail->next;
} else {
greattail->next=pcur;
greattail=greattail->next;
}
pcur=pcur->next;
}
greattail->next=NULL;
lesstail->next=greathead->next;
ListNode* ret=lesshead->next;
free(lesshead);
free(greathead);
return ret;
}
第二部分:基于单链表再实现通讯录
在上一篇文章中,我们用顺序表实现了通讯录。现在,我们用单链表重新实现,并对比两者的差异。
2.1 设计思路
- 链表节点中的数据域存储联系人信息(结构体
PeoInfo)
- 链表提供动态添加、删除、查找、修改等功能
- 数据持久化:程序启动时从文件加载,退出时保存
2.2 代码结构
SList.h(链表定义及接口)
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "contact.h"
typedef struct PersonInfo 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 SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
void SLTEraseAfter(SLTNode* pos);
void SLTDestroy(SLTNode** pphead);
contact.h(联系人信息及通讯录接口)
#pragma once
#include <stdio.h>
#include <string.h>
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 12
#define ADDR_MAX 100
typedef struct PersonInfo {
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
} PeoInfo;
typedef struct SListNode contact;
void InitContact(contact** con);
void AddContact(contact** con);
void DelContact(contact** con);
void ShowContact(contact* con);
void FindContact(contact* con);
void ModifyContact(contact** con);
void DestroyContact(contact** con);
contact.c(通讯录功能实现)
#define _CRT_SECURE_NO_WARNINGS
#include "contact.h"
#include "SList.h"
static void LoadContact(contact** con) {
FILE* pf = fopen("contact.dat", "rb");
if (pf == NULL) {
return;
}
PeoInfo info;
while (fread(&info, sizeof(PeoInfo), 1, pf)) {
SLTPushBack(con, info);
}
fclose(pf);
printf("历史数据加载成功!\n");
}
static void SaveContact(contact* con) {
FILE* pf = fopen("contact.dat", "wb");
if (pf == NULL) {
perror("保存文件失败");
return;
}
contact* cur = con;
while (cur) {
fwrite(&(cur->data), sizeof(PeoInfo), 1, pf);
cur = cur->next;
}
fclose(pf);
printf("数据保存成功!\n");
}
void InitContact(contact** con) {
*con = NULL;
LoadContact(con);
}
static contact* FindByName(contact* con, const char* name) {
contact* cur = con;
while (cur) {
if (strcmp(cur->data.name, name) == 0) {
return cur;
}
cur = cur->next;
}
return NULL;
}
void AddContact(contact** con) {
PeoInfo info;
printf("请输入姓名:");
scanf("%s", info.name);
printf("请输入性别:");
scanf("%s", info.sex);
printf("请输入年龄:");
scanf("%d", &info.age);
printf("请输入电话:");
scanf("%s", info.tel);
printf("请输入地址:");
scanf("%s", info.addr);
SLTPushBack(con, info);
printf("添加成功!\n");
}
void DelContact(contact** con) {
char name[NAME_MAX];
printf("请输入要删除的联系人姓名:");
scanf("%s", name);
contact* pos = FindByName(*con, name);
if (pos == NULL) {
printf("未找到该联系人!\n");
return;
}
SLTErase(con, pos);
printf("删除成功!\n");
}
void ShowContact(contact* con) {
if (con == NULL) {
printf("通讯录为空!\n");
return;
}
printf("\n%-10s %-4s %-4s %-15s %-20s\n", "姓名", "性别", "年龄", "电话", "地址");
printf("------------------------------------------------\n");
contact* cur = con;
while (cur) {
printf("%-10s %-4s %-4d %-15s %-20s\n", cur->data.name, cur->data.sex, cur->data.age, cur->data.tel, cur->data.addr);
cur = cur->next;
}
}
void FindContact(contact* con) {
char name[NAME_MAX];
printf("请输入要查找的联系人姓名:");
scanf("%s", name);
contact* pos = FindByName(con, name);
if (pos == NULL) {
printf("未找到该联系人!\n");
return;
}
printf("\n姓名:%s\n性别:%s\n年龄:%d\n电话:%s\n地址:%s\n", pos->data.name, pos->data.sex, pos->data.age, pos->data.tel, pos->data.addr);
}
void ModifyContact(contact** con) {
char name[NAME_MAX];
printf("请输入要修改的联系人姓名:");
scanf("%s", name);
contact* pos = FindByName(*con, name);
if (pos == NULL) {
printf("未找到该联系人!\n");
return;
}
printf("请输入新的姓名:");
scanf("%s", pos->data.name);
printf("请输入新的性别:");
scanf("%s", pos->data.sex);
printf("请输入新的年龄:");
scanf("%d", &pos->data.age);
printf("请输入新的电话:");
scanf("%s", pos->data.tel);
printf("请输入新的地址:");
scanf("%s", pos->data.addr);
printf("修改成功!\n");
}
void DestroyContact(contact** con) {
SaveContact(*con);
SLTDestroy(con);
}
test.c(主程序)
#include "SList.h"
#include "contact.h"
void menu() {
printf("\n========== 通讯录管理系统(链表版) ==========\n");
printf("1. 添加联系人\n");
printf("2. 删除联系人\n");
printf("3. 查找联系人\n");
printf("4. 修改联系人\n");
printf("5. 显示所有联系人\n");
printf("0. 退出系统\n");
printf("==============================================\n");
printf("请选择:");
}
int main() {
contact* con = NULL;
InitContact(&con);
int choice;
do {
menu();
scanf("%d", &choice);
switch (choice) {
case 1: AddContact(&con); break;
case 2: DelContact(&con); break;
case 3: FindContact(con); break;
case 4: ModifyContact(&con); break;
case 5: ShowContact(con); break;
case 0: break;
default: printf("无效选择!\n"); break;
}
} while (choice != 0);
DestroyContact(&con);
return 0;
}
2.3 顺序表版与链表版通讯录对比
从对比可以看出,链表在插入删除操作上更灵活,但牺牲了随机访问的能力。在实际应用中,应根据需求选择合适的数据结构。
总结
通过解决经典链表算法题和实现通讯录项目,我们深入体会了单链表的实际应用:
- 算法题训练让我们掌握了链表操作的常用技巧(双指针、哨兵节点、节点删除等)
- 通讯录项目展示了如何用链表组织动态数据,并与顺序表进行对比
链表作为基础数据结构,其指针操作的思维方式为后续学习树、图等复杂结构奠定了基础。