【算法系列 四】 String

1. 字符串循环左移(九度OJ1362),要求时间复杂度O(N),空间复杂度O(1)

这是一道基本的题目,简单说来就是三次翻转

比如:abcdef 左移两位 cdefab

过程:

ab 翻转 ba

cdef 翻转 fedc

将上面两个翻转后的结果拼接 bafedc

再翻转cdefab得到结果

代码:

import java.io.IOException;
import java.util.Scanner;

public class Main
{
	public static void main(String[] args) throws IOException
	{
		Scanner cin = new Scanner(System.in);
		int N;
		String str;
		while (cin.hasNext())
		{
			str = cin.next();
			N = cin.nextInt();
			N = N % str.length();
			String a = str.substring(0, N);
			String b = str.substring(N);
			StringBuffer abuffer = new StringBuffer(a);
			StringBuffer bbuffer = new StringBuffer(b);
			StringBuffer areverse = abuffer.reverse();
			StringBuffer breverse = bbuffer.reverse();
			StringBuffer creverse = areverse.append(breverse);
			System.out.println(creverse.reverse().toString());
		}
	}

}
另外:循环左移K位等价于循环右移n-K位

2. 完美洗牌算法,要求时间复杂度O(N),时间复杂度O(1)

将{a1,a2,a3,...,an,b1,b2,b3,...,bn}变成{a1,b1,a2,b2,a3,b3,...,an,bn}

0、朴素的想法:记录一半的数据,依次重新插入。显然空间复杂度是O(N)。需要做点“高级”的分析。

1、题目要求空间复杂度为O(1),显然除了固定数目的临时变量不能额外开辟内存。这个要求变相的告诉我们:只能在原始数组上就地整理,不能新申请数组。

2、对原始位置的变化做如下分析:

【算法系列 四】 String_第1张图片

依次kao察每个位置的变化规律:

a1:1 -> 2
a2:2 -> 4
a3:3 -> 6
a4:4 -> 8
b1:5 -> 1
b2:6 -> 3
b3:7 -> 5
b4:8 -> 7

2.1、立刻可以发现变化规律:
对于原数组位置i的元素,新位置是(2*i)%(2n+1),注意,这里用2n表示原数组的长度。后面依然使用该表述方式。

2.2、有了该表达式:i' = (2*i)%(2n+1),困难的不是寻找元素在新数组中的位置,而是为该元素“腾位置”。如果使用暂存的办法,空间复杂度必然要达到O(N),因此,需要换个思路。

2.3、我们这么思kao:a1从位置1移动到位置2,那么,位置2上的元素a2变化到了哪里呢?继续这个线索,我们得到一个“封闭”的环:
1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1
沿着这个环,可以把a1、a2、a4、b4、b3、b1这6个元素依次移动到最终位置;显然,因为每次只移动一个元素,代码实现时,只使用1个临时空间即可完成。(即:a=t;t=b;b=a)
此外,该变化的另外一个环是:
3 -> 6 -> 3
沿着这个环,可以把a3、b2这2个元素依次移动到最终位置。

2.4、上述过程可以通过若干的“环”的方式完整元素的移动,这是巧合吗?事实上,该问题的研究成果已经由Peiyush Jain在10nian前公开发表在A Simple In-Place Algorithm for In-Shuffle, Microsoft, July, 2004中。原始论文直接使用了一个结论,这里不再证明:对于2*n =(3^k-1)这种长度的数组,恰好只有k个环,且每个换的起始位置分别是1,3,9,...3^(k-1)。
对于2.3的例子,长度为8,是3^2-1,因此,只有2个环。环的其实位置分别是1和3。

2.5、至此,完美洗牌算法的“主体工程”已经完工,只存在一个“小”问题:如果数组长度不是(3^k-1)呢?

2.5.1、若2n!=(3^k-1),则总可以找到最大的整数m,使得m<n,并且2m=(3^k-1)。

2.5.2、对于长度为2m的数组,调用2.3和2.4中的方法整理元素,剩余的(2n-2m)长度,递归调用2.5.1即可。

2.5.3、在2.5.2中,需要交换一部分数组元素:

【算法系列 四】 String_第2张图片

