判断回文链表

我们之前有两篇文章写了回文串和回文序列相关的问题。

寻找回文串的核心思想是从中心向两端扩展:

string palindrome(string& s, int l, int r) {
    // 防止索引越界
    while (l >= 0 && r < s.size()
            && s[l] == s[r]) {
        // 向两边展开
        l--; r++;
    }
    // 返回以 s[l] 和 s[r] 为中心的最长回文串
    return s.substr(l + 1, r - l - 1);
}

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

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

bool isPalindrome(string s) {
    int left = 0, right = s.length - 1;
    while (left < right) {
        if (s[left] != s[right])
            return false;
        left++; right--;
    }
    return true;
}

以上代码很好理解吧,因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键

下面扩展这一最简单的情况,来解决:如何判断一个「单链表」是不是回文。

一、判断回文单链表

输入一个单链表的头结点,判断这个链表中的数字是不是回文:

/**
 * 单链表节点的定义:
 * public class ListNode {
 *     int val;
 *     ListNode next;
 * }
 */

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函数很容易实现:

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),已经是最优的了。

我知道肯定有读者会问:这种解法虽然高效,但破坏了输入链表的原始结构,能不能避免这个瑕疵呢?

其实这个问题很好解决,关键在于得到p, q这两个指针位置:

这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序:

p.next = reverse(q);

篇幅所限,我就不写了,读者可以自己尝试一下。

三、最后总结

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

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

我最近精心制作了一份电子书《labuladong的算法小抄》,分为【动态规划】【数据结构】【算法思维】【高频面试】四个章节,共 60 多篇原创文章,绝对精品!限时开放下载,在我的公众号 labuladong 后台回复关键词【pdf】即可免费下载!

欢迎关注我的公众号 labuladong,技术公众号的清流,坚持原创,致力于把问题讲清楚!

原文地址:https://www.cnblogs.com/labuladong/p/12320497.html

时间: 2024-10-28 15:25:16

判断回文链表的相关文章

如何判断一个单向链表是否为回文链表(Palindrome Linked List)

题目:给定一个单向链表,判断它是不是回文链表(即从前往后读和从后往前读是一样的).原题见下图,还要求了O(n)的时间复杂度O(1)的空间复杂度. 我的思考: 1,一看到这个题目,大脑马上想到的解决方案就是数组.遍历链表,用数组把数据存下来,然后再进行一次遍历,同时用数组反向地与之比较,这样就可以判断是否回文.这个方法时间复杂度是O(n),达到了要求,然而空间复杂度显然不满足要求.所以,开数组这一类的方法显然不是最佳的. 2,既然要满足O(1)的空间复杂度,我就想到了用一个变量来存储这些数据,恰好

判断一个链表是不是回文链表

第一种方法: 额外空间复杂度O(N) ,遍历链表时,将元素入栈,再次遍历时,从栈中弹出元素,比较两者的大小,就可以判断是不是回文链表第二种方法:利用快慢指针,先找到链表的中间位置,然后反转链表的后半部分,再分别从链表两头遍历比较大小,最后将链表恢复为原始结构 public class PalindromeLinkedList { public static void main(String[] args) { Node head = new Node(1); head.next = new No

ACM之判断回文数

题目如下 这道题比较简单,先上Python代码感受一下,就一行搞定: #判断回文数 def isPalindrom(x):     return  str(x) == str(x)[::-1] 这种方法虽然简单,但是耗时比较长.再用Java解决一下看看 方法一 显然负数不可能是回文数,区间[0,9]的整数肯定是回文数,所以把这些确定的条件先进行判断 将整数的每一位放在链表中,然后将链表逆序,比较逆序链表与顺序链表元素是否一样,一样则是回文数,否则不是 代码如下: public class Pal

【leetcode 简单】 第六十七题 回文链表

请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? # Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def r

LeetCode 234——回文链表

1. 题目 请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? 2. 思路 此题可以看做是反转链表 和 链表中间结点 的结合. 定义快慢两个指针,寻找中间结点,同时在慢指针移动的过程中反转前半部分子链表.当找到中间结点时,再分别向前向后比较前后两个子链表的每一个结点值是否相同. 偶数结点情况如下 此时,我们分别得到了以

Leetcode 234. 回文链表(进阶)

题目描述: 请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? 解法一:(空间复杂度O(n)) 遍历一遍链表压栈,借助栈把链表倒序,然后依次比较"原链表元素"和"新栈中元素",如果都相等则返回true,否则返回false. 这样简单粗暴,代码的主体包含在解法二中了,这里不列出了.

领扣(LeetCode)回文链表 个人题解

请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? 一个最暴力的做法,遍历一次,内容保存在数组内,然后判断是否回文. 遇到一个比较严重的问题需要记录一下,使用Vector类时,设定的类型为Integer,必须使用Equals函数来判断两数是否相等,使用==则不行.奇怪的是,只有部分判断是错误的. 查阅百度,发现了问题所在:

234. 回文链表

请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 1 import java.util.ArrayList; 2 3 public class PalindromeLinkedList { 4 static class ListNode { 5 int val; 6 ListNode next; 7 ListNode(int x) { 8 val = x; 9 } 10 } 11 12 //

【Leetcode链表】回文链表(234)

题目 请判断一个链表是否为回文链表. 示例 1: 输入: 1->2 输出: false 示例 2: 输入: 1->2->2->1 输出: true 进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? 解答 两种方法: 遍历链表,用数组存值,再比较.时间复杂度O(n),空间复杂度O(n) 指针法:找到中点,反转中点之后的链表,再比较.时间复杂度O(n),空间复杂度O(1) 通过代码如下: # Definition for singly-linked list.