算法思想
分治算法把一个问题实例分解成若干个小型而独立的实例,从而可以在并行计算机上执行;那些小型而独立的实例可以在并行计算机的不同处理器上完成。
分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治算法的特征
分治法所能解决的问题一般具有以下几个特征:
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。