当前位置: 首页 > news >正文

反转链表I和II(迭代和递归)

前言

有句话叫做:如果面试官跟你看顺眼的话,就给你出一道反转链表,否则就出一道 hard。
所以反转链表不能不会吧,要不面试官想要你都没有机会了。

206. 反转链表

class Solution {
    public ListNode reverseList(ListNode head) {

    }
}

迭代

迭代就是遍历链表,需要有个 cur 指针在链表上游走。
先思考一般情况,假设遍历到某一节点,应该做什么?

如上图所示,应该修改其 next 指针,指向前一个节点。由于单链表的性质,无法从当前节点获取前一个节点的指针,所以需要有个 pre 指针记录当前节点的前一个节点。把 cur 的指针修改后,就无法获取其后一个节点了,因此需要有个 next 指针保存 cur 的下一个节点。更新 precur 指针,让它们后移一位即可。

class Solution {
    public ListNode reverseList(ListNode head) {
        // pre记录cur的前一个节点,cur是当前遍历到的节点
        ListNode pre = null, cur = head;

        // 遍历链表一般用while循环
        while(cur != null){
            // 保存cur的下一个节点
            ListNode next = cur.next;
            // 修改cur指针,指向它前一个节点
            cur.next = pre;
            // 更新pre、cur, 同时后移一位
            pre = cur;
            cur = next;
        }

        // 此时cur为null, pre指向最后一个节点
        return pre;
    }
}

递归

递归有两个性质:

  1. 边界条件
  2. 转化为子问题调用自身

先思考第二个问题,如何转化为子问题调用自身?
转化为子问题也就是缩小问题的规模,如果 head 指向的链表有 n 个节点,那么 head.next 指向的链表就有 n - 1 个节点,因此 head.next 就是一个子问题。为了更好地复用递归函,需要明确递归函数的定义,这里递归函数的定义是,输入一个单链表的头指针,返回该链表反转后的链表的头指针。

调用 head.next 的示意图如下:

根据递归函数的定义,它计算后返回值的情况如下所示:

接下来,就是要处理 head 和 reverseList(head.next) 的关系了,这个例子中就是 1 号节点和后面部分的关系。这里有两步,第一步,让 2 号节点指向 1 号节点;第二步,让 1 号节点指向 null。第一步翻译成代码就是head.next.next = head,第二步翻译成代码就是head.next = null

现在,第二个问题就处理完了,代码如下

class Solution {
    public ListNode reverseList(ListNode head) {
        // 1、边界条件
        // if(){

        // }

        // 2、转化为子问题调用自身
        ListNode last = reverseList(head.next);
        // 修改head和后面部分指针的关系
        head.next.next = head;
        head.next = null;
        return last;
    }
}

对于递归函数,我觉得第二部分是核心,边界条件是特殊情况,可以先不考虑。自己去想可能会遗漏一些情况,这时如果允许执行测试用例,可以借助执行测试用例来帮助我们补全边界条件。

比如,这里我暂时不清楚边界条件,先执行测试用例,看看它会报什么错,然后根据它的错误提示来写边界条件。

直接执行上面的代码,会报这样的错

这样就知道 head 不能为空,加上条件

// 1、边界条件
if(head == null){
    return head;
}

再执行代码,会报如下的错

这样就知道 head.next 不能为空,加上条件

// 1、边界条件
if(head == null || head.next == null){
	return head;
}

完整代码

class Solution {
    public ListNode reverseList(ListNode head) {
        // 1、边界条件
        if(head == null || head.next == null){
            return head;
        }

        // 2、转化为子问题调用自身
        ListNode last = reverseList(head.next);
        // 修改head和后面部分指针的关系
        head.next.next = head;
        head.next = null;
        return last;
    }
}

92. 反转链表 II

迭代

希望复用反转单链表的代码,这样就需要把需要反转的部分单独抽取出来,反转好了之后再拼接回去。因此需要有 4 个指针,leftNode 和 rightNode 分别指向需要反转部分的首尾节点,pre 指针指向 leftNode 的前一个,cur 指针指向 rightNode 的后一个,pre 和 cur 的作用是为了保存断开链表和拼接链表的位置。4 个指针的位置关系如下图所示。
在这里插入图片描述
确定 4 个指针的位置并不复杂,循环指定步数即可。确定好 4 个指针的位置后,就是断开 pre 和 leftNode、rightNode 和 cur,调用之前的反转单链表将中间部分反转,最后再拼接回去即可。