(下面使用[a,b]表示从a到b的一段子数组,包括端点)
①图中斜线阴影部分的子数组[1,m]应该和[n 1,n m]组成一个序列,调用2.3和2.4中的算法;
②因此,数组[m,n-m]循环左移n-m次即可。(注:字符串旋转是有空间复杂度O(1)的算法的,详情请看本文第一题)

2.6、以上,完成了该问题的全部求解过程。关于2*n =(3^k-1)满足k个环的问题,赘述很长,不妨kao察一下ψ(3)和ψ(9)。这里,ψ(N)即欧拉函数,表示小于N的自然数中,和N互素的数目。

2.7、原始问题要输出a1,b1,a2,b2……an,bn,而完美洗牌却输出的是b1,a1,b2,a2,……bn,an。解决办法非常简单:忽略原数组中的a1和bn,对于a2,a3,……an,b1,b2,……bn-1调用完美洗牌算法,即为结论。

3. 最长公共子序列(九度OJ 1042)

动态规划思想

【算法系列 四】 String_第3张图片

思想简单描述:

如果两个字符串最后一位相同,则最后一位字符肯定是最长公共子序列的最后一位。

如果最后一位不同,则有可能第一个字符串中的最后一位是公共子序列,也可能是第二个字符串中的最后一位。当然也可能都不是则LCS(Xm,Yn)=LCS(Xm-1,Yn-1),但是这种情况包含在max{LCS(Xm-1,Yn),LCS(Xm,Yn-1)}中。

所以得出上面的式子。

Coding:

最直观的代码就是递归:

import java.io.IOException;
import java.util.Scanner;

public class Main
{
	static char[] x = new char[50];
	static char[] y = new char[50];

	public static void main(String[] args) throws IOException
	{
		Scanner cin = new Scanner(System.in);
		String a, b;
		while (cin.hasNext())
		{
			a = cin.next();
			b = cin.next();
			x = a.toCharArray();
			y = b.toCharArray();
			int res = LCS(x.length - 1, y.length - 1);
			System.out.println(res);
		}
	}

	public static int LCS(int i, int j)
	{
		if (i < 0 || j < 0)
		{
			return 0;
		}
		if (x[i] == y[j])
		{
			return LCS(i - 1, j - 1) + 1;
		}
		else
		{
			int aa = LCS(i, j - 1);
			int bb = LCS(i - 1, j);
			return aa > bb ? aa : bb;
		}
	}

}
但是递归的效率太低,并且有太多的重复操作。

我们使用打表的方式来避免递归操作:

使用一个二维数组C[m,n]来保存LCS

C[i,j]代表Xi,Yj的最长公共子序列

当i=0或者j=0时代表有一个字符串为空,则C[i,j] =0

【算法系列 四】 String_第4张图片

import java.io.IOException;
import java.util.Scanner;

public class Main
{
	static char[] x = new char[50];
	static char[] y = new char[50];

	public static void main(String[] args) throws IOException
	{
		Scanner cin = new Scanner(System.in);
		String a, b;
		while (cin.hasNext())
		{
			a = cin.next();
			b = cin.next();
			x = a.toCharArray();
			y = b.toCharArray();
			int res = LCS(x.length, y.length);
			System.out.println(res);
		}
	}

	public static int LCS(int i, int j)
	{
		int[][] c = new int[50][50];
		for (int k = 0; k <= i; k++)
		{
			c[k][0] = 0;
		}
		for (int k = 0; k <= j; k++)
		{
			c[0][k] = 0;
		}
		for (int k = 1; k <= i; k++)
		{
			for (int k2 = 1; k2 <= j; k2++)
			{
				if (x[k - 1] == y[k2 - 1])
				{
					c[k][k2] = c[k - 1][k2 - 1] + 1;
				}
				else
				{
					c[k][k2] = (c[k][k2 - 1] > c[k - 1][k2] ? c[k][k2 - 1]
							: c[k - 1][k2]);
				}
			}
		}
		return c[i][j];
	}

}
时间复杂度O(m*n)

4. 最长递增子序列(Leetcode 300)

例如:10, 9, 2, 5, 3, 7, 101, 18

输出:2, 3, 7, 101

很简单的思路就是,使用最长公共子序列来解决这个问题

最长公共子序列的解法在第3题中已经解释了。

解决最长递增子序列只需要

将原序列:10, 9, 2, 5, 3, 7, 101, 18

将原序列排序后的序列: 2, 3, 5, 7, 9, 10, 18, 101

