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

作者:寒小阳

时间: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那么子序列就一直向后扩展,否则丢弃之前的子序列开始新的子序列,同时我们要记下各个子序列的和,最后找到和最大的子序列。但是你可能需要谨慎一些,在整个数组都为负的情况下,所以初始的和最大值赋值不当的话可能会出问题。

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

/**********************************************************************
动态规划求最大子序列和
**********************************************************************/
int Maxsum(int * arr, int size)
{
    int maxSum = -INF; //很重要,初始值赋值为负无穷大
    int sum = 0;
    for(int i = 0; i < size; ++i)
{
//小于0则舍弃
        if(sum < 0)
        {
            sum = arr[i];
        }else
        {
            sum += arr[i];
        }
//比现有最大值大,则替换
        if(sum > maxSum)
        {
            maxSum = sum;
        }
    }
    return maxSum;
}


/*************************************************************************
如果想获得最大子序列和的初始和结束位置怎么办呢?我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便对应可能的结束下标,这个时候,只要顺便用本轮的起始和结束位置更新始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了
*****************************************************************************/
void Maxsum_location(int * arr, int size, int & start, int & end)
{
    int maxSum = -INF;
    int sum = 0;
    int curstart = start = 0;  /* curstart记录每次当前起始位置 */
    for(int i = 0; i < size; ++i)
    {
        if(sum < 0)
        {
            sum = arr[i];
            curstart = i;     /* 记录当前的起始位置 */
        }else
        {
            sum += arr[i];
        }
        if(sum > maxSum)
        {
            maxSum = sum;
            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我们可以得到以下关系:

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

#include <iostream>
using namespace std;
 
//动态规划法求最长递增子序列 LIS
 
int dp[101]; /* 设数组长度不超过100,dp[i]记录到[0,i]数组的LIS */
int lis;    /* LIS 长度 */
 
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长度; sizeof 计算数组长度 */
    printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
 
    /* 输出LIS */
    outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
    printf("\n");
}


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

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

#include <iostream>
using namespace std;
 
/* 最长递增子序列 LIS
 * 设数组长度不超过 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);
 
    /* 计算LCS,即LIS长度 */
    int len = sizeof(arr)/sizeof(int);
    printf("%d\n",LCS(arr,arrcopy,len));
}
  • 19
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值