【数据结构与算法】剑指 Offer 35. 复杂链表的复制

简介: 1.将链表节点和next复制(也就是上面普通链表的复制)2.遍历旧的链表查看每个节点N对应的random指向的节点M,然后从旧链表的首部开始遍历, 看什么时候走几步能到M, 得到步数3.根据步数在新链表里查找M* 节点, 并让节点 N* 指向 M*

剑指 Offer 35. 复杂链表的复制

👉题目:请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next指针指向下一个节点,

还有一个 random 指针指向链表中的任意节点或者 null。👈


🌟普通链表

Node(int value) {
        val = value;
        next = NULL;
}




🎄题目中定义的复杂链表

Node(int _val) {
        int val = _val;
        Node *next = NULL;
        Node *random = NULL;
}


🌈难点:


random的复制, 因为新的链表对应的random不是相邻的节点, 是随机的节点, 因此将这种关系复制给新链表就不是一件很容易的事.


💡法一:暴力(保底法)

1.将链表节点和next复制(也就是上面普通链表的复制)

2.遍历旧的链表查看每个节点N对应的random指向的节点M,然后从旧链表的首部开始遍历, 看什么时候走几步能到M, 得到步数

3.根据步数在新链表里查找M* 节点, 并让节点 N* 指向 M*


👉注意: 找M*节点时, 不是按照值找的(a == b), 而是按照地址来找的(*a == *b), 因为在遇到真正的M之前可能会遇到与它值相同的节点, 所以按地址值查找


✨复杂度的分析 : 对于一个含有 N 个节点的链表, 由于定位每一个节点的 random 都需要从链表头节点开始经过 O(n)的时间复杂度才能找到, 因此这种方法的时间复杂度是 O(n^2)

Node* copyRandomList(Node* head) {
    if(head == nullptr) return nullptr;
    Node* cur = head;   // cur相当于头节点的副本
    Node* temp = nullptr;
    Node* res;
    res = temp;
    // 1. 复制各节点
    while(cur != nullptr) {
        Node *ptmp = new Node(cur->val);
        ptmp->next = nullptr;
        temp->next = ptmp;
        temp = temp->next;
        cur = cur->next;
    }
  res = res->next;
  Node *ans = res;
    // 2. 遍历旧链表得到步数并进行复制
    cur = head;
    while(cur != nullptr) {
    temp = cur.random;
    Node *pp = head;
    int count = 0;
    while(pp != nullptr && pp != temp) {
      pp = pp->next;
      count++;
    }
    pp = res;
    while(pp != nullptr && --count != 0) {
      pp = pp->next;
    }
    res->random = pp;
    cur = cur->next;
    res = res->next;
  }
    return res;      // 返回新链表头节点
}


💡法二 : 哈希法(空间换时间)

优化 : 找链表的节点, 找这个行为可以用什么来优化

🎉没错就是哈希🎉

哈希可以进行很快的查找, 时间复杂度为 O(1)

体现了空间换时间



将旧链表的节点和新链表的节点插入到哈希表中得到对应关系 <N, N*>, 则通过N->random得到的节点M, 通过哈希就可以得到 M* , 岂不美哉.


✨复杂度的分析 : 相较于法一, 我们用空间换时间, 需要一个大小为 O(n)的哈希表, 等同于用 O(n)的空间将时间复杂度从 O(n^2)降低到 O(n)

Node* copyRandomList(Node* head) {
    if(head == nullptr) return nullptr;
    Node* cur = head;
    unordered_map<Node*, Node*> map;
    // 1. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
    while(cur != nullptr) {
        map[cur] = new Node(cur->val);
        cur = cur->next;
    }
    cur = head;
    // 2. 构建新链表的 next 和 random 指向
    while(cur != nullptr) {
        map[cur]->next = map[cur->next];
        map[cur]->random = map[cur->random];
        cur = cur->next;
    }
    // 3. 返回新链表的头节点
    return map[head];
}


💡法三 : 叠加复制 + 截取

🌈有辅助空间的写法, 那么就有 没有辅助空间的写法

一种巧妙但又不容易发现的方法, 也是需要我们去积累的方法叠加复制 + 截取

具体步骤是:

1.在每一个节点的后面加一个节点

2.复制新的指针的时候 next 正常前后连接

3.新节点的 random 根据前一个节点的random 得到并向后移动一个单位


具体如图所示:



✨复杂度的分析 : 相对前两种方法, 我们用空间复杂度和时间复杂度都是最优的, 时间上我们需要三次遍历, 第一次建立新的节点, 第二次复制random, 最后再删除奇数位上的节点即可, 空间上, 只需要一个新链表的节点的空间时间复杂度也为 O(n)


Node* copyRandomList(Node* head) {
    if(head == nullptr) return nullptr;
    Node* cur = head;   // cur相当于头节点的副本
    // 1. 复制各节点,并构建拼接链表
    while(cur != nullptr) {
        Node* tmp = new Node(cur->val);
        tmp->next = cur->next;
        cur->next = tmp;
        cur = tmp->next;
    }
    // 2. 构建各新节点的 random 指向
    cur = head;
    while(cur != nullptr) {
        if(cur->random != nullptr)
            cur->next->random = cur->random->next;
        cur = cur->next->next;
    }
    // 3. 拆分两链表
    cur = head->next;
    Node* pre = head, *res = head->next;
    while(cur->next != nullptr) {
        pre->next = pre->next->next;
        cur->next = cur->next->next;
        pre = pre->next;
        cur = cur->next;
    }
    pre->next = nullptr; // 单独处理原链表尾节点
    return res;      // 返回新链表头节点
}



🏁总结 : 将复杂问题, 分为多个小问题, 一个一个进行解决, 首先是节点和next的复制, 比较容易, 但是刚开始会发现random的复制不是很容易, 这个时候你就应该想, 那种数据结构可以将查找的速率优化呢, 是二分和哈希, 然后你进行分析, 发现这里不具有二段性, 所以你使用哈希进行优化, 如此你得到了O(n)的时间复杂度, 但是无巧不成题, 你还是会再进行深究, 那么你会发现更深的结构, 那就是链表自身的结构, 链表的连接方法就产生了这种巧妙的方法拼接+拆分, 于是乎你再进行整改, 得到几乎很快的方法, 然后你将这道题完全解决, 🎉从此在算法界里又离蒟蒻远了那么一点点, 🌈离算法岗又近了那么一个点.


相关文章
|
2天前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
76 64
|
14天前
|
Java
java数据结构,双向链表的实现
文章介绍了双向链表的实现,包括数据结构定义、插入和删除操作的代码实现,以及双向链表的其他操作方法,并提供了完整的Java代码实现。
java数据结构,双向链表的实现
|
2天前
|
缓存
数据结构之 - 链表数据结构详解: 从基础到实现
数据结构之 - 链表数据结构详解: 从基础到实现
17 6
|
2天前
|
存储
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)(一)
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)
|
1月前
|
存储 Java 索引
【数据结构】链表从实现到应用,保姆级攻略
本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。
49 10
【数据结构】链表从实现到应用,保姆级攻略
|
1天前
|
存储 C语言
数据结构--双链表
数据结构--双链表
|
2天前
|
存储
【初阶数据结构】深入解析带头双向循环链表:探索底层逻辑
【初阶数据结构】深入解析带头双向循环链表:探索底层逻辑
|
2天前
|
存储 缓存
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)(二)
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)
|
2月前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
20 0
|
2月前
|
存储 测试技术
【初阶数据结构篇】双向链表的实现(赋源码)
因为头结点的存在,plist指针始终指向头结点,不会改变。
23 0