这两个序列求最长公共子序列,得到的序列就是最长递增子序列

代码:

public class Solution {
    public int lengthOfLIS(int[] nums) {
        Integer[] s = new Integer[nums.length];
		for (int i = 0; i < nums.length; i++)
		{
			s[i] = nums[i];
		}
		TreeSet<Integer> treeSet = new TreeSet<Integer>(Arrays.asList(s));
		s =  treeSet.toArray(new Integer[0]);
		int[][] c = new int[nums.length + 1][nums.length + 1];
		for (int i = 0; i <= nums.length; i++)
		{
			c[i][0] = 0;
		}
		for (int i = 0; i <= s.length; i++)
		{
			c[0][i] = 0;
		}
		for (int i = 1; i <= nums.length; i++)
		{
			for (int j = 1; j <= s.length; j++)
			{
				if (nums[i - 1] == s[j - 1])
				{
					c[i][j] = c[i - 1][j - 1] + 1;
				}
				else
				{
					c[i][j] = c[i - 1][j] > c[i][j - 1] ? c[i - 1][j]
							: c[i][j - 1];
				}
			}
		}
		return c[nums.length][s.length];
    }
}

由于是递增子序列,所以排序后的序列需要去重,这里用TreeSet即做了排序又去了重。时间复杂度O(n*n)

当然我们也可以使用动态规划的思想去解决这个问题

维护一个dp[]数组,dp[i]的意思是,必须以arr[i]结尾的最大递增子序列是多少。

比如arr = {1,2,3,2}

那么dp[3]的意思是,必须以arr[3]=2为最后的最大递增子序列,即{1,2}

那么我们知道了dp[i]后,如何得到dp[i+1]呢。

根据最大递增子序列的定义我们就能知道,dp[i+1]是dp[0...i]中最大的值dp[j],并且arr[j]<arr[i+1],这样我们也得到了一个O(n*n)的算法

public class Solution
{
	public int lengthOfLIS(int[] nums)
	{
		if(nums.length == 0)
			return 0;
		int[] dp = new int[nums.length];
		dp[0] = 1;
		for (int i = 1; i < nums.length; i++)
		{
			int max = 0;
			for (int j = i - 1; j >= 0; j--)
			{
				if (dp[j] > max && nums[j] < nums[i])
				{
					max = dp[j];
				}
			}
			dp[i] = max + 1;
		}
		int max = 0;
		for (int i = 0; i < dp.length; i++)
		{
			if(dp[i] > max)
			{
				max = dp[i];
			}
		}
		return max;
	}
}
最大递增子序列就是dp数组中最大的一个值

那么我们发现求dp[i]时需要遍历dp[0...i-1]的所有元素,能否优化这个操作呢?

假设存在一个序列d[1..9] ={ 2,1 ,5 ,3 ,6,4, 8 ,9, 7},可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个查看这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

注意,这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

代码如下(代码中的数组B从位置0开始存数据):

public class Solution
{
	public int lengthOfLIS(int[] nums)
	{
		if (nums.length == 0)
			return 0;
		int[] dp = new int[nums.length];
		int[] B = new int[nums.length];
		dp[0] = 1;
		B[0] = nums[0];
		int begin = 0;
		int middle = 0;
		int end = 0;
		int right = 0;
		for (int i = 1; i < nums.length; i++)
		{
			begin = 0;
			end = right;
			while (begin <= end)
			{
				middle = (begin + end) / 2;
				if (nums[i] > B[middle])
				{
					begin = middle + 1;
				}
				else
				{
					end = middle - 1;
				}
			}
			right = right > begin ? right : begin;
			B[begin] = nums[i];
			dp[i] = begin + 1;
		}
		int max = 0;
		for (int i = 0; i < dp.length; i++)
		{
			if (dp[i] > max)
			{
				max = dp[i];
			}
		}
		return max;
	}
}

5. KMP算法(LeetCode 28)

给定文本串text与模式串pattern。从文本串text中找到模式串pattern第一次出现的位置。

KMP是一种线性时间复杂度的字符串匹配算法,它是对暴力算法的改进。

文本串长度为N,模式串长度为M,KMP算法时间复杂度为O(M+N),空间复杂度为O(M)(next数组)

【算法系列 四】 String_第5张图片

