作者:寒小陽 時(shí)間:2013年9月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/11596001媳瞪。
聲明:版權(quán)所有骗炉,轉(zhuǎn)載請(qǐng)注明出處,謝謝蛇受。
0句葵、前言
這一部分的內(nèi)容原本是打算在之后的字符串或者數(shù)組專題里面寫的,但看著目前火熱進(jìn)行的各家互聯(lián)網(wǎng)公司筆試面試中兢仰,出現(xiàn)了其中的一兩個(gè)內(nèi)容乍丈,就隨即將這些經(jīng)典問題整理整理,單寫一篇發(fā)上來了把将。這里爭(zhēng)取覆蓋面廣一些轻专,列舉了7個(gè)最經(jīng)典的問題,也會(huì)是之后大家筆試面試常見到的問題察蹲,而每個(gè)問題下都列舉了幾種思路请垛,掌握這些經(jīng)典問題的解題思路和算法相信對(duì)同類型問題的解答都能有幫助。
這里總結(jié)的幾個(gè)問題分別是最大子序列和递览,最長(zhǎng)遞增子序列叼屠,最長(zhǎng)公共子串,最長(zhǎng)公共子序列绞铃,字符串編輯距離镜雨,最長(zhǎng)不重復(fù)子串,最長(zhǎng)回文子串儿捧。其中前兩個(gè)問題是針對(duì)數(shù)組求解的荚坞,后五個(gè)問題是針對(duì)字符串求解的。多數(shù)問題都有動(dòng)態(tài)規(guī)劃的解法(博主不堪地表示菲盾,自己動(dòng)態(tài)規(guī)劃也較弱颓影,只能想到一些基本的思路),這些解法需要細(xì)細(xì)琢磨懒鉴,可發(fā)散式地使用在很多其他的題目上诡挂。
一碎浇、最大子序列和
這里把最大子序列和放在第一個(gè)位置,它并不是字符串相關(guān)的問題璃俗,事實(shí)上它的目的是要找出由數(shù)組成的一維數(shù)組中和最大的連續(xù)子序列奴璃。比如[0,-2城豁,3苟穆,5,-1唱星,2]應(yīng)返回9雳旅,[-9,-2间聊,-3攒盈,-5,-3]應(yīng)返回-2甸饱。
1沦童、動(dòng)態(tài)規(guī)劃法
你也許從這兩個(gè)例子中已經(jīng)可以看出仑濒,使用動(dòng)態(tài)規(guī)劃的方法很容易完成這個(gè)任務(wù)叹话,墩瞳,但是你可能需要謹(jǐn)慎一些喉酌,在整個(gè)數(shù)組都為負(fù)的情況下热凹,所以初始的和最大值賦值不當(dāng)?shù)脑捒赡軙?huì)出問題。
根據(jù)以上的思路我們可以有以下的代碼:
/**********************************************************************
動(dòng)態(tài)規(guī)劃求最大子序列和
**********************************************************************/
int Maxsum(int * arr, int size)
{
int maxSum = -INF; //很重要泪电,初始值賦值為負(fù)無窮大
int sum = 0;
for(int i = 0; i < size; ++i)
{
//小于0則舍棄
if(sum < 0)
{
sum = arr[i];
}else
{
sum += arr[i];
}
//比現(xiàn)有最大值大般妙,則替換
if(sum > maxSum)
{
maxSum = sum;
}
}
return maxSum;
}
/*************************************************************************
如果想獲得最大子序列和的初始和結(jié)束位置怎么辦呢?我們知道相速,每當(dāng)當(dāng)前子數(shù)組和的小于0時(shí)碟渺,
便是新一輪子數(shù)組的開始,每當(dāng)更新最大和時(shí)突诬,便對(duì)應(yīng)可能的結(jié)束下標(biāo)苫拍,這個(gè)時(shí)候,
只要順便用本輪的起始和結(jié)束位置更新始末位置就可以旺隙,程序結(jié)束绒极,最大子數(shù)組和以及其始末位置便一起被記錄下來了
*****************************************************************************/
void Maxsum_location(int * arr, int size, int & start, int & end)
{
int maxSum = -INF;
int sum = 0;
int curstart = start = 0; /* curstart記錄每次當(dāng)前起始位置 */
for(int i = 0; i < size; ++i)
{
if(sum < 0)
{
sum = arr[i];
curstart = i; /* 記錄當(dāng)前的起始位置 */
}else
{
sum += arr[i];
}
if(sum > maxSum)
{
maxSum = sum;
start = curstart; /* 記錄并更新最大子數(shù)組起始位置 */
end = i;
}
}
}
2、分治法
其實(shí)數(shù)組的問題蔬捷,最好留點(diǎn)心垄提,,比如說這道題里面:最大子序列和可能出現(xiàn)在三個(gè)地方,1)整個(gè)出現(xiàn)在輸入數(shù)據(jù)的左半部分铡俐,2)整個(gè)出現(xiàn)在輸入數(shù)據(jù)的右半部分摘昌,3)或者跨越輸入數(shù)據(jù)的中部從而占據(jù)左右兩個(gè)半部分「叻洌可以有以下代碼:
/**************************************************************
分治法求解最大子序列和
***************************************************************/
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 );
}
二聪黎、最長(zhǎng)遞增子序列
和上一問題一樣,這是數(shù)組序列中的問題备恤,比如arr={1,5,8,2,3,4}的最長(zhǎng)遞增子序列是1,2,3,4
1稿饰、動(dòng)態(tài)規(guī)劃法
結(jié)合上一題的思路,在數(shù)組的這類問題里面使用動(dòng)態(tài)規(guī)劃還是很常見的露泊,從后向前分析喉镰,很容易想到,侣姆,我們可以得到以下關(guān)系:
,其中沉噩,對(duì)于任意的,這樣arr[i]才能在arr[k]的基礎(chǔ)上構(gòu)成一個(gè)新的遞增子序列川蒙。這種方法代碼如下:
#include <iostream>
using namespace std;
//動(dòng)態(tài)規(guī)劃法求最長(zhǎng)遞增子序列 LIS
int dp[101]; /* 設(shè)數(shù)組長(zhǎng)度不超過100蚜厉,dp[i]記錄到[0,i]數(shù)組的LIS */
int lis; /* LIS 長(zhǎng)度 */
int LIS(int * arr, int size)
{
for(int i = 0; i < size; ++i)
{
dp[i] = 1;
for(int j = 0; j < i; ++j)
{
if(arr[i] > arr[j] && dp[i] < dp[j] + 1)
{
dp[i] = dp[j] + 1;
if(dp[i] > lis)
{
lis = dp[i];
}
}
}
}
return lis;
}
/* 輸出LIS */
void outputLIS(int * arr, int index)
{
bool isLIS = 0;
if(index < 0 || lis == 0)
{
return;
}
if(dp[index] == lis)
{
--lis;
isLIS = 1;
}
outputLIS(arr,--index);
if(isLIS)
{
printf("%d ",arr[index+1]);
}
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 輸出LIS長(zhǎng)度; sizeof 計(jì)算數(shù)組長(zhǎng)度 */
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
/* 輸出LIS */
outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
printf("\n");
}
2畜眨、數(shù)組排序后昼牛,與原數(shù)組求最長(zhǎng)公共子序列
這個(gè)方法還是非常巧妙的,因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Ccolor%7Bred%7D%7BLIS%E6%98%AF%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%80%A7%E8%B4%A8%EF%BC%8C%E6%89%80%E4%BB%A5%E4%BB%BB%E6%84%8F%E4%B8%80%E4%B8%AALIS%E4%B8%80%E5%AE%9A%E8%B7%9F%E6%8E%92%E5%BA%8F%E5%90%8E%E7%9A%84%E5%BA%8F%E5%88%97%E6%9C%89%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97%EF%BC%8C%E5%B9%B6%E4%B8%94%E5%B0%B1%E6%98%AFLIS%E6%9C%AC%E8%BA%AB%7D" alt="\color{red}{LIS是單調(diào)遞增的性質(zhì)康聂,所以任意一個(gè)LIS一定跟排序后的序列有最長(zhǎng)公共子序列贰健,并且就是LIS本身}" mathimg="1">。不過這里還沒有提到最長(zhǎng)公共子序列恬汁,可以先移步下一節(jié)伶椿,看完后再回來看這個(gè)方法的代碼,代碼如下:
#include <iostream>
using namespace std;
/* 最長(zhǎng)遞增子序列 LIS
* 設(shè)數(shù)組長(zhǎng)度不超過 100
* quicksort + LCS
*/
void swap(int * arr, int i, int j)
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
void qsort(int * arr, int left, int right)
{
if(left >= right) return ;
int index = left;
for(int i = left+1; i <= right; ++i)
{
if(arr[i] < arr[left])
{
swap(arr,++index,i);
}
}
swap(arr,index,left);
qsort(arr,left,index-1);
qsort(arr,index+1,right);
}
int dp[101][101];
int LCS(int * arr, int * arrcopy, int len)
{
for(int i = 1; i <= len; ++i)
{
for(int j = 1; j <= len; ++j)
{
if(arr[i-1] == arrcopy[j-1])
{
dp[i][j] = dp[i-1][j-1] + 1;
}else if(dp[i-1][j] > dp[i][j-1])
{
dp[i][j] = dp[i-1][j];
}else
{
dp[i][j] = dp[i][j-1];
}
}
}
return dp[len][len];
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
int arrcopy [sizeof(arr)/sizeof(int)];
memcpy(arrcopy,arr,sizeof(arr));
qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);
/* 計(jì)算LCS蕊连,即LIS長(zhǎng)度 */
int len = sizeof(arr)/sizeof(int);
printf("%d\n",LCS(arr,arrcopy,len));
}
3悬垃、動(dòng)態(tài)規(guī)劃和二分查找結(jié)合
我們期望在前i個(gè)元素中的所有長(zhǎng)度為len的遞增子序列中找到這樣一個(gè)序列,它的最大元素比arr[i+1]小甘苍,而且長(zhǎng)度要盡量的長(zhǎng)尝蠕,如此,
在這里我們維護(hù)一個(gè)數(shù)組MaxV[i]看彼,記錄長(zhǎng)度為i的遞增子序列中最大元素的最小值廊佩,并對(duì)于數(shù)組中的每個(gè)元素考察其是哪個(gè)子序列的最大元素,二分更新MaxV數(shù)組靖榕,最終i的值便是最長(zhǎng)遞增子序列的長(zhǎng)度标锄。這個(gè)方法真是太巧妙了,妙不可言茁计。
具體代碼如下:
#include <iostream>
using namespace std;
/* 最長(zhǎng)遞增子序列 LIS
* 設(shè)數(shù)組長(zhǎng)度不超過 30
* DP + BinarySearch
*/
int MaxV[30]; /* 存儲(chǔ)長(zhǎng)度i+1(len)的子序列最大元素的最小值 */
int len; /* 存儲(chǔ)子序列的最大長(zhǎng)度 即MaxV當(dāng)前的下標(biāo)*/
/* 返回MaxV[i]中剛剛大于x的那個(gè)元素的下標(biāo) */
int BinSearch(int * MaxV, int size, int x)
{
int left = 0, right = size-1;
while(left <= right)
{
int mid = (left + right) / 2;
if(MaxV[mid] <= x)
{
left = mid + 1;
}else
{
right = mid - 1;
}
}
return left;
}
int LIS(int * arr, int size)
{
MaxV[0] = arr[0]; /* 初始化 */
len = 1;
for(int i = 1; i < size; ++i) /* 尋找arr[i]屬于哪個(gè)長(zhǎng)度LIS的最大元素 */
{
if(arr[i] > MaxV[len-1]) /* 大于最大的自然無需查找料皇,否則二分查其位置 */
{
MaxV[len++] = arr[i];
}else
{
int pos = BinSearch(MaxV,len,arr[i]);
MaxV[pos] = arr[i];
}
}
return len;
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 計(jì)算LIS長(zhǎng)度 */
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}
三、最長(zhǎng)公共子串(LCS)
回到最常見的字符串問題了星压,這里的找兩個(gè)字符串的最長(zhǎng)公共子串践剂,要求在原字符串中是連續(xù)的。其實(shí)和上面兩個(gè)問題一樣娜膘,這里依舊可以用動(dòng)態(tài)規(guī)劃來求解逊脯,其實(shí)博主自己也不大擅長(zhǎng)動(dòng)態(tài)規(guī)劃,但是可以仿照上面的思路來操作竣贪。军洼。這個(gè)二維矩陣怎么構(gòu)造呢?直接舉個(gè)例子吧:"bab"和"caba"演怎,則數(shù)組如下:
b | a | b | |
c | 0 | 0 | 0 |
a | 0 | 0 | |
b | 0 | ||
a | 0 | 0 |
我們看匕争。
那怎么求最長(zhǎng)的由1組成的斜對(duì)角線呢?可以做這樣的操作:當(dāng)要在颤枪。
b | a | b | |
c | 0 | 0 | 0 |
a | 0 | 0 | |
b | 0 | ||
a | 0 | 0 |
這樣矩陣中的最大元素就是 最長(zhǎng)公共子串的長(zhǎng)度汗捡。
在構(gòu)造這個(gè)二維矩陣的過程中由于得出矩陣的某一行后其上一行就沒用了淑际,所以(這樣空間復(fù)雜度就降低了哈)畏纲。
代碼如下:
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
//str1為橫向,str2這縱向
const string LCS(const string& str1,const string& str2){
int xlen=str1.size(); //橫向長(zhǎng)度
vector<int> tmp(xlen); //保存矩陣的上一行
vector<int> arr(tmp); //當(dāng)前行
int ylen=str2.size(); //縱向長(zhǎng)度
int maxele=0; //矩陣元素中的最大值
int pos=0; //矩陣元素最大值出現(xiàn)在第幾列
for(int i=0;i<ylen;i++){
string s=str2.substr(i,1);
arr.assign(xlen,0); //數(shù)組清0
for(int j=0;j<xlen;j++){
if(str1.compare(j,1,s)==0){
if(j==0)
arr[j]=1;
else
arr[j]=tmp[j-1]+1;
if(arr[j]>maxele){
maxele=arr[j];
pos=j;
}
}
}
tmp.assign(arr.begin(),arr.end());
}
string res=str1.substr(pos-maxele+1,maxele);
return res;
}
int main(){
string str1("21232523311324");
string str2("312123223445");
string lcs=LCS(str1,str2);
cout<<lcs<<endl;
return 0;
}
四春缕、最長(zhǎng)公共子序列
這才是筆試面試中出現(xiàn)頻度最高的問題盗胀,前面提到了一個(gè)最長(zhǎng)公共子串,這里的锄贼,比如ADEFG和ABCDEG的最長(zhǎng)公共子序列是ADEG票灰。
1)遞歸方法求解
這個(gè)地方可能最容易想到的方法就是遞歸處理了,設(shè)有字符串a(chǎn)[0...n]宅荤,b[0...m]屑迂,,冯键;手报,用公式表示如下:
代碼如下:
#include<stdio.h>
#include<string.h>
char a[100],b[100];
int lena,lenb;
int LCS(int,int);///兩個(gè)參數(shù)分別表示數(shù)組a的下標(biāo)和數(shù)組b的下標(biāo)
int main()
{
strcpy(a,"ABCBDAB");
strcpy(b,"BDCABA");
lena=strlen(a);
lenb=strlen(b);
printf("%d\n",LCS(0,0));
return 0;
}
int LCS(int i,int j)
{
if(i>=lena || j>=lenb)
return 0;
if(a[i]==b[j])
return 1+LCS(i+1,j+1);
else
return LCS(i+1,j)>LCS(i,j+1)? LCS(i+1,j):LCS(i,j+1);
}
這種處理方法優(yōu)點(diǎn)是蚯舱,。缺點(diǎn)是效率太低了掩蛤,有枉昏,一般情況下面試官是不會(huì)滿意的。另一個(gè)致命的缺點(diǎn)是只能求出最大公共子序列的長(zhǎng)度揍鸟,求不出具體的最大公共子序列兄裂,而在大部分筆試或者面試時(shí)會(huì)要求我們求出具體的最大公共子序列。
2)動(dòng)態(tài)規(guī)劃
這里依舊可以采用動(dòng)態(tài)規(guī)劃的方法來解決這個(gè)問題阳藻,可以,可能需要消耗一部分空間稚配,但是時(shí)間復(fù)雜度大大降低畅涂。
如下圖所示的兩個(gè)串,求解最長(zhǎng)公共子序列的過程很明了:
設(shè)有字符串a(chǎn)[0...n]道川,b[0...m]午衰,字符串a(chǎn)對(duì)應(yīng)的是二維數(shù)組num的行,字符串b對(duì)應(yīng)的是二維數(shù)組num的列冒萄。我們有以下的遞推公式:
我們?cè)诔绦蛑校?div id="gs4cg4s" class="image-package">