【数据结构考研真题】链表大题

1)算法基本思想

采用快慢指针(双指针)法,在不改变链表结构、仅遍历一次链表的前提下找到倒数第 k 个节点:

  • 定义两个指针 fast(快指针)和 slow(慢指针),初始都指向链表的第一个数据节点(表头节点的下一个节点)。

  • 先让 fast 指针向前移动 k 步,此时 fastslow 之间恰好间隔 k-1 个节点,两者距离为 k

  • 之后让 fastslow 同时以相同速度向后移动,直到 fast 指向 NULL(到达链表末尾)。

  • 此时 slow 指针指向的节点就是倒数第 k 个节点

  • 若在 fast 先走 k 步的过程中就遇到 NULL,说明链表长度不足 k,查找失败。


2)算法详细实现步骤

  1. 初始化指针
    • 定义 fastslow 指针,均指向表头节点的下一个节点(即链表第一个有效数据节点)。
  2. 快指针先行
    • 循环 k 次,让 fast 指针向后移动;若移动过程中 fast 变为 NULL,说明链表长度小于 k,直接返回 0
  3. 同步移动指针
    • fast 不为 NULL 时,fastslow 同时向后移动,直到 fast 指向 NULL
  4. 输出结果
    • 若成功找到,输出 slow 指向节点的 data 值,并返回 1;否则返回 0

3)核心代码逐行注释

cpp 复制代码
int findNodeFS(Node *L, int k) {
    // 初始化快慢指针,均指向第一个有效数据节点(表头节点L不存数据)
    Node *fast = L->link;
    Node *slow = L->link;

    // 快指针先走k步,拉开与慢指针的距离
    for (int i = 0; i < k; i++) {
        // 若快指针在k步内遇到NULL,说明链表长度 < k,直接返回0
        if (fast == NULL) {
            return 0;
        }
        fast = fast->link; // 快指针向后移动一个节点
    }

    // 快慢指针同步移动,直到快指针到达链表末尾(NULL)
    while (fast != NULL) {
        fast = fast->link; // 快指针后移
        slow = slow->link; // 慢指针后移,始终与快指针保持k步距离
    }

    // 此时慢指针恰好停在倒数第k个节点上
    printf("倒数第%d个节点的值为:%d\n", k, slow->data);
    return 1; // 查找成功
}

完整可运行代码(C 语言)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    int data;          // 数据域
    struct Node *link; // 指针域,指向下一个节点
} Node;

// 函数:查找倒数第k个节点
// 参数:L-链表头指针(带表头节点),k-倒数位置
// 返回:1-查找成功,0-查找失败
int findNodeFS(Node *L, int k) {
    // 1. 初始化快慢指针,指向第一个数据节点(跳过表头节点)
    Node *fast = L->link;  // 快指针
    Node *slow = L->link;  // 慢指针

    // 2. 快指针先向前走k步
    for (int i = 0; i < k; i++) {
        // 若快指针提前走到NULL,说明链表长度不足k,查找失败
        if (fast == NULL) {
            return 0;
        }
        fast = fast->link; // 快指针后移
    }

    // 3. 快慢指针同时向后移动,直到快指针到达链表末尾
    while (fast != NULL) {
        fast = fast->link;  // 快指针后移
        slow = slow->link;  // 慢指针后移
    }

    // 4. 此时慢指针指向倒数第k个节点,输出结果
    printf("倒数第%d个节点的值为:%d\n", k, slow->data);
    return 1; // 查找成功
}

// 辅助函数:创建新节点
Node* createNode(int data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->link = NULL;
    return newNode;
}

// 辅助函数:尾插法创建链表(带表头节点)
Node* createList(int arr[], int n) {
    Node *head = (Node*)malloc(sizeof(Node)); // 创建表头节点(不存数据)
    head->link = NULL;
    Node *tail = head; // 尾指针,便于尾插

    for (int i = 0; i < n; i++) {
        Node *newNode = createNode(arr[i]);
        tail->link = newNode; // 新节点插入尾部
        tail = newNode;        // 尾指针后移
    }
    return head;
}

// 辅助函数:释放链表内存
void freeList(Node *head) {
    Node *p = head;
    while (p != NULL) {
        Node *temp = p;
        p = p->link;
        free(temp);
    }
}

// 主函数:测试
int main() {
    int arr[] = {1, 2, 3, 4, 5, 6}; // 测试数据
    int n = sizeof(arr)/sizeof(arr[0]);
    Node *list = createList(arr, n); // 创建链表

    int k = 3; // 查找倒数第3个节点
    int result = findNodeFS(list, k);

    if (result == 0) {
        printf("查找失败:链表长度不足%d\n", k);
    }

    freeList(list); // 释放内存
    return 0;
}

