目錄
兩個數組的最長公共子數組
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 ;
}