找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串

[置顶] 找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串

分类: 笔试面试知识   442人阅读  评论(3)  收藏  举报
数组 字符串 最长公共子序列子串 编辑距离 回文子串

目录(?)[+]

作者:寒小阳

时间:2013年9月。

出处:http://blog.csdn.net/han_xiaoyang/article/details/11969497
声明:版权所有,转载请注明出处,谢谢。


0、前言

        这一部分的内容原本是打算在之后的字符串或者数组专题里面写的,但看着目前火热进行的各家互联网公司笔试面试中,出现了其中的一两个内容,就随即将这些经典问题整理整理,单写一篇发上来了。这里争取覆盖面广一些,列举了7个最经典的问题,也会是之后大家笔试面试常见到的问题,而每个问题下都列举了几种思路,掌握这些经典问题的解题思路和算法相信对同类型问题的解答都能有帮助。

       这里总结的几个问题分别是最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串。其中前两个问题是针对数组求解的,后五个问题是针对字符串求解的。多数问题都有动态规划的解法(博主不堪地表示,自己动态规划也较弱,只能想到一些基本的思路),这些解法需要细细琢磨,可发散式地使用在很多其他的题目上。

一、最大子序列和

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

1、动态规划法

你也许从这两个例子中已经可以看出,使用动态规划的方法很容易完成这个任务,只要前i项的和还没有小于0那么子序列就一直向后扩展,否则丢弃之前的子序列开始新的子序列,同时我们要记下各个子序列的和,最后找到和最大的子序列。但是你可能需要谨慎一些,在整个数组都为负的情况下,所以初始的和最大值赋值不当的话可能会出问题。

    根据以上的思路我们可以有以下的代码:

[cpp]  view plain copy
  1. /********************************************************************** 
  2. 动态规划求最大子序列和 
  3. **********************************************************************/  
  4. int Maxsum(int * arr, int size)  
  5. {  
  6.     int maxSum = -INF; //很重要,初始值赋值为负无穷大  
  7.     int sum = 0;  
  8.     for(int i = 0; i < size; ++i)  
  9. {  
  10. //小于0则舍弃  
  11.         if(sum < 0)  
  12.         {  
  13.             sum = arr[i];  
  14.         }else  
  15.         {  
  16.             sum += arr[i];  
  17.         }  
  18. //比现有最大值大,则替换  
  19.         if(sum > maxSum)  
  20.         {  
  21.             maxSum = sum;  
  22.         }  
  23.     }  
  24.     return maxSum;  
  25. }  


[cpp]  view plain copy
  1. /************************************************************************* 
  2. 如果想获得最大子序列和的初始和结束位置怎么办呢?我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便对应可能的结束下标,这个时候,只要顺便用本轮的起始和结束位置更新始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了 
  3. *****************************************************************************/  
  4. void Maxsum_location(int * arr, int size, int & start, int & end)  
  5. {  
  6.     int maxSum = -INF;  
  7.     int sum = 0;  
  8.     int curstart = start = 0;  /* curstart记录每次当前起始位置 */  
  9.     for(int i = 0; i < size; ++i)  
  10.     {  
  11.         if(sum < 0)  
  12.         {  
  13.             sum = arr[i];  
  14.             curstart = i;     /* 记录当前的起始位置 */  
  15.         }else  
  16.         {  
  17.             sum += arr[i];  
  18.         }  
  19.         if(sum > maxSum)  
  20.         {  
  21.             maxSum = sum;  
  22.             start = curstart; /* 记录并更新最大子数组起始位置 */  
  23.             end = i;  
  24.         }  
  25.     }  
  26. }  


2、分治法

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

