剑指 Offer 24. 反转链表

题目

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

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000

其中,链表的结构为:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
思路
1、用头插法实现链表的反转

刚开始看到这道题的思路,就是最简单、最暴力的解法----再定义一个头节点,用头插法实现链表的反转。但是,在实现的过程中,花费了不小功夫来调bug。主要的原因,还是读题目不够清晰!

题目说的是:输入一个链表的头节点,要求实现链表的反转。
但是,需要注意的是,给的示例为:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
给出的示例表明,其实这个 输入的“链表的头节点”,并不是真正的头节点 => 实际上,输入的链表,是没有头节点的,所以输入的head,是该链表中,第一个节点不假,但是该节点的val属性,即:在链表不空的情况下,head.val 存储的是有数据的!!!!!!

所以,代码实现为:

class Solution {
    public ListNode reverseList(ListNode head) {
        /*
        实际上进行的操作:将head对应的单链表,进行遍历,并将遍历到的节点按照头插法,插入到新的头节点reverseHead对应的单链表中
        注意:输入的单链表实际上是不带头节点的,head就是实际存储数据的第一个节点
        */
        if(head == null){
            //一个节点都没有的情况下,直接返回原来的头节点即可
            return head;
        }

        //定义新的"头节点reverseHead,
        ListNode reverseHead = new ListNode(head.val);
        //定义一个辅助节点,用于帮助在head对应的链表中进行遍历
        //cur实际上对应的是head对应的单链表中,head节点后的第一个节点
        ListNode cur = head.next;
        ListNode next = null;//用于:帮助实现head所在链表中的cur的后移

        //循环遍历的过程
        while(cur != null ){//当head对应的链表中,除了head节点之外还有其他节点时
            //暂存当前节点cur的下一个节点
            next = cur.next;
            //实现在reverseHead的链表中的头插法"reverseHead是真正的带头节点"
            cur.next = reverseHead;
            reverseHead = cur;
            //在head的链表中,要进行实现遍历的必要操作------后移
            cur = next;//这里一定要有next这个变量的存在,不然在进行过reverseHead中的头插操作后,会找不到head中原来的cur后面的那个节点
        }
        return reverseHead;
    }
}
2、手动实现相邻节点直接链接的反向(也叫双指针法)
----时间复杂度、空间复杂度,均要比 1 的直接头插法实现的方式好一些
  • 使用双指针(实际上是有三个指针,一个用来遍历,其余两个都是作为辅助指针)来解题:
  • 定义三个指针:pre、cur、next,其中cur用来遍历,而pre.next==cur,next=cur.next;
  1. 将原来的单链表的整个方向进行转换,即:

    • 将pre.next=cur改为:pre=cur.next;
    • 然后,将三个指针整体往正在进行的遍历的方向移动一个单位:
    • ---->pre=cur,cur=next,next=next.next
  2. 简言之,pre指针是用来辅助 实现链表方向转换的一个变量;

    • =>而next指针是用来辅助遍历的一个变量(因为一个节点的next域只能存储一个其他节点,所以,如果cur建立起一个新的next域指向的pre的链连接,就需要一个next指针来保存该cur节点原来的next域指向的节点);
    • =>而cur指针,就是用来进行遍历的指针,它既可以用来建立逆向的链表连接,又可以作为三个指针向前遍历的一个参考基准
    • 而控制循环进行的条件为:cur!=null,即:pre要作为最终返回的“头节点”

所以,代码实现为:

class Solution {
    public ListNode reverseList(ListNode head) {
        //链表一个节点都没有或者只有一个节点,没有反转的必要
        if( head==null || head.next==null){
            return head;
        }
        //此时链表至少有两个节点
        ListNode cur = head.next;
        ListNode pre = head;
        ListNode next = null ;
        //head节点最终在实现反转之后是链表的末尾
        head.next = null;
        while(cur != null){//当cur是最后一个节点时,循环终止
            //在建立新的链接之前,先保留下来原来的链接
            next = cur.next;
            //建立反向的链接
            cur.next = pre;
            //跟新剩余的2个指针
            pre = cur;
            cur = next;
        }
        return pre;
    }
}
3、 使用递归的方法实现反转
----是一个提升吧,强大又难搞的递归:
   *
  1. 可以这样对问题进行分析:把大问题划分成小问题:

    • 1)假设链表的其余部分已经实现反转,那么如何实现前面的部分的反转?
      ----cur.next.next = cur;
    • 2)递归的结束条件是什么?
      ----cur.next == null;
    • 3)实现递归的方法中,如何确定1)、2)两步骤的顺序?
      实际上是跟树的遍历的顺序是一样的,应该在递归结束之后,回溯的过程中,实现链接方向的改变,不然的话,会由于链接方向的改变,导致还没有遍历到的节点丢失(因为没有节点链接向该节点而丢失)
    • 4)需要注意的是,要将最初的head节点的next域的值,设置为null。不然会出现 环。
      Attention:回溯时,每到一个节点,就将该节点当做新链表的尾节点来进行处理
    • 5)以上几步的顺序为:2)=> 1) => 4)
  2. 其实,当该链表 不包含节点 或者 只包含一个节点的情况下,可以不用调用递归,在刚开始,直接return head。

    • ------可以和递归的终止条件 合并

所以,代码实现为:

class Solution {
    public ListNode reverseList(ListNode head) {
       
        if(head == null || head.next == null){
            //1、没有节点或者只包含一个节点,不用反转
            //2、也作为递归的终止条件
            return head;
        }
		//当前节点不是递归终点时,进行的操作
        ListNode newHead = reverseList(head.next);//递归调用
        head.next.next = head;
        head.next = null;
        return newHead;
       
    }
}

总结一下数据结构中链表问题 解决时候的真理:

涉及到链表的操作,一定要在纸上把过程先画出来,再写程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值