完整代码

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        // 虚拟头节点
        ListNode dummy = new ListNode(-1, head);
        ListNode pre = dummy, cur = null;
        ListNode leftNode = null, rightNode = head;

        // 从虚拟头节点走left-1步,来到left前一个节点
        for(int i = 0; i < left - 1; i++){
            pre = pre.next;
        }
        // left节点在pre节点下一个
        leftNode = pre.next;
        // 从头节点走right-1步,来到right节点
        for(int i = 0; i < right - 1; i++){
            rightNode = rightNode.next;
        }
        // cur节点在right节点下一个
        cur = rightNode.next;

        // 断开指针
        pre.next = null;
        rightNode.next = null;
        // 反转between
        reverseList(leftNode);
        // 拼接指针
        pre.next = rightNode;
        leftNode.next = cur;

        return dummy.next;
    }

    // 反转单链表
    private ListNode reverseList(ListNode head){
        ListNode pre = null, cur = head;
        while(cur != null){
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

递归

首先需要实现反转前 n 个节点的函数 ListNode reverseN(ListNode head, int n)

比如要反转如下链表的前 3 个节点
在这里插入图片描述
那就是要反转以 head.next 开头的链表的前 2 个节点
在这里插入图片描述
如果reverseN函数写的是正确的,那么按照它的定义,返回值如下
在这里插入图片描述
这里又涉及到 head 和 reverseN(head.next, n - 1) 指针关系的处理,和递归反转单链表是类似的,第一步是让 2 号节点指向 1 号节点,第二步略有不同,反转单链表是直接让1号节点指向 null,而这里应该让 1 号节点指向 4 号节点,也就是不需要反转的部分。因此,就需要一个指针记录 4 号节点的位置。

reverseN代码如下

	// 记录不用反转的第一个节点
    ListNode suc = null;

    private ListNode reverseN(ListNode head, int n){
        // 1、边界条件
        if(n == 1){
            suc = head.next;
            return head;
        }
        // 2、转化为子问题调用自身
        ListNode last = reverseN(head.next, n - 1);
        // 修改head和后面部分指针的关系
        head.next.next = head;
        head.next = suc;
        return last;
    }

接下来实现reverseBetween
当 left 等于 1 的时候,就转化成了reverseN的问题。
对于 head,反转 left 和 right;对于 head.next,应该反转 left - 1 和 right - 1;对于head.next.next,应该反转 left - 2 和 right - 2…

所以,reverseBetween函数代码如下

public ListNode reverseBetween(ListNode head, int left, int right) {
        if(left == 1){
            return reverseN(head, right);
        }
        head.next = reverseBetween(head.next, left - 1, right - 1);
        return head;
    }

完整代码

class Solution {

    public ListNode reverseBetween(ListNode head, int left, int right) {
        // 当left==1, 就转化成立reverseN问题
        if(left == 1){
            return reverseN(head, right);
        }
        head.next = reverseBetween(head.next, left - 1, right - 1);
        return head;
    }

    // 记录不用反转的第一个节点
    ListNode suc = null;

    private ListNode reverseN(ListNode head, int n){
        // 1、边界条件
        if(n == 1){
            suc = head.next;
            return head;
        }
        // 2、转化为子问题调用自身
        ListNode last = reverseN(head.next, n - 1);
        // 修改head和后面部分指针的关系
        head.next.next = head;
        head.next = suc;
        return last;
    }
}

参考资料

递归魔法:反转单链表

相关文章:

  • (附源码)ssm教材管理系统 毕业设计 011229
  • 系统运维管理小记
  • 最全解决方式java.net.BindException Address already in use JVM_Bind
  • Java配置40-配置ELK+Kafka集成
  • 《论文阅读》MOJITALK: Generating Emotional Responses at Scale
  • 统计字符出现次数(区分大小写和不区分大小写两种方式)
  • Java开发之高并发必备篇(二)——线程为什么会不安全?
  • 低代码技术研究路径解读|低代码的产生不是偶然,是数字技术发展的必然
  • OPT华东产业园封顶,机器视觉产业版图再扩大!
  • 多肽RGD修饰乳清白蛋白/肌白蛋白/豆清白蛋白/蓖麻蛋白/豌豆白蛋白1b ( PA1b)纳米粒(实验原理)
  • 基于Mybatis-Plus扩展批量插入或更新InsertOrUpdateBath
  • LeetCode·701.二叉搜索树中的插入操作·递归
  • 数据结构试题(一)
  • DevSecOps 安全即代码基础指南
  • js字符串对比之localeCompare()方法-对字符串进行排序——大于0-升序、小于0-降序 对el-table的列进行排序sort-change
  • CSS 三角实现
  • input实现文字超出省略号功能
  • Java应用性能调优
  • Laravel 菜鸟晋级之路
  • node入门
  • Spring Cloud中负载均衡器概览
  • 电商搜索引擎的架构设计和性能优化
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 前嗅ForeSpider中数据浏览界面介绍
  • 使用docker-compose进行多节点部署
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 详解NodeJs流之一
  • 用Python写一份独特的元宵节祝福
  • 优化 Vue 项目编译文件大小
  • 大数据全解:定义、价值及挑战
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • ​插件化DPI在商用WIFI中的价值
  • #if #elif #endif
  • #Linux(权限管理)
  • #每天一道面试题# 什么是MySQL的回表查询
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (3)选择元素——(17)练习(Exercises)
  • (java)关于Thread的挂起和恢复
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (算法)Game
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (转) Face-Resources
  • (转)ORM
  • (转)树状数组
  • (转)用.Net的File控件上传文件的解决方案
  • (转载)跟我一起学习VIM - The Life Changing Editor
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .net 怎么循环得到数组里的值_关于js数组
  • .net 桌面开发 运行一阵子就自动关闭_聊城旋转门家用价格大约是多少,全自动旋转门,期待合作...
  • .NET/C# 获取一个正在运行的进程的命令行参数
  • .pyc文件是什么?