[cpp]  view plain copy
  1. /************************************************************** 
  2. 分治法求解最大子序列和 
  3. ***************************************************************/  
  4. int MaxSumRec( const vector<int> & a, int left, int right )  
  5. {  
  6.     if( left == right )  // Base case  
  7.         if( a[ left ] > 0 )  
  8.             return a[ left ];  
  9.         else  
  10.             return 0;  
  11.     int center = ( left + right ) / 2;  
  12.     int maxLeftSum  = maxSumRec( a, left, center );  
  13.     int maxRightSum = maxSumRec( a, center + 1, right );  
  14.     int maxLeftBorderSum = 0, leftBorderSum = 0;  
  15.     forint i = center; i >= left; i-- )  
  16.     {  
  17.         leftBorderSum += a[ i ];  
  18.         if( leftBorderSum > maxLeftBorderSum )  
  19.             maxLeftBorderSum = leftBorderSum;  
  20.     }  
  21.     int maxRightBorderSum = 0, rightBorderSum = 0;  
  22.     forint j = center + 1; j <= right; j++ )  
  23.     {  
  24.         rightBorderSum += a[ j ];  
  25.         if( rightBorderSum > maxRightBorderSum )  
  26.             maxRightBorderSum = rightBorderSum;  
  27.     }  
  28.     return max3( maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum );  
  29. }  


二、最长递增子序列

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

1、动态规划法

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

LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1arr[i] > arr[k],这样arr[i]才能在arr[k]的基础上构成一个新的递增子序列。这种方法代码如下:

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.    
  4. //动态规划法求最长递增子序列 LIS  
  5.    
  6. int dp[101]; /* 设数组长度不超过100,dp[i]记录到[0,i]数组的LIS */  
  7. int lis;    /* LIS 长度 */  
  8.    
  9. int LIS(int * arr, int size)  
  10. {  
  11.     for(int i = 0; i < size; ++i)  
  12.     {  
  13.         dp[i] = 1;  
  14.         for(int j = 0; j < i; ++j)  
  15.         {  
  16.             if(arr[i] > arr[j] && dp[i] < dp[j] + 1)  
  17.             {  
  18.                 dp[i] = dp[j] + 1;  
  19.                 if(dp[i] > lis)  
  20.                 {  
  21.                     lis = dp[i];  
  22.                 }  
  23.             }  
  24.         }  
  25.     }  
  26.     return lis;  
  27. }  
  28.    
  29. /* 输出LIS */  
  30. void outputLIS(int * arr, int index)  
  31. {  
  32.     bool isLIS = 0;  
  33.     if(index < 0 || lis == 0)  
  34.     {  
  35.         return;  
  36.     }  
  37.     if(dp[index] == lis)  
  38.     {  
  39.         --lis;  
  40.         isLIS = 1;  
  41.     }  
  42.    
  43.     outputLIS(arr,--index);  
  44.    
  45.     if(isLIS)  
  46.     {  
  47.         printf("%d ",arr[index+1]);  
  48.     }  
  49. }  
  50.    
  51. void main()  
  52. {  
  53.     int arr[] = {1,-1,2,-3,4,-5,6,-7};  
  54.    
  55.     /* 输出LIS长度; sizeof 计算数组长度 */  
  56.     printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));  
  57.    
  58.     /* 输出LIS */  
  59.     outputLIS(arr,sizeof(arr)/sizeof(int) - 1);  
  60.     printf("\n");  
  61. }  


2、数组排序后,与原数组求最长公共子序列