运行结果截图


算法复杂度分析

  • 时间复杂度 :O(n),仅遍历链表一次,快指针总共走 n 步,慢指针走 n-k 步,整体为线性时间。
  • 空间复杂度:O(1),仅使用两个额外指针,无其他辅助空间。

该方法高效且无需额外存储,是链表找倒数第 k 个节点的最优解法之一。

1)算法基本设计思想

这道题本质是求两个带头结点单链表的公共后缀起始节点 (即链表的 "相交节点"),采用长度差 + 双指针的高效思路:

  • 第一步:求长度 :分别遍历两个链表,得到它们的长度 len1len2
  • 第二步:对齐起点 :让较长链表的指针先向前移动 |len1 - len2| 步,使两个指针距离链表末尾的步数相同。
  • 第三步:同步遍历 :两个指针同时向后移动,直到指向同一个节点,该节点就是共同后缀的起始位置;若遍历到末尾仍未相遇,则无公共后缀。

这种方法只需要遍历链表两次,时间效率很高,且不需要额外存储空间。


核心代码(查找公共后缀的核心逻辑)

cpp 复制代码
// 定义链表节点结构
typedef struct Node {
    char data;          // 存储字符(单词的字母)
    struct Node *next;  // 指向下一个节点的指针
} Node;

// 核心函数:找到两个带头结点单链表的共同后缀起始节点
// 参数:str1、str2 分别为两个链表的头指针(带头结点)
// 返回:共同后缀的起始节点指针,无公共后缀则返回 NULL
Node* findCommonSuffix(Node *str1, Node *str2) {
    // 边界处理:若任一链表为空,直接返回 NULL
    if (str1 == NULL || str2 == NULL) {
        return NULL;
    }

    // 步骤1:计算两个链表的长度
    Node *p = str1->next;  // 跳过表头节点,指向第一个数据节点
    int len1 = 0;
    while (p != NULL) {    // 遍历str1统计长度
        len1++;
        p = p->next;
    }

    p = str2->next;        // 重置指针,遍历str2
    int len2 = 0;
    while (p != NULL) {    // 遍历str2统计长度
        len2++;
        p = p->next;
    }

    // 步骤2:让较长链表的指针先走长度差步,对齐起点
    Node *longList = str1->next;  // 初始假设str1更长
    Node *shortList = str2->next;
    int diff = abs(len1 - len2);  // 计算长度差

    // 修正长短链表指针(若str2更长则交换)
    if (len1 < len2) {
        longList = str2->next;
        shortList = str1->next;
    }
    // 长链表指针先走diff步,与短链表指针对齐
    for (int i = 0; i < diff; i++) {
        longList = longList->next;
    }

    // 步骤3:同步遍历,寻找第一个相同的节点(公共后缀起点)
    while (longList != NULL && shortList != NULL) {
        if (longList == shortList) {  // 找到公共后缀起始节点
            return longList;
        }
        // 两个指针同步后移
        longList = longList->next;
        shortList = shortList->next;
    }

    // 无公共后缀
    return NULL;
}

核心代码关键注释说明

  1. 长度计算:通过两次遍历分别统计两个链表的有效长度(跳过表头节点),为后续对齐指针做准备;
  2. 指针对齐:让较长链表的指针先移动「长度差」步,确保两个指针到链表末尾的距离一致;
  3. 同步遍历:两个指针同时后移,第一个指向同一地址的节点就是公共后缀的起始位置;
  4. 边界处理:提前判断空链表,避免空指针访问错误。
  • 时间复杂度 :O(m+n),其中 m、n 分别为两个链表的长度。
    • 第一次遍历:分别计算两个链表长度,共 O(m+n)。
    • 第二次遍历:对齐指针后同步遍历,最多 O(min(m,n)) 步。
    • 整体时间复杂度为线性,是该问题的最优解法。
  • 空间复杂度:O(1),仅使用了常数个额外指针变量,无其他辅助空间。

完整代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    char data;          // 存储字符(单词的字母)
    struct Node *next;  // 指向下一个节点的指针
} Node;

