备战Java后端【Day5】

本文介绍了如何通过递归和双指针技巧解决Java后端中的链表问题,包括按k个一组反转链表和判断回文链表。递归反转链表时,通过找到子问题并递归处理,最后合并结果。判断回文链表则利用了链表的后序遍历特性,甚至能在空间复杂度为O(1)的情况下完成。此外,还提供了具体的LeetCode题目25和234的解决方案。
摘要由CSDN通过智能技术生成

备战Java后端【Day5】

数据结构1-1

链表5


  • k个一组反转链表
  • 回文链表

学习目标

  • 复习Day1至Day4内容
  • 递归根据k个一组反转链表
  • 回文链表

学习内容

单链表k个一组反转操作

  • 思想: 链表是一种兼具递归和迭代性质的数据结构,因此k个一组进行链表反转具有递归性质。

比如说我们对这个链表调用 reverseKGroup(head, 2),即以 2 个节点为一组反转链表:
在这里插入图片描述
如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫子问题。
在这里插入图片描述
我们可以把原先的 head 指针移动到后面这一段链表的开头,然后继续递归调用 reverseKGroup(head, 2),因为子问题(后面这部分链表)和原问题(整条链表)的结构完全相同,这就是所谓的递归性质。

发现了递归性质,就可以得到大致的算法流程:

  1. 先反转以 head 开头的 k 个元素
    在这里插入图片描述

  2. 将第 k+1 个元素作为 head 递归调用 reserseKGroup 函数
    在这里插入图片描述

  3. 将上述两个过程的结果连接起来
    在这里插入图片描述

整体思路就是这样了,最后一点值得注意的是,递归函数都有个 base case,对于这个问题是什么呢?

题目说了,如果最后的元素不足 k 个,就保持不变。这就是 base case,待会会在代码里体现。


回文链表

回文串,是指一个正读和反读都一样的字符串。顾名思义,回文链表就是一个正向遍历和反向遍历得到结果都一样的链表。
寻找回文串的核心思想是从中心向两端扩展:

// 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串
String palindrome(String s, int left, int right) {
    // 防止索引越界
    while (left >= 0 && right < s.length()
            && s.charAt(left) == s.charAt(right)) {
        // 双指针,向两边展开
        left--;
        right++;
    }
    // 返回以 s[left] 和 s[right] 为中心的最长回文串
    return s.substring(left + 1, right);
}

因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入 1 和 r。

而判断一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要 双指针技巧,从两端向中间逼近即可:

