单链表的应用:经典OJ题与通讯录项目实战

单链表的应用:经典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. 反转链表

题目:反转单链表。

思路 :迭代法,使用三个指针(prevcurrnext)逐个反转。

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 加载数据(二进制读取),退出时自动保存。
  • 接口复用 :将单链表的 PushBackEraseFindPrint 等接口直接用于通讯录业务。

关键:链表的动态特性避免了顺序表扩容和移动数据的开销,适合频繁增删的场景。

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题,并完成单链表版通讯录,体会链表与顺序表的不同适用场景。

相关推荐
SoftLipaRZC1 小时前
单链表专题:从概念到实现
数据结构
花间相见14 小时前
【LeetCode02】—— 两数之和:哈希表入门经典详解
数据结构·散列表
zhengzhouliuhaha16 小时前
智能医疗设备控费系统:以全院一体化管控,筑牢医疗资源“安全阀”
大数据·数据结构·人工智能·算法·安全·机器学习·软件需求
Yiyaoshujuku17 小时前
化合物数据集API接口(数据结构及样例)
java·网络·数据结构
fu的博客17 小时前
【数据结构16】图:基于邻接矩阵、邻接表实现DFS/BFS
数据结构·算法
言存18 小时前
力扣热题283 移动零
数据结构·算法·leetcode
Lewiis19 小时前
白话桶排序
数据结构·算法·golang·排序算法
iiiiyu20 小时前
IO流相关编程题
java·大数据·开发语言·数据结构·数据库·mysql
Darling噜啦啦20 小时前
JS 数据结构实战:从栈队列到链表,一文吃透数组底层原理与线性数据结构
前端·javascript·数据结构