编程之美7:字符串,那些你必须要会的事。

哈喽,各位小伙伴们。南京今天终于停雨了呢,虽然是个阴天,也是很有感觉的哦。有没有会莫文蔚《阴天》的小伙伴?

阴天,在不开灯的房间,让所有思绪一点一点沉淀。

是的,阴天就是适合一个人在房间里面沉淀的天气。昨天还和小伙伴们谈到现在大家因为谈恋爱而产生快乐依赖于对方的现象,在这儿分享给大家一句话:想要谈恋爱,咱得先在感情上能自我满足了再去。

楼主就希望借助这些算法题来沉淀和提升自己。因为楼主脑子不是很好使,天子不聪颖,就只能借助于后天的努力了啊。

说多了,楼主这篇文章可能会一直处于更新状态。因为我想把字符串中常见的面试算法提都给整理一下,今天暂时只准备了两道题。第一道是字符串移位包含的问题,第二道题是字符串相似度(距离)问题,后面再继续更新。Are you ready?

题目一:字符串移位包含

问题描述:

给定两个字符串s1和s2,要求判定s2是否能够被通过循环移位得到的字符串包含。例如,给定s1 = AABCD和s2 = CDAA,返回true;给定s1 = ABCD和s2 = ACBD,返回false.

问题解答:

解法一:枚举

暴力求解是我们的第一反应,题目要求判定s2是否能被通过循环移位得到的字符串包含,那我们就把s1和它所有的循环移位全部求出来,一个一个判断是否存在字符串包含。话不多说,见代码:

#include <iostream>
#include <string>

using namespace std;  

bool isContain(char *src, char* dest);

void main()
{
    char s1[] = "AABCD";
    char s2[] = "CDAA";

    int len = strlen(s1); //总共可移位length-1次
    char temp = s1[len - 1];
    if (isContain(s1, s2))
    {
        cout << "包含" << endl;
        return;
    }

    for (int i = 0; i < len - 1; i++)
    {
        for (int j = len - 1; j > 0; j--)
        {
            s1[j] = s1[j - 1];
        }
        s1[0] = temp;
        temp = s1[len - 1];

        //一次右移完毕
        if (isContain(s1, s2))
        {
            cout << "包含" << endl;
            return;
        }
    }
    cout << "不包含" << endl;
    system("pause");
} 

bool isContain(char *src, char* dest)
{
    if (NULL != strstr(src, dest))
        return true;
    return false;
}

解法二:规律分析法

我们队循环移位之后的结果进行分析,以s1 = “ABCD”而言,我们将ABCD所有的移位之后的字符串序列写出来,如下所示:

ABCD -> DABC -> CDAB -> BCDA

可以发现,s1及其所有循环移位的序列都是ABCDABCD的子序列。那么如果s2是s1的循环移位子序列,那么s2肯定是s1s1的子序列。那么原题就转换成判断s2是不是s1s1的子序列了,不需要再进行循环移位的操作了。有同学肯定会疑问,属于s1s1的子序列,并不一定是s1循环移位的子序列啊。但是同学们可以试一试,属于s1s1的子序列,并且长度等于或者小于s1的子序列一定是s1的循环移位子序列。那么代码就可以变成:

#include <iostream>
#include <string>

using namespace std;  

bool isContain(char *src, char* dest, int lenSrc, int lenDest);

void main()
{
    char s1[] = "AABCD";
    char s2[] = "CDAAF";

    int len1 = strlen(s1); //总共可移位length-1次
    int len2 = strlen(s2);

    if (isContain(s1, s2, len1, len2))
        cout << "包含" << endl;
    else
        cout << "不包含" << endl;
    system("pause");
} 

bool isContain(char *src, char* dest, int lenSrc, int lenDest)
{
    if (lenDest > lenSrc)
        return false;
    char *doubleSrc = new char[2 * lenSrc + 1]();
    strcpy_s(doubleSrc, 2 * lenSrc + 1, src);
    strcat_s(doubleSrc, 2 * lenSrc + 1, src);
    doubleSrc[2 * lenSrc + 1] = ‘\0‘;

    if (NULL == strstr(doubleSrc, dest))
        return false;
    return true;
}

题目二:计算字符串相似度

问题描述:

为计算将str1变换到str2所需最小操作步骤,必须先对变换操作进行定义:

1.修改一个字符(如把“a”替换为“g”);