这个方法还是非常巧妙的,因为LIS是单调递增的性质,所以任意一个LIS一定跟排序后的序列有最长公共子序列,并且就是LIS本身不过这里还没有提到最长公共子序列,可以先移步下一节,看完后再回来看这个方法的代码,代码如下:

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.    
  4. /* 最长递增子序列 LIS 
  5.  * 设数组长度不超过 100 
  6.  * quicksort + LCS 
  7. */  
  8.    
  9. void swap(int * arr, int i, int j)  
  10. {  
  11.     int tmp = arr[i];  
  12.     arr[i] = arr[j];  
  13.     arr[j] = tmp;  
  14. }  
  15.    
  16. void qsort(int * arr, int left, int right)  
  17. {  
  18.     if(left >= right)    return ;  
  19.     int index = left;  
  20.     for(int i = left+1; i <= right; ++i)  
  21.     {  
  22.         if(arr[i] < arr[left])  
  23.         {  
  24.             swap(arr,++index,i);  
  25.         }  
  26.     }  
  27.     swap(arr,index,left);  
  28.     qsort(arr,left,index-1);  
  29.     qsort(arr,index+1,right);  
  30. }  
  31.    
  32. int dp[101][101];  
  33.    
  34. int LCS(int * arr, int * arrcopy, int len)  
  35. {  
  36.     for(int i = 1; i <= len; ++i)  
  37.     {  
  38.         for(int j = 1; j <= len; ++j)  
  39.         {  
  40.             if(arr[i-1] == arrcopy[j-1])  
  41.             {  
  42.                 dp[i][j] = dp[i-1][j-1] + 1;  
  43.             }else if(dp[i-1][j] > dp[i][j-1])  
  44.             {  
  45.                 dp[i][j] = dp[i-1][j];  
  46.             }else  
  47.             {  
  48.                 dp[i][j] = dp[i][j-1];  
  49.             }  
  50.         }  
  51.     }  
  52.     return dp[len][len];  
  53. }  
  54.    
  55. void main()  
  56. {  
  57.     int arr[] = {1,-1,2,-3,4,-5,6,-7};  
  58.     int arrcopy [sizeof(arr)/sizeof(int)];  
  59.    
  60.     memcpy(arrcopy,arr,sizeof(arr));  
  61.     qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);  
  62.    
  63.     /* 计算LCS,即LIS长度 */  
  64.     int len = sizeof(arr)/sizeof(int);  
  65.     printf("%d\n",LCS(arr,arrcopy,len));  
  66. }  


3、动态规划和二分查找结合

       我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长

       在这里我们维护一个数组MaxV[i],记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。这个方法真是太巧妙了,妙不可言。

具体代码如下:

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.    
  4. /* 最长递增子序列 LIS 
  5.  * 设数组长度不超过 30 
  6.  * DP + BinarySearch 
  7. */  
  8.    
  9. int MaxV[30]; /* 存储长度i+1(len)的子序列最大元素的最小值 */  
  10. int len;      /* 存储子序列的最大长度 即MaxV当前的下标*/  
  11.    
  12. /* 返回MaxV[i]中刚刚大于x的那个元素的下标 */  
  13. int BinSearch(int * MaxV, int size, int x)  
  14. {  
  15.     int left = 0, right = size-1;  
  16.     while(left <= right)  
  17.     {  
  18.         int mid = (left + right) / 2;  
  19.         if(MaxV[mid] <= x)  
  20.         {  
  21.             left = mid + 1;  
  22.         }else  
  23.         {  
  24.             right = mid - 1;  
  25.         }  
  26.     }  
  27.     return left;  
  28. }  
  29.    
  30. int LIS(int * arr, int size)  
  31. {  
  32.     MaxV[0] = arr[0]; /* 初始化 */  
  33.     len = 1;  
  34.     for(int i = 1; i < size; ++i) /* 寻找arr[i]属于哪个长度LIS的最大元素 */  
  35.     {  
  36.         if(arr[i] > MaxV[len-1]) /* 大于最大的自然无需查找,否则二分查其位置 */  
  37.         {  
  38.             MaxV[len++] = arr[i];  
  39.         }else  
  40.         {  
  41.             int pos = BinSearch(MaxV,len,arr[i]);  
  42.             MaxV[pos] = arr[i];  
  43.         }  
  44.     }  
  45.     return len;  
  46. }  
  47.    
  48. void main()  
  49. {  
  50.     int arr[] = {1,-1,2,-3,4,-5,6,-7};  
  51.    
  52.     /* 计算LIS长度 */  
  53.     printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));  
  54. }  


三、最长公共子串(LCS

       回到最常见的字符串问题了,这里的找两个字符串的最长公共子串,要求在原字符串中是连续的。其实和上面两个问题一样,这里依旧可以用动态规划来求解,其实博主自己也不大擅长动态规划,但是可以仿照上面的思路来操作。我们采用一个二维矩阵来记录中间的结果。这个二维矩阵怎么构造呢?直接举个例子吧:"bab""caba",则数组如下:

   b    a     b

c       0    0    0

a      0    1 0

b      1 0    1

a      0    1 0

    我们看矩阵的斜对角线最长的那个就是我们找的最长公共子串

    那怎么求最长的由1组成的斜对角线呢?可以做这样的操作:当要在矩阵是填1时让它等于其左上角元素加1

   b    a     b

c       0    0    0

a      0    1 0

b      1 0    2

a      0    2 0

     这样矩阵中的最大元素就是 最长公共子串的长度。

     在构造这个二维矩阵的过程中由于得出矩阵的某一行后其上一行就没用了,所以实际上在程序中可以用一维数组来代替这个矩阵(这样空间复杂度就降低了哈)

代码如下:

[cpp]  view plain copy
  1. #include<iostream>  
  2. #include<cstring>  
  3. #include<vector>  
  4. using namespace std;  
  5. //str1为横向,str2这纵向  
  6. const string LCS(const string& str1,const string& str2){  
  7.     int xlen=str1.size();       //横向长度  
  8.     vector<int> tmp(xlen);        //保存矩阵的上一行  
  9.     vector<int> arr(tmp);     //当前行  
  10.     int ylen=str2.size();       //纵向长度  
  11.     int maxele=0;               //矩阵元素中的最大值  
  12.     int pos=0;                  //矩阵元素最大值出现在第几列  
  13.     for(int i=0;i<ylen;i++){  
  14.         string s=str2.substr(i,1);  
  15.         arr.assign(xlen,0);     //数组清0  
  16.         for(int j=0;j<xlen;j++){  
  17.             if(str1.compare(j,1,s)==0){  
  18.                 if(j==0)  
  19.                     arr[j]=1;  
  20.                 else  
  21.                     arr[j]=tmp[j-1]+1;  
  22.                 if(arr[j]>maxele){  
  23.                     maxele=arr[j];  
  24.                     pos=j;  
  25.                 }  
  26.             }         
  27.         }  
  28.         tmp.assign(arr.begin(),arr.end());  
  29.     }  
  30.     string res=str1.substr(pos-maxele+1,maxele);  
  31.     return res;  
  32. }  
  33. int main(){  
  34.     string str1("21232523311324");  
  35.     string str2("312123223445");  
  36.     string lcs=LCS(str1,str2);  
  37.     cout<<lcs<<endl;  
  38.     return 0;  
  39. }  

 

四、最长公共子序列

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

1)递归方法求解

        这个地方可能最容易想到的方法就是递归处理了,设有字符串a[0...n]b[0...m]则易知当数组a的i位置上b的j位置上对应位相同时,则直接求解两个串从下一个位置开始的剩下部分的最长公共子序列即可;当不同时,则求a[i+1...n]b[j...m]a[i...n]b[j+1...m]两种情况中的较大数值即可,用公式表示如下:


代码如下:

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<string.h>  
  3. char a[100],b[100];  
  4. int lena,lenb;  
  5. int LCS(int,int);///两个参数分别表示数组a的下标和数组b的下标  
  6. int main()  
  7. {  
  8.     strcpy(a,"ABCBDAB");  
  9.     strcpy(b,"BDCABA");  
  10.     lena=strlen(a);  
  11.     lenb=strlen(b);  
  12.     printf("%d\n",LCS(0,0));  
  13.     return 0;  
  14. }  
  15. int LCS(int i,int j)  
  16. {  
  17.     if(i>=lena || j>=lenb)  
  18.         return 0;  
  19.     if(a[i]==b[j])  
  20.         return 1+LCS(i+1,j+1);  
  21.     else  
  22.         return LCS(i+1,j)>LCS(i,j+1)? LCS(i+1,j):LCS(i,j+1);  
  23. }  


        这种处理方法优点是编程简单,非常容易理解。缺点是效率太低了,有大量的重复执行递归调用,一般情况下面试官是不会满意的。另一个致命的缺点是只能求出最大公共子序列的长度,求不出具体的最大公共子序列,而在大部分笔试或者面试时会要求我们求出具体的最大公共子序列。

