链表的深拷贝:带随机指针的链表的深拷贝

带随机指针的链表,说实话,是面试里的一道经典题。我第一次遇到它是在一个嵌入式项目的内存管理模块里——当时需要把一个复杂的数据结构完整复制一份,每个节点除了指向下一个节点,还有一个随机指针指向任意节点。嗯,浅拷贝肯定不行,因为两个链表会共享同一块内存,改一个就全乱了。

今天我们就来聊聊,怎么实现这种链表的深拷贝。

什么是带随机指针的链表?

先看定义。每个节点有三个字段:

  • val:节点存储的数据
  • next:指向下一个节点
  • random:指向链表中任意一个节点(也可以是 NULL)

结构体定义长这样:

struct Node {
    int val;
    struct Node *next;
    struct Node *random;
};

你想想看,如果只是简单地把每个节点复制一遍,然后让新节点的 next 指向新复制的下一个节点——那 random 指针怎么办?它指向的是原链表中的节点,而不是新链表中的节点。这就是问题的核心。

深拷贝的难点在哪?

说白了,难点就一个:如何建立原节点和新节点之间的映射关系

举个例子:原链表的节点 A 的 random 指向节点 C。复制之后,新链表的节点 A' 的 random 应该指向 C',而不是 C。但我们怎么知道 C' 是哪个?

核心思路:先复制节点,再建立映射,最后修正 random 指针。

三种常见解法

我个人习惯把解法分成三类,每种都有它的适用场景。

1. 哈希表法(最直观)

用一个哈希表,把原节点和新节点一一对应存起来。第一次遍历复制节点,第二次遍历修正 random 指针。

struct Node* copyRandomList(struct Node* head) {
    if (!head) return NULL;

    // 第一步:复制每个节点,存入哈希表
    struct Node* cur = head;
    struct Node* dummy = (struct Node*)malloc(sizeof(struct Node));
    struct Node* prev = dummy;
    // 这里省略哈希表实现,实际可用数组或uthash

    while (cur) {
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = cur->val;
        newNode->next = NULL;
        newNode->random = NULL;
        // 存入哈希表:map[cur] = newNode
        prev->next = newNode;
        prev = newNode;
        cur = cur->next;
    }

    // 第二步:修正 random 指针
    cur = head;
    struct Node* newCur = dummy->next;
    while (cur) {
        if (cur->random) {
            newCur->random = map[cur->random]; // 从哈希表取出对应新节点
        }
        cur = cur->next;
        newCur = newCur->next;
    }

    struct Node* result = dummy->next;
    free(dummy);
    return result;
}

我的经验:哈希表法最容易理解,适合笔试或面试时先讲思路。但嵌入式环境里哈希表实现可能比较重,内存开销也大。

2. 原地复制法(空间 O(1))

这个方法我特别喜欢,因为它不需要额外空间。思路是:

  1. 在原链表的每个节点后面插入一个复制节点
  2. 利用原节点的 random 指针,修正复制节点的 random
  3. 把两个链表拆开
struct Node* copyRandomList(struct Node* head) {
    if (!head) return NULL;

    // 第一步:在每个原节点后面插入复制节点
    struct Node* cur = head;
    while (cur) {
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = cur->val;
        newNode->next = cur->next;
        newNode->random = NULL;
        cur->next = newNode;
        cur = newNode->next;
    }

    // 第二步:修正复制节点的 random 指针
    cur = head;
    while (cur) {
        if (cur->random) {
            cur->next->random = cur->random->next;
        }
        cur = cur->next->next;
    }

    // 第三步:拆开两个链表
    struct Node* newHead = head->next;
    struct Node* oldCur = head;
    struct Node* newCur = newHead;
    while (oldCur) {
        oldCur->next = oldCur->next->next;
        newCur->next = newCur->next ? newCur->next->next : NULL;
        oldCur = oldCur->next;
        newCur = newCur->next;
    }

    return newHead;
}

注意:拆链表时一定要小心,别把原链表的结构破坏了。我曾经在调试时忘了恢复原链表的 next 指针,结果后续操作全乱了。

3. 递归法(代码简洁,但栈风险)

递归法用了一个哈希表来缓存已经复制过的节点,避免重复复制。

struct Node* copyNode(struct Node* node, struct Node** cache) {
    if (!node) return NULL;
    if (cache[node]) return cache[node]; // 已经复制过,直接返回

    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->val = node->val;
    cache[node] = newNode; // 先存入缓存

    newNode->next = copyNode(node->next, cache);
    newNode->random = copyNode(node->random, cache);
    return newNode;
}

我的建议:递归法代码很优雅,但链表一长就容易栈溢出。嵌入式环境里我基本不用,除非你能确定链表长度很小。

三种方法对比

方法 时间复杂度 空间复杂度 适用场景
哈希表法 O(n) O(n) 通用,容易理解
原地复制法 O(n) O(1) 内存受限的嵌入式环境
递归法 O(n) O(n) + 栈空间 链表较短时

核心流程图

下面这张图展示了原地复制法的完整流程,我建议你仔细看看第二步——那是整个算法的精髓。

原地复制法流程图 第一步:在每个原节点后插入复制节点 A B C A' B' C' 第二步:利用原节点 random 修正复制节点 random A B C A' B' C' random random' 第三步:拆开两个链表,恢复原链表结构 原链表:A → B → C 新链表:A' → B' → C'

避坑指南

我曾经在项目里用原地复制法时踩过一个坑:拆链表的时候,如果原链表后面还有其他数据,一定要先把原链表的 next 恢复好。否则后续遍历原链表就会出问题。

另外,内存泄漏也是个常见问题。复制过程中如果 malloc 失败,记得把已经分配的内存释放掉。我一般会写一个辅助函数来统一处理。

总结一下:带随机指针的链表深拷贝,核心就是「先复制节点,再建立映射」。哈希表法最直观,原地复制法最省空间,递归法最简洁。根据你的实际场景选一个就好。

嗯,今天就聊到这。代码写完了记得跑几个测试用例,特别是 random 指向自身或者形成环的情况——这些边界条件最容易出问题。