2.增加一个字符(如把“abcd”变为“abcdz”);

3.删除一个字符(如把“travelling”变为“traveling”);

字符串变换过程中执行了上述三个操作之间的任一操作,则两字符串间距离就加1。例如将上文中的str1变换到str2,即“abcd”到“gbcdz”,通过“abcd”->(操作1)->“gbcd”->(操作2)->“gbcdz”,需进行一次修改和一次添加字符操作,故两字符串距离为2,那么字符串相似度则为距离+1的倒数,即1/3=0.333。这是由俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。因此也叫Levenshtein Distance。

问题解答:

解法一:递归法

很多时候看起来不知道怎么下手的题目,可以尝试一下递归法。递归法的思想就是把大问题不断转换成小问题。我们看下面一个例子,A=xabcdae和B=xfdfa,先比较第一个元素,相等,那么直接进入到下一个字符的比较。如果不相等,我们可以进行如下的操作,使得这个不相等的字符串相等起来。

操作1:删除A串的第一个字符,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离;

操作2:删除B串的第一个字符,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;

操作3:将A的第一个字符修改为B的第一个字符,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;

操作4:将B的第一个字符修改为A的第一个字符,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;

操作5:增加B的第一个字符串到A的第一个字符串前面,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;

操作6:增加A的第一个字符串到B的第一个字符串前面,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离;

我们并不在乎具体需要什么操作(删除,修改或添加),我们只关心最后的距离,所以我们将上面六种操作总结一下就是

操作1:一步操作之后,然后计算A[1,…, LenA - 1]和B[0, …., LenB - 1]的距离。比如这个操作可以是删除A串的第一个字符或者是将B的第一个字符修改为A的第一个字符,但是我们不关心具体的操作;

操作2:一步操作之后,然后计算A[0,…, LenA - 1]和B[1, …., LenB - 1]的距离;

操作3:一步操作之后,然后计算A[1,…, LenA - 1]和B[1, …., LenB - 1]的距离;

递归结束的条件

上面的操作1-操作3只是告诉我们写程序的时候有这么三个分支,递归总是要有退出或者返回条件的啊。所以下面我们讨论一下,这个递归程序几个技术时的情况。

由上面的分析,我们可以大概知道,肯定要传入的参数是,A字符串和B字符串的地址,然后A/B的起始和末尾索引,假设分别为aStartIndex/bStartIndex和aEndIndex/bEndIndex。结束的三种情况:

情况一:A字符串全部遍历完了,那么step保留的是前面操作的步骤,bEndIndex - bStartIndex + 1保留的时候的字符串的距离,所以返回bEndIndex - bStartIndex + step + 1

情况二:B字符串全部遍历完了,那么step保留的是前面操作的步骤,bEndIndex - bStartIndex + 1保留的时候的字符串的距离,所以返回bEndIndex - bStartIndex + step + 1

下面直接看代码吧:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int min2(int a, int b);
int min3(int a, int b, int c);
int calc(char* a, char* b, int aStartIndex, int aEndIndex, int bStartIndex, int bEndIndex, int step);

int main(void)
{
    char *a = "abdd";
    char *b = "aebdd";
    int step = 0;
    printf("%d",calc(a, b, 0, strlen(a) - 1, 0, strlen(b) - 1, step));
    return EXIT_SUCCESS;
}

int calc(char* a, char* b, int aStartIndex, int aEndIndex, int bStartIndex, int bEndIndex, int step)
{
    int d1,d2,d3;
    if (aStartIndex > aEndIndex)
    {
        return bEndIndex - bStartIndex + step + 1;
    }

    if (bStartIndex > bEndIndex)
    {
        return aEndIndex - aStartIndex + step + 1;
    }

    if (a[aStartIndex] == b[bStartIndex])
    {
        //如果该位置处两个字符串的字符相等,那么perfect,直接进入下一个位置的比较
        return calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex + 1, bEndIndex, step);
    }
    else
    {
        step ++;
        d1 = calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex, bEndIndex, step);
        d2 = calc(a, b, aStartIndex, aEndIndex, bStartIndex + 1, bEndIndex, step);
        d3 = calc(a, b, aStartIndex + 1, aEndIndex, bStartIndex + 1, bEndIndex, step);
        return min3(d1, d2, d3);
    }

}

int min2(int a, int b)
{
    return a>b ? b : a;
}