2)动态规划

        这里依旧可以采用动态规划的方法来解决这个问题,可以借助一个二维数组来标识中间计算结果,避免重复的计算来提高效率,可能需要消耗一部分空间,但是时间复杂度大大降低。

        如下图所示的两个串,求解最长公共子序列的过程很明了:

找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串_第1张图片

       设有字符串a[0...n]b[0...m],字符串a对应的是二维数组num的行,字符串b对应的是二维数组num的列。我们有以下的递推公式:

找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串_第2张图片

       我们在程序中,可以使用二维数组flag来记录下标i和j的走向。数字"1"表示,斜向下;数字"2"表示,水平向右;数字"3"表示,竖直向下。这样我们可以求解出行进的路径,从而得到最长公共子序列。代码如下:

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<string.h>  
  3. char a[500],b[500];  
  4. char num[501][501]; ///记录中间结果的数组  
  5. char flag[501][501];    ///标记数组,用于标识下标的走向,构造出公共子序列  
  6. void LCS(); ///动态规划求解  
  7. void getLCS();    ///采用倒推方式求最长公共子序列  
  8. int main()  
  9. {  
  10.     int i;  
  11.     strcpy(a,"ABCBDAB");  
  12.     strcpy(b,"BDCABA");  
  13.     memset(num,0,sizeof(num));  
  14.     memset(flag,0,sizeof(flag));  
  15.     LCS();  
  16.     printf("%d\n",num[strlen(a)][strlen(b)]);  
  17.     getLCS();  
  18.     return 0;  
  19. }  
  20. void LCS()  
  21. {  
  22.     int i,j;  
  23.     for(i=1;i<=strlen(a);i++)  
  24.     {  
  25.         for(j=1;j<=strlen(b);j++)  
  26.         {  
  27.             if(a[i-1]==b[j-1])   ///注意这里的下标是i-1与j-1  
  28.             {  
  29.                 num[i][j]=num[i-1][j-1]+1;  
  30.                 flag[i][j]=1;  ///斜向下标记  
  31.             }  
  32.             else if(num[i][j-1]>num[i-1][j])  
  33.             {  
  34.                 num[i][j]=num[i][j-1];  
  35.                 flag[i][j]=2;  ///向右标记  
  36.             }  
  37.             else  
  38.             {  
  39.                 num[i][j]=num[i-1][j];  
  40.                 flag[i][j]=3;  ///向下标记  
  41.             }  
  42.         }  
  43.     }  
  44. }  
  45. void getLCS()  
  46. {  
  47.     char res[500];  
  48.     int i=strlen(a);  
  49.     int j=strlen(b);  
  50.     int k=0;    ///用于保存结果的数组标志位  
  51.     while(i>0 && j>0)  
  52.     {  
  53.         if(flag[i][j]==1)   ///如果是斜向下标记  
  54.         {  
  55.             res[k]=a[i-1];  
  56.             k++;  
  57.             i--;  
  58.             j--;  
  59.         }  
  60.         else if(flag[i][j]==2)  ///如果是斜向右标记  
  61.             j--;  
  62.         else if(flag[i][j]==3)  ///如果是斜向下标记  
  63.             i--;  
  64.     }  
  65.     for(i=k-1;i>=0;i--)  
  66.         printf("%c",res[i]);  
  67. }  

五、字符串编辑距离

给定一个源字符串和目标字符串,能够对源串进行如下操作:

   1.在给定位置上插入一个字符

   2.替换任意字符

   3.删除任意字符

