子序列相关算法在面试中经常出现,现在总结下单个子序列的相关算法:
1、最长递增子序列
问题描述:
设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
动态规划求解:
以坐标i结尾的序列的最长递增子序列和其[0,i-1]“前缀”的最长递增子序列有关,设C[i]保存“以i结尾的最长递增子序列”的长度,若i=0,则 C[i]=1。否则C[i]的值和其[0,i-1]前缀的最长递增子序列长度有关,用j遍历[0,i-1]得到其最长递增子序列为C[j],对每一个C[j],如果L[j]<L[i]并且C[j]+1>C[i](初始化为0),则C[i]的值变为C[j]+1。即:
LIS[i]=max{1,LIS[j]+1},
其中array[i]>array[j],且j=[0,i-1]。
实现如下:
public void lis(float[] L)
{
int n = L.length;
int[] f = new int[n];//用于存放f(i)值;
f[0]=1;//以第a1为末元素的最长递增子序列长度为1;
for(int i = 1;i<n;i++)//循环n-1次
{
f[i]=1;//f[i]的最小值为1;
for(int j=0;j<i;j++)//循环i 次
{
if(L[j]<L[i]&&f[j]>f[i]-1)
f[i]=f[j]+1;//更新f[i]的值。
}
}
System.out.println(f[n-1]);
}
其中最长递增子序列的值为max(f[n-1])。
2、子序列的最大和
问题描述:
输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。要求时间复杂度为O(n)。例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,因此输出为该子数组的和18。
扫描法求解:
int maxSum(int* a, int n)
{
intsum=0;
//其实要处理全是负数的情况,很简单,直接把这句改成:"int sum=a[0]"即可
//也可以不改,当全是负数的情况,直接返回0,也不见得不行。
intb=0;
for(int i=0; i<n; i++)
{
if(b<0) //...
b=a[i];
else
b+=a[i];
if(sum<b)
sum=b;
}
return sum;
}
3、子序列中找到第一个只出现一次的字符
问题描述:
在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。(2006年Google的一道笔试题)
扫描法求解:
struct Record
{
intindex;
intcount;
};
char getFirstOnlyOneChar(string&str)
{
//all char in str should be lower case
Record records[256];
memset(records, 0, sizeof(Record) * 26);
for(int i = 0; i < str.length(); i++)
{
records[str[i]].count++;
records[str[i]].index = i;
}
intindex = str.length();
for(int i = 0; i < 256; i++)
{
if (records[i].count == 1)
{
if (records[i].index < index)
index = records[i].index;
}
}
return str[index];
}
4、数组中出现次数超过一半的数字
问题描述:
数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
删除法求解:
出现的次数超过数组长度的一半,表明这个数字出现的次数比其他数字出现的次数的总和还多。所以我们可以考虑每次删除两个不同的数,那么在剩下的数中,出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了上述思路一、二的排序,也避免了思路三空间O(N)的开销,总得说来,时间复杂度只有O(N),空间复杂度为O(1),不失为最佳方法。
例:数组 a[5]={0,1,2,1,1};
我们要查找的数字为1,操作步骤为:遍历整个数组,然后每次删除不同的两个数字,过程如下:
0 1 2 1 1 =>2 1 1=>1
具体实现:我们在考虑删除两个不同的数字的时候,实际上可以同过计数来实现,而不是物理上真正的删除。 在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1。如果下一个数字和我们之前保存的数字不同,则次数减1。如果次数为零,我们需要保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。
基于这个思路,我们不难写出如下代码:
bool g_bInputInvalid = false;
//////////////////////////////////////////////////////////////////////////
// Input: an array with "length" numbers. Anumber in the array
// appear more than "length / 2 + 1" times.
// Output: If the input is valid, return the numberappearing more than
// "length / 2 + 1" times. Otherwise, return 0and set flag g_bInputInvalid
// to be true.
//////////////////////////////////////////////////////////////////////////
int MoreThanHalfNum(int* numbers, unsigned int length)
{
if(numbers ==NULL && length == 0)
{
g_bInputInvalid = true;
return 0;
}
g_bInputInvalid= false;
int result =numbers[0];
int times = 1;
for(int i = 1;i < length; ++i)
{
if(times ==0)
{
result= numbers[i];
times =1;
}
elseif(numbers[i] == result)
times++;
else
times--;
}
// verifywhether the input is valid
times = 0;
for(int i = 0;i < length; ++i)
{
if(numbers[i] == result)
times++;
}
if(times * 2<= length)
{
g_bInputInvalid = true;
result = 0;
}
return result;
}
5、对称子字符串的最大长度
问题描述:
输入一个字符串,输出该字符串中对称的子字符串的最大长度。比如输入字符串“google”,由于该字符串里最长的对称子字符串是“goog”,因此输出4。
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。
三种方法:
要判断一个字符串是不是对称的,不是一件很难的事情。我们可以先得到字符串首尾两个字符,判断是不是相等。如果不相等,那该字符串肯定不是对称的。否则我们接着判断里面的两个字符是不是相等,以此类推。基于这个思路,我们不难写出如下代码:
/*
判断起始指针为pBegin,结束指针为pEnd的字符串是否对称
*/
bool IsSymmetrical(char* pBegin, char* pEnd)
{
if(pBegin == NULL || pEnd == NULL || pBegin > pEnd)
return false;
while(pBegin < pEnd)
{
if(*pBegin != *pEnd)
return false;
pBegin++;
pEnd --;
}
return true;
}
要判断一个字符串pString是不是对称的,我们只需要调用IsSymmetrical(pString, &pString[strlen(pString) – 1])就可以了。
O(N^3)算法——暴力破解
现在我们试着来得到对称子字符串的最大长度。最直观的做法就是得到输入字符串的所有子字符串,并逐个判断是不是对称的。如果一个子字符串是对称的,我们就得到它的长度。这样经过比较,就能得到最长的对称子字符串的长度了。于是,我们可以写出如下代码:
/*
取得所有对称子串的最大长度
时间复杂度: O(n^3)
*/
int GetLongestSymmetricalLength_1(char* pString)
{
if(pString == NULL)
return 0;
int symmeticalLength = 1;
char* pFirst = pString;
int length = strlen(pString);
while(pFirst < &pString[length - 1])
{
char* pLast = pFirst + 1;
while(pLast <= &pString[length - 1])
{
if(IsSymmetrical(pFirst, pLast))
{
int newLength = pLast - pFirst + 1;
if(newLength > symmeticalLength)
symmeticalLength = newLength;
}
pLast++;
}
pFirst++;
}
return symmeticalLength;
}
我们来分析一下上述方法的时间效率。由于我们需要两重while循环,每重循环需要O(n)的时间。另外,我们在循环中调用了IsSymmetrical,每次调用也需要O(n)的时间。因此整个函数的时间效率是O(n^3)。
O(N^2)算法——扫描判断
通常O(n^3)不会是一个高效的算法。如果我们仔细分析上述方法的比较过程,我们就能发现其中有很多重复的比较。假设我们需要判断一个子字符串具有aAa的形式(A是aAa的子字符串,可能含有多个字符)。我们先把pFirst指向最前面的字符a,把pLast指向最后面的字符a,由于两个字符相同,我们在IsSymtical函数内部向后移动pFirst,向前移动pLast,以判断A是不是对称的。接下来若干步骤之后,由于A也是输入字符串的一个子字符串,我们需要再一次判断它是不是对称的。也就是说,我们重复多次地在判断A是不是对称的。
造成上述重复比较的根源在于IsSymmetrical的比较是从外向里进行的。在判断aAa是不是对称的时候,我们不知道A是不是对称的,因此需要花费O(n)的时间来判断。下次我们判断A是不是对称的时候,我们仍然需要O(n)的时间。
如果我们换一种思路,我们从里向外来判断。也就是我们先判断子字符串A是不是对称的。如果A不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不是对称的。如果A对称,那么我们只需要判断A两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。因此在知道A是否对称之后,只需要O(1)的时间就能知道aAa是不是对称的。
我们可以根据从里向外比较的思路写出如下代码:
/*
取得所有对称子串的最大长度
时间复杂度: O(n^2)
*/
int GetLongestSymmetricalLength(char* pString)
{
if(pString == NULL)
return 0;
int symmeticalLength = 1;
char* pChar = pString;
while(*pChar != '\0')
{
// Substrings with odd length
char* left = pChar - 1;
char* right = pChar + 1;
while(left >= pString && *right != '\0' && *left == *right)
{
left--;
right++;
}
int newLength = right - left - 1; //退出while循环时,*left != *right
if(newLength > symmeticalLength)
symmeticalLength = newLength;
// Substrings with even length
left = pChar;
right = pChar + 1;
while(left >= pString && *right != '\0' && *left == *right)
{
left--;
right++;
}
newLength = right - left - 1; //退出while循环时,*left != *right
if(newLength > symmeticalLength)
symmeticalLength = newLength;
pChar++;
}
return symmeticalLength;
}
由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从只有一个字符的中心向两端延长出来,而长度为偶数的字符串是从一个有两个字符的中心向两端延长出来。因此我们的代码要把这种情况都考虑进去。
在上述代码中,我们从字符串的每个字符串两端开始延长,如果当前的子字符串是对称的,再判断延长之后的字符串是不是对称的。由于总共有O(n)个字符,每个字符可能延长O(n)次,每次延长时只需要O(1)就能判断出是不是对称的,因此整个函数的时间效率是O(n^2)。
O(N)算法——动态规划
回文串定义:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
回文子串,顾名思义,即字符串中满足回文性质的子串。
经常有一些题目围绕回文子串进行讨论,比如 HDOJ_3068_最长回文,求最长回文子串的长度。朴素算法是依次以每一个字符为中心向两侧进行扩展,显然这个复杂度是O(N^2)的,关于字符串的题目常用的算法有KMP、后缀数组、AC自动机,这道题目利用扩展KMP可以解答,其时间复杂度也很快O(N*logN)。但是,今天笔者介绍一个专门针对回文子串的算法,其时间复杂度为O(n),这就是manacher算法。
大家都知道,求回文串时需要判断其奇偶性,也就是求aba和abba的算法略有差距。然而,这个算法做了一个简单的处理,很巧妙地把奇数长度回文串与偶数长度回文串统一考虑,也就是在每个相邻的字符之间插入一个分隔符,串的首尾也要加,当然这个分隔符不能再原串中出现,一般可以用‘#’或者‘$’等字符。例如:
原串:abaab
新串:#a#b#a#a#b#
这样一来,原来的奇数长度回文串还是奇数长度,偶数长度的也变成以‘#’为中心的奇数回文串了。
接下来就是算法的中心思想,用一个辅助数组P记录以每个字符为中心的最长回文半径,也就是P[i]记录以Str[i]字符为中心的最长回文串半径。P[i]最小为1,此时回文串为Str[i]本身。
我们可以对上述例子写出其P数组,如下
新串: # a # b # a # a # b #
P[] : 1 2 1 4 1 2 5 2 1 2 1
我们可以证明P[i]-1就是以Str[i]为中心的回文串在原串当中的长度。
证明:
1、显然L=2*P[i]-1即为新串中以Str[i]为中心最长回文串长度。
2、以Str[i]为中心的回文串一定是以#开头和结尾的,例如“#b#b#”或“#b#a#b#”所以L减去最前或者最后的‘#’字符就是原串中长度的二倍,即原串长度为(L-1)/2,化简的P[i]-1。得证。
依次从前往后求得P数组就可以了,这里用到了DP(动态规划)的思想,也就是求P[i]的时候,前面的P[]值已经得到了,我们利用回文串的特殊性质可以进行一个大大的优化。我先把核心代码贴上:
for(i=1;i<n;i++)
{
if(MaxId>i)
{
p[i]=Min(p[2*id-i],MaxId-i);
}
else
{
p[i]=1;
}
while(Str[i+p[i]]==Str[i-p[i]])
{
p[i]++;
}
if(p[i]+i>MaxId)
{
MaxId=p[i]+i;
id=i;
}
}
为了防止求P[i]向两边扩展时可能数组越界,我们需要在数组最前面和最后面加一个特殊字符,令P[0]=‘$’最后位置默认为‘\0’不需要特殊处理。此外,我们用MaxId变量记录在求i之前的回文串中,延伸至最右端的位置,同时用id记录取这个MaxId的id值。通过下面这句话,算法避免了很多没必要的重复匹配。
if(MaxId>i)
{
p[i]=Min(p[2*id-i],MaxId-i);
}
那么这句话是怎么得来的呢,其实就是利用了回文串的对称性,如下图:
j=2*id-1即为i关于id的对称点,根据对称性,P[j]的回文串也是可以对称到i这边的,但是如果P[j]的回文串对称过来以后超过MaxId的话,超出部分就不能对称过来了,如下图,所以这里P[i]为的下限为两者中的较小者,p[i]=Min(p[2*id-i],MaxId-i)。
算法的有效比较次数为MaxId次,所以说这个算法的时间复杂度为O(n)。
附HDOJ_3068_最长回文代码:
#include <stdio.h>
#define M 110010
char b[M],a[M<<1];
int p[M<<1];
int Min(int a,int b)
{
return a<b?a:b;
}
int main(void)
{
int i,n,id,MaxL,MaxId;
while(scanf("%s",&b[1])!=EOF)
{
MaxL=MaxId=0;
for(i=1;b[i]!='\0';i++)
{
a[(i<<1)]=b[i];
a[(i<<1)+1]='#';
}
a[0]='?';a[1]='#';
n=(i<<1)+2;a[n]=0;
MaxId=MaxL=0;
for(i=1;i<n;i++)
{
if(MaxId>i)
{
p[i]=Min(p[2*id-i],MaxId-i);
}
else
{
p[i]=1;
}
while(a[i+p[i]]==a[i-p[i]])
{
p[i]++;
}
if(p[i]+i>MaxId)
{
MaxId=p[i]+i;
id=i;
}
if(p[i]>MaxL)
{
MaxL=p[i];
}
}
printf("%d\n",MaxL-1);
}
return 0;
}
转载自:http://blog.youkuaiyun.com/hackbuteer1/article/details/6686263
6、找出数组中唯一的重复元素
问题描述:
1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次.每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?
异或操作求解:
void FindRepeat(int array[], int length)
{
intresult = 0;
for(inti=1;i<=1000;i++)
result^= i;
for(inti=0;i<=1000;i++)
result^= array[i];
cout<< result << endl;
}
7、数组中至少存在一个重复数,求其中的任意一个重复数
问题描述:
取值为[1,n-1]含n个元素的整数数组至少存在一个重复数,O(n)时间内找出其中任意一个重复数。如a[]={1,2,2,4,5,4},则2和4均是重复元素。
单链表存在环思想求解:
设数组A共有n个元素,即A={ a0, a1, a2, …, an-1 }。
首先给出下标n-1,则第一个元素为A[n-1],然后用A[n-1]-1作为下标,可以到达元素A[A[n-1]-1],再以A[A[n-1]-1]为下标,可以得到元素A[A[A[n-1]-1]]…可以看到这里并没用直接用元素值作索引,而是用元素值减1,这样做是为了避免陷入死循环。
如果A[i]=A[j]=x,即x在数组中出现了两次。则A[i]--->A[x]--->…---> A[j]---> A[x],因此链表边形成了环。
一旦链表产生后,问题就简单多了。因为重复出现得到元素恰好是环的入口点。于是,问题就相当于单链表求环的入口点。用指针追过的办法,指针x每次步长为2,指针y每次步长为1。直到x、y相遇,然后重置x,使x重新开始。这次同步移动x、y,每次步长都为1,当x、y再次相遇时,恰好是环的入口点。
代码如下:
//在O(n)的时间内,找出任意重复的一个数
int FindRepeat( int *data, int size )
{
int x = size;
int y = size;
//找到相遇点
do{
x = data[data[x-1]-1];
y = data[y-1];
}while( x != y );
//找到重复的元素
x = size;
do{
x = data[x-1];
y = data[y-1];
}while( x != y );
return x;
}