boolean isPalindrome(String s) {
    // 一左一右两个指针相向而行
    int left = 0, right = s.length() - 1;
    while (left < right) {
        if (s.charAt(left) != s.charAt(right)) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键。

  • 一、判断回文单链表
    输入一个单链表的头结点,判断这个链表中的数字是不是回文,函数签名如下:
boolean isPalindrome(ListNode head);

比如说:

输入: 1->2->null
输出: false
输入: 1->2->2->1->null
输出: true

然而对于单链表来说,单链表无法倒着遍历,无法使用双指针技巧。
那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。
其实,借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表,下面对于二叉树的几种遍历方式,如下所示:

void traverse(TreeNode root) {
    // 前序遍历代码
    traverse(root.left);
    // 中序遍历代码
    traverse(root.right);
    // 后序遍历代码
}

链表兼具递归结构,树结构不过是链表的衍生。那么,链表其实也可以有前序遍历和后序遍历

void traverse(ListNode head) {
    // 前序遍历代码
    traverse(head.next);
    // 后序遍历代码
}

如果我想正序打印链表中的 val 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:

/* 倒序打印单链表中的元素值 */
void traverse(ListNode head) {
    if (head == null) return;
    traverse(head.next);
    // 后序遍历代码
    print(head.val);
}

可以稍作修改,模仿双指针实现回文判断的功能:

// 左侧指针
ListNode left;

boolean isPalindrome(ListNode head) {
    left = head;
    return traverse(head);
}

boolean traverse(ListNode right) {
    if (right == null) return true;
    boolean res = traverse(right.next);
    // 后序遍历代码
    res = res && (right.val == left.val);
    left = left.next;
    return res;
}

实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然,无论造一条反转链表还是利用后序遍历,算法的时间和空间复杂度都是 O(N)。

  • 二、优化空间复杂度
    1、先通过 双指针技巧 中的快慢指针来找到链表的中点:
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
// slow 指针现在指向链表中点

在这里插入图片描述
2、如果 fast 指针没有指向 null,说明链表长度为奇数,slow 还要再前进一步:

if (fast != null)
    slow = slow.next;

在这里插入图片描述
3、从 slow 开始反转后面的链表,现在就可以开始比较回文串了:

ListNode left = head;
ListNode right = reverse(slow);

while (right != null) {
    if (left.val != right.val)
        return false;
    left = left.next;
    right = right.next;
}
return true;

在这里插入图片描述
至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中 reverse 函数很容易实现。

boolean isPalindrome(ListNode head) {
    ListNode slow, fast;
    slow = fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    
    if (fast != null)
        slow = slow.next;
    
    ListNode left = head;
    ListNode right = reverse(slow);
    while (right != null) {
        if (left.val != right.val)
            return false;
        left = left.next;
        right = right.next;
    }
    
    return true;
}

ListNode reverse(ListNode head) {
    ListNode pre = null, cur = head;
    while (cur != null) {
        ListNode next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。这种解法虽然高效,但破坏了输入链表的原始结构。其实这个问题很好解决,关键在于得到 pq 这两个指针位置:
在这里插入图片描述
这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序

p.next = reverse(q);
  • 首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。

  • 具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。

学习时间

2022年5月30日

  • 下午5点-下午6点
  • 晚上7点-晚上9点

学习产出

  • 本文档链表基础知识和基础操作
  • 力扣25题,K个一组翻转链表
  • 力扣234题,回文链表

今日刷题

25.K个一组翻转链表
在这里插入图片描述
输入 headreverseKGroup 函数能够把以 head 为头的这条链表进行翻转。我们要充分利用这个递归函数的定义,把原问题分解成规模更小的子问题进行求解。

  • 1、先反转以 head 开头的 k 个元素。
    在这里插入图片描述
  • 2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数。
    在这里插入图片描述
  • 3、将上述两个过程的结果连接起来。
    在这里插入图片描述
    最后函数递归完成之后就是这个结果,完全符合题意:
    在这里插入图片描述
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        if (head == null) return null;
        // 区间 [a, b) 包含 k 个待反转元素
        ListNode a, b;
        a = b = head;
        for (int i = 0; i < k; i++) {
            // 不足 k 个,不需要反转,base case
            if (b == null) return head;
            b = b.next;
        }
        // 反转前 k 个元素
        ListNode newHead = reverse(a, b);
        // 递归反转后续链表并连接起来
        a.next = reverseKGroup(b, k);
        return newHead;
    }

    /* 反转区间 [a, b) 的元素,注意是左闭右开 */
    ListNode reverse(ListNode a, ListNode b) {
        ListNode pre, cur, nxt;
        pre = null;
        cur = a;
        nxt = a;
        // while 终止的条件改一下就行了
        while (cur != b) {
            nxt = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nxt;
        }
        // 返回反转后的头结点
        return pre;
    }
}

234.回文链表
在这里插入图片描述
这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。
那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。
更聪明一些的办法是借助双指针算法:

  • 1、先通过 双指针技巧 中的快慢指针来找到链表的中点:

在这里插入图片描述

  • 2、如果 fast 指针没有指向 null,说明链表长度为奇数,slow 还要再前进一步:
    在这里插入图片描述
  • 3、从 slow 开始反转后面的链表,现在就可以开始比较回文串了:
    在这里插入图片描述
class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode slow, fast;
        slow = fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        if (fast != null)
            slow = slow.next;

        ListNode left = head;
        ListNode right = reverse(slow);
        while (right != null) {
            if (left.val != right.val)
                return false;
            left = left.next;
            right = right.next;
        }

        return true;
    }

    ListNode reverse(ListNode head) {
        ListNode pre = null, cur = head;
        while (cur != null) {
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值