// 函数:找到两个链表的共同后缀起始节点
// 参数:str1、str2 分别为两个链表的头指针(带头结点)
// 返回:共同后缀的起始节点指针,无公共后缀则返回 NULL
Node* findCommonSuffix(Node *str1, Node *str2) {
    // 边界处理:若任一链表为空,直接返回 NULL
    if (str1 == NULL || str2 == NULL) {
        return NULL;
    }

    // --- 步骤1:分别计算两个链表的长度 ---
    Node *p = str1->next;  // p 指向 str1 的第一个数据节点
    int len1 = 0;
    while (p != NULL) {    // 遍历 str1 统计长度
        len1++;
        p = p->next;
    }

    p = str2->next;        // p 指向 str2 的第一个数据节点
    int len2 = 0;
    while (p != NULL) {    // 遍历 str2 统计长度
        len2++;
        p = p->next;
    }

    // --- 步骤2:对齐两个指针的起始位置 ---
    Node *longList = str1->next;  // 指向较长链表的第一个数据节点
    Node *shortList = str2->next; // 指向较短链表的第一个数据节点
    int diff = abs(len1 - len2);  // 计算长度差

    // 让较长链表的指针先走 diff 步,对齐到与短链表末尾同距的位置
    if (len1 < len2) {
        longList = str2->next;
        shortList = str1->next;
    }
    for (int i = 0; i < diff; i++) {
        longList = longList->next;
    }

    // --- 步骤3:同步遍历,寻找共同节点 ---
    while (longList != NULL && shortList != NULL) {
        if (longList == shortList) {  // 找到共同后缀的起始节点
            return longList;
        }
        // 两个指针同步向后移动
        longList = longList->next;
        shortList = shortList->next;
    }

    // 遍历到末尾仍未相遇,说明无公共后缀
    return NULL;
}

// ------------------- 辅助函数(用于创建和测试链表) -------------------

// 创建一个新节点
Node* createNode(char data) {
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 尾插法创建带头结点的单链表
Node* createList() {
    Node *head = (Node*)malloc(sizeof(Node)); // 头结点(不存数据)
    head->next = NULL;
    return head;
}

// 在链表尾部插入新节点
void insertTail(Node *head, char data) {
    Node *p = head;
    while (p->next != NULL) {
        p = p->next;
    }
    p->next = createNode(data);
}

// 释放链表内存
void freeList(Node *head) {
    Node *p = head;
    while (p != NULL) {
        Node *temp = p;
        p = p->next;
        free(temp);
    }
}

// ------------------- 主函数(测试) -------------------
int main() {
    // 创建 str1: "loading"
    Node *str1 = createList();
    insertTail(str1, 'l');
    insertTail(str1, 'o');
    insertTail(str1, 'a');
    insertTail(str1, 'd');

    // 创建 str2: "being"
    Node *str2 = createList();
    insertTail(str2, 'b');
    insertTail(str2, 'e');

    // 手动构造共享后缀 "ing"
    Node *nodeI = createNode('i');
    Node *nodeN = createNode('n');
    Node *nodeG = createNode('g');
    nodeI->next = nodeN;
    nodeN->next = nodeG;

    // 将共享后缀接到两个链表的尾部
    Node *p = str1;
    while (p->next != NULL) p = p->next;
    p->next = nodeI;

    p = str2;
    while (p->next != NULL) p = p->next;
    p->next = nodeI;

    // 查找共同后缀起始节点
    Node *result = findCommonSuffix(str1, str2);
    if (result != NULL) {
        printf("共同后缀的起始字符为:%c\n", result->data); // 应输出 'i'
    } else {
        printf("无共同后缀\n");
    }

    // 释放内存(注意:共享部分只释放一次)
    free(nodeG);
    free(nodeN);
    free(nodeI);
    freeList(str1);
    freeList(str2);

    return 0;
}

总结

  1. 核心思路是长度差对齐 + 双指针同步遍历,时间复杂度 O(m+n),空间复杂度 O(1);
  2. 关键逻辑是先让长链表指针 "追上" 短链表指针,再同步移动找公共节点;
  3. 最终返回的节点指针直接指向公共后缀的第一个节点,可通过该节点遍历完整公共后缀。

运行结果截图

相关推荐
m0_528174452 小时前
ZLibrary反爬机制概述
开发语言·c++·算法
你这个代码我看不懂2 小时前
引用计数法存在的问题
java·jvm·算法
yunyun321232 小时前
嵌入式C++驱动开发
开发语言·c++·算法
Storynone2 小时前
【Day29】LeetCode:62. 不同路径,63. 不同路径 II,343. 整数拆分,96. 不同的二叉搜索树
python·算法·leetcode
小O的算法实验室2 小时前
2025年SEVC SCI2区,基于强化学习辅助粒子群算法的污水处理厂进水流量估算及出水调度问题研究,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
ShineWinsu2 小时前
sqlite api
数据结构
2301_785403522 小时前
代码随想录算法营总结
算法
yeflx2 小时前
三维空间坐标转换早期笔记
人工智能·算法·机器学习
格林威3 小时前
C++ 工业视觉实战:Bayer 图转 RGB 的 3 种核心算法(邻域平均、双线性、OpenCV 源码级优化)
开发语言·c++·人工智能·opencv·算法·计算机视觉·工业相机