暴力算法思想很简单,就是不断匹配,如上图,文本串从i位置开始,模式串从0位置开始匹配,若匹配失败,则文本串从i+1位置开始,模式串回溯到0位置。

暴力求解的时间辅助度为O(M*N),空间复杂度为O(1)

而KMP算法的思想则是尽量减少回溯的发生

【算法系列 四】 String_第6张图片

如上图两个字符串,当发现绿色部分与黄色部分不相等时,如果是暴力算法,则模式串要从0开始重新匹配,而KMP的思想则是,如果A和B是相同的,d与黄色部分不相等,不需要从0开始比较,可以从c开始比较。

因为A与B是相同的,能比较到绿色和黄色是否相等,即绿色前面和黄色前面是相等的,所以A与黄色前面字符串是相等的。这样就减少了回溯。

所以KMP的问题就归结到如果求出模式串中的最大相等的k前缀与k后缀。

【算法系列 四】 String_第7张图片

那么该如何高效地求得next[j]呢?

next数组有如下递推关系:

【算法系列 四】 String_第8张图片

当next[j]=k,且p[k]==p[j]时,则很明显next[j+1]=next[j]+1

p[k]不等于p[j]

【算法系列 四】 String_第9张图片

记next[k]=h,所以上图中的1,3,2都是相等的,即1和2是相等的,那么只需要比较蓝色部分和p[j]是否相等,如果相等又回到了第一种情况,如果不相同则再查看next[h]

代码:

public class Solution
{
	public int strStr(String haystack, String needle)
	{
		if(needle.length() == 0)
		{
			return 0;
		}
		if(haystack.length() == 0)
		{
			return -1;
		}
		int ans = -1;
		int needle_n = needle.length();
		int haystack_n = haystack.length();
		int[] next = new int[needle_n];
		goNext(next, needle);
		char[] haystackchar = haystack.toCharArray();
		char[] needlechar = needle.toCharArray();
		int i = 0;
		int j = 0;
		while (i < haystack_n)
		{
			if (j == -1 || haystackchar[i] == needlechar[j])
			{
				++i;
				++j;
			}
			else
			{
				j = next[j];
			}
			if(j == needle_n)
			{
				ans = i - needle_n;
				break;
			}
			
		}

		return ans;
	}

	public void goNext(int[] next, String needle)
	{
		next[0] = -1;
		int j = 0;
		int k = -1;
		int length = needle.length();
		char[] p = needle.toCharArray();
		while (j < length - 1)
		{
			// k表示next[j - 1],p[k]是前缀,p[j]是后缀
			if (k == -1 || p[k] == p[j])
			{
				++j;
				++k;
				next[j] = k;
			}
			else
			{
				k = next[k];
			}
		}
	}
}
优化:

【算法系列 四】 String_第10张图片

如果i与j不相等,按照上述描述,应该讲模式串移到next[j]处,假设next[j]=k,如果k与j是相等的,那么k与i必然不相等,所以还要继续移到next[k]处。

那么何不直接将next[j]=next[k]呢,少了一步比较,效率更高。

public void goNext(int[] next, String needle)
	{
		next[0] = -1;
		int j = 0;
		int k = -1;
		int length = needle.length();
		char[] p = needle.toCharArray();
		while (j < length - 1)
		{
			// k表示next[j - 1],p[k]是前缀,p[j]是后缀
			if (k == -1 || p[k] == p[j])
			{
				++j;
				++k;
				if(p[k] == p[j])
				{
					next[j] = next[k];
				}else
				{
					next[j] = k;
				}
			}
			else
			{
				k = next[k];
			}
		}
	}
KMP(没有优化)的最好情况是,模式串中不存在相等的k前缀和k后缀,则next数组都是-1。一旦不匹配就跳过,比较次数是N

最差情况是,模式串中所有字符都是相等的,next数组是递增序列-1,0,1,2……

最差情况:

【算法系列 四】 String_第11张图片

比较次数<2N

当优化后的KMP,最差情况也变成了最好情况

【算法系列 四】 String_第12张图片

比较次数为N

6. Power Strings(POJ 2406)

求字符串的最小周期串,

例如

ababab的最小周期串是ab,重复了3次

aaaa的最小周期串是a,重复了4次

很直观的想法就是,暴力求解,

假设字符串是ababab

