中判断回文的代码_LeetCode刷题实战5:判断回文子串

算法的重要性,我 就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试。 所以,为了提高大家的算法能力,这个公众号后续每天带大家做一道算法题,题目就从LeetCode上面选 ! 今天和大家聊的问题叫做 判断回文子串 ,这道题很有意思,我们先来看题面:

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

https://leetcode.com/problems/longest-palindromic-substring/

翻译

给定一个字符串s,要求它当中的最长回文子串。可以假设s串的长度最大是1000。

样例
Example 1:Input: "babad"Output: "bab"Note: "aba" is also a valid answer.Example 2:Input: "cbbd"Output: "bb"
分析 虽然LeetCode里给这道题的难度是Medium,但实际上并不简单,我们通过自己思考很难想到最佳解法。 我们先把各种算法放在一边,先从最简单的方法开始。最简单的方法当然是暴力枚举,但是这道题和之前的字符串问题不同。我们在暴力枚举的时候,并不需要枚举所有的起始位置,再判断这个子串是否回文。实际上我们可以利用回文串两边相等的性质,直接枚举回文串的中心位置,如果两边相等就往两边延伸。这样我们最多需要枚举n个回文中心,每次枚举最多遍历n次。所以最终的复杂度是O(n²) 。 有经验的同学看到这个复杂度就能反应过来,这明显不是最优解法。但是对于当前问题,暴力枚举固然不是最佳解法,但其实也算得上是不错了,并没有我们想的那么糟糕,不信的话,我们来看另一个看起来高端很多的解法。 动态规划(DP)

这道题中利用回文串的性质还有一个trick,对于一个字符串S,如果我们对它进行翻转,得到S_,显然它当中的回文子串并不会发生变化。所以如果我们对翻转前后的两个字符串求最长公共子序列的话,得到的结果就是回文子串。

算法导论当中对这个问题的讲解是使用动态规划算法,即是对于字符串S中所有的位置i和S_中所有的位置j,我们用一个dp数组记录下以i和j结尾的S和S_的子串能够组成的公共子序列的最大的结果。

显然,对于i=0,j=0,dp[i][j] = 0(假设字符串下标从1开始)

我们写出DP的代码:

for i in range(1, n):  for j in range(1, m):    if S[i] == S_[j]:      dp[i][j] = dp[i-1][j-1] + 1    else:      dp[i][j] = max(dp[i-1][j], dp[i][j-1])

我们不难观察出来,这种解法的复杂度同样是。并且空间复杂度也是O(n),也就是说我们费了这么大劲,并没有起到任何优化。所以从这个角度来看,暴力搜索并不是这题当中很糟糕的解法。

分析到了这里,也差不多了,下面我们直接进入正题,这题的最佳解法,O(n)时间内获取最大回文子串的曼彻斯特算法。

曼切斯特算法 回文串除了我们刚刚提到的性质之外,还有一个性质,就是它分奇偶。简而言之,就是回文串的长度可以是奇数也可以是偶数。如果是奇数的话,那么回文串的回文中心就是一个字符,如果是偶数的话,它的回文中心其实是落在两个字符中间。举个例子:ABA和ABBA都是回文串,前者是奇回文,后者是偶回文。这两种情况不一致,我们想要一起讨论比较困难,为了简化问题,我们需要做一个预处理,将所有的回文串都变成奇回文。怎么做呢,其实很简单,我们在所有两个字符当中都插入一个特殊字符#。比如:abba -> #a#b#b#a#这样一来,回文中心就变成中间的#了。我们再来看原本是奇回文的情况:aba -> #a#b#a#回文中心还是在b上,依然还是奇回文。
预处理的代码:
def preprocess(text):    new_str = '#'    for c in text:        new_str += c + '#'    return new_str

曼切斯特算法用到三个变量,分别是数组p,idx和mr。我们接下来一个一个介绍。

首先是数组radis,它当中存在的是每个位置能构成的最长回文串的半径。注意,这里不是长度,是半径。

我们举个例子:

字符串S     # a # b # b # a #radis      1 2 1 2 5 2 1 2 1
我们先不去想这个radis数组应该怎么求,我们来看看它的性质。首先,i位置的回文串的半径是radis[i],那么它的长度是多少?很简单: radis[2] * 2- 1。那么,这个串中去掉#之后剩下的长度是多少?也就是说预处理之前的长度是多少?答案是radis[i] - 1,推算也很简单,总长度是radis[i] * 2 - 1,其中#比字母的数量多一个,所以原串的长度是(radis[i] * 2 - 1 - 1)/2 = radis[i] - 1。也就是说原串的长度和radis数组就算是挂钩了。idx很好理解,它就是指的是数组当中的一个下标,最后是mr,它是most_right的缩写。它记录的是在当前位置i之前的回文串所向右能延伸到的最远的位置。听起来有些拗口,我们来看个例子:

