【Leetcode -- 剑指OfferDay02 -- 链表】

剑指Offer Day2 – Leetcode练习题

第二天 – 链表
在这里插入图片描述

剑指 Offer 06. 从尾到头打印链表

题目:

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:
输入:head = [1,3,2]
输出:[2,3,1]

题目来源力扣官网

链接: https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof

昨天Day01做了栈和队列有关的题目,一看到这个 逆序,首先就想到用辅助栈存储

但写到一半发现,返回的是int数组啊!!!

再一个个地放到新建的数组中?并没有觉得有多简便……
跟遍历到底部再处理成数组没啥区别,相反感觉浪费了空间。

解决:
遍历到链表底层,记录下链表长度
根据链表长度创建对应大小的数组
从头结点开始遍历,元素的存放顺序是从数组的 右边 到左边

代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        int length = 0;
        ListNode node = head;
        while (node != null) {
            ++ length;
            node = node.next;
        }
        int[] nums = new int[length];
        for (int i = nums.length-1; i >= 0; --i) {
            nums[i] = head.val;
            head = head.next;
        }
        return nums;
    }
}

在这里插入图片描述
事实证明,处理逆序元素,辅助栈并不是万能的,大道至简。
要结合场景使用,有时候遍历反而是最优的解法。

剑指 Offer 24. 反转链表

题目:

输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

题目来自力扣官网

链接:https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/

这是第一道自己很快就做出来的题目!
1、遍历链表,用辅助栈存储值得到逆序输出
2、递归改变各个节点值
提交成功
在这里插入图片描述
代码:

class Solution {


    public ListNode reverseList(ListNode head) {
        Deque<Integer> deque = new ArrayDeque<>();
        ListNode node  = head;
        while (node != null) {
            deque.push(node.val);
            node = node.next;
        }
        change(deque,head);
        return head;
    }

    public  void change(Deque<Integer> deque,ListNode head) {
        if (head != null && deque.size() > 0) {
            head.val = deque.pop();
            if (head.next != null) {
                change(deque,head.next);
            }
        }
    }
}

然而跟评论区大佬一比较发现自己写的太拉了,比如这位:
在这里插入图片描述

太优雅了!

剑指 Offer 35. 复杂链表的复制 (完全不会做)

题目:

请实现 copyRandomList 函数,复制一个复杂链表。
在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

在这里插入图片描述

来源:力扣(LeetCode)

链接:https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof

看了半天才理解题意,题目就是要我们复制这个链表。谜底就在谜面儿上
因为链表的每个节点都存有一个 random的Node,是随机的,我们要考虑指向的问题。
题目难度就在这儿。

链表的遍历是存在顺序的,由于random是随机位置的。
节点被引用前不一定就是完整的。

官方给出两种解法:

  • 哈希+回溯
  • 迭代+节点拆分

(1)哈希+回溯 (算法技巧)

HashMap 记录 原节点(key) + 复制节点(value)

    /**原节点,复制节点**/
    Map<Node, Node> cachedNode = new HashMap<Node, Node>();

    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        if (!cachedNode.containsKey(head)) {//当前节点未进行复制
            Node headNew = new Node(head.val);//复制一个新的节点
            cachedNode.put(head, headNew);//原节点和新节点放进map
            headNew.next = copyRandomList(head.next);//主动去创建新节点的next
            headNew.random = copyRandomList(head.random);//主动去创建新节点的ramdom
        }
        //如果已经存在复制好的节点则直接返回
        return cachedNode.get(head);//返回当前节点的复制节点
    }

利用map确实方便,只是牺牲了内存。
在这里插入图片描述

这里涉及到回溯,因为copyRandomList过程中,虽然调用了多次本身
但在递归地创造新的复制节点,这个过程中,并不是无脑地去遍历,而是带有返回值的。
一旦map中存在当前节点的复制节点,那么直接拿了返回上一层,完全不用再往下递归一步。
这种在步骤中及时返回结果的思想称为回溯

自己调自己,体现的是递归在代码结构上的特点。
回溯的思想,是可以基于递归实现的。

(2)迭代 + 节点拆分 (链表特性)

迭代递归

递归: 不断地往下层探索,直到获取到目标
迭代: 迭代是一个循环,在达到结束状态跳出之前,每次迭代,都是旧值代替新值,然后继续迭代,直到达到结束的状态。

官方的解法是:
逐个地复制新的节点在原节点的后面
每次操作的,都是上一次复制后产生的新链表

相比较Map,原地修改 更考验操作链表的步骤细节

  • 原节点: 原链表中的每个Node
  • 复制节点: 针对每个原节点,复制的一个Node。
    1. 值和原节点的值一样
    2. 复制节点.next指向原节点.next复制节点
    3. 复制节点.ramdom指向原节点.ramdom复制节点
