算法思想
分治算法把一個問題實例分解成若干個小型而獨立的實例,從而可以在並行計算機上執行;那些小型而獨立的實例可以在並行計算機的不同處理器上完成。
分治法的思想:將原問題分解爲幾個規模較小但類似於原問題的子問題,遞歸地求解這些子問題,然後再合併這些子問題的解來建立原問題的解。
分治算法的特徵
分治法所能解決的問題一般具有以下幾個特徵:
1) 該問題的規模縮小到一定的程度就可以容易地解決;
2) 該問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質;
3) 利用該問題分解出的子問題的解可以合併爲該問題的解;
4) 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。
第一條特徵是絕大多數問題都可以滿足的,因爲問題的計算複雜性一般是隨着問題規模的增加而增加;
第二條特徵是應用分治法的前提它也是大多數問題可以滿足的,此特徵反映了遞歸思想的應用;
第三條特徵是關鍵,能否利用分治法完全取決於問題是否具有第三條特徵,如果具備了第一、二條特徵,而不具備第三條特徵,則可以考慮用貪心算法或動態規劃算法。
第四條特徵涉及到分治法的效率,如果各子問題是不獨立,則分治法要做很多不必要的工作,重複地解公共的子問題,此時一般使用動態規劃算法。
解題步驟
分治算法在每層遞歸中包含三個步驟:
1)分解原問題爲若干子問題,這些子問題是原問題的規模較小的實例;
2)解決這些子問題,遞歸地求解各個子問題。然而,若子問題的規模足夠小,則直接求解;
3)合併這些子問題的解爲原問題的解。
應用
二分查找
問題描述:
給定已按升序排好序的n個元素a[0:n-1],現要在這n個元素中找出一特定元素x。
C++非遞歸實現:
template <typename T>
int binarySearch(T *a, const T& x, int length)
{
int left = 0;
int right = length - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (x == a[mid])
return mid;
else if (x < a[mid])
right = mid - 1;
else
left = mid + 1;
}
//未找到與x相等的元素
return -1;
}
C++遞歸實現:
template <typename T>
int binarySearch(T *a, const T& x, int left, int right)
{
if (left > right)
return -1;
int mid = (left + right) / 2;
if (x == a[mid])
return mid;
else if (x < a[mid])
binarySearch(a, x, left, mid - 1);
else
binarySearch(a, x, mid + 1, right);
}
歸併排序(merge sort)
問題描述:
把n個元素按照非遞減順序排列。
分析:
可以用分治策略來設計算法,這種算法的結構爲:若n爲1,則算法終止;否則,將序列劃分爲k個子序列(k是不小於2的整數)。先對每一個子序列排序,然後將有序子序列歸併爲一個序列。(歸併排序時k=2)
假如有10個元素,關鍵字分別爲[10,6,7,2,5,8,3,1,4,9]。
則歸併排序過程如下:
[10] [6] [7] [2] [5] [8] [3] [1] [4] [9]
[6 10] [2 7] [5 8] [1 3] [4 9]
[2 6 7 10] [1 3 5 8] [4 9]
[1 2 3 5 6 7 8 10] [4 9]
[1 2 3 4 5 6 7 8 9 10]
C++實現:
template <typename T>
void mergeSort(T a[], int n)
{// Sort a[0 : n - 1] using the merge sort method.
T *b = new T [n];
int segmentSize = 1;
while (segmentSize < n)
{
mergePass(a, b, n, segmentSize); // merge from a to b
segmentSize += segmentSize;
mergePass(b, a, n, segmentSize); // merge from b to a
segmentSize += segmentSize;
}
}
template <typename T>
void mergePass(T x[], T y[], int n, int segmentSize)
{// Merge adjacent segments from x to y.
int i = 0; // start of the next segment
while (i <= n - 2 * segmentSize)
{// merge two adjacent segments from x to y
merge(x, y, i, i + segmentSize - 1, i + 2 * segmentSize - 1);
i = i + 2 * segmentSize;
}
// fewer than 2 full segments remain
if (i + segmentSize < n)
// 2 segments remain
merge(x, y, i, i + segmentSize - 1, n - 1);
else
// 1 segment remains, copy to y
for (int j = i; j < n; j++)
y[j] = x[j];
}
template <typename T>
void merge(T c[], T d[], int startOfFirst, int endOfFirst,
int endOfSecond)
{// Merge two adjacent segments from c to d.
int first = startOfFirst, // cursor for first segment
second = endOfFirst + 1, // cursor for second
result = startOfFirst; // cursor for result
// merge until one segment is done
while ((first <= endOfFirst) && (second <= endOfSecond))
if (c[first] <= c[second])
d[result++] = c[first++];
else
d[result++] = c[second++];
// take care of leftovers
if (first > endOfFirst)
for (int q = second; q <= endOfSecond; q++)
d[result++] = c[q];
else
for (int q = first; q <= endOfFirst; q++)
d[result++] = c[q];
}
算法複雜度:歸併排序的平均和最好最壞情況下的複雜度均爲O(nlogn)。
排序排序(quick sort)
問題描述:
把n個元素按照非遞減順序排列。
分析:
用分治算法可以實現另一種完全不同的排序算法——快速排序。
//對a[0:n-1]快排
從a[0:n-1]中選擇一個元素作爲支點,組成中間段
把剩餘元素分爲左段left和有段right。是左段的元素關鍵字都不大於支點關鍵字,有段的元素關鍵字都不小於支點關鍵字
對左段遞歸快排
對右段遞歸快排
最終結果按左段、中間段和右段排列
假如有10個元素,關鍵字分別爲[10,6,7,2,5,8,3,1,4,9]。
則快速排序過程如下:
選擇首元素爲支點,從1位置往右遍歷,尋找不小於支點的元素;從n-1位置往左遍歷,尋找不大於支點的元素。支點保證向左遍歷不越界,n-1位置爲數組最大值保證向右遍歷不越界。
綠色爲支點,黃色爲不小於支點元素,紅色爲不大於支點的元素
1)找到不小於支點的元素爲7,不大於支點的元素爲4,交換兩個元素,繼續尋找;
2)找到不小於支點的元素爲8,不大於支點的元素爲1,交換兩個元素,繼續尋找;
3)找到不小於支點的元素爲8,不大於支點的元素爲3,但8的位置比3靠前,交換支點和不大於支點的元素3;數組分成兩段,[3 4 2 5 1]和[6 8 7 9 10];
4)分別對兩段重複上述過程。
C++實現:
template <typename T>
void quickSort(T a[], int n)
{// Sort a[0 : n - 1] using the quick sort method.
if (n <= 1) return;
// move largest element to right end
int max = indexOfMax(a,n);
swap(a[n - 1], a[max]);
quickSort(a, 0, n - 2);
}
template <typename T>
void quickSort(T a[], int leftEnd, int rightEnd)
{// Sort a[leftEnd:rightEnd], a[rightEnd+1] >= a[leftEnd:rightEnd].
if (leftEnd >= rightEnd) return;
int leftCursor = leftEnd, // left-to-right cursor
rightCursor = rightEnd + 1; // right-to-left cursor
T pivot = a[leftEnd];
// swap elements >= pivot on left side
// with elements <= pivot on right side
while (true)
{
// find >= element on left side
while (a[++leftCursor] < pivot);
// find <= element on right side
while (a[--rightCursor] > pivot);
if (leftCursor >= rightCursor)
break; // swap pair not found
swap(a[leftCursor], a[rightCursor]);
}
// place pivot
a[leftEnd] = a[rightCursor];
a[rightCursor] = pivot;
quickSort(a, leftEnd, rightCursor - 1); // sort left segment
quickSort(a, rightCursor + 1, rightEnd); // sort right segment
}
template<typename T>
int indexOfMax(T a[], int n)
{// Locate the largest element in a[0:n-1].
if (n <= 0)
throw illegalParameterValue("n must be > 0");
int indexOfMax = 0;
for (int i = 1; i < n; i++)
if (a[indexOfMax] < a[i])
indexOfMax = i;
return indexOfMax;
}
算法複雜度:快速排序的平均複雜度爲O(nlogn),最壞情況下爲n2。
值得一提的是:
STL的sort()算法,數據量大時採用Quick Sort,分段遞歸排序,一旦分段後的數據量小於logn的某個常數倍時,爲避免Quick Sort的遞歸調用帶來過大的額外負荷,就改用Insertion Sort。如果遞歸層次過深,還會改用Heap Sort。
STL的stable_sort()算法,數據量大時使用Merge Sort,數據量小於logn的某個常數倍時,使用Insertion Sort。