17514dcec59605be484e41e085c0c69e.png

此时i小于mr,mr对应的回文中心是id。那么i在id的回文范围当中,对于i而言,我们可以获取到它关于id的对称位置:id * 2 - i,我们令它等于i_。知道这个对称的位置有什么用呢?很简单,我们可以快速的确定radis[i]的下界。在遍历到i的时候,我们已经有了i_位置的结果。通过i_位置的结果,我们可以推算i位置的范围。

radis[i]  >= min(radis[i_], mr-i)

为什么是这个结果呢?

我们把情况写全,假设mr-i > radis[i_]。那么i_位置的回文串全部都落在id位置的回文串里。这个时候,我们可以确定radis[i]=radis[i_]。为什么呢?

因为根据对称原理,如果以i为中心的回文串更长的话,我们假设它的长度是radis[i_]+1。会导致什么后果呢?如果这个发生,那么根据关于id的对称性,这个字符串关于id的对称位置也是回文的。那么radis[i_1]也应该是这么多才对,这就构成了矛盾。如果你从文字描述看不明白的话,我们来看下面这个例子:

S:       c a b c b d b c b a cradis:     x_  i_  5   i   x
在这个例子当中,mr-i=5,radis[i_]=2。所以mr - i > radis[i_]。如果radis[i]=3,那么x的位置就应该等于id的位置,同理根据对称性,x_的位置也应该等于id的位置。那么radis[i_]也应该是3。这就和它等于2矛盾,所以这是不可能出现的, 在mr距离足够远的情况下,radis[i_]的值限制了i位置的可能性。我们再来看另一种情况,如果mr - i < radis[i_]时会怎么样呢?在这种情况下,由于mr距离i太近,导致i对称位置的半径无法在i位置展开。但是mr的右侧可能还存在字符,这些字符可以构成新的回文吗?
字符串S     XXXXXXXXSXXXXXXXXXXXXXXXradis        i_    id    i mr
也就是说S[mr+1]会和S[i*2-mr-1]的位置相同吗?其实我们可以不用判断就可以知道答案,答案是不会。举个例子:

cef11a9250830aef764de3a186131d98.png

根据对称性,如果mr+1的位置对于i可以构成新的对称。由于radis[i_] > mr-i,也就是说对于i_位置而言,它的对称范围能够辐射到mr对称点的左边。我们假设这个地方的字母是a,根据对称性,我们可以得出mr+1的位置也应该是a。如此一来,这两个a又能构成新的对称,那么id位置的半径就可以再拓展1, 这就构成了矛盾。所以,这种情况下,由于mr-i的限制,使得radis[i]只能等于mr - i。 那什么情况下i位置的半径可以继续拓展呢?只有mr - i == radis[i_]的时候,id构成的回文串的左侧对于i_可能构不成新的回文,但是右侧却存在这种可能性。举个例子:

8e8ac8bc1e036787ef742c49a8c499b5.png

在上图这个例子当中,i_的位置的回文串向左只能延伸到ml,因为ml-1的位置和关于i_对称的位置不相等。对于mr的右侧,它完全可以既和i点对称,又不会影响raids[id]的正确性。这个时候,我们就可以通过循环继续遍历,拓展i位置的回文串。

整个过程的分析虽然很多,也很复杂,但是写成代码却并不多。

# 初始化idx, mr = 0, 0# 为了防止超界,设置字符串从1开始for i in range(1, n):  # 通过对称性直接计算radis[i]  radis[i] = 1 if mr < i else min(radis[2 * idx - i], mr - i)  # 只有radis[i_] = mr - i的时候才继续往下判断  if radis[2 * idx - i] != mr - i and mr > i:    continue  # 继续往下判断后面的位置  while s[radis[i] + i] == s[i - radis[i]]:    radis[i] += 1  # 更新idx和mr的位置  if radis[i] + i > mr:    mr = radis[i] + i    idx = i

到这里,曼切斯特算法就算是实现完了。虽然我们用了这么多篇幅去介绍它,可是真正写出来,它只有几行代码而已。不得不说,实在是非常巧妙,第一次学习可能需要反复思考,才能真正理解。

