云计算百科
云计算领域专业知识百科平台

当链表相遇:从相交节点到回文对称的算法交响曲》

在算法的幽深回廊中,链表以简洁的指针结构编织着数据的迷宫。当两条孤链在内存中悄然交汇,或是节点序列暗藏回文密码时,一场关于空间与时间的精密博弈便悄然上演。今日,我们以双指针为刃,剖开「链表相交」的拓扑谜题;再借快慢指针之力,破解「回文链表」的对称玄机——不写一行代码,纯享算法设计的思维风暴。

问题一:链表相交——内存拓扑的邂逅
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

▍ 问题本质与约束
给定两条无环单链表 headA 和 headB,若存在共享子链(物理内存相同),需返回相交节点;否则返回 null。关键约束:

  • 拓扑守恒:不可修改链表结构(如反转、破坏链接)。

  • 时空敏感:节点数达 3×1043×104,需规避 O(n2)O(n2) 暴力扫描。

▍ 算法策略:双指针的路径调和

  • 哈希表法(空间换时间)

    • 原理:遍历链表A,节点存入哈希表;遍历链表B,首个存在于哈希表的节点即为交点。

    • 复杂度:时间 O(m+n)O(m+n),空间 O(m)O(m) 或 O(n)O(n)。

    • 缺陷:空间复杂度违反本题理想解要求(尽管题目未明示,但最优解应追求 O(1)O(1) 空间)。

  • 双指针路径调和法(最优解)

    • 核心洞察:若两链相交,头节点到交点的路径差可被消除。

    • 操作流程:

      • 指针 pA 遍历链表A,指针 pB 遍历链表B。

      • 当 pA 达尾节点时,跳至 headB 继续;当 pB 达尾节点时,跳至 headA 继续。

      • 若相交,pA 和 pB 必在交点相遇(路径长度均为 m+n−km+n−k,k为共享链长度);若不相交,最终同步指向 null。

    • 复杂度:时间 O(m+n)O(m+n),空间 O(1)O(1)。

    ▶ 示例推演(示例1)

    A: 4→1→8→4→5
    B: 5→0→1→8→4→5

    • pA 路径:4→1→8 (交点) →4→5→跳至B头→5→0→1→8 (相遇点)

    • pB 路径:5→0→1→8 (交点) →4→5→跳至A头→4→1→8 (相遇点)

  • 题目程序:

    #include <stdio.h>
    #include <stdlib.h>

    // 定义链表节点结构
    struct ListNode {
    int val; // 节点存储的值
    struct ListNode *next; // 指向下一个节点的指针
    };

    // 查找两个链表相交节点的函数
    struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    // 初始化两个指针,分别指向两个链表的头部
    struct ListNode *pA = headA;
    struct ListNode *pB = headB;

    // 当两个指针不相交时继续遍历
    while (pA != pB) {
    // 如果pA到达链表A末尾,则跳转到链表B头部
    pA = (pA == NULL) ? headB : pA->next;
    // 如果pB到达链表B末尾,则跳转到链表A头部
    pB = (pB == NULL) ? headA : pB->next;
    }
    // 返回相交节点(若不相交则返回NULL)
    return pA;
    }

    // 创建新节点的辅助函数
    struct ListNode *createNode(int val) {
    // 动态分配内存创建新节点
    struct ListNode *node = (struct ListNode *)malloc(sizeof(struct ListNode));
    node->val = val; // 设置节点值
    node->next = NULL; // 初始化next指针为NULL
    return node; // 返回新节点指针
    }

    // 释放链表内存的辅助函数
    void freeList(struct ListNode *head) {
    // 遍历链表释放所有节点内存
    while (head != NULL) {
    struct ListNode *temp = head; // 临时保存当前节点
    head = head->next; // 移动到下一个节点
    free(temp); // 释放当前节点内存
    }
    }

    int main() {
    // ========== 创建测试链表 ==========
    // 创建链表A:4 → 1 → 8 → 4 → 5
    struct ListNode *headA = createNode(4);
    headA->next = createNode(1);
    headA->next->next = createNode(8);
    headA->next->next->next = createNode(4);
    headA->next->next->next->next = createNode(5);

    // 创建链表B:5 → 0 → 1
    struct ListNode *headB = createNode(5);
    headB->next = createNode(0);
    headB->next->next = createNode(1);
    // 让链表B的第三个节点指向链表A的第三个节点(值为8的节点)
    headB->next->next->next = headA->next->next;

    // ========== 查找相交节点 ==========
    struct ListNode *intersection = getIntersectionNode(headA, headB);

    // ========== 打印结果 ==========
    if (intersection != NULL) {
    printf("Intersected at node with value: %d\\n", intersection->val);
    } else {
    printf("No intersection found\\n");
    }

    // ========== 清理内存 ==========
    // 注意:只释放链表A和B的非共享部分
    struct ListNode *temp = headB->next->next->next; // 保存共享节点地址
    headB->next->next->next = NULL; // 断开链表B与共享部分的连接
    freeList(headA); // 释放链表A(包含共享部分)
    freeList(headB); // 释放链表B(不包含共享部分)

    return 0;
    }

    输出结果:


    问题二:回文链表——时空约束下的对称审判
    编写一个函数,检查输入的链表是否是回文的。但必须用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题

    ▍ 问题本质与约束
    验证单链表是否为回文序列,要求:

    • 时间复杂度 O(n)O(n):禁止多次遍历。

    • 空间复杂度 O(1)O(1):禁用栈、数组等额外存储。

    ▍ 算法策略:快慢指针与局部反转

  • 快慢指针定位中点

    • 快指针 fast 每次移动两步,慢指针 slow 移动一步。

    • 当 fast 达末尾时,slow 指向链表中点(偶数为左中点)。

  • 反转后半链表

    • 从中点开始反转后半部分节点,生成新链 reversed。

    • 关键操作:修改节点指针方向,不占用额外空间。

  • 双指针比对序列

    • 指针 p1 从头部开始,p2 从 reversed 头部开始。

    • 依次比对 p1.val 与 p2.val,若全部相同则为回文。

  • 恢复链表结构(可选)

    • 再次反转后半部分,恢复原始拓扑。

  • ▶ 示例推演(示例2)

    原链: 1 → 2 → 2 → 1
    1. 快慢指针:slow 停于第2个节点(值2)
    2. 反转后半:2 → 1 反转为 1 → 2
    3. 比对:
    – p1: 1 → 2 → …
    – p2: 1 → 2 → …
    – 两两相同,确认为回文。

    题目程序:

    #include <stdio.h> // 包含标准输入输出库
    #include <stdlib.h> // 包含内存分配等标准库函数

    // 定义链表节点结构
    struct ListNode {
    int val; // 节点存储的整数值
    struct ListNode *next; // 指向下一个节点的指针
    };

    // 反转链表的函数
    struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode *prev = NULL; // 前驱节点指针,初始化为NULL
    struct ListNode *curr = head; // 当前节点指针,初始化为链表头

    // 遍历链表进行反转
    while (curr != NULL) {
    struct ListNode *nextTemp = curr->next; // 临时保存下一个节点
    curr->next = prev; // 当前节点指向前驱节点(反转操作)
    prev = curr; // 前驱节点前进到当前节点
    curr = nextTemp; // 当前节点前进到下一个节点
    }
    return prev; // 返回反转后的新头节点
    }

    // 检查链表是否为回文的函数
    int isPalindrome(struct ListNode* head) {
    // 处理空链表或单节点链表的情况
    if (head == NULL || head->next == NULL) {
    return 1; // 空链表或单节点链表默认为回文
    }

    // 初始化快慢指针
    struct ListNode *slow = head; // 慢指针,每次移动一步
    struct ListNode *fast = head; // 快指针,每次移动两步

    // 使用快慢指针找到链表中点
    while (fast->next != NULL && fast->next->next != NULL) {
    slow = slow->next; // 慢指针前进一步
    fast = fast->next->next; // 快指针前进两步
    }

    // 反转链表的后半部分
    struct ListNode *secondHalf = reverseList(slow->next); // 从中点后开始反转
    struct ListNode *firstHalf = head; // 前半部分从头开始
    struct ListNode *secondHalfCopy = secondHalf; // 保存反转后链表的头指针(用于后续恢复)

    int result = 1; // 结果标志,初始假设是回文

    // 比较前半部分和反转后的后半部分
    while (secondHalf != NULL) {
    if (firstHalf->val != secondHalf->val) {
    result = 0; // 发现不匹配,设置结果为非回文
    break; // 提前结束比较
    }
    firstHalf = firstHalf->next; // 前半部分指针前移
    secondHalf = secondHalf->next; // 后半部分指针前移
    }

    // 恢复链表原始结构(再次反转后半部分)
    slow->next = reverseList(secondHalfCopy); // 将反转的后半部分再次反转恢复

    return result; // 返回最终结果
    }

    // 创建新节点的辅助函数
    struct ListNode* createNode(int val) {
    struct ListNode *node = (struct ListNode*)malloc(sizeof(struct ListNode)); // 分配内存
    node->val = val; // 设置节点值
    node->next = NULL; // 初始化next指针为NULL
    return node; // 返回新节点指针
    }

    // 释放链表内存的辅助函数
    void freeList(struct ListNode* head) {
    while (head != NULL) {
    struct ListNode *temp = head; // 临时保存当前节点
    head = head->next; // 移动到下一个节点
    free(temp); // 释放当前节点内存
    }
    }

    // 打印链表的辅助函数
    void printList(struct ListNode* head) {
    struct ListNode *current = head; // 当前节点指针
    while (current != NULL) {
    printf("%d -> ", current->val); // 打印节点值
    current = current->next; // 移动到下一个节点
    }
    printf("NULL\\n"); // 打印链表结束标记
    }

    int main() {
    // ====== 测试用例1:回文链表(奇数个节点)1->2->3->2->1 ======
    struct ListNode *head1 = createNode(1);
    head1->next = createNode(2);
    head1->next->next = createNode(3);
    head1->next->next->next = createNode(2);
    head1->next->next->next->next = createNode(1);

    printf("链表1: ");
    printList(head1); // 打印链表

    // 检查是否为回文并打印结果
    if (isPalindrome(head1)) {
    printf("链表1是回文链表\\n");
    } else {
    printf("链表1不是回文链表\\n");
    }

    // 验证链表结构已恢复
    printf("恢复后的链表1: ");
    printList(head1);
    freeList(head1); // 释放内存

    // ====== 测试用例2:回文链表(偶数个节点)1->2->2->1 ======
    struct ListNode *head2 = createNode(1);
    head2->next = createNode(2);
    head2->next->next = createNode(2);
    head2->next->next->next = createNode(1);

    printf("\\n链表2: ");
    printList(head2); // 打印链表

    // 检查是否为回文并打印结果
    if (isPalindrome(head2)) {
    printf("链表2是回文链表\\n");
    } else {
    printf("链表2不是回文链表\\n");
    }

    // 验证链表结构已恢复
    printf("恢复后的链表2: ");
    printList(head2);
    freeList(head2); // 释放内存

    // ====== 测试用例3:非回文链表 1->2->3->4 ======
    struct ListNode *head3 = createNode(1);
    head3->next = createNode(2);
    head3->next->next = createNode(3);
    head3->next->next->next = createNode(4);

    printf("\\n链表3: ");
    printList(head3); // 打印链表

    // 检查是否为回文并打印结果
    if (isPalindrome(head3)) {
    printf("链表3是回文链表\\n");
    } else {
    printf("链表3不是回文链表\\n");
    }

    // 验证链表结构已恢复
    printf("恢复后的链表3: ");
    printList(head3);
    freeList(head3); // 释放内存

    // ====== 测试用例4:单个节点链表 ======
    struct ListNode *head4 = createNode(1);

    printf("\\n链表4: ");
    printList(head4); // 打印链表

    // 检查是否为回文并打印结果
    if (isPalindrome(head4)) {
    printf("链表4是回文链表\\n");
    } else {
    printf("链表4不是回文链表\\n");
    }
    freeList(head4); // 释放内存

    return 0; // 程序正常结束
    }

    输出结果:


    双雄争锋:算法策略对比图谱
    维度链表相交(双指针路径调和)回文链表(快慢指针+局部反转)
    核心目标 探测拓扑交点 验证序列对称性
    关键约束 保持原始结构 O(n)O(n) 时间 + O(1)O(1) 空间
    核心算法 双指针路径长度补偿 快慢指针分区 + 局部反转
    时间复杂度 O(m+n)O(m+n) O(n)O(n)
    空间复杂度 O(1)O(1) O(1)O(1)
    思维突破点 消除路径差的艺术 时间与空间的共生优化

    总结:指针美学的二重奏
    • 链表相交 是空间协作的哲学:通过路径长度的动态调和,双指针在内存迷宫中实现精准定位,揭示了算法中的“相遇是精心设计的必然”。

    • 回文链表 是时间与对称的共舞:在苛刻的时空约束下,快慢指针切割时间,局部反转重塑空间,将对称验证转化为一场原地操作的芭蕾。

    链表的灵魂在指针,算法的精髓在权衡。当相交问题以路径长度弥合差异,回文问题以局部反转重构对称时,我们看到的不仅是解法的巧妙,更是计算机科学中时空统一性的绝美体现。


    博客结语

    下一次当你面对纠缠的链表时,请记住:指针不仅是地址的向导,更是时空的诗人——它们在内存的舞台上,用相遇讲述协作,用反转书写对称。算法之美,尽在这动静之间。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 当链表相遇:从相交节点到回文对称的算法交响曲》
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!