分治法的概念:
作为五大算法之一的分治法,可算是最早接触的一种算法。分治法,与其说是一种算法,不如将其称为策略来的更贴切一些。算法的思想就是将大问题分成小问题,并解决小问题之后合并起来生成大问题的解。
分治法的精髓:
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;
分治法的作用,自然是让程序更加快速地处理问题。比如一个n的问题分解成两个n/2的问题,并由两个人来完成,效率就会快一些。当然单线程的程序的分治法,就是把n的问题剔除掉可以省略的步骤,从而提高程序运行的速度。
图解分治法:
二分法:
二分法针对于有序集合的处理来说显得比按序遍历来得更快一些。比如:有一个城市到另一个城市之间的电杆不通电了,该如何排查?作为常用思维大概就是逐个去查了,如果能确定两地间的电杆是串联的节点,那么二分法显然是比较有效率的,因为它跳过了很多不必要排查的电杆。
我们一般使用二分法去寻找一个流中的特定的数。比如查找有序数列中是否存在一个数,或者使用二分法求一个数的根号。
找数字:
假设我们需要在1-10000里面找一个数200,使用逐个搜索的方法,我们会消耗200步。如果使用二分法呢?
第一步我们找到1-10000中间的那个数:5000。它大于200,所以200应该在1-4999这个区间内,这样我们就丢掉了后5000个数。
第二步我们找到2500,也比200要大,200在1-2500这个区间内。
第三步找到1250这个数,也比200大。
第四步找到750。
第五步找到375。
第六步找到167,它比200要小了,说明200在167-375之间。
第七步找到271,它在167-271之间。
第八步找到219,它在167-219之间。
第九步找到193,它在193-219之间。
第十步找到206,它在193-206之间。
第十一步找到199,它在199-206之间。
第十二步找到202,它在199-202之间。
第十三步找到200。
二分法查找函数:
代码示例1:
/*
写一个函数BinarySearch,在包含size个元素的、从小到大排序的int数组a里查找元素p,
如果找到,则返回元素下标,如果找不到,则返回-1。要求复杂度o(log(n))
*/
int BinarySearch(int a[], int size, int p)
{
int L = 0;
int R = size - 1;
while (L <= R)
{
int mind = L + (R - L) / 2;
if (p == a[mid])
{
return mid;
}
else if (p > a[mid])
{
L = mid + 1;
}
else
{
R = mid - 1;
}
}
return -1;
}
代码示例2:
/*
写一个函数LowerBound,在包含size个元素的、从小到大排序的int数组a里查找比给定元素p,
小的,如果找到,则返回元素下标,如果找不到,则返回-1。要求复杂度o(log(n))
*/
int LowerBound(int a[], int size, int p)
{
int L = 0;
int R = size - 1;
int LastPos = - 1;
while(L <= R)
{
int mid = L + (R - L) / 2;
if (a[mid] >= p)
{
R = mid - 1;
}
else
{
LastPos = mid;
L = mid - 1;
}
}
return LastPos
}
二分法求方程的根:
/*
f(x) = x^3 - 5x^2 + 10x - 80
解法:f'(x) > 0,f(x)是单调递增的,故可用二分法在区间[0,100]
中寻找根。
*/
#include <iostream>
#include <cmath>
using namespace std;
double EPS = 1e-6;
double f(double x)
{
return x * x * x - 5 * x * x + 10 * x - 80;
}
int main()
{
double root, x1 = 0, x2 = 100, y;
root = x1 + (x2 - x1) / 2;
y = f(root);
while (fabs(y) >= EPS)
{
if (y > 0)
{
x2 = root;
}
else
{
x1 = root;
}
root = x1 + (x2 - x1) / 2;
y = f(root);
}
printf("%.5f\n", root);
return 0;
}
/*
通用技巧
1. 判定条件为start+1小于end,start=0, end=size-1
2. mid=start+((end-start)>>1)
3. 如果data[mid]满足条件直接返回,如果满足条件的数据在mid的右边则将start=mid,如果满足条件的数据在mid左边则将end=mid
4. 根据条件再次判定start,end两个元素是否满足条件
5. 其实整个过程就不断缩小范围的过程,最后一步就是将整个数组的范围缩小到start
end两个元素而已。
这里第3步特别注意的地方就是不管什么情况下都应该单独判data[mid]大于等于和小于的情况,而不应该合并,因为会比较容易出错。
*/
归并排序:
将两个的有序数列合并成一个有序数列,我们称之为"归并"。
归并排序(Merge Sort)就是利用归并思想对数列进行排序。根据具体的实现,归并排序包括"从上往下"和"从下往上"2种方式。
-
从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。
-
从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 – 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
② 求解 – 递归地对两个子区间a[low…mid] 和 a[mid+1…high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 – 将已排序的两个子区间a[low…mid]和 a[mid+1…high]归并为一个有序的区间a[low…high]
归并排序图解:
从上往下归并排序:
从下往上归并排序:
代码示例:
#include <iostream>
using namespace std;
int a[20], b[20];
void mergesort(int L, int R)
{
if (L >= R)
{
return;
}
int mid = L + R;
mid /= 2;
mergesort(L, mid);
mergesort(mid + 1, R);
int i = L, k = L;
int j = mid + 1;
while (i <= mid && j <= R)
{
if (a[i] <= a[j])
{
b[k++] = a[i++];
}
else
{
b[k++] = a[j++];
}
}
while (i <= mid)//将imid-i和R-j两组有序序列,归并在一个有序序列中
{
b[k++] = a[i++];//将mid-i剩余的数放在最后
}
while (j <= R)
{
b[k++] = a[j++];//将r-j剩余的数放在最后
}
for(k = L;k <= R; k++)//将b数组中的数据拷贝到原数组中
{
a[k] = b[k];
}
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
mergesort(0, n - 1);
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
return 0;
}