求通过以上操作使得源字符串和目标字符串一致的最小操作步数。

    简单描述一下解该题的思想,源字符串和目标字符串分别为str_astr_b,二者的长度分别为lalb,定义f[i,j]为子串str_a[0...i]str_b[0...j]的最小编辑距离,简单分析可知求得的str_a[0...i]str_b[0...j]的最小编辑距离有一下三种可能:

  1)去掉str_a[0...i]的最后一个字符跟str_b[0...j]匹配,则f[i, j]的值等于f[i-1, j]+1

  (2)去掉str_b[0...j]的最后一个字符跟str_a[0...i]匹配,则f[i, j]的值等于f[i, j-1]+1

  (3)去掉str_a[0...i]str_b[0...j]的最后一个字符,让二者匹配求得f[i-1, j-1],计算f[i, j]时要考虑当前字符是否相等,如果str_a[i]==str_b[j]说明该字符不用编辑,所以f[i, j]的值等于f[i-1, j-1],如果str_a[i]!=str_b[j]说明该字符需要编辑一次(任意修改str_a[i]或者str_b[j]即可),所以f[i, j]的值等于f[i-1, j-1]+1

    因为题目要求的是最小的编辑距离,所以去上面上中情况中的最小值即可,因此可以得到递推公式:

    f[i, j] = Min ( f[i-1, j]+1,   f[i, j-1]+1,   f[i-1, j-1]+(str_a[i]==str_b[j] ? 0 : 1) )

维基百科中的描述如下:

找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串_第3张图片

1)递归方法(用到动态规划)

           由上述的递归公式可以有以下代码:

[cpp]  view plain copy
  1. //求两个字符串的编辑距离问题  
  2. //递归版本,备忘录C[i,j]表示strA[i]...strA[size_A-1]与strB[j]...strB[size_B-1]的编辑距离  
  3. int editDistance_mem(char *strA,int size_A,char *strB,int size_B){  
  4.  int **C=new int*[size_A+1];  
  5.  for(int i=0;i<=size_A;i++){  
  6.   C[i]=new int[size_B+1]();  
  7.  }  
  8.  //初始化  
  9.  for(int i=0;i<=size_A;i++){  
  10.   for(int j=0;j<=size_B;j++)  
  11.    C[i][j]=INT_MAX;  
  12.  }  
  13.  int res=EDM(C,strA,0,size_A-1,strB,0,size_B-1);  
  14.  //free mem  
  15.  for(int i=0;i<=size_A;i++){  
  16.   delete [] C[i];  
  17.  }  
  18.  delete [] C;  
  19.  return res;  
  20. }  
  21. int EDM(int **C,char *strA,int i,int A_end,char *strB,int j,int B_end){  
  22.  if(C[i][j]<INT_MAX)//做备忘  
  23.   return C[i][j];  
  24.  if(i>A_end){  
  25.   if(j>B_end)  
  26.    C[i][j]=0;  
  27.   else  
  28.    C[i][j]=B_end-j+1;  
  29.  }else if(j>B_end){  
  30.   if(i>A_end)  
  31.    C[i][j]=0;  
  32.   else  
  33.    C[i][j]=A_end-i+1;  
  34.  }  
  35.  else if(strA[i]==strB[j])  
  36.   C[i][j]=EDM(C,strA,i+1,A_end,strB,j+1,B_end);  
  37.  else{  
  38.   int a=EDM(C,strA,i+1,A_end,strB,j+1,B_end);  
  39.   int b=EDM(C,strA,i,A_end,strB,j+1,B_end);  
  40.   int c=EDM(C,strA,i+1,A_end,strB,j,B_end);  
  41.   C[i][j]=min(a,b,c)+1;  
  42.  }  
  43.  return C[i][j];  
  44. }  