int min3(int a, int b, int c)
{
    if(a > b)
        a = b;
    if(a > c)
        a = c;
    return a;
}

很显然,递归方法最为诟病的就是重复计算的问题,用递归方法的很多都有这个问题。那我们这时候呢,又可以拿出我们的第二把刷子啦,叫做”动态递归法“,动态递归的主要原理就是保存小规模时候程序运行的结果,然后计算大规模数据的时候就可以直接调用小规模的结果了。

解法二:动态递归法

设str1=“abcd”,str2=“gbcdz”,定义一个二维数组d[][],d[i][j]表示str1中取前i个字符和str2中取前j个字符的最短距离,例如d[3][2]表示“abc”到“gb”的最短距离。

d[i][j]的计算规则有三条:

  • 来自d[i - i][j - 1],即 “str1的前i-1个字符组成的子串” 到 “str2的前j-1个字符组成的子串” 的最小距离,此时如果str1[i] = str2[j],则最短距离不变,否则最短距离加1(即把str1[i]变为str2[j] ),所以d[i][j] = d[i - 1][j - 1] + (str1[i] == str2[j] ? 0 : 1)
  • 来自d[i - 1][j],即 “A的前i-1个字符组成的子串” 到 “B的前j个字符组成的子串” 的编辑距离。此时删除在A的第i个位置上的字符即可,所以d[i][j] = d[i - 1][j] + 1
  • 来自d[i][j - 1], 即 “A的前i个字符组成的子串” 到 “B的前j-1个字符组成的子串” 的编辑距离。此时在A的子串后面添加一个字符B[j]即可,所以d[i][j] = d[i][j - 1] + 1

    于是状态转移方程:d[i][j] = min (d[i - 1][j - 1] + (str1[i] == str2[j] ? 0 : 1) , d[i - 1][j] + 1 , d[i][j - 1] + 1)

例如str1=“abcd”,str2=“gbcdz”的d[][]就为(注意i,j的取值范围):

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int min(int a, int b, int c);
int calc(char* a, char* b);
int main(void)
{
    printf("%d",calc("abcd", "gbcdz"));
    return EXIT_SUCCESS;
}
int min(int a, int b, int c)
{
    if(a > b)
        a = b;
    if(a > c)
        a = c;
    return a;
}
int calc(char* a, char* b)
{
    int lenA = strlen(a);
    int lenB = strlen(b);
    int **d = new int*[lenA + 1];
    for (int i = 0; i < lenA + 1; i++)
    {
        d[i] = new int[lenB + 1];
    }
    for(int i = 0;i <= lenA; ++i)
        d[i][0] = i;
    for(int i = 0; i <= lenB; ++i)
        d[0][i] = i;
    for(int i = 1; i <= lenA; ++i)
        for(int j = 1; j <= lenB; ++j)
            d[i][j] = min(d[i-1][j-1] + (a[i-1] == b[j-1] ? 0:1), d[i-1][j]+1, d[i][j-1]+1);
    return d[lenA][lenB];
}

题目三:字符串逆序

未完待续,敬请期待

题目四:如何统计一行字符串中有多少个单词?

未完待续, 敬请期待

题目五:自己编写字符串库函数strlen(), strcat(), strcpy()。

未完待续, 敬请期待

题目六:如何找出一个字符串中第一个只出现一次的字符

未完待续, 敬请期待

题目七:如何输出字符串所有组合

未完待续, 敬请期待

题目八:字符转整数及整数转字符串

未完待续, 敬请期待

时间: 2024-10-23 00:39:54

编程之美7:字符串,那些你必须要会的事。的相关文章

编程之美Ex2——字符串移位包含的问题

给定两个字符串s1,s2,要求判定s2是否能够被s1做循环移位得到的字符串包含. 例如, 给定s1=AABCD和s2=CDAA,返回true: 给定s1=ABCD和s2=ACBD,返回false. 法一:直接循环移位,用strstr()比较 1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 bool Check(char src[], char des[]); 6 7 int main() 8

编程之美2.14 求数组的子数组之和的最大值

问题描述: 一个有N个整数元素的一维数组(A[0], A[1], A[2],...,A[n-1]),这个数组当然有很多子数组,那么子数组之和的最大值是什么呢? 解法: 1. 暴力解法-------O(N^3) 2. 改进版暴力解法-------O(N^2) *3. 分治算法-------O(NlogN)(暂时未去实现) 4. 数组间关系法-------O(N) 具体思路和代码: 1.暴力解法 思路:Sum[i,...,j]为数组第i个元素到第j个元素的和,遍历所有可能的Sum[i,...,j].

