求一个串中出现的第一个最长重复子串

题目描述

给定一个字符串,找出这个字符串的最长的重复子串,比如给定字符串"banana",子字符串"ana"就是最长的重复子串.

问题分析

题目要求寻找最长的重复子串.“最长"和"重复"是关键.既然是最长,根据我们的编程经验,必然需要一个变量用以保存这个最长的子串,并且这个变量在遍历比较中不断地被更新.那么比较又怎么来的?如何比较?这里我们看"重复”,既然是重复,代表出现两次以上,所以一个字符子串只要出现两次就有可能被我们选为答案.那么很明显,这样的字符子串就是我们的比较对象,而比较的,正是它们的长度.对于算法而言,一个难点就是,**应当如何开始,如何不遗漏地遍历到所有的情况,以一个怎样的顺序进行?**这是一个算法能否实现的核心.一个复杂问题的经典算法必然伴随着一个巧妙的解决思路,就拿这个问题来说.我们就把问题转换成了找这个字符串是否有两个最长的相同子串,根据这个想法,我们可以有一个朴素(暴力)的算法,找到这个字符串的最长子串(设这个字符串长度为n,那么就从n-1开始),找这个字符串有没有其他这样的子串,比如说一个字符串有两个n-1长度的子串,那么就比较它们是否相等就可以了.如果不相等,那么就找长度为n-2的子串,再寻找有没有其他长度为n-2的子串与这个子串相等.这个寻找方式可以这么进行:首先从头开始截取长度为n-2的子串,再从第二个字符开始截取长度为n-2的子串,两两比较是否相同,若相同,则算法终止,这边是最长的第一个重复子串,如果不相同,再从下一个字符开始截取子串,这个子串就要和前两个子串比较了,所以前面两个的子串数据还要保存起来,丢失的话就没法跟踪比较了.
在这里插入图片描述
这个思想简单粗暴,所以也称暴力法.它一定可以达到目的,但为此付出的时间代价和空间代价往往是高昂的.就比如这里只要没有找到相等的字符子串,就不能够判断没有这样的重复子串,而每减小一次子串长度,子串的数量就会加1,比较的次数更是增加了很多.子串数量增加导致我们需要一个足够长的数组用以保存子串的数据,而比较次数的增加则直接增加了我们的时间复杂度.一个问题,暴力法的得来往往能给它的改进带来思路,而所谓改进,就是新的算法能够减小相对于暴力法而言的时间复杂度或空间复杂度,或兼而有之.
那么接下来我们的的后缀数组法就将闪亮登场了.

后缀数组法

首先介绍一下后缀数组的概念.它一定是一个数组,它保存的是一个字符串的所有后缀,后缀就是一种字符串切片,不同的后缀它们的区别是起始元素的不同,但所有的后缀都会切片到这个字符串的最后一个元素.举个例子,一个字符串strs=“abcdef”,那么"def"就是它的一个后缀,而"abc"就不是它的后缀,因为这种切片方式并没有切到原字符串的末尾,所以不同的后缀的区别仅在于它们从哪里开始,因为它们的终点永远是这个字符串的末尾元素.与之相反的是前缀这个概念,它是起始位置始终是字符串的起始元素,终点元素可以是字符串的任意一个元素的一种切片.比如"abc"就是strs的一个前缀,而"bcde"就不是,所有的后缀共有一个结尾,所有的前缀共有一个开头.
暴力法的思想是寻找两个长度最长且相等的子串,它的方法是寻找所有这样长度的子串,并逐个比较.我们说这样的方式比较费时费力.而后缀数组法则可以减少很多没有必要的比较.我们试想,反推,如果我们已经知道这个最长的重复子串是"abc",我们把这个子串复制一份,分别插入到一个元素各不相同的字符串中,比如说"defghijk".经过插入"abc"以后,得到"abcdefabcghijk",我们用暴力法的话,要比较到长度为3的子串就要花很多时间,而且这两个子串还不相邻,在遍历到第二个"abc"的时候又要花一点时间,比较又要花一点时间,而且还要保存之前的长度为3的子串信息.现在我们考虑以"abc"为起始字符串的后缀,分别是"abcdefabcghijk"和"abcghijk",可以发现它们具有相同的前缀"abc",而这个字符串的所有后缀共有14个(相当于字符串长度),找到这两个后缀的时间复杂度跟前面找"abc"这个子串的时间复杂度相比,减少了不少.

简单分析一下,前面暴力法是有两个循环的,第一个循环跟子串的长度有关,长度为i对应的子串数目是n-i,第二个循环跟第几个子串有关,到第二个子串最多比较一次,第三个子串最多比较两次,以此类推.这里是第7个子串,最多要比较6次(虽然这里只比较了一次就可以了),所以这里遍历到长度为3的子串之前的比较次数是1+(1+2)+(1+2+3)+…+(1+2+3+4+5+6)=56次,再加上长度为3的子串时的比较次数1+2+3+4+5+6=21次,共比较77次,即使最后算一次,那也比较了72次.

利用后缀数组法,将所有的后缀存到一个后缀数组中去,通过一个方法得到两个后缀数组的共同前缀,将它的长度记做一个变量maxLen,每遍历到一个新的后缀,就能够与前面的后缀得到若干个共同前缀,若有更长的前缀,就更新maxLen,直到遍历完所有的后缀.当遍历到第i个后缀时,比较次数为1+2+…+(i-1)=i(i-1)/2.所以长度为14的字符串strs一共要比较1+2+…+13=91次.这样看来,反而增加了比较次数,这不是没事找事吗!其实中间有很多不必要的比较我们没有舍弃,比如说第i个后缀没必要跟前面所有i-1个后缀比较共同前缀.只要这个后缀数组是按字典排好序的,就只要比较相邻的后缀数组就可以了.因为相邻的后缀数组具有最大的公共前缀(相对于不相邻的两个后缀而言).

举个例子,设有字符串"banana",它的所有后缀如果按照起始元素从左到右的顺序是:“banana”, “anana”, “nana”, “ana”, “na”, "a"这6个,现在我们要按照字典序给这些后缀重新排序,所谓字典序,就是两个字符串比较按照第一个不相等的字符的大小排序的手段,可以从大到小,也可以从小到大.这样排序可以保证相邻的后缀数组的公共前缀最大.这里按照字典序排序(从小到大)得到的新的后缀数组是:“a”,“ana”,“anana”,“banana”,“na”,“nana”.

因为字典序排列的这个好处,可以极大地节省比较的次数,因为只要将相邻的两个后缀比较前缀就可以了,这样就只需要比较5次(对"banana"而言).无论如何,这个算法的时间复杂度为O(N).而暴力法是 O ( N 2 ) O(N^{2})

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值