深拷贝的常用写法

1. 使用JSON.parse来实现

cloneObj = JSON.parse(JSON.stringify(obj))

为什么它可以实现深拷贝

  1. 序列化和反序列化

    • JSON.stringify(obj):将对象转换为 JSON 字符串。这个过程会递归遍历对象的所有属性,并将其转换为字符串形式。
    • JSON.parse(string):将 JSON 字符串解析回一个新对象。这个过程会创建一个新的对象,并填充从字符串中解析出来的属性。
  2. 断开引用关系

    • 由于经过了序列化(stringify)和反序列化(parse),原对象和新对象之间没有任何引用关系,因此修改新对象不会影响原对象,反之亦然。

局限性

  • 无法处理函数 :函数在序列化时会被忽略,因此新对象中不会包含任何函数属性。
    const obj = { a: 1, b: function() {} };
    const clone = JSON.parse(JSON.stringify(obj));
    console.log(clone.b); // undefined

  • 无法处理复杂的数据类型:例如 DateRegExpMapSet 等复杂数据类型,在序列化时会被转换为普通对象或丢失信息。
    const obj = { date: new Date(), regExp: /abc/ };
    const clone = JSON.parse(JSON.stringify(obj));
    console.log(clone.date instanceof Date); // false
    console.log(clone.regExp instanceof RegExp); // false

  • Symbol 类型Symbol 类型的键在序列化时会被忽略。
    const obj = { [Symbol('a')]: 'value' };
    const clone = JSON.parse(JSON.stringify(obj));
    console.log(clone); // {}
    
     

 2.手动递归拷贝

为了克服上述局限性,可以使用更健壮的深拷贝方法,例如:

手动递归拷贝:如你之前提供的代码片段所示,通过递归遍历对象并复制每个属性。

function deepClone(obj){
    let cloneObj;
    // 判断当输入的数据是简单数据类型时,直接复制
    if(obj && typeof obj !== 'object'){
        cloneObj = obj;
    }
    // 当输入的数据是对象或者数组时
    else if(obj && typeof obj === 'object'){
        // 检测输入的数据是数组还是对象
        cloneObj = Array.isArray(obj) ? [] : {};

        // 变量数据对象
        for(let key in obj){
            // 判断对象是否存在key属性
           //判断是否存在自定义属性 hasownProperty
            if(obj.hasOwnProperty(key)){
                if(obj[key] && typeof obj[key] === 'object'){
                    // 若当前元素类型为对象时,递归调用
                    cloneObj[key] = deepClone(obj[key]);
                }
                // 若当前元素类型为基本数据类型
                else{
                    cloneObj[key] = obj[key];
                }
            }
        }
    }
    return cloneObj;
}

 可以进一步改进:

使用 Object.keys() 或 Object.entries():这些方法只会遍历对象自身的可枚举属性,从而避免了显式检查 hasOwnProperty

if (obj && typeof obj == 'object') {
    const cloneObj = Array.isArray(obj) ? [] : {};
    
    Object.keys(obj).forEach(key => {
        if (typeof obj[key] === 'object' && obj[key] !== null) {
            cloneObj[key] = deepClone(obj[key]);
        } else {
            cloneObj[key] = obj[key];
        }
    });

    return cloneObj;
}

