字符串、数组 算法总结

一、最大子序列和

这里把最大子序列和放在第一个位置,它并不是字符串相关的问题,事实上它的目的是要找出由数组成的一维数组中和最大的连续子序列。比如[0-235-12]应返回9[-9-2-3-5-3]应返回-2


1、动态规划法

  设状态为f[j],表示以S[j]结尾的最大连续子序列和(即最大连续子序列的最后一项为S[j],但开头一项没有规定,不一定是S[1]),状态转移方程如下:

                                    f[j] = max(f[j-1]+S[j],S[j])    1<=j<=n

                                         target = max(f[j])   1<=j<=n

int MaxSubarray(int data[],int length)
{
	vector<int> f(length+1,0);

	int result=INT_MIN;

	for (int i=0;i<length;i++)
	{
		f[i+1] = max(f[i]+data[i],data[i]);
		
		result= max(f[i+1],result);
	}
	
	return result;
}

   如果想获得最大子序列和的初始和结束位置怎么办呢?我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便可能对应结束的下标,这个时候,只要利用本轮的起始和结束位置更新上一次的始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了 

void MaxSubarray(int data[],int length, int &start, int &end, int &result)
{
	vector<int> f(length+1,0);
	result=INT_MIN;
	int curstart =0;
	for (int i=0;i<length;i++)
	{
		if (f[i]+data[i]>data[i])
		{
			f[i+1] = f[i]+data[i];
		}
		else
		{
			f[i+1] = data[i];	
			curstart =i;		
		}
		if (f[i+1]>result)
		{
                	result = f[i+1];			
			start = curstart;
			end =i;
		}
	}	
}

2、分治法

其实数组的问题,最好留点心,有一大部分题目是可以用分治的办法完成的,比如说这道题里面:最大子序列和可能出现在三个地方,1整个出现在输入数据的左半部分,2整个出现在输入数据的右半部分,3或者跨越输入数据的中部从而占据左右两个半部分。可以有以下代码:

int MaxSumRec( const vector<int> & a, int left, int right )  
{  
    if( left == right )  // Base case  
        if( a[ left ] > 0 )  
            return a[ left ];  
        else  
            return 0;  
    int center = ( left + right ) / 2;  
    int maxLeftSum  = maxSumRec( a, left, center );  
    int maxRightSum = maxSumRec( a, center + 1, right );  
    int maxLeftBorderSum = 0, leftBorderSum = 0;  
    for( int i = center; i >= left; i-- )  
    {  
        leftBorderSum += a[ i ];  
        if( leftBorderSum > maxLeftBorderSum )  
            maxLeftBorderSum = leftBorderSum;  
    }  
    int maxRightBorderSum = 0, rightBorderSum = 0;  
    for( int j = center + 1; j <= right; j++ )  
    {  
        rightBorderSum += a[ j ];  
        if( rightBorderSum > maxRightBorderSum )  
            maxRightBorderSum = rightBorderSum;  
    }  
    return max3( maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum );  
} 


二、最长递增子序列

和上一问题一样,这是数组序列中的问题,比如arr={1,5,8,2,3,4}的最长递增子序列是1,2,3,4

1、动态规划法

    结合上一题的思路,在数组的这类问题里面使用动态规划还是很常见的,从后向前分析,很容易想到,i个元素之前的最长递增子序列的长度要么是1比如说递减的数列),要么就是第i-1个元素之前的最长递增子序列加1


假设在目标数组array的前i个元素中,最长递增子序列的长度为LIS[i],那么

                                                  LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1array[i] > arr[k]


即如果array[i+1]大于array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列,或者array[i+1]自己构成一个长度为1的子序列。

int LongestAscendSubarray(int data[],int length)
{
	vector<int> LIS(length,1);
	for (int i=1;i<length;i++)
		for (int j=0;j<i;j++)
		{
			if (data[i]>data[j] && LIS[j]+1>LIS[i])
			{
				LIS[i] = LIS[j]+1;
			}
		}

		int maxValue =0;

		for (int i=0;i<LIS.size();i++)
		{
			if (LIS[i]>maxValue)
			{
				maxValue = LIS[i];
			}
		}

		return maxValue;
}