先拿一个字符a试,无法遍历babab

再拿两个字符试ab,可以遍历abab。

总之拿字符串长度能够整除的数去尝试。

import java.io.IOException;
import java.util.Scanner;

public class Main
{
	public static void main(String[] args) throws IOException
	{
		Scanner cin = new Scanner(System.in);
		String str;
		while (cin.hasNext())
		{
			str = cin.next();
			if(str.equals("."))
			{
				break;
			}
			for (int i = 1; i <= str.length(); i++)
			{
				if(str.length() % i != 0)
				{
					continue;
				}
				String pattern = str.substring(0, i);
				String temp = str;
				while(temp.length() > 0)
				{
					if(temp.startsWith(pattern))
					{
						temp = temp.substring(i);
					}else {
						break;
					}
				}
				if(temp.length() == 0)
				{
					System.out.println(str.length()/i);
					break;
				}
			}
		}
		
	}

}
时间辅助度为O(n^2),有没有更好的方法呢?

我们想到了KMP

【算法系列 四】 String_第13张图片

求KMP中的next数组(非优化求法),记p =len - next[len],如果len%p==0,则p就是最小周期长度。

证明:

如上图,黄色部分就是next中的最长相等前后缀,两个绿色部分相等,即上图中下面部分的1=1,又1=2,而且2=2……如此迭代,,如果整个字符串的长度整除1,刚好能够遍历完整个字符串,则1就是最小周期长度。

代码:

import java.io.IOException;
import java.util.Scanner;

public class Main
{
	public static void main(String[] args) throws IOException
	{
		Scanner cin = new Scanner(System.in);
		String str;
		int[] next = new int[1000001];
		while (cin.hasNext())
		{
			str = cin.next();
			if(str.equals("."))
			{
				break;
			}
			getNext(next , str);
			int minlength = str.length() - next[str.length()] ;
			if(str.length() % minlength == 0)
			{
				System.out.println(str.length() / minlength);
			}else {
				System.out.println(1);
			}
		}
		
	}
	public static void getNext(int[] next ,String str)
	{
		int length = str.length();
		char[] p = str.toCharArray();
		next[0] = -1;
		int k = -1;
		int j = 0;
		while(j < length)
		{
			if(k == -1 || p[k] == p[j])
			{
				++j;
				++k;
				next[j] = k;
			}else {
				k = next[k];
			}
		}
	}

}

7. 用二进制来编码字符串“abcdabaa",需要能够根据编码,解码回原来的字符串,最少需要__位的二进制字符串?

使用哈夫曼树来解决这个问题:

哈夫曼树是基于统计的编码方式,概率高的字符使用较短编码,例子如下:

【算法系列 四】 String_第14张图片

结点上面的数字表示频数。

同理,上述题目中使用哈夫曼树得到的结果就是:

a:1

b:01

c:001

d:000

所以需要14位。

因为哈夫曼编码是前缀编码,即任何一个字符的编码都不是另外一个字符编码的前缀。所以是可以解码唯一的。

8. 最长回文子串(Leetcode 5)

给定一个字符串,求它的最长回文子串的长度。

最直接的方法就是枚举每个子串,看看是否是回文子串,然后保存最长的子串。

由于奇数和偶数子串不同,所以要遍历两次。每个字符遍历时都当做回文的中心向两边扩展遍历。

public class Solution
{
	public String longestPalindrome(String s)
	{
		char[] c = s.toCharArray();
		int max = 1;
		int maxBegin = 0;
		int maxEnd = 0;
		int temp = 0;
		int tempBegin = 0;
		int tempEnd = 0;
		for (int i = 0; i < c.length; i++)
		{
			//奇数
			for (int j = 0; i - j >= 0 && i + j < c.length; j++)
			{
				if (c[i - j] != c[i + j])
				{
					break;
				}
				temp = j * 2 + 1;
				tempBegin = i - j;
				tempEnd = i + j;
			}
			if (temp > max)
			{
				max = temp;
				maxBegin = tempBegin;
				maxEnd = tempEnd;
			}
			//偶数
			for (int j = 0; i - j >= 0 && i + j + 1 < c.length; j++)
			{
				if (c[i - j] != c[i + j + 1])
				{
					break;
				}
				temp = j * 2 + 2;
				tempBegin = i - j;
				tempEnd = i + j + 1;
			}
			if (temp > max)
			{
				max = temp;
				maxBegin = tempBegin;
				maxEnd = tempEnd;
			}
		}
		return s.substring(maxBegin, maxEnd + 1);
	}
}
时间复杂度为O(n^2)

