對編程之法中算法的實現
王禹 406130917327
摘要:將編程之美中的15個題目調試成功,並且對其做了詳細的報告,報告中包括該數據結構,以及這個題目的簡單應用。還有這些代碼的出處,和測試情況。並且根據這些調試以及應用的情況,思考這些代碼的巧妙之處以及可能會有哪些不足。並針對這些地方提出一些自己的看法。
關鍵字:編程之美,調試,應用。
1.數塔問題
在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得路徑上所經過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只需要求出這個最大和即可,不必給出具體路徑。 三角形的行數大於1小於等於100,數字爲 0 - 99
解法一:
剛拿到這道題目想到的是用遞歸的方法來解決題目
具體代碼爲:
運行結果爲:
但是遞歸算法的時間複雜度較爲高,並且由於是遞歸的方式,會對一個子問題進行多次計算。於是我們可以想到是否可以將子問題的答案保存起來。這樣就不必多次計算了。
解法2
編譯成功,但是未產生合理的值。
經過調試發現第10行應該是maxsum[i][j]!=-1代表已經i,j位置上的值已經被計算出來了。才能夠返回。
調試完畢 成功得出想要的結果
但是可否能夠寫出一個不需要遞歸的算法呢?應該是可以的。
解法3
選擇如下算法
編譯結果爲:
編譯成功,但是沒有得到想要的解。
觀察到第22行應該爲cout<<maxsum[1[1]
纔對修改後的出正確值
2.揹包問題
用動態規劃求解0,1揹包問題代碼如下:
這個時候雖然能編譯過,但是報錯如圖所示。
通過在
加入打印here以及lala語句發現進入這個循環之後就出現錯誤。於是認爲這個地方出現了錯誤。
發現了一個邏輯錯誤。我只在C<0的時候返回了值。但是還有C不小於0的情況沒有考慮。通過添加
else return 0;
語句編譯通過。
通過的解爲
3.旋轉字符串
問題描述:
給定一個字符串,要求把字符串前面的若干個字符移動到字符串的尾部,如把字符串“abcdef”前面的2個字符’a’和’b’移動到字符串的尾部,使得原字符串變成字符串“cdefab”。請寫一個函數完成此功能,要求對長度爲n的字符串操作的時間複雜度爲 O(n),空間複雜度爲 O(1)。
解法一:暴力移位法
可以逐個將字符串的最前面的元素移動到最後的位置,以達到這種效果。具體代碼爲:
並且可以運行成功。
但是其中left_shift_one函數需要調用m次每次的時間複雜度爲n。那麼總的時間複雜度就爲O(n*m)遠遠超出了要求。於是可以有下面一種方法。
解法2:三步反轉法
將一個字符串分成X和Y兩個部分,在每部分字符串上定義反轉操作,如X^T,即把X的所有字符反轉(如,X=”abc”,那麼X^T=”cba”),那麼就得到下面的結論:(X^TY^T)^T=YX,顯然就解決了字符串的反轉問題。
代碼分兩個部分第一部分實現反轉功能。第二部分分別對X和Y進行反轉之後對整個字符串進行反轉。於是便實現了功能。
代碼爲:
運行結果爲:
4.字符串包含
問題描述
給定兩個分別由字母組成的字符串A和字符串B,字符串B的長度比字符串A短。請問,如何最快地判斷字符串B中所有字母是否都在字符串A裏?
暴力解法
針對B中的每一個字符判斷它是否存在於A串中。
代碼可編寫爲下:
代碼運行結果:
顯然對於A,B兩個串長度分別爲n和m來說這個算法需要O(n*m)的時間複雜度。
排序解法
如果允許排序的話,我們可以考慮下排序。比如可先對這兩個字符串的字母進行排序,然後再同時對兩個字串依次輪詢。兩個字串的排序需要(常規情況)O(m log m) + O(n log n)次操作,之後的線性掃描需要O(m+n)次操作。
其代碼爲:
結果爲:
想法:當n和m相進的時候才適合用這個方法。設n=m則原先的暴力算法複雜度爲
運用素數求解
用26個素數分別代表A到Z並且將長字符串中的字母對應的數字相乘。得到一個整數。利用上面字母和素數的對應關係,對應第二個字符串中的字母,然後輪詢,用每個字母對應的素數除前面得到的整數。如果結果有餘數,說明結果爲false。如果整個過程中沒有餘數,則說明第二個字符串是第一個的子集了(判斷是不是真子集,可以比較兩個字符串對應的素數乘積,若相等則不是真子集)
具體步驟如下:
1.按照從小到大的順序,用26個素數分別與字符’A’到’Z’一一對應。
2.遍歷長字符串,求得每個字符對應素數的乘積。
3.遍歷短字符串,判斷乘積能否被短字符串中的字符對應的素數整除。
4.輸出結果。
代碼爲:
該算法之中有兩個循環長度分別是n與m其時間複雜度爲O(n+m).。最好情況爲O(n)即遍歷短串的第一個數與長串相除就有餘數。該算法時間複雜度較爲優秀,但是多個素數相乘有可能會產生溢出的情況。
用hash表求解
這種包含問題很容易便能想到hash表求解。並且只有26個字母hash表的表長很短。
其代碼爲:
運行結果爲
該算法時間複雜度爲O(n+m)並且不會有上溢的風險。
5.字符串轉換爲整數問題
題目描述
輸入一個由數字組成的字符串,把它轉換成整數並輸出。例如:輸入字符串”123”,輸出整數123。
給定函數原型int StrToInt(const char *str) ,實現字符串轉換成整數的功能.
3.2分析與解法
對於一個字符串來說。比如說字符串”123” 。
- 首先我們掃描到了字符 ‘1’我們知道這是第一位。則令結果爲1。
- 之後我們掃描到了字符 ‘2’我們知道這是第二位。將原先的結果一往左移動一位並且往後面加入2。
- 最後我們掃描到了字符’3’按照以前的步驟將原先的結果往前移動一位。之後在後面加入3。
所以我們具有了思路,即對一個字符串我們從左到右的掃描,把以前的數字乘上10,之後加上現在的數字。
具體算法爲:
需要加上許多限制條件
6. 尋找數組中最小的K個數
問題描述:
輸入n個整數,輸出其中最小的k個。
解法1
可以先將數組排序,之後將排序號的數組的前k個元素拿過來使用。
該方法雖然簡單,但是其中後n-k個元素並沒有要求你排序,這樣就浪費了時間。所以應該存在更號的方法。
解法2
1、遍歷n個數,把最先遍歷到的k個數存入到大小爲k的數組中,假設它們即是最小的k個數;
2、對這k個數,利用選擇或交換排序找到這k個元素中的最大值kmax(找最大值需要遍歷這k個數,時間複雜度爲O(k));
3、繼續遍歷剩餘n-k個數。假設每一次遍歷到的新的元素的值爲x,把x與kmax比較:如果x < kmax ,用x替換kmax,並回到第二步重新找出k個元素的數組中最大元素kmax‘;如果x >= kmax,則繼續遍歷不更新數組。
每次遍歷,更新或不更新數組的所用的時間爲O(k)或O(0)。故整趟下來,時間複雜度爲n*O(k)=O(n*k)。
其代碼爲
運行結果爲:
解法3
有一種平均複雜度爲O(n)的算法
選取S中一個元素作爲樞紐元v,將集合S-{v}分割成S1和S2,就像快速排序那樣
如果k <= |S1|,那麼第k個最小元素必然在S1中。在這種情況下,返回QuickSelect(S1, k)。
如果k = 1 + |S1|,那麼樞紐元素就是第k個最小元素,即找到,直接返回它。
否則,這第k個最小元素就在S2中,即S2中的第(k - |S1| - 1)個最小元素,我們遞歸調用並返回QuickSelect(S2, k - |S1| - 1)。
具體代碼如下
7.尋找數組中和爲定值的兩個數
輸入一個數組和一個數字,在數組中查找兩個數,使得它們的和正好是輸入的那個數字。
要求時間複雜度是O(N)。如果有多對數字的和等於輸入的數字,輸出任意一對即可。
例如輸入數組1、2、4、7、11、15和數字15。由於4+11=15,因此輸出4和11。
:問題觀察
這個問題相當於對於每個a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的時間都要花費爲O(N),這樣下來,最終找到兩個數還是需要O(N^2)的複雜度。那麼如何減少查找的時間複雜度呢?
:解法2
此時我們相既然時間複雜度都爲O(n*n)了不如先排個序吧。先將數組排序後。排序了之後我們發現確實可以減少查找的時間複雜度了。因爲這個時候我們可以使用二分查找了。使用二分查找的話,相當與對於每個a[i]只用log n的時間複雜度就可以將它找出來。那麼總的時間複雜度就爲nlogn
:解法三
對於數組有序的情況,我們是否有更高效的方法去得到問題的解呢?我們可以這樣:
用兩個指針i,j,各自指向數組的首尾兩端,令i=0,j=n-1,然後i++,j–,逐次判斷a[i]+a[j]?=sum,
如果某一刻a[i]+a[j] > sum,則要想辦法讓sum的值減小,所以此刻i不動,j–;
如果某一刻a[i]+a[j] < sum,則要想辦法讓sum的值增大,所以此刻i++,j不動。
這樣的話找到算法的解的時間複雜度爲O(n)。
在數組有序的情況下總的時間複雜度爲O(n),在數組無序的情況下總的時間複雜度爲O(nlogn).
具體代碼如下:
代碼的運行結果
8.尋找和爲定值的多個數
6解法1
注意到取n,和不取n個區別即可,考慮是否取第n個數的策略,可以轉化爲一個只和前n-1個數相關的問題。
如果取第n個數,那麼問題就轉化爲“取前n-1個數使得它們的和爲sum-n”,對應的代碼語句就是sumOfkNumber(sum - n, n - 1);
如果不取第n個數,那麼問題就轉化爲“取前n-1個數使得他們的和爲sum”,對應的代碼語句爲sumOfkNumber(sum, n - 1)。
具體代碼爲
運行結果爲:
9.最大連續子數組和
輸入一個整形數組,數組裏有正數也有負數。數組中連續的一個或多個整數組成一個子數組,每個子數組都有一個和。 求所有子數組的和的最大值,要求時間複雜度爲O(n)。
這樣的時間複雜度爲O(n^3) 當然有更好的方法
int MaxSubArray(int* a, int n)
{
int currSum = 0;
int maxSum = a[0]; //全負情況,返回最大數
for (int j = 0; j < n; j++)
{
currSum = (a[j] > currSum + a[j]) ? a[j] : currSum + a[j];
maxSum = (maxSum > currSum) ? maxSum : currSum;
}
return maxSum;
}
代碼運行結果爲。
10.跳臺階問題
一個臺階總共有n 級,如果一次可以跳1 級,也可以跳2 級。
求總共有多少總跳法,並分析算法的時間複雜度。其可以簡化爲求fibonacci數問題
long long Fibonacci(unsigned int n)
{
int result[3] = {0, 1, 2};
if (n <= 2)
return result[n];
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
當然fibonacci是可以優化的可以將其優化爲
int ClimbStairs(int n)
{
int dp[3] = { 1, 1 };
if (n < 2)
{
return 1;
}
for (int i = 2; i <= n; i++)
{
dp[2] = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = dp[2];
}
return dp[2];
}
這樣就可以降低時間複雜度。
換硬幣問題。想兌換100元錢,有1,2,5,10四種錢,問總共有多少兌換方法
const int N = 100;
int dimes[] = { 1, 2, 5, 10 };
int arr[N + 1] = { 1 };
for (int i = 0; i < sizeof(dimes) / sizeof(int); ++i)
{
for (int j = dimes[i]; j <= N; ++j)
{
arr[j] += arr[j - dimes[i]];
}
}
11.奇偶調序
輸入一個整數數組,調整數組中數字的順序,使得所有奇數位於數組的前半部分,所有偶數位於數組的後半部分。要求時間複雜度爲O(n)。
解法1:
藉助快速排序的想法,用一個主元將這個數組劃分爲兩個堆一堆奇數一堆偶數,具體代碼爲:
得到的結果爲:
12.荷蘭國旗問題
下面是問題的正規描述: 現有n個紅白藍三種不同顏色的小球,亂序排列在一起,請通過兩兩交換任意兩個球,使得從左至右,依次是一些紅球、一些白球、一些藍球。
荷蘭國旗問題類似於有三個指針的快速排序問題。
具體代碼如下
代碼運行結果爲
13.完美洗牌算法
有個長度爲2n的數組{a1,a2,a3,…,an,b1,b2,b3,…,bn},希望排序後{a1,b1,a2,b2,….,an,bn},請考慮有無時間複雜度o(n),空間複雜度0(1)的解法。
解法一爲暴力求解法逐次找到b1,b2,的位置之後將其插入進去。其代碼爲:O(n^2);
可以採用利用空間來換取時間複雜度的想法。算法如下:
時間空間複雜度均爲O(n);
完美洗牌算法的進階版本:
void CycleLeader(int *a, int from, int mod)
{
int t,i;
for (i = from * 2 % mod; i != from; i = i * 2 % mod)
{
t = a[i];
a[i] = a[from];
a[from] = t;
}
}
void RightRotate(int *a, int num, int n)
{
reverse(a, 1, n - num);
reverse(a, n - num + 1, n);
reverse(a, 1, n);
}
void reverse(int *a,int i,int j)
{
while(i<j)
{
temp=a[i];
a[i]=a[j];
a[j]=temp;
i++;
j--;
}
}
void PerfectShuffle2(int *a, int n)
{
int n2, m, i, k, t;
for (; n > 1;)
{
// step 1
n2 = n * 2;
for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)
;
m /= 2;
// 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)
// step 2
right_rotate(a + m, m, n);
// step 3
for (i = 0, t = 1; i < k; ++i, t *= 3)
{
cycle_leader(a , t, m * 2 + 1);
}
//step 4
a += m * 2;
n -= m;
}
// n = 1
t = a[1];
a[1] = a[2];
a[2] = t;
}
程序主體爲:
其獲得的結果爲
14.最大連續乘積字串問題
給一個浮點數序列,取最大乘積連續子串的值,例如 -2.5,4,0,3,0.5,8,-1,則取出的最大乘積連續子串爲3,0.5,8。也就是說,上述數組中,3 0.5 8這3個數的乘積30.58=12是最大的,而且是連續的。
解法一:暴力解法
double maxProductSubstring(double *a, int length)
{
double maxResult = a[0];
for (int i = 0; i < length; i++)
{
double x = 1;
for (int j = i; j < length; j++)
{
x *= a[j];
if (x > maxResult)
{
maxResult = x;
}
}
}
return maxResult;
}
該方法將數組中每一個子串都進行計算。顯然時間複雜度爲O(n^2)。
解法2:
運行結果爲:
15.交替字符串問題
輸入三個字符串s1、s2和s3,判斷第三個字符串s3是否由前兩個字符串s1和s2交錯而成,即不改變s1和s2中各個字符原有的相對順序,例如當s1 = “aabcc”,s2 = “dbbca”,s3 = “aadbbcbcac”時,則輸出true,但如果s3=“accabdbbca”,則輸出false。
public boolean IsInterleave(String s1, String 2, String 3){
int n = s1.length(), m = s2.length(), s = s3.length();
if (n + m != s)
return false;
boolean[][]dp = new boolean[n + 1][m + 1];
//在初始化邊界時,我們認爲空串可以由空串組成,因此dp[0][0]賦值爲true。
dp[0][0] = true;
for (int i = 0; i < n + 1; i++){
for (int j = 0; j < m + 1; j++){
if ( dp[i][j] || (i - 1 >= 0 && dp[i - 1][j] == true &&
//取s1字符
s1.charAT(i - 1) == s3.charAT(i + j - 1)) ||
(j - 1 >= 0 && dp[i][j - 1] == true &&
//取s2字符
s2.charAT(j - 1) == s3.charAT(i + j - 1)) )
dp[i][j] = true;
else
dp[i][j] = false;
}
}
return dp[n][m]
}
main函數代碼爲
運行結果爲: