目录
两个数组的最长公共子数组
1.暴力破解,显然是个O(n3)的解法
int findLength(vector<int>& A, vector<int>& B) {
int size1=A.size();
int size2=B.size();
if(size1==0 || size2==0)
return 0;
int start1=0,start2=0;
int ans=0;
int length=0;
for(int i=0;i<size1;i++){
for(int j=0;j<size2;j++){
int m=i,n=j;
length=0;
while(m < size1 && n < size2){
if(A[m]!=B[n])
break;
length++;
m++;
n++;
}
if(ans<length){
start1=i;
start2=j;
ans=length;
}
}
}
return ans;
2.带记录矩阵的dp
有了一个解决问题的方法是一件很不错的事情了,但是拿着上边的解法回答面试题肯定不会得到许可,面试官还是会问有没有更好的解法呢?不过上述解法虽然不是最优的,但是依然可以从中找到一个改进的线索。不难发现在子串比较中有很多次重复的比较。
比如再比较以i
和j
分别为起始点字符串时,有可能会进行i+1
和j+1
以及i+2
和j+2
位置的字符的比较;而在比较i+1
和j+1
分别为起始点字符串时,这些字符又会被比较一次了。也就是说该问题有非常相似的子问题,而子问题之间又有重叠,这就给动态规划法的应该提供了契机。
暴力解法是从字符串开端开始找寻,现在换个思维考虑以字符结尾的子串来利用动态规划法。
假设两个字符串分别为s和t,s[i]
和t[j]
分别表示其第i
和第j
个字符(字符顺序从0
开始),再令L[i, j]
表示以s[i]
和s[j]
为结尾的相同子串的最大长度。应该不难递推出L[i, j]
和L[i+1,j+1]
之间的关系,因为两者其实只差s[i+1]
和t[j+1]
这一对字符。若s[i+1]
和t[j+1]
不同,那么L[i+1, j+1]
自然应该是0
,因为任何以它们为结尾的子串都不可能完全相同;而如果s[i+1]
和t[j+1]
相同,那么就只要在以s[i]
和t[j]
结尾的最长相同子串之后分别添上这两个字符即可,这样就可以让长度增加一位。合并上述两种情况,也就得到L[i+1,j+1]=(s[i]==t[j]?L[i,j]+1:0)
这样的关系。
最后就是要小心的就是临界位置:如若两个字符串中任何一个是空串,那么最长公共子串的长度只能是0
;当i
为0
时,L[0,j]
应该是等于L[-1,j-1]
再加上s[0]
和t[j]
提供的值,但L[-1,j-1]
本是无效,但可以视s[-1]
是空字符也就变成了前面一种临界情况,这样就可知L[-1,j-1]==0
,所以L[0,j]=(s[0]==t[j]?1:0)
。对于j
为0
也是一样的,同样可得L[i,0]=(s[i]==t[0]?1:0)
。
int findLength(vector<int>& A, vector<int>& B) {
int size1=A.size();
int size2=B.size();
if(size1==0 || size2==0)
return 0;
int start1=0,start2=0;
int ans=0;
int length=0;
vector<vector<int>> dp (size1,vector<int>(size2,0));
for(int j=0;j<size2;j++) //初始化第一行
dp[0][j] = A[0] == B[j] ? 1 : 0;
for(int i=1;i<size1;i++){
dp[i][0] = A[i] == B[0] ? 1 : 0; //初始化第一列
for(int j=1;j<size2;j++){
dp[i][j]= A[i] ==B[j] ? dp[i-1][j-1]+1 : 0; //当前一个字符相等,才有可能使长度+1
if(ans<dp[i][j]){
ans=dp[i][j];
start1=i-ans+1; //记录此子串开始的位置
start2=j-ans+1;
}
}
}
return ans;
}
字符串模式
给定一种 pattern(模式)
和一个字符串 str
,判断 str
是否遵循相同的模式。
这里的遵循指完全匹配,例如, pattern
里的每个字母和字符串 str
中的每个非空单词之间存在着双向连接的对应模式。
示例1:
输入: pattern ="abba"
, str ="dog cat cat dog"
输出: true
示例 2:
输入:pattern ="abba"
, str ="dog cat cat fish"
输出: false
示例 3:
输入: pattern ="aaaa"
, str ="dog cat cat dog"
输出: false
示例 4:
输入: pattern ="abba"
, str ="dog dog dog dog"
输出: false
说明:
你可以假设 pattern
只包含小写字母, str
包含了由单个空格分隔的小写字母。
bool wordPattern(string pattern, string str) {
istringstream strcin(str);
string s;
vector<string> res;
while(strcin >> s) res.push_back(s);
if(res.size()!=pattern.size())
return false;
map<string,int> s2i; //开两个map,记录两个string中单词出现的位置
map<char,int> c2i;
for(int i=0;i<pattern.size();i++){
if (s2i[res[i]] != c2i[pattern[i]]) //若位置不同,返回false
return false;
s2i[res[i]] = c2i[pattern[i]] = i+1; //若还未记录此单词和字母,则插入位置信息
}
return true;
}
这里要重点看一下istringstream 的用法,
istringstream是C++里面的一种输入输出控制类,它可以创建一个对象,然后这个对象就可以绑定一行字符串,然后以空格为分隔符把该行分隔开来。
getline(cin,str);
//从屏幕读取一行字符并赋给str
istringstream str1(str); //创建istringstream对象并同时初始化,使其和字符串str绑定
str1>>c1>>c2;
//以空格为分隔符把该行分隔开来
#include<sstream> //istringstream 必须包含这个头文件
自己手动写一个分割字符串的函数:
#include<iostream>
#include<vector>
#include<set>
#include<algorithm>
#include<string>
#include<map>
using namespace std;
class Solution {
public:
void split(string s, vector<string>& res, string mark) {
string::size_type pos1, pos2;
pos2 = s.find(mark);
pos1 = 0; //初始化
while (pos2 != string::npos) {
res.push_back( s.substr(pos1, pos2 - pos1) ); //截出字符串放在数组
pos1 = pos2 + mark.size();
pos2 = s.find(mark, pos1); //从pos1后再开始查找
}
if (pos1 != s.length())
res.push_back(s.substr(pos1));
}
};
int main() {
Solution a;
vector<string>res;
string s = "dog dog cat cat fish fish";
a.split(s, res, " ");
for (int i = 0; i < res.size(); i++)
cout << res[i] << " ";
system("pause");
return 0;
}
其中:
size_type: 由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型。string::size_type它在不同的机器上,长度是可以不同的,并非固定的长度。但只要你使用了这个类型,就使得你的程序适合这个机器。与实际机器匹配。
string::size_type从本质上来说,是一个整型数。关键是由于机器的环境,它的长度有可能不同。 例如:我们在使用 string::find的函数的时候,它返回的类型就是 string::size_type类型。而当find找不到所要找的字符的时候,它返回的是 npos的值,这个值是与size_type相关的。假如,你是用 string s; int rc = s.find(.....); 然后判断,if ( rc == string::npos ) 这样在不同的机器平台上表现就不一样了。如果,你的平台的string::size_type的长度正好和int相匹配,那么这个判断会侥幸正确。但换成另外的平台,有可能 string::size_type的类型是64位长度的,那么判断就完全不正确了。 所以,正确的应该是: string::size_type rc = s.find(.....); 这个时候使用 if ( rc == string::npos )就回正确了。
反转字符串中的单词顺序
如hello world ==> world hello
//反转字符串中的单词,用一个栈
void reverse_string(const string & str) {
stack<string> s;
string tmp="";
for (int i = 0; i < str.size(); i++) {
if (str[i] == ' ') {
s.push(tmp);
tmp = "";
}
else
tmp += str[i];
}
s.push(tmp); //加上最后一个单词
while (!s.empty()) {
cout << s.top() << " ";
s.pop();
}
最长回文子串
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
方法1:暴力法
两层循环,表示从i开始到第j位之间的字符串,再一层循环判断他是不是回文串。
时间复杂度O(n^3)
方法2:动态规划
开辟一个n*n的数组来存放从尾到头的回文子串情况,其中
例如对于"ababa"来说,若已经知道"bab"是回文,那么"ababa"一定也是回文。
string longestPalindrome(string s) {
if(s.size()<2)
return s;
string res="";
int n=s.size();
bool dp[n][n];
for(int i=n-1;i>=0;i--)
for(int j=i;j<n;j++){
dp[i][j]= s[i]==s[j] && (j-i<3 || dp[i+1][j-1]); //j-i<3 是为了解决i=j时的dp[i+1][j-1]越界情况
if(dp[i][j] && (res=="" || j-i+1 > res.size())) //res==""条件不需要也能通过啊
res=s.substr(i,j-i+1); //截取子串
}
return res;
}
方法二:
string longestPalindrome(string s) {
if (s.size() < 1) return s;
int min_start = 0, max_len = 1;
for (int i = 0; i < s.size();) {
if (s.size() - i <= max_len / 2) break;
int j = i, k = i;
while (k < s.size()-1 && s[k+1] == s[k]) ++k; // Skip duplicate characters.
i = k+1;
while (k < s.size()-1 && j > 0 && s[k + 1] == s[j - 1]) { ++k; --j; } // Expand.
int new_len = k - j + 1;
if (new_len > max_len) { min_start = j; max_len = new_len; }
}
return s.substr(min_start, max_len);
}
大数相乘
给定两个很大的正整数,求乘积。化为字符串进行运算
#include <iostream>
#include <string>
using namespace std;
string multiply(string num1, string num2) {
string res(num1.size() + num2.size(), '0'); //用来保存答案的每一位
for (int i = num1.size() - 1; i >= 0; i--) {
for (int j = num2.size() - 1; j >= 0; j--) {
int prod = (num1[i] - '0') * (num2[j] - '0') + (res[i + j + 1] - '0');
res[i+j+1] = (prod % 10) + '0'; //保存当前两个位之乘积的个位
res[i+j] = ((prod /10) + (res[i + j] - '0')) + '0'; //保存进位,与下一对的乘积相加
}
}
//remove the trailing zeros
int it = res.find_first_not_of("0"); //找到第一个非零的数
return ( it < 0 ? "0" : res.substr(it) );
}
kmp
原文 https://segmentfault.com/a/1190000008575379
背景:
给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题。
Knuth-Morris-Pratt 算法(简称 KMP)是解决这一问题的常用算法之一,这个算法是由高德纳(Donald Ervin Knuth)和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终三人于1977年联合发表。
在继续下面的内容之前,有必要在这里介绍下两个概念:真前缀 和 真后缀。
由上图所得, "真前缀"指除了自身以外,一个字符串的全部头部组合;"真后缀"指除了自身以外,一个字符串的全部尾部组合。
方法1:暴力匹配
int myfind(string s, string p){
int i=0;
int j=0;
int len_s = s.size();
int len_p = p.size();
while(i<len_s && j<len_p){
if(s[i] == p[j]){ //相等
i++;
j++;
}
else{ //不相等
i = i - j + 1;
j = 0;
}
}
if(j == len_p){ //匹配成功
return i - j;
}
return -1;
}
暴力匹配的时间复杂度为 O(nm),其中 n为 S 的长度,m 为 P 的长度。很明显,这样的时间复杂度很难满足我们的需求。
接下来进入正题:时间复杂度为 Θ(n+m)的 KMP 算法。
算法流程
以下摘自阮一峰的字符串匹配的KMP算法,并作稍微修改。
(1)
首先,主串"BBC ABCDAB ABCDABCDABDE"的第一个字符与模式串"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以模式串后移一位。
(2)
因为B与A又不匹配,模式串再往后移。
(3)
就这样,直到主串有一个字符,与模式串的第一个字符相同为止。
(4)
接着比较主串和模式串的下一个字符,还是相同。
(5)
直到主串有一个字符,与模式串对应的字符不相同为止。
(6)
这时,最自然的反应是,将模式串整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。(这个就是上面那个暴力匹配的做法,效率很差)
(7)
一个基本事实是,当空格与D不匹配时,你其实是已经知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。
(8)
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
怎么做到这一点呢?可以针对模式串,设置一个跳转数组int next[]
,这个数组是怎么计算出来的,后面再介绍,这里只要会用就可以了。
(9)
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。根据跳转数组可知,不匹配处D的next值为2,因此接下来从模式串下标为2的位置开始匹配。
(10)
因为空格与C不匹配,C处的next值为0,因此接下来模式串从下标为0处开始匹配。
(11)
因为空格与A不匹配,此处next值为-1,表示模式串的第一个字符就不匹配,那么直接往后移一位。
(12)
逐位比较,直到发现C与D不匹配。于是,下一步从下标为2的地方开始匹配。
(13)
逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成。
next数组是如何求出的
next数组的求解基于“真前缀”和“真后缀”,即next[i]
等于P[0]...P[i - 1]
最长的相同真前后缀的长度(请暂时忽视i等于0时的情况,下面会有解释)。我们依旧以上述的表格为例,为了方便阅读,我复制在下方了。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[ i ] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
- i = 0,对于模式串的首字符,我们统一为
next[0] = -1
; - i = 1,前面的字符串为
A
,其最长相同真前后缀长度为0,即next[1] = 0
; - i = 2,前面的字符串为
AB
,其最长相同真前后缀长度为0,即next[2] = 0
; - i = 3,前面的字符串为
ABC
,其最长相同真前后缀长度为0,即next[3] = 0
; - i = 4,前面的字符串为
ABCD
,其最长相同真前后缀长度为0,即next[4] = 0
; - i = 5,前面的字符串为
ABCDA
,其最长相同真前后缀为A
,即next[5] = 1
; - i = 6,前面的字符串为
ABCDAB
,其最长相同真前后缀为AB
,即next[6] = 2
; - i = 7,前面的字符串为
ABCDABD
,其最长相同真前后缀长度为0,即next[7] = 0
。
那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?举个代表性的例子:假如i = 6
时不匹配,此时我们是知道其位置前的字符串为ABCDAB
,仔细观察这个字符串,首尾都有一个AB
,既然在i = 6
处的D不匹配,我们为何不直接把i = 2
处的C拿过来继续比较呢,因为都有一个AB
啊,而这个AB
就是ABCDAB
的最长相同真前后缀,其长度2正好是跳转的下标位置。
有的读者可能存在疑问,若在i = 5
时匹配失败,按照我讲解的思路,此时应该把i = 1
处的字符拿过来继续比较,但是这两个位置的字符是一样的啊,都是B
,既然一样,拿过来比较不就是无用功了么?其实不是我讲解的有问题,也不是这个算法有问题,而是这个算法还未优化,关于这个问题在下面会详细说明,不过建议读者不要在这里纠结,跳过这个,下面你自然会恍然大悟。
思路如此简单,接下来就是代码实现了,如下:
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
int p_len = P.size();
int i = 0; // P 的下标
int j = -1;
next[0] = -1;
while (i < p_len - 1)
{
if (j == -1 || P[i] == P[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
一脸懵逼,是不是。。。上述代码就是用来求解模式串中每个位置的next[]
值。
下面具体分析,我把代码分为两部分来讲:
(1):i和j的作用是什么?
i和j就像是两个”指针“,一前一后,通过移动它们来找到最长的相同真前后缀。
(2):if...else...语句里做了什么?
假设i和j的位置如上图,由next[i] = j
得,也就是对于位置i来说,区段[0, i - 1]的最长相同真前后缀分别是[0, j - 1]和[i - j, i - 1],即这两区段内容相同。
按照算法流程,if (P[i] == P[j])
,则i++; j++; next[i] = j;
;若不等,则j = next[j]
,见下图:
next[j]
代表[0, j - 1]区段中最长相同真前后缀的长度。如图,用左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以else语句就是利用第一个椭圆和第四个椭圆内容相同来加快得到[0, i - 1]区段的相同真前后缀的长度。
细心的朋友会问if语句中j == -1
存在的意义是何?第一,程序刚运行时,j是被初始为-1,直接进行P[i] == P[j]
判断无疑会边界溢出;第二,else语句中j = next[j]
,j是不断后退的,若j在后退中被赋值为-1(也就是j = next[0]
),在P[i] == P[j]
判断也会边界溢出。综上两点,其意义就是为了特殊边界判断。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int simple_string_search(string s, string p) {
int s_len = s.size();
int p_len = p.size();
int i = 0;
int j = 0;
while (i < s_len && j < p_len) {
if (s[i] == p[j]) {
i++;
j++;
}
else {
i = i - j + 1;
j = 0;
}
}
if (j == p_len) {
return i - j;
}
else
return -1;
}
void get_next(string p, vector<int>& next) {
int p_len = p.size();
int i = 0;
int j = -1;
next[0] = -1;
while (i < p_len) {
if (j == -1 || p[i] == p[j]) {
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
int KMP(string s, string p, vector<int>& next) {
get_next(p, next);
int s_len = s.size();
int p_len = p.size();
int i = 0;
int j = 0;
while (i < s_len && j < p_len) {
if (j == -1 || s[i] == p[j]) {
i++;
j++;
}
else
j = next[j];
}
if (j == p_len)
return i - j;
else
return -1;
}
int main()
{
string a = "bbc abcd abcdefgbbb abc";
string b = "abcde";
vector<int> next(b.size() + 1, 0);
int res = KMP(a, b, next);
if (res)
cout << "pos = " << res << endl;
else
cout << "not found!" << endl;
system("pause");
return 0;
}
kmp算法的优化可见原文 https://segmentfault.com/a/1190000008575379
最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
解法1:暴力
int lengthOfLIS(vector<int>& nums) {
int len=nums.size();
return help(nums,INT_MIN,0);
}
int help(vector<int>& a,int pre, int curpos){
if(curpos==a.size())
return 0;
int take=0;
if(a[curpos]>pre)
take=1+help(a,a[cur],curpos+1);
int notake=help(a,pre,curpos+1);
return max(take,notake);
}
时间复杂度O(2^n),
空间复杂度O(n*n)
解法2:动态规划
dp[i]表示以第i位结尾的最长上升子序列长度
当查看第i位元素时,遍历0~i-1位的元素及其dp记录,若a[i]>a[j] 则dp[i] = max(dp[i],dp[j]+1)
int lengthOfLIS(vector<int>& nums) {
int len=nums.size();
vector<int> dp(len,1);
for(int i=1;i<len;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
int ans=0;
for(int i=0;i<len;i++)
ans=max(ans,dp[i]);
return ans;
}
时间复杂度O(n^2)
空间复杂度O(n)
字符串的全组合和全排列
全排列:
vector<string> Permutation(string str) {
vector<string> a;
Permutation(a,str,0);
sort(a.begin(),a.end());
return a;
}
void Permutation(vector<string> &a,string str,int k){
if(k==str.size()-1){
a.push_back(str);
return ;
}
for(int i=k;i<str.size();i++){
if(i!=k && str[i]==str[k]) //可能有重复的字符,不用交换
continue;
swap(str[i],str[k]);
Permutation(a,str,k+1);
swap(str[i],str[k]);
}
return;
}
所有子序列
void printAllsub(string s, int i, string res) {
if (i == s.size()) {
cout << res << endl;
return;
}
printAllsub(s, i + 1, res); //此位置不取
printAllsub(s, i + 1, res + s[i]); //此位置取
}
方法2:我们知道s共有s.size()=n个字符,因此所有子序列的总数共2^n,用2进制表示则是n个1,因此我们可以从1开始计数,每次+1一直到n位全为1,在这个过程中每一个为1的位都表示s中该位的字符是取的,为0表示该位的字符是不取的。这样就能将全部组合列出来了
void printAllsub(string s){
int len = s.size();
long long total = pow(2,len);
long long count = 1;
while(count<=total){
string tmp;
int i = 1;
while(i<=len){
if(count && i)
tmp+=s[i-1];
i<<=1;
}
cout<<tmp<<' ';
}
return ;
}