2)矩阵标记法

        递推方法(也可称为矩阵标记法),通过分析可知可以将f[i, j]的计算在一个二维矩阵中进行,上面的递推式实际上可以看做是矩阵单元的计算递推式,只要把矩阵填满了,f[la-1, lb-1]的值就是要求得最小编辑距离。代码如下:

[cpp]  view plain copy
  1. //求两个字符串的编辑距离问题  
  2. //递推版本 C[i,j]表示strA[i]...strA[size_A-1]与strB[j]...strB[size_B-1]的编辑距离  
  3. int editDistance_iter(char *strA,int size_A,char *strB,int size_B){  
  4.  int **C=new int*[size_A+1];  
  5.  for(int i=0;i<=size_A;i++){  
  6.   C[i]=new int[size_B+1]();  
  7.  }  
  8.  for(int i=size_A;i>=0;i--){  
  9.   for(int j=size_B;j>=0;j--){  
  10.    if(i>size_A-1){  
  11.     if(j>size_B-1)  
  12.      C[i][j]=0;  
  13.     else  
  14.      C[i][j]=size_B-j;  
  15.    }else if(j>size_B-1){  
  16.     if(i>size_A-1)  
  17.      C[i][j]=0;  
  18.     else  
  19.      C[i][j]=size_A-i;  
  20.    }else if(strA[i]==strB[j])  
  21.     C[i][j]=C[i+1][j+1];  
  22.    else  
  23.     C[i][j]=min(C[i+1][j+1],C[i+1][j],C[i][j+1])+1;  
  24.   }  
  25.  }  
  26.  int res=C[0][0];  
  27.  //free mem  
  28.  for(int i=0;i<=size_A;i++){  
  29.   delete [] C[i];  
  30.  }  
  31.  delete [] C;  
  32.  return res;  
  33. }  


六、最长不重复子串

很好理解,即求一个串内最长的不重复子串。

1使用Hash

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

[cpp]  view plain copy
  1. /* 最长不重复子串 我们记为 LNRS */  
  2. int maxlen;  
  3. int maxindex;  
  4. void output(char * arr);  
  5. /* LNRS 基本算法 hash */  
  6. char visit[256];  
  7. void LNRS_hash(char * arr, int size)  
  8. {  
  9.     for(int i = 0; i < size; ++i)  
  10.     {  
  11.         memset(visit,0,sizeof(visit));  
  12.         visit[arr[i]] = 1;  
  13.         for(int j = i+1; j < size; ++j)  
  14.         {  
  15.             if(visit[arr[j]] == 0)  
  16.             {  
  17.                 visit[arr[j]] = 1;  
  18.             }  
  19. else  
  20.             {  
  21.                 if(j-i > maxlen)  
  22.                 {  
  23.                     maxlen = j - i;  
  24.                     maxindex = i;  
  25.                 }  
  26.                 break;  
  27.             }  
  28.         }  
  29.     }  
  30.     output(arr);  
  31. }  


2)动态规划法

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

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

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

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

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

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

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

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

[cpp]  view plain copy
  1. /* LNRS 动态规划求解 */  
  2. int dp[100];  
  3. void LNRS_dp(char * arr, int size)  
  4. {  
  5.     int i, j;  
  6.     maxlen = maxindex = 0;  
  7.     dp[0] = 1;  
  8.     for(i = 1; i < size; ++i)  
  9.     {  
  10.         for(j = i-1; j >= 0; --j)  
  11.         {  
  12.             if(arr[j] == arr[i])  
  13.             {  
  14.                 dp[i] = i - j;  
  15.                 break;  
  16.             }  
  17.         }  
  18.         if(j == -1)  
  19.         {  
  20.             dp[i] = dp[i-1] + 1;  
  21.         }  
  22.         if(dp[i] > maxlen)  
  23.         {  
  24.             maxlen = dp[i];  
  25.             maxindex = i + 1 - maxlen;  
  26.         }  
  27.     }  
  28.     output(arr);  
  29. }  


3)动态规划和hash结合

        我们发现在动态规划方法中,每次都要“回头去寻找重复元素的位置,所以时间复杂度徒增到O(n^2),结合方法1)中的Hash思路,我们可以用hash记录元素是否出现过,我们当然也可以用hash记录元素出现过的下标,,这样就不必回头,而时间复杂度必然降为O(N),只不过需要一个辅助的常数空间visit[256]这也是之前我另外一篇文章找工作笔试面试那些事儿(15)---互联网公司面试的零零种种和多家经验提到的的空间换时间思路,不过一般我们的面试里面优先考虑时间复杂度,所以这是可取的方法。


[cpp]  view plain copy
  1. /* LNRS 动态规划 + hash 记录下标 */  
  2. void LNRS_dp_hash(char * arr, int size)  
  3. {  
  4.     memset(visit, -1, sizeof visit); //visit数组是-1的时候代表这个字符没有在集合中  
  5.     memset(dp, 0, sizeof dp);  
  6.     maxlen = maxindex = 0;  
  7.     dp[0] = 1;  
  8.     visit[arr[0]] = 0;  
  9.     for(int i = 1; i < size; ++i)  
  10.     {  
  11.         if(visit[arr[i]] == -1) //表示arr[i]这个字符以前不存在  
  12.         {  
  13.             dp[i] = dp[i-1] + 1;  
  14.             visit[arr[i]] = i; /* 记录字符下标 */  
  15.         }else  
  16.         {  
  17.             dp[i] = i - visit[arr[i]];  
  18.         }  
  19.         if(dp[i] > maxlen)  
  20.         {  
  21.             maxlen = dp[i];  
  22.             maxindex = i + 1 - maxlen;  
  23.         }  
  24.     }  
  25.     output(arr);  
  26. }  


4)空间再优化

      上面的方法3)已经将时间复杂度降到了O(n),可是这时面试官又发言了,说你用的辅助空间多了,还有优化方法吗,我们仔细观察动态规划最优子问题解的更新方程:

dp[i] = dp[i-1] + 1;

       dp[i-1]不就是更新dp[i]当前的最优解么?这又与之前提到的最大子数组和问题的优化几乎同出一辙,我们不需要O(n)的辅助空间去存储子问题的最优解,而只需O(1)的空间就可以了,至此,我们找到了时间复杂度O(N),辅助空间为O(1)(一个额外变量与256大小的散列表)的算法,代码如下:

[cpp]  view plain copy
  1. /* LNRS 动态规划+hash,时间复杂度O(n) 空间复杂度O(1)算法*/  
  2. void LNRS_dp_hash_ultimate(char * arr, int size)  
  3. {  
  4.     memset(visit, -1, sizeof visit);  
  5.     maxlen = maxindex = 0;  
  6.     visit[arr[0]] = 0;  
  7. int curlen = 1;  
  8.     for(int i = 1; i < size; ++i)  
  9.     {  
  10.         if(visit[arr[i]] == -1)  
  11.         {  
  12.             ++curlen;  
  13.             visit[arr[i]] = i; /* 记录字符下标 */  
  14.         }  
  15. else  
  16.         {  
  17.             curlen = i - visit[arr[i]];  
  18.         }  
  19.         if(curlen > maxlen)  
  20.         {  
  21.             maxlen = curlen;  
  22.             maxindex = i + 1 - maxlen;  
  23.         }  
  24.     }  
  25.     output(arr);  
  26. }  


七、最长回文子串

给出一个字符串S,找到一个最长的连续回文串。例如串 babcbabcbaccba 最长回文是:abcbabcba

1)自中心向两端寻找

       回文是一种特殊的字符串,我们可以以源字符串的每个字符为中心,依次寻找出最长回文子串P0, P1,...,Pn。这些最长回文子串中的最长串Pi = max(P1, P2,...,Pn)即为所求核心代码如下:

[cpp]  view plain copy
  1. string find_lps_method1(const string &str)  
  2. {  
  3.     

你可能感兴趣的:(笔试面试知识)