早就在知乎上聽說了《算法導論》的威名,也因為學編程以來大多寫的都是工程代碼,算法方面接觸的太少了,但是算法又是程序員的內功,又是面試必考的一個環節,所以我這個寒假開始準備開始啃《算法導論》。這個系列的博客也權當是一種筆記吧。
算法的正確性
算法的正確性我是第一次聽說,之前在學校上數據結構課時老師也沒講過(講過才奇怪了)。
算法的正確性是由循環不定式來證明的:
Initialization: It is true prior to the first iteration of the loop.
Maintenance: If it is true before an iteration of the loop, it remains true before the next iteration.
Termination: When the loop terminates, the invariant gives us a useful property that helps show that the algorithm is correct.
總結來說就是,由于算法一般會牽扯到循環,那么就要驗證這個循環的正確性,在進入這個循環之前,你得是正確的,也就是說在某種條件下,不進行循環,算法也能得出正確的解。進入循環后,在循環過程是正確的,也就是說你這個循環要一次比一次更加接近解,而不是遠離解。循環要能正確終止,不能是無限循環,而且退出要是有意義的,能提供有用的性質。
分治法
分治法的思想就是將原問題分解位幾個規模較小但類似的問題,將子問題再進行分解知道足夠簡單解,然后合并解,建立原方程的解。分治模式有三個基本步驟:
- 分解。
- 解決。
- 合并。
歸并排序
歸并排序就是運用了一個分治遞歸的思想。
- 分解。將原數組通過不停的調用一個函數,將自身一分為二。直到不能再分為止。
- 解決。用一個合并函數將兩個有序數組合并。最開始是兩個數組各只有一個數字。其排序方法可用兩堆撲克牌想象。想象左右兩堆撲克牌有序,第一次比較左右牌堆頂誰的數小,就把它移到中間牌堆去,第二次,第三次重復比較。。。。。直至比較完或有一個牌堆放完,直接將另一個牌堆的剩余牌依次放置中間牌堆。
- 合并。合并函數返回合并后的數組。
#include <iostream>
#include <iomanip>
using namespace std;
void Merge_Sort(int *A, int p, int r);
void Merge(int *A, int p, int q, int r);
int main()
{
int A[8] = {8, 7, 6, 5, 4, 3, 2, 1};
Merge_Sort(A, 0, 7);
for (int i = 0; i < 8; i++)
{
cout << A[i] << " ";
}
return 0;
}
void Merge_Sort(int *A, int p, int r)
{
if (p < r)
{
int q = (p + r) / 2;
Merge_Sort(A, p, q);
Merge_Sort(A, q + 1, r);
Merge(A, p, q, r);
}
}
void Merge(int *A, int p, int q, int r)
{
int n1 = q - p + 1;
int n2 = r - q;
int L[n1 + 1];
int R[n2 + 1];
for (int i = 0; i < n1; i++)
{
L[i] = A[p + i];
}
for (int i = 0; i < n2; i++)
{
R[i] = A[q + i + 1];
}
L[n1] = 100;
R[n2] = 100;
int i = 0;
int j = 0;
for (int k = p; k <= r; k++)
{
if (L[i] <= R[j])
{
A[k] = L[i];
i++;
} else
{
A[k] = R[j];
j++;
}
}
}
注意這里使用了哨兵法避免每次都判斷數組是否已用完。哨兵的值應該待排數組的任何一個數都大。(實際使用絕不可能是100,這里只是為了方便)
二分查找
二分查找是指一個有序數組,每次都把這個數組的中間下標值與待查數比較,若相同則返回,若待查數更大(設該有序數組為升序),則將中間下標設為start下標,end下標不變,重新計算新的中間下標與待查數比較。重復此過程。
#include <iostream>
#include <iomanip>
using namespace std;
int binary_find(int *num, int key, int start, int end);
int main()
{
int num[7] = {1, 2, 3, 4, 5, 6, 7};
int a;
cin >> a;
cout<<"in NO "<<binary_find(num,a,0,6)+1<<endl;
return 0;
}
int binary_find(int *num, int key,int start,int end)
{
if (start > end)
{
return -2;
}
int mid = (start + end) / 2;
if(key>num[mid])
{
binary_find(num, key, mid+1, end);
} else if (key < num[mid])
{
binary_find(num, key, start, mid-1);
} else if(key==num[mid])
{
return mid;
}
}
如果start下標都比end下標大了,這就說明沒有找到,返回一個無意義的值或者返回false就行了。
題目:假設A[1...n]是一個有n個不同數的數組。若i < j且A[i] > A[j],則稱其為一個逆序對。 給出一個確定在n個元素的任何排列中逆序對數量的算法,最壞情況需要θ(nlogn)的時間。
這道題通過修改歸并排序的代碼就可以完成。考慮到排序的過程有兩種情況,一是待排數組本來就有序,這種情況就不存在逆序對。二是待排數組無序,這就存在逆序對。那我們所要做的就是將需要進行排序操作的下標記下來。
#include <iostream>
#include <iomanip>
using namespace std;
int Merge_Sort(int *A, int p, int r);
int Merge(int *A, int p, int q, int r);
int main()
{
int A[8] = {7, 8, 6, 5, 4, 3, 2, 1};
cout<<Merge_Sort(A, 0, 7);
return 0;
}
int Merge_Sort(int *A, int p, int r)
{
if (p < r)
{
int inversion = 0;
int q = (p + r) / 2;
inversion+=Merge_Sort(A, p, q);
inversion+=Merge_Sort(A, q + 1, r);
inversion+=Merge(A, p, q, r);
return inversion;
} else
{
return 0;
}
}
int Merge(int *A, int p, int q, int r)
{
int inversion = 0;
int n1 = q - p + 1;
int n2 = r - q;
int L[n1 + 1];
int R[n2 + 1];
for (int i = 0; i < n1; i++)
{
L[i] = A[p + i];
}
for (int i = 0; i < n2; i++)
{
R[i] = A[q + i + 1];
}
L[n1] = 100;
R[n2] = 100;
int i = 0;
int j = 0;
for (int k = p; k <= r; k++)
{
if (L[i] <= R[j])
{
A[k] = L[i];
i++;
} else
{
A[k] = R[j];
j++;
inversion += n1 - i;
}
}
return inversion;
}
我們設左牌堆(即左邊的數組)為較小堆,右邊為較大堆。代碼中最為關鍵的一行是
inversion += n1 - i;
為什么呢?因為當我們要將右邊的牌放到中間(即排序后的數組)去時,右邊的這張牌比左邊牌堆的所有牌都大而右邊的這張牌與左邊所有牌都構成一對逆序對, n1 - i
就是計算左邊一共有多少少張牌的。
由于初學算法,以上全是我的個人理解,代碼中的不足錯誤之處還請各位大牛批評指出。