2.上面是一个比较基本的解法,我们可以转换一下思路。现在我们想找到前i个元素中的一个递增子序列,是的这个递增子序列的最大值比array[i+1]小,且长度尽可能的长。这样将array[i+1]加载递增子序列后,便可以找到以array[i+1]为最大元素的最长递增子序列。

  假设在数组的前i个元素中,以array[i]为最大元素的最长递增子序列的长度为LIS[i],同时利用MaxV[i]保存长度为i的的递增序列最大元素的最小值,这样就可以减少判断的次数

int LongestAscendSubarray(int data[],int length)
{
	vector<int> LIS(length,1);

	vector<int> MaxV(length+1,0);
	MaxV[1] = data[0];
	int nMaxLen=1;
	for (int i=0;i<length;i++)
	{			
		for (int j=nMaxLen;j>0;j--)
		{
			if (data[i]>MaxV[j])
			{
				LIS[i]=j+1;
				break;
			}
		}
		if (LIS[i]>nMaxLen)
		{
			nMaxLen = LIS[i];
			MaxV[nMaxLen] = data[i];
		}
		if (MaxV[LIS[i]>data[i]])
		{
			MaxV[LIS[i]] = data[i];
		}
	}
	return nMaxLen;
}

3. 上面的解法时间复杂度仍为O(n^2),能不能进一步降低复杂度呢?可以!!

在递增序列中,如果i<j,那么MaxV[i]<MaxV[j]。因此上面的代码

for (int j=nMaxLen;j>0;j--)
{
	if (data[i]>MaxV[j])
	{
		LIS[i]=j+1;
		break;
	}
}
可以利用二分搜索法进行加速,这样就可以吧时间复杂度降到O(nlogn)。

int LongestSubarray(int data[],int length)
{
	vector<int> LIS(length,1);
	vector<int> MaxV(length,0);

	MaxV[0] = data[0];
	int nMaxLen=1;

	for (int i=1;i<length;i++)
	{

		if (data[i]>MaxV[nMaxLen-1])
		{
			++nMaxLen;
			MaxV[nMaxLen] = data[i];
		}
		else
		{
			int pos=BinarySearch(MaxV,1,nMaxLen,data[i]);		
			MaxV[pos] = data[i];	

		}	

	}

	return nMaxLen;

}


 
  

二分搜索代码如下:
int BinarySearch(vector<int>& data,int start, int end, int target)  
{  
	int mid;  
	while (start<=end)           
	{  
		mid=(start+end)>>1;  

		if (data[mid]==target)  
		{  
			return mid;  
		}         
		else if (data[mid]<target)  
			start = mid+1;  
		else  
			end = mid-1;  
	}  

	return end;  
} 


三、最长公共子串

 这个也可以用动态规划来解决。对于s1,s2字符串,设f[i][j]表示s1[0..i]与s2[0..j]的最长公共子串,那么

f[i][j] = f[i-1][j-1]+1  if s1[i]==s2[j]


例如:

 b    a     b

c       0    0    0

a      0    1 0

b      1 0    2

a      0    2 0

计算完f之后,找到f的最大值,然后按照斜对角线读取,从而得到最长公共子串。

string LCS(string &s1,string &s2)
{
	vector<vector<int>> f(s1.size()+1,vector<int>(s2.size()+1,0));

	int result=0;
	int row=0;    //记录最大LCS最后一个字符的行位置
	
	for (int i=0;i<s1.size();i++)
		for (int j=0;j<s2.size();j++)
		{
			if (s1[i]==s2[j])
			{
				f[i+1][j+1]=f[i][j]+1;
			}
			
			if (f[i+1][j+1]>result)
			{
				result = f[i+1][j+1];
				row = i;
			}
		}
	
		string lcs = s1.substr(row-result+1,result);

		return lcs;

}

