单链表的应用:经典OJ题与通讯录项目实战
单链表在实际开发与算法面试中应用广泛。本文将通过6道经典OJ题目(移除链表元素、反转链表、合并有序链表、中间结点、约瑟夫环、分割链表)深入讲解单链表的操作技巧,并基于单链表重新实现通讯录项目,实现数据的持久化存储。
目录
- 一、单链表经典算法OJ题目
- [1. 移除链表元素](#1. 移除链表元素)
- [2. 反转链表](#2. 反转链表)
- [3. 合并两个有序链表](#3. 合并两个有序链表)
- [4. 链表的中间结点](#4. 链表的中间结点)
- [5. 环形链表的约瑟夫问题](#5. 环形链表的约瑟夫问题)
- [6. 分割链表](#6. 分割链表)
- 二、基于单链表再实现通讯录项目
- [1. 设计思路](#1. 设计思路)
- [2. 核心代码实现](#2. 核心代码实现)
- [3. 文件持久化](#3. 文件持久化)
一、单链表经典算法OJ题目
1. 移除链表元素
题目 :删除链表中所有值等于 val 的节点。
思路 :使用哨兵位头节点(或双指针)简化操作。遍历链表,当遇到目标值时,将前驱节点的 next 指向当前节点的 next。
代码示例:
c
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode* prev = dummy;
struct ListNode* cur = head;
while (cur) {
if (cur->val == val) {
prev->next = cur->next;
free(cur);
cur = prev->next;
} else {
prev = cur;
cur = cur->next;
}
}
struct ListNode* newHead = dummy->next;
free(dummy);
return newHead;
}
注意:使用虚拟头节点可以统一处理头部删除的情况。
2. 反转链表
题目:反转单链表。
思路 :迭代法,使用三个指针(prev、curr、next)逐个反转。
c
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr) {
struct ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
迭代法空间复杂度 O(1)。
3. 合并两个有序链表
题目:将两个升序链表合并为一个新的升序链表。
思路:使用虚拟头节点,不断比较两个链表当前节点值,将较小者尾插。
c
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail = dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
tail->next = l1 ? l1 : l2;
struct ListNode* newHead = dummy->next;
free(dummy);
return newHead;
}
4. 链表的中间结点
题目:返回链表的中间节点(若有偶数个,返回靠后的那个)。
思路:快慢指针,快指针每次走两步,慢指针每次走一步,快指针走到末尾时慢指针即中间。
c
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
5. 环形链表的约瑟夫问题
题目:编号为1~n的n个人围成一圈,从第一个人开始报数,数到m的人出列,下一个人重新报数,求最后剩下的人的编号。
思路:构建单向循环链表,模拟删除过程。
c
// 定义节点
typedef struct ListNode {
int val;
struct ListNode* next;
} ListNode;
int josephus(int n, int m) {
// 创建循环链表
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
head->val = 1;
ListNode* prev = head;
for (int i = 2; i <= n; i++) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->val = i;
prev->next = node;
prev = node;
}
prev->next = head; // 成环
ListNode* cur = head;
ListNode* before = prev;
while (n > 1) {
// 报数 m-1 次,定位到待删除节点的前驱
for (int i = 1; i < m; i++) {
before = cur;
cur = cur->next;
}
// 删除 cur 节点
before->next = cur->next;
free(cur);
cur = before->next;
n--;
}
int result = cur->val;
free(cur);
return result;
}
注意:需要处理只有一个人时的边界情况,以及构建循环链表的细节。
6. 分割链表
题目 :给定一个链表和一个值 x,将小于 x 的节点放在大于等于 x 的节点之前,保持原顺序。
思路:创建两个临时链表(小值链表和大值链表),分别收集节点,最后拼接。
c
struct ListNode* partition(struct ListNode* head, int x) {
struct ListNode smallDummy, largeDummy;
smallDummy.next = NULL;
largeDummy.next = NULL;
struct ListNode* smallTail = &smallDummy;
struct ListNode* largeTail = &largeDummy;
struct ListNode* cur = head;
while (cur) {
if (cur->val < x) {
smallTail->next = cur;
smallTail = cur;
} else {
largeTail->next = cur;
largeTail = cur;
}
cur = cur->next;
}
smallTail->next = largeDummy.next;
largeTail->next = NULL;
return smallDummy.next;
}
二、基于单链表再实现通讯录项目
利用单链表的动态特性,实现通讯录的增删改查,并支持文件持久化。
1. 设计思路
- 数据结构 :单链表每个节点存储一个
PersonInfo结构体(姓名、性别、年龄、电话、地址)。 - 文件操作 :程序启动时从
contact.txt加载数据(二进制读取),退出时自动保存。 - 接口复用 :将单链表的
PushBack、Erase、Find、Print等接口直接用于通讯录业务。
关键:链表的动态特性避免了顺序表扩容和移动数据的开销,适合频繁增删的场景。
2. 核心代码实现
单链表适配(SList.h)
c
typedef struct PersonInfo SLTDataType; // 数据类型改为通讯录结构体
struct SListNode {
SLTDataType data;
struct SListNode* next;
};
typedef struct SListNode SLTNode;
// 接口声明(同前)
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
void SLTPrint(SLTNode* phead);
void SListDestroy(SLTNode** pphead);
通讯录业务(contact.c 片段)
c
void AddContact(SLTNode** 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(SLTNode** con) {
char name[NAME_MAX];
printf("请输入要删除的姓名:"); scanf("%s", name);
// 根据姓名查找节点
SLTNode* cur = *con;
while (cur && strcmp(cur->data.name, name) != 0)
cur = cur->next;
if (cur == NULL) {
printf("用户不存在!\n");
return;
}
SLTErase(con, cur);
printf("删除成功!\n");
}
3. 文件持久化
c
// 加载历史数据
void LoadContact(SLTNode** con) {
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL) return;
PeoInfo info;
while (fread(&info, sizeof(PeoInfo), 1, pf)) {
SLTPushBack(con, info);
}
fclose(pf);
printf("历史数据加载成功!\n");
}
// 保存通讯录
void SaveContact(SLTNode* con) {
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL) return;
SLTNode* cur = con;
while (cur) {
fwrite(&(cur->data), sizeof(PeoInfo), 1, pf);
cur = cur->next;
}
fclose(pf);
printf("数据保存成功!\n");
}
void DestroyContact(SLTNode** con) {
SaveContact(*con);
SListDestroy(con);
}
注意 :务必在程序退出前调用 DestroyContact 保存数据,避免丢失。
总结:单链表在算法面试中高频出现,掌握其反转、合并、找中点、分割、约瑟夫环等经典题目能够显著提升解题能力。同时,基于单链表实现通讯录,不仅避免了顺序表的扩容与数据搬移,还能动态管理内存,非常适合增删频繁的场景。读者应动手实践所有OJ题,并完成单链表版通讯录,体会链表与顺序表的不同适用场景。