1、复制新的节点 并放在 原节点的后面

因为复制节点的ramdom一定指向另一个复制节点,但那个复制节点并不一定已经创建了。
所以,先把每个原节点复制节点都建起来,并插入到原节点后面,但先不管ramdom

比如:
原链表:1 – > 2 – > 3 – > 4
第一次:1 – > 1 – > 2 – > 3 – > 4
第二次:1 – > 1 – > 2 – > 2 – > 3 – > 4
……
复制完后:
1 – > 1 – > 2 – > 2 – > 3 – > 3 – > 4 – > 4
1 一定在 1 后面,2 一定在 2 的后面……

a作为原节点 ,ab中间,插入了a 的复制节点,b不再是a的next了。

a.next 一定是a复制节点,那么要保持后面的链表元素不丢失。

a复制节点的代替a 指向 b
然后a只要指向 它的复制节点就行了。

a.next.next -> b 
a.next -> a.next.next

同理:
b.next – > b.next.next
b.next.next – > c
c.next – > c.next.next
……

复制新节点next代码:

Node nowNode = head;
while (nowNode != null) {//当前不是最后的null节点
            Node copyNode = new Node(nowNode.val);//复制原节点的值
            copyNode.next = nowNode.next;//代替原节点指向下一个原节点
            nowNode.next = copyNode;	//原节点指向复制节点
            nowNode = nowNode.next.next;//切换到下一个原节点继续进行复制
        }

(注:迭代的循环,并不局限于使用whilefor也可以)

此时形成的组合链表

  • 当前原节点的.next必定是对应的复制节点
  • 当前原节点的下一个的下一个Node,才是下一个原节点
  • 只要原节点不是null,这个原节点的.next必定是对应的复制节点
2、复制随机指针ramdom

我要想要指向某个复制节点,只需要知道它的原节点

复制节点 必定在对应的 原节点 之后。

当前节点的ramdom的next,就是复制节点需要指向的ramdom。

        nowNode = head;
        while (nowNode != null) { //是否为null节点
            if (nowNode.random != null) { //随机指针是否指向某个复制节点 
                nowNode.next.random = nowNode.random.next;
            }
            nowNode = nowNode.next.next;//切换到下一个原节点
        }
3、拆分链表

完成节点及节点指针的复制后,只需要

每个复制节点都摘出来,重新组成一个链表。

拆分链表需要考虑的步骤:

  • 下一个原节点.next.next 重现放在当前原节点的后面.next
  • 判断当前复制节点 是否还存在下一个复制节点
    如何判断?
    此时当前复制节点.next暂时还指向下一个原节点
    下一个原节点不是null,下一个原节点.next自然是对应的复制节点
  • 当前复制节点.next.next自然就是它需要重新指向的.next
        Node copyHead = head.next;
        nowNode = head;
        Node copyNode = head.next;//首个复制节点
        while (nowNode != null) {
            nowNode.next = nowNode.next.next;//下一个原节点重新回到当前原节点的后面
            nowNode = nowNode.next;//切换到下一个原节点
            if (copyNode.next != null) {//复制节点后面是否还存在原节点
                copyNode.next = copyNode.next.next;//复制节点和它后面原节点的复制节点相连
                copyNode = copyNode.next;//切换到下一个复制节点
            }
        }
        return copyHead;

所有:

    public Node copyRandomList(Node head) {
 		if (head == null) {
            return head;
        }
        // 复制节点,先不管ramdom
        Node nowNode = head;
        while (nowNode != null) {
            Node copyNode = new Node(nowNode.val);//建立复制节点
            copyNode.next = nowNode.next;//下一个原节点放在复制节点后面
            nowNode.next = copyNode;//复制节点放在原节点后面
            nowNode = nowNode.next.next;//切换到下一个原节点
        }

        // 针对原节点,完成复制节点的ramdom复制
        nowNode = head;
        while (nowNode != null) {
            if (nowNode.random != null) { // 判断当前节点随机指针是否指向某个复制节点
                nowNode.next.random = nowNode.random.next;
            }
            nowNode = nowNode.next.next;//切换到下一个原节点
        }

        // 复制节点单独摘出来组成新链表
        Node copyHead = head.next;
        nowNode = head;
        Node curCopy = head.next;
        while (nowNode != null) {
            nowNode.next = nowNode.next.next;
            nowNode = nowNode.next;
            if (curCopy.next != null) {
                curCopy.next = curCopy.next.next;
                curCopy = curCopy.next;
            }
        }
        return copyHead;
    }

总结:

三个问题主要还是体现的链表的数据结构的特点。

  • 因为知道头结点就能遍历整个链表,所以在操作对象涉及到整个链表的节点操作步骤可重复,考虑使用迭代递归.
  • 熟悉前后节点 建立链接和断链。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

上岸撒尿的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值