四、最长公共子序列(LCS

        这才是笔试面试中出现频度最高的问题,前面提到了一个最长公共子串,这里的最长公共子序列与它的区别在于最长公共子序列不要求在原字符串中是连续的,比如ADEFG和ABCDEG的最长公共子序列是ADEG。


设c[i][j]表示s1[1..i]与s2[1..j]的最长公共子序列,则状态转移方程为:

                                           

1.递归方法

 

int LCS_Recursive(string &s1,string& s2,int m,int n)
{
	if (m<0 || n<0)
		return 0;

	if (s1[m] == s2[n])
	{
		return LCS_Recursive(s1,s2,m-1,n-1)+1;
	}
	else
	{
		return LCS_Recursive(s1,s2,m-1,n)>LCS_Recursive(s1,s2,m,n-1)?LCS_Recursive(s1,s2,m-1,n):LCS_Recursive(s1,s2,m,n-1);
	}
}

2. 动态规划方法

void  LCS(string &s1, string &s2,int &lcsLen,vector<vector<int>>& flag)
{
	vector<vector<int>> f(s1.size()+1,vector<int>(s2.size()+1,0));

	for (int i=0;i<s1.size();i++)
		for (int j=0;j<s2.size();j++)
		{
			if (s1[i]==s2[j])
			{
				f[i+1][j+1] = f[i][j]+1;
				flag[i][j] = 3;
			}
			else
			{
				if (f[i][j+1]>f[i+1][j])
				{
					f[i+1][j+1] = f[i][j+1];
					flag[i][j] = 2;
				}
				else
				{
					f[i+1][j+1] = f[i+1][j];
					flag[i][j] = 1;
				}
			}

		}
	
	lcsLen = f[s1.size()][s2.size()];

}

打印LCS:

void PrintLCS(string& s1,vector<vector<int>>& flag) //迭代
{
	int i = flag.size()-1;
	int j = flag[0].size()-1;
	
	vector<char> result;

	while (i>=0 && j>=0)
	{
		if (flag[i][j]==1)
		{
			j--;
		}
		else if (flag[i][j]==2)
		{
			i--;
		}
		else
		{
			result.push_back(s1[i]);
			i--;
			j--;
		}
	}

	for (int k=result.size()-1;k>=0;k--)
	{
		cout<<result[k]<<" ";
	}

	cout<<endl;
		
}
// 递归
void PrintLCS2(string& s1,vector<vector<int>>& flag,int m,int n)
{
	if (m<0 || n<0)
		return;
	
	if (flag[m][n]==3)
	{
		PrintLCS2(s1,flag,m-1,n-1);
		cout<<s1[m]<<endl;
	}

	if (flag[m][n]==1)
		PrintLCS2(s1,flag,m,n-1);

	if (flag[m][n]==2)
		PrintLCS2(s1,flag,m-1,n);
	
}



五、最长不重复子串


1使用Hash

       要求子串中的字符不能重复,判重问题首先想到的就是hash,寻找满足要求的子串,最直接的方法就是遍历每个字符起始的子串,辅助hash,寻求最长的不重复子串,由于要遍历每个子串故复杂度为O(n^2)n为字符串的长度,辅助的空间为常数hash[256]。代码如下:

/* 最长不重复子串 我们记为 LNRS */  
int maxlen;  
int maxindex;  
void output(char * arr);  
/* LNRS 基本算法 hash */  
char visit[256];  
void LNRS_hash(char * arr, int size)  
{  
    for(int i = 0; i < size; ++i)  
    {  
        memset(visit,0,sizeof(visit));  
        visit[arr[i]] = 1;  
        for(int j = i+1; j < size; ++j)  
        {  
            if(visit[arr[j]] == 0)  
            {  
                visit[arr[j]] = 1;  
            }  
            else  
            {  
                if(j-i > maxlen)  
                {  
                    maxlen = j - i;  
                    maxindex = i;  
                }  
                break;  
            }  
        }  
    }  
    output(arr);  
} 

2)动态规划法

       字符串的问题,很多都可以用动态规划处理,比如这里求解最长不重复子串,和前面讨论过的最长递增子序列问题就有些类似,在LIS(最长递增子序列)问题中,对于当前的元素,要么是与前面的LIS构成新的最长递增子序列,要么就是与前面稍短的子序列构成新的子序列或单独构成新子序列

        这里我们采用类似的思路:某个当前的字符,如果它与前面的最长不重复子串中的字符没有重复,那么就可以以它为结尾构成新的最长子串;如果有重复,那么就与某个稍短的子串构成新的子串或者单独成一个新子串。

        我们来看看下面两个例子:

        1字符串“abcdeab”,第二个a之前的最长不重复子串是“abcde”a与最长子串中的字符有重复,但是它与稍短的“bcde”串没有重复,于是它可以与其构成一个新的子串,之前的最长不重复子串“abcde”结束;

        2字符串“abcb”,跟前面类似,最长串“abc”结束,第二个字符b与稍短的串“c”构成新的串;

        我们貌似可以总结出一些东西:当一个最长子串结束时(即遇到重复的字符),新的子串的长度是与(第一个重复的字符)的下标有关的

        于是类似LIS对于每个当前的元素,我们回头去查询是否有与之重复的,如没有,则最长不重复子串长度+1,如有,则是与第一个重复的字符之后的串构成新的最长不重复子串,新串的长度便是当前元素下标与重复元素下标之差