<think>我们讨论的是单向链表的深拷贝,特别是不带随机指针的情况。根据引用[2]和[3],有一种空间复杂度为$O(1)$的方法,即通过在原链表的每个节点后插入新节点,然后设置新节点的指针,最后分离链表。这种方法不需要哈希表,因此节省空间。 步骤: 1. 遍历原链表,对每个节点,创建一个新节点(拷贝节点),将其插入到原节点之后。这样原链表结构变为:A -> A' -> B -> B' -> ...。 2. 遍历新组合链表,设置新节点的指针(如果有随机指针,则设置随机指针,但这里题目是单向链表,所以只有next指针,所以这一步实际上在单向链表中可以省略,因为下一步在分离链表时设置next指针即可。但是注意,如果链表节点中有其他指针(如随机指针),则需要在这一步设置。而本题是简单的单向链表,只有next指针,所以在分离链表时同步设置next指针即可)。 3. 将新节点从组合链表中分离出来,形成新链表,同时恢复原链表。 注意:题目要求是单向链表,节点结构通常为: struct Node { int val; struct Node *next; }; 因此,我们只需要拷贝val和next指针。 具体步骤(无随机指针): 第一步:复制节点并插入原节点后面 第二步:因为只有next指针,所以我们不需要额外设置新节点的next指针?实际上,在第一步插入后,新节点的next已经指向原节点的下一个节点(即原节点的next),但这样并不正确,因为新链表的下一个节点应该是原节点的下一个节点的复制节点(即原节点next的next)。所以我们在分离链表时,需要同时设置新链表的next。 实际上,在第一步插入后,原链表结构为: A -> A' -> B -> B' -> C -> C' -> ... -> NULL 因此,当我们分离时,新链表的每个节点(A',B',C')的next应该是当前节点的next的next(如果存在的话)。 第三步:分离链表 - 初始化两个指针,一个指向原链表的头(用于恢复原链表),一个指向新链表的头(即原链表头的next)。 - 遍历组合链表,将原节点的next指向新节点的next(即原节点的原下一个节点),同时将新节点的next指向下一个新节点(即原节点的原下一个节点的next,如果存在的话)。 具体代码实现: 注意:由于是深拷贝,我们不仅要创建新节点,还要保持原链表的结构不被破坏(除非题目允许修改原链表,否则应该恢复原链表)。 但是,引用[2]和[3]中的方法修改了原链表,最后又恢复了原链表。所以我们也可以这样做。 代码: 1. 如果链表为空,返回NULL。 2. 第一步:复制节点并插入 struct Node* cur = head; while (cur) { struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); newNode->val = cur->val; newNode->next = cur->next; // 将新节点插入到当前节点后面 cur->next = newNode; cur = newNode->next; // 移动到原链表的下一个节点 } 3. 第二步:设置新节点的next指针(实际上在分离过程中设置,所以这一步不需要额外操作) 4. 第三步:分离链表 struct Node* newHead = head->next; // 新链表的头 struct Node* oldCur = head; // 用于遍历原链表 struct Node* newCur = newHead; // 用于遍历新链表 while (oldCur) { // 恢复原链表的next:原节点指向新节点的next(即原节点的原下一个节点) oldCur->next = oldCur->next->next; // 设置新链表的next:如果当前新节点后面还有原节点,则新节点的next指向下一个新节点(即下一个原节点的next) if (newCur->next) { // 注意:newCur->next实际上是原链表中下一个原节点(因为当前结构是原节点->新节点->原节点...) newCur->next = newCur->next->next; } else { newCur->next = NULL; } // 移动指针 oldCur = oldCur->next; if (oldCur) { // 如果原链表还有下一个节点,则新链表的下一个节点就是oldCur的next newCur = oldCur->next; } } 但是,上述分离过程在移动指针时,当oldCur指向原链表的下一个节点(即oldCur=oldCur->next)后,此时oldCur已经是原链表的下一个原节点,那么newCur应该是该原节点的next(即下一个新节点)。但是循环条件中,我们每次处理一对节点(一个原节点和一个新节点),然后移动到下一对。 另一种更清晰的分离方式(参考引用[2]和[3]): struct Node* cur = head; struct Node* copyHead = NULL; struct Node* copyTail = NULL; // 先设置新链表的头 if (cur) { copyHead = cur->next; copyTail = copyHead; cur->next = copyTail->next; // 恢复原链表当前节点的next(跳过新节点) cur = cur->next; // 移动到原链表的下一个节点 } while (cur) { // 此时cur是原链表的第二个节点(原始顺序) copyTail->next = cur->next; // 新链表的下一个节点就是当前原节点的下一个节点(即新节点) copyTail = copyTail->next; // 新链表尾指针移动 cur->next = copyTail->next; // 恢复原链表当前节点的next(指向原链表的下一个节点,跳过新节点) cur = cur->next; // 原链表指针后移 } 或者更简洁的写法(参考引用[3]): pcur = head; Node* phead = pcur->next; // 新链表头 Node* ptail = phead; while (pcur) { // 恢复原链表当前节点的next(指向新节点的next,即原链表的下一个原始节点) pcur->next = ptail->next; pcur = pcur->next; // 原链表指针后移 if (pcur) { // 新链表当前节点的next指向下一个新节点(即原链表当前节点的next的next,因为原链表当前节点后面跟着新节点,然后才是下一个原节点) ptail->next = pcur->next; ptail = ptail->next; } } 因此,我们可以采用如下代码: 注意:这种方法修改了原链表,但在最后又恢复了原链表。 完整代码(C语言): */ #include <stdlib.h> typedef struct Node { int val; struct Node *next; } Node; Node* deepCopy(Node* head) { if (head == NULL) { return NULL; } // 第一步:复制每个节点,并插入到原节点后面 Node* cur = head; while (cur != NULL) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->val = cur->val; newNode->next = cur->next; cur->next = newNode; cur = newNode->next; // 移动到原链表的下一个节点 } // 第二步:分离两个链表 Node* newHead = head->next; // 新链表的头 Node* oldCur = head; // 原链表当前节点 Node* newCur = newHead; // 新链表当前节点 while (oldCur != NULL) { // 恢复原链表的next指针:原节点指向新节点的next(即原链表的下一个原节点) oldCur->next = oldCur->next->next; // 移动原链表指针到下一个原节点 oldCur = oldCur->next; // 设置新链表的next指针:如果还有下一个原节点,则新节点的next指向下一个原节点的next(即下一个新节点) if (oldCur != NULL) { newCur->next = oldCur->next; newCur = newCur->next; } else { newCur->next = NULL; // 最后一个新节点 } } return newHead; } /* 测试代码(这里不提供main函数,仅作为示例) 注意:这种方法在分离链表后,原链表被恢复,新链表被正确链接。 但是,如果链表节点中除了next指针还有随机指针呢?那么我们需要在第二步中设置随机指针(在分离之前)。不过本题是单向链表,没有随机指针,所以不需要。 然而,如果我们考虑一个扩展,即节点中可能包含随机指针,那么我们需要在分离之前设置随机指针。设置方法如下(参考引用[2]和[3]): 在第一步插入节点后,遍历链表,对于每一个原节点cur,其拷贝节点为cur->next,那么拷贝节点的随机指针应该指向原节点随机指针指向的节点的拷贝(即如果cur->random不为空,则cur->next->random = cur->random->next;否则为NULL)。然后再进行分离。 因此,回答用户问题:C语言实现单向链表深拷贝,我们可以使用上述空间复杂度为$O(1)$的方法。 另外,如果题目要求不能修改原链表,我们就必须使用哈希表(即空间复杂度$O(n)$)的方法,但题目没有说明,这里我们使用修改原链表再恢复的方法。 总结: - 时间复杂度:$O(n)$,三次遍历(插入、设置随机指针(如果有的话)、分离)。 - 空间复杂度:$O(1)$(除了新链表占用的空间,我们只使用了常数个额外指针)。 注意:深拷贝新链表需要分配新内存,所以空间复杂度至少为$O(n)$,但这里我们说的额外空间复杂度是$O(1)$。 最后,生成相关问题。 </think>### C语言实现单向链表深拷贝的方法 在C语言中实现单向链表深拷贝,核心挑战是创建完全独立的节点副本并正确重建指针关系。以下是两种常用方法: #### 方法1:哈希表映射法(空间复杂度 $O(n)$) ```c #include <stdlib.h> typedef struct Node { int val; struct Node* next; } Node; Node* deepCopy(Node* head) { if (!head) return NULL; // 创建哈希表(简化实现,实际需用哈希库) Node* map[1000] = {0}; // 假设节点数<1000 Node* cur = head; // 第一遍:创建新节点并建立映射 while (cur) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->val = cur->val; newNode->next = NULL; map[(int)cur] = newNode; // 原节点地址作key cur = cur->next; } // 第二遍:连接指针 cur = head; while (cur) { if (cur->next) map[(int)cur]->next = map[(int)cur->next]; cur = cur->next; } return map[(int)head]; } ``` **原理**: 1. 使用哈希表存储`原节点地址->新节点地址`的映射 2. 两次遍历完成节点复制和指针重建 3. 时间复杂度:$O(n)$,空间复杂度:$O(n)$ #### 方法2:节点交错法(空间复杂度 $O(1)$) ```c Node* deepCopy(Node* head) { if (!head) return NULL; // 第一步:在原节点后插入拷贝节点 Node* cur = head; while (cur) { Node* copy = (Node*)malloc(sizeof(Node)); copy->val = cur->val; copy->next = cur->next; cur->next = copy; cur = copy->next; } // 第二步:分离链表(重建原链表和新链表) Node* newHead = head->next; cur = head; while (cur) { Node* copy = cur->next; cur->next = copy->next; // 恢复原链表连接 if (copy->next) copy->next = copy->next->next; // 建立新链表连接 cur = cur->next; } return newHead; } ``` **原理**: 1. 复制节点插入原节点后(`A→A'→B→B'`) 2. 分离时直接定位新节点位置 3. 时间复杂度:$O(n)$,空间复杂度:$O(1)$(额外空间) 4. 保持原链表结构不变[^2][^3] ### 关键注意事项 1. **内存管理**: - 每次`malloc()`必须对应`free()` - 拷贝失败时应回滚已分配内存 2. **特殊处理**: ```c // 空链表处理 if (head == NULL) return NULL; // 单节点处理 if (head->next == NULL) { Node* copy = (Node*)malloc(sizeof(Node)); copy->val = head->val; copy->next = NULL; return copy; } ``` 3. **循环链表检测**: ```c // 快慢指针检测环 Node* slow = head; Node* fast = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; if (slow == fast) break; // 存在环 } ``` ### 性能对比 | 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------|------------|------------|------------------------| | 哈希表映射 | $O(n)$ | $O(n)$ | 带随机指针的链表 | | 节点交错法 | $O(n)$ | $O(1)$ | 内存受限环境[^3] | > **深拷贝验证方法**: > ```c > int isDeepCopy(Node* orig, Node* copy) { > while (orig && copy) { > if (orig == copy) return 0; // 地址相同为浅拷贝 > if (orig->val != copy->val) return 0; > orig = orig->next; > copy = copy->next; > } > return (orig == NULL && copy == NULL); > } > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值