时间复杂度那么高的原因是每次都要重新扩展,i为中心的扩展并没有影响到i+1,导致很多重复扩展。

有没有什么好的方法能够降低时间复杂度呢?

这里就提到了著名的Manacher算法。

首先Manacher算法不再需要区分奇数回文和偶数回文,它使用一种技巧回避了这个问题。

它将子串中都加入特殊字符

比如aba -> #a#b#a#    abba -> #a#b#b#a#

这样都只用kao虑奇数情况往外扩展就可以了。

那么如何使扩展更高效呢?

这里使用了3个变量来辅助扩展

pArr[] 这个是回文半径,pArr[i]表示以i为中心的回文半径

pR表示已遍历过的回文半径的最大边界的下一个

index表示pR的回文中心

那么当遍历到i时,分为以下几种情况,取j和i于index对称

因为此时j的回文已经遍历过了,我们希望通过j来直接得到i的回文,而不需要进行扩展

1. j的回文包含在index回文中,那么由图可以知道,必然i的回文就等于j的回文

【算法系列 四】 String_第15张图片

2. j的回文不包含在index内,出了边界,可以得知a不等于d(因为如果相等,index的回文会扩展到d),a==b==c,所以c不等于d。那么必然i的回文会比j的范围要小一点,i的回文的右边界一定是pR-1处

【算法系列 四】 String_第16张图片

3.j的回文与左边界重合,此时很明显a==b==c==d,但是i的回文不一定只到d,它可以继续尝试扩展

【算法系列 四】 String_第17张图片

4.i在最大回文边界之外,此时没有优化手段,直接由i进行扩展

【算法系列 四】 String_第18张图片

从以上的情况中我们发现,只有3,4需要进行扩展,并且3的只需要扩展一部分。这样大大优化了扩展的次数,降低了时间复杂度,Manacher的时间复杂度为O(n),因为pR最大包括整个字符串。

代码:

public class Solution
{
	public String longestPalindrome(String s)
	{
		char[] d = s.toCharArray();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < s.length(); i++)
		{
			sb.append("#");
			sb.append(d[i]);
		}
		sb.append("#");
		char[] c = sb.toString().toCharArray();
		int[] pArr = new int[c.length];
		int pR = 0;
		int index = 0;
		int begin = 0;
		int end = 0;
		int max = 0;
		for (int i = 0; i < c.length; i++)
		{
			if (pR > i)
			{
				if (pArr[2 * index - i] < pR - i)//情况1
				{
					pArr[i] = pArr[2 * index - i];
				}
				else//情况2,3
				{
					pArr[i] = pR - i;
				}
			}
			else//情况4
			{
				pArr[i] = 1;
			}
			while (i + pArr[i] < c.length && i - pArr[i] >= 0
					&& c[i + pArr[i]] == c[i - pArr[i]])
			{
				pArr[i]++;
			}
			if (pArr[i] + i > pR)
			{
				pR = i + pArr[i];
				index = i;
			}
			if(pArr[i] > max)
			{
				max = pArr[i];
				begin = i - pArr[i] + 1;
				end = i + pArr[i] - 1;
			}
		}
		StringBuffer res = new StringBuffer();
		for (int i = begin + 1; i < end; i++)
		{
			if (c[i] != '#')
			{
				res.append(c[i]);
			}
		}
		return res.toString();
	}
}

字符串总结:

字符串查找:CRUD

KMP/BM

map/set;RBtree

hash

trie树

对字符串本身操作

全排列

Manacher

回文划分




系列:

【算法系列 一】 Linked List

【算法系列 二】 Stack

【算法系列 三】 Quene

【算法系列 四】 String


Reference:

1. 七月算法十月算法在线班

2. http://ask.julyedu.com/question/33

3. http://qiemengdao.iteye.com/blog/1660229

4. http://v.qq.com/page/s/j/g/s0157v08yjg.html?start=3

你可能感兴趣的:(KMP,哈夫曼树,最长公共子序列,最长回文子串,Manacher,完美洗牌,最长递增子序列,最小周期串,字符串循环左移)