编程之美2.17 数组循环移位

问题描述: 设计一个算法,把一个含有N元素的数组循环左移或者右移K位. 解决方法: 1. 暴力解法------O(KN) 2. 颠倒位置------O(N) 具体思路和代码: 1. 暴力解法------O(KN) 思路:循环K次,每次移动一位 代码: 1 //右移 2 void s1(int A[], int n, int k) 3 { 4 k = k % n; 5 for(int i = 0; i < k; i++) 6 { 7 int t = A[n-1]; 8 for(int j = n-

编程之美leetcode之编辑距离

Edit Distance Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2. (each operation is counted as 1 step.) You have the following 3 operations permitted on a word: a) Insert a character b) Delete a char

编程之美2.17之数组循环移位

题目描述:设计一个算法,把一个含有N个元素的数组循环右移K位,要求算法的时间复杂度位O(Log2N),且只允许使用两个附加变量. 什么意思呢,就是说如果输入序列为:abcd1234,右移2位即变为34abcd12.唯一的要求就是使用两个附加变量. 其实这道题编程珠玑上面也出现过,书中给出的一种符合题意的解法是巧妙地进行翻转.以把abcd1234右移4位为例: 第一步:翻转1234,abcd1234---->abcd4321 第二步:翻转abcd,abcd4321---->dcba4321 第三

编程之美2.3: 寻找发帖水王

题目:传说,Tango有一大"水王",他不但喜欢发帖,还会回复其他ID发的帖子,发帖数目超过帖子总数的一半,如果你有一个当前论坛上所有帖子的列表,其中帖子作者的ID也在表中,你能快速找到这个传说中的Tango水王吗? 解题思路:由于水王的发帖数目超过一半,当每次删除两个不同ID的帖子时,水王占得帖子数目仍然大于剩下帖子的一半,重复整个过程,将ID列表中的ID总数降低,转化为更小的问题,从而得到最后水王的ID. #include <iostream> #include <

编程之美2.13 子数组最大乘积

问题描述: 给定一个长度为N的整数数组,只允许用乘法,不能用除法,计算任意(N-1)个数的组合乘积中最大的一组,并写出算法的时间复杂度. 解法: 1.暴力解法------O(n^2) 2.前后缀法------O(n) 3.统计法--------O(n) 具体思路和代码: 1.暴力解法: 思路:利用两层循环,依次删掉一个,其余的做乘法,计算出最大的. 代码: 1 int s1(int A[], int n) 2 { 3 int s = 1; 4 int max; 5 for(int i = 1;

编程之美2.1 求二进制中1的个数

最近一段的时间,一直在看编程之美之类的算法书籍,刚开始看编程之美,感觉到难度太大,有时候也不愿意去翻动这本书,不过,经过一段时间的修炼,我也彻底的喜欢上这本书了, 书中的算法涉及到很多方面,树,链表,位运算,数组,hash表应用等等. 由于最近事情也忙得差不多了,我重新写了一遍编程之美中的算法,在这里记录下来,以便以后阅读方便. 第一道题从2.1写起,这道题目难度不是很大,首先,给出这个题目的函数声明: /*2.1 求二进制中1的个数*/ int DutCountOf1InBin_1(unsig

Java 编程之美:并发极速赛车平台出租编程高级篇

借用 Java 并发极速赛车平台出租haozbbs.comQ1446595067 编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了. 相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的. 并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步: 而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够真正掌握并发编程的人才成为市场比较迫

编程之美5:求数组中最长递增子序列

最近楼楼被男朋友带着玩dota,有点上瘾,终于在昨天晚上作出了一个重大的决定,shift+delete删掉warIII文件夹,从此退出dota的明争暗斗.不过最近看男票已经将战场从11转到了topcoder,嗯,这是个好现象,希望楼楼也能跟着玩儿起来. 理想是美好的,唉,可是楼主还在编程之美的初级阶段啊.话不多说了,希望自己加油加油再加油!!(^o^)/~ 今天要看的一道题目是求数组中最长递增子序列. 题目简介: 写一个时间复杂度尽可能低的程序,求一个一维数组(N)个元素中的最长递增子序列的长度