可以看出这里的动态规划方法时间复杂度为O(N^2),我们可以与最长递增子序列的动态规划方案进行对比,是一个道理的。代码如下:

/* LNRS 动态规划求解 */  
int dp[100];  
void LNRS_dp(char * arr, int size)  
{  
    int i, j;  
    maxlen = maxindex = 0;  
    dp[0] = 1;  
    for(i = 1; i < size; ++i)  
    {  
        for(j = i-1; j >= 0; --j)  
        {  
            if(arr[j] == arr[i])  
            {  
                dp[i] = i - j;  
                break;  
            }  
        }  
        if(j == -1)  
        {  
            dp[i] = dp[i-1] + 1;  
        }  
        if(dp[i] > maxlen)  
        {  
            maxlen = dp[i];  
            maxindex = i + 1 - maxlen;  
        }  
    }  
    output(arr);  
}

以上方法的时间复杂度都为O(N^2),那么有没有O(N)的算法呢?有!!

O(N)的算法,具体思路如下:
以abcbef这个串为例,用一个数组pos记录每个元素曾出现的下标,初始化为-1。从s[0]开始,依次考察每个字符,例如pos['a'] == -1,说明a还未出现过,令pos['a'] = 0,视为将‘a’加入当前串,同时长度+1,同理pos['b'] = 1,pos['c'] = 2,考察s[3],pos['b'] != -1,说明'b'在前面已经出现过了,此时可得到一个不重复串"abc",刷新当前的最大长度,然后更新pos['b']及起始串位置。
过程如下:
1、建一个256个单元的数组,每一个单元代表一个字符,数组中保存上次该字符出现的位置;
2、依次读入字符串,同时维护数组的值;
3、如果遇到冲突了,考察当前起始串位置到冲突字符的长度,如果大于当前最大长度,则更新当前最大长度并维护冲突字符的位置,更新起始串位置,继续第二步。
char* GetMaxSubStr( char* str )
{
	int hash[256]; //hash记录每个字符的出现位置
	memset(hash,-1,sizeof(hash));
	int strLen=strlen(str);
	int curStart=0;
	int maxStart=0;
	int maxEnd =0;
	int curLen=0;

	for (int i=0;i<strLen;i++)
	{
		if (hash[str[i]]==-1) //如果没有重复
		{
			
			hash[str[i]] = i;
			cout<<hash[str[i]]<<endl;
		}
		else
		{
			curLen = i-curStart;   //当前长度
			if (curLen>maxEnd-maxStart+1) //如果当前长度最长
			{
				maxStart=curStart;
				maxEnd = i-1;
			}

			curStart = hash[str[i]]+1; //更新当前最长的起点
			hash[str[i]] = i;          //更新字符出现的位置
		}
	}

	if (maxEnd == 0)//没有重复字符,返回源串
	{
		char* reStr = new char[strLen + 1];
		strcpy(reStr, str);
		return reStr;
	}

	curLen = strLen-curStart;   //当前长度
	if (curLen>maxEnd-maxStart+1) //如果当前长度最长
	{
		maxStart=curStart;
		maxEnd = strLen-1;
	}

	int MaxLength = maxEnd-maxStart+1;
	char* res=new char[MaxLength+1];
	memset(res,0,MaxLength+1);
	strncpy(res,str+maxStart,MaxLength);

	return res;
}




六、最长回文

回文串就是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。回文子串,顾名思义,即字符串中满足回文性质的子串。比如输入字符串 "google”,由于该字符串里最长的对称子字符串是 "goog”,因此输出4。

1.问题解决的基本方法

分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。 
要判断一个字符串是不是对称的,不是一件很难的事情。我们可以先得到字符串首尾两个字符,判断是不是相等。如果不相等,那该字符串肯定不是对称的。否则我们接着判断里面的两个字符是不是相等,以此类推。

<span style="font-family:SimSun;font-size:14px;">#include<iostream>
using namespace std;
//字符串是否对称
bool isAym(char *cbegin, char *cend)
{
    if(cbegin == NULL || cend ==NULL || cbegin > cend)
    {
        return false;
    }
    while(cbegin<cend)
    {
        if(*cbegin!=*cend)
        {
            return false;
        }
        cbegin++;
        cend--;
    }
    return true;
}</span>

现在我们试着来得到对称子字符串的最大长度。最直观的做法就是得到输入字符串的所有子字符串,并逐个判断是不是对称的。如果一个子字符串是对称的,我们就得到它的长度,最后经过比较,就能得到最长的对称子字符串的长度了。

<span style="font-family:SimSun;font-size:14px;">//O(n*n*n)复杂度的子字符串
int getMaxSym(char * str)
{
    if(str == NULL)
        return 0;
    int maxlength = 0, strlength = 0;
    char *pFirst = str;
    char *strEnd = str + strlen(str);
    while(pFirst < strEnd)
    {
        char *pLast = strEnd;
        while(pLast > pFirst)
        {
            if(isAym(pFirst, pLast))
            {
                strlength = pLast - pFirst + 1;
                if(strlength > maxlength)
                {
                    maxlength = strlength;
                }
            }
            pLast --;
        }
        pFirst ++;
    }
    return maxlength;
}</span>

上述方法的时间效率:由于需要两重while循环,每重循环需要O(n)的时间。另外,我们在循环中调用了IsSym,每次调用也需要O(n)的时间。因此整个函数的时间效率是O(n^3)。 
假设输入:abcddcba,按照上述程序,要分割成 'abcddcba’, 'bcddcb’, 'cddc’, 'dd’…等字符串,并对这些字符串分别进行判断。不难发现,很多短子字符串在长些的子字符串中比较过,这导致了大量的冗余判断,根本原因是:对字符串对称的判断是由外向里进行的。 
换一种思路,从里向外来判断。也就是先判断子字符串(如dd)是不是对称的。如果它(dd)不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不是对称的。如果它(dd)对称,那么只需要判断它(dd)两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。


2.改进的解决方案

根据从里向外比较的思路写出如下代码:

<span style="font-family:SimSun;font-size:14px;">//改进后的程序
int getMaxSym2(char * str)
{
    if(str == NULL)
        return 0;
    int maxlength = 0;
    char *ptag = str;
    while(*ptag !='\0')
    {
        //奇数子字符串
         char *left = ptag - 1;
        char *right = ptag + 1;
        int oddlenght = 1;
        while(left >= str && *right != '\0' && *left == *right)
        {
            left--;
            right++;
            oddlenght += 2;
        }
        if(oddlenght > maxlength)
        {
            maxlength = oddlenght;
        }
        //偶数子字符串
         left = ptag;
        right = ptag + 1;
        int evenlength = 0;
        while(left >= str && *right != '\0' && *left == *right)
        {
            left--;
            right++;
            evenlength += 2;
        }
        if(evenlength > maxlength)
        {
            maxlength = evenlength;
        }

        ptag++;
    }
    return maxlength;
}</span>

