标签搜索

目 录CONTENT

文章目录

最长回文子串和回文链表.md

小小城
2021-08-22 / 0 评论 / 0 点赞 / 6 阅读 / 3,587 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-02,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

回文子串和回文链表

@[toc]

一、最长回文子串

1.题目描述

在这里插入图片描述

2.分析

  •  对于这个问题,我们首先应该思考的是,给一个字符串 s,如何在 s 中找到一个回文子串?
  •  有一个很有趣的思路:既然回文串是一个正着反着读都一样的字符串,那么如果我们把 s 反转,称为 s',然后在 s 和 s' 中寻找最长公共子串,这样应该就能找到最长回文子串。
  •  比如说字符串 abacd,反过来是 dcaba,它的最长公共子串是 aba,也就是最长回文子串。
  •  但是这个思路是错误的,比如说字符串 aacxycaa,反转之后是 aacyxcaa,最长公共子串是 aac,但是最长回文子串应该是 aa。
  •  虽然这个思路不正确,但是这种把问题转化为其他形式的思考方式是非常值得提倡的。
  •  下面,就来说一下正确的思路,如何使用 双指针
  •  寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串。对于最长回文子串,就是这个意思:
for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    更新答案

但是呢,我们刚才也说了,回文串的长度可能是奇数也可能是偶数,如果是 abba这种情况,没有一个中心字符,上面的算法就没辙了。所以我们可以修改一下:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    找到以 s[i] 和 s[i+1] 为中心的回文串
    更新答案

3.代码实现

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);
}

为什么要传入两个指针 l 和 r 呢?因为这样实现可以同时处理回文串长度为奇数和偶数的情况:

for 0 <= i < len(s):
    # 找到以 s[i] 为中心的回文串
    palindrome(s, i, i)
    
    # 找到以 s[i] 和 s[i+1] 为中心的回文串
    palindrome(s, i, i + 1)
    更新答案

longestPalindrome完整代码

string longestPalindrome(string s) 
{
    string res;
    for (int i = 0; i < s.size(); i++) 
    {
        // 以 s[i] 为中心的最长回文子串
        string s1 = palindrome(s, i, i);
        
        // 以 s[i] 和 s[i+1] 为中心的最长回文子串
        string s2 = palindrome(s, i, i + 1);
        
        // res = longest(res, s1, s2)
        res = res.size() > s1.size() ? res : s1;
        res = res.size() > s2.size() ? res : s2;
    }
    return res;
}
  •  至此,这道最长回文子串的问题就解决了,时间复杂度 O(N^2),空间复杂度 O(1)。
  •  值得一提的是,这个问题可以用动态规划方法解决时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。

二、判断回文链表

1.问题描述

在这里插入图片描述

2. 分析

普通暴力求解就不说了!!!

  •  借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表,下面来具体聊聊。
  •  对于二叉树的几种遍历方式,我们再熟悉不过了:
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);
}
  •  说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能

3.代码

// 左侧指针
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)。下面我们想想,能不能不用额外的空间,解决这个问题呢?

4.优化

  •  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)。

三、回文子串

1.问题描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.代码

#include <bits/stdc++.h>
using namespace std;

int palindrome(string str,int l,int r)
{
    int count = 0;
    while(l >= 0 && r < str.size() && str[l] == str[r])
    {
        count++;
        l--;
        r++;
    }
    
    return count;
}

int main()
{
    string str;
    int ret = 0;
    while(cin>>str)
    {
        for(int i = 0;i < str.size();i++)
        {
            int num1 = palindrome(str, i, i);
            
            int num2 = palindrome(str, i, i + 1);
            
            ret += (num1 + num2);
        }
        
        cout<<ret<<endl;
    }
    return 0;
}
0

评论区