不过我们还有一个问题没有解决,为什么这样一个两重循环的算法会是 O(n)的复杂度呢?

想要理解这一点,需要我们抛开所有的虚幻来直视本质。虽然我们并不知道循环进行了多少次,但是有两点可以肯定。通过这两点,我们就可以抓到复杂度的本质。

第一点,mr是递增的,只会变大,不会减小。

第二点,mr的范围是0到n,每次mr增加的数量就是循环的次数。

所以即使我们不知道mr变化了多少次,每次变化了多少,我们依然可以确定,这是一个O(n)的算法。

如果喜欢本文,请顺手点个赞或者转发吧。

上期推文:

LeetCode刷题实战1:在数组上遍历出花样

LeetCode刷题实战2:用链表模拟加法

LeetCode刷题实战3:最长不重复子串

LeetCode刷题实战4:两个正序数组的中位数

e494840c4e7121788a12a766d2dd0122.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 1. 首先,你需要在VS Code安装LeetCode插件。打开VS Code,点击左侧的“扩展”图标,搜索“LeetCode”,然后点击“安装”按钮。 2. 安装完成后,你需要登录你的LeetCode账号。点击左侧的“LeetCode”图标,然后点击“登录”按钮。输入你的LeetCode账号和密码,然后点击“登录”按钮。 3. 登录成功后,你可以开始刷题了。点击左侧的“LeetCode”图标,然后点击“题库”按钮。在题库选择你想要刷的题目,然后点击“开始”按钮。 4. 在刷题过程,你可以使用VS Code提供的一些功能,比如自动补全、代码提示、代码格式化等。这些功能可以帮助你更快地完成题目。 5. 刷完题目后,你可以提交你的代码并查看测试结果。点击左侧的“LeetCode”图标,然后点击“提交”按钮。输入你的代码,然后点击“提交”按钮。稍等片刻,你就可以看到测试结果了。 总之,使用VS Code安装LeetCode插件刷题非常方便,可以帮助你更快地提高算法能力。 ### 回答2: VS Code是一个很流行的开发工具,LeetCode是很多程序员用来刷算法题的平台。在VS Code安装LeetCode插件可以提供更好的刷题体验,还可以方便地保存和管理自己的代码。 下面是以Windows系统为例介绍在VS Code安装LeetCode插件的步骤: 第一步:安装VS Code 在官网下载安装VS Code,https://code.visualstudio.com/ 第二步:打开Extensions标签 点击左侧菜单栏的Extensions标签,在搜索框输入“LeetCode”。 第三步:安装LeetCode插件 在搜索结果,找到LeetCode插件并点击“Install”安装。 第四步:配置LeetCode插件 安装完毕后,配置LeetCode插件。点击左侧菜单栏的“LeetCode”标签,根据提示进行配置。 第五步:使用LeetCode插件 在左侧菜单栏的“LeetCode”标签下,点击“Problems”即可进入刷题页面。 总结:安装LeetCode插件可以为刷题提供更好的开发环境和代码管理工具,操作简便,适合程序员们使用。 ### 回答3: VSCode是一款功能强大的代码编辑器,不仅支持多种编程语言,还能通过插件进行扩展,其包括LeetCode刷题插件。安装LeetCode插件,可以大大提升刷题效率和舒适度。安装LeetCode插件,大致可以分为以下几个步骤: 1. 安装VSCode:首先需要下载并安装VSCode,可以从官网(https://code.visualstudio.com/)获取。 2. 安装LeetCode插件:在VSCode打开“扩展”选项卡,搜索“LeetCode”,找到对应的插件,点击“安装”。 3. 配置LeetCode插件:在VSCode点击左下角的“LeetCode插件设置”按钮,进入插件的配置页面。在这里可以设置LeetCode的默认语言、代码保存目录、登录账号等信息。 4. 登录LeetCode账号:在配置页面设置好账号信息后,可以点击登录按钮,跳转到LeetCode的官网,进行账号登录。 5. 开始刷题:在LeetCode插件的“输入问题编号”处输入题目编号,点击“搜索”即可进入题目页面,进行刷题操作。LeetCode插件还提供了多种题目操作,如代码提交、答案对比、题目复制等功能,可根据需求进行使用。 总之,安装LeetCode插件需要一些简单的操作步骤和注意事项,但对于提高刷题效率来说是非常方便实用的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑笑妈咪YK

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值