由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从只有一个字符的中心向两端延长出来,而长度为偶数的字符串是从一个有两个字符的中心向两端延长出来。因此程序中要把这两种情况都考虑进去。 
由于总共有O(n)个字符,每个字符可能延长O(n)次,每次延长时只需要O(1)就能判断出是不是对称的,因此整个函数的时间效率是O(n^2)。 
上述方法称为朴素算法,关于字符串的题目常用的算法有KMP、后缀数组、AC自动机,这道题目利用扩展KMP可以解答,其时间复杂度也很快O(N*logN)。但是,这里介绍一个专门针对回文子串的算法,其时间复杂度为O(n),这就是manacher算法。

3.manacher算法

算法的基本思路是这样的:把原串每个字符中间用一个串中没出现过的字符分隔#开来(统一奇偶),同时为了防止越界,在字符串的首部也加入一个特殊符$,但是与分隔符不同。同时字符串的末尾也加入'\0'。算法的核心:用辅助数组p记录以每个字符为核心的最长回文字符串半径。也就是p[i]记录了以str[i]为中心的最长回文字符串半径。p[i]最小为1,此时回文字符串就是字符串本身。 
示例:原字符串 'abba’,处理后的新串 ' $#a#b#b#a#\0’,得到对应的辅助数组p=[0,1,2,1,2,5,2,1,2,1]。 
程序如下,对应的变量解释在后面

<span style="font-family:SimSun;font-size:14px;">char * pre(char *str)
{
	int length = strlen(str);
	char *prestr = new char[2*length + 3];
	prestr[0] = '$';
	for(int i=0;i<length;i++)
	{
		prestr[2*i+1] = '#';
		prestr[2*i+2] = str[i];
	}
	prestr[2*length+1]='#';
	prestr[2*length+2]='\0';
	return prestr;
}

int getMaxSym3(char *str)
{
	char *prestr = pre(str);
	int mx =0, pi=1;//边界和对称中心
	int len = strlen(prestr);
	//辅助数组
	int *p = new int[len];
	p[0] = 0;
	for(int i=1;i<len;i++)
	{
		if(mx>i)
		{
			p[i]=min(mx-i,p[2*pi-i]);//核心
		}
		else
		{
			p[i]=1;
		}
		while(prestr[i-p[i]]==prestr[i+p[i]]&&i-p[i]>0&&i+p[i]<len)
		{
			p[i]++;
		}
		if(i+p[i] > mx)
		{
			mx = p[i] + i;
			pi = i;
		}

		
	}
	//最大回文字符串长度
	int maxlen = 0;
	for(int i=0;i<len;i++)
	{
		if(p[i]>maxlen)
		{
			maxlen = p[i];
		}
		printf("%d  ",p[i]);
	}
	delete []prestr;
	delete []p;
	return maxlen - 1;
}
</span>

上面几个变量说明:pi记录具有遍历过程中最长半径的回文字符串中心字符串。mx记录了具有最长回文字符串的右边界的下一个字符。 
字符串、数组 算法总结_第1张图片 
pi是最长回文字符串(淡蓝色)的中心,如果以j为中心的最大回文串如上如所示,那么i处的情况与j处相同(关于pi的两侧是对称的)。这样便减少了运算量,i的对称位置是2*pi-i。 
但是有另外一种情况,就是j的一部分超出蓝色部分,这时p[i]=p[j]就不一定对了,如下图 
字符串、数组 算法总结_第2张图片 
这就为什么有取最小值这个操作:

if(mx>i) { p[i]=min(mx-i,p[2*pi-i]);//核心
}

剩下的代码就很容易看懂了。

最后遍历一边p数组,找出最大的p[i]-1就是所求的最长回文字符串长度,说明如下:
(1)因为p[i]记录插入分隔符之后的回文字符串半径,所以以i为中心的回文字符串长度为2*p[i]-1。例如:bb=>#b#b#,中间#的半径为3,回文字符串长度为2*3-1; 
(2)注意上面两个串的关系。 #b#b#减去一个#号的长度就是原来的2倍。即((2*p[i]-1)-1)/2 = p[i]-1,得证。




你可能感兴趣的:(C++,算法,lcs)