文章目录
- 1.分治算法
- 1.1 如何判断分治问题
- 1.2 为什么通过分治可以提升效率
- 1.2.1 操作数量的优化
- 1.2.2 并行计算优化
- 1.3 分治常见应用
1.分治算法
分治(divide and conquer),全称是分而治之,是一种非常重要且非常常见的算法。分治通常基于递归实现,主要包括"分"和"治"两个阶段。
- 分(划分阶段):递归地将原问题分解为两个或多个子问题,直到到达最小子问题时结束。
- 治(合并阶段):从已知解地最小子问题开始,从底到顶地将问题地解进行合并,从而构建出原问题地解。
其实在排序阶段我们就已经使用过分治算法了,当我们在处理归并排序时,就用到了归并排序。
同样也是分成了"分"和"治": - 分:递归地将原数组划分为两个子数组(子问题),直到子数组只剩一个元素(最小子问题)
- 治:从底到顶地将有序子数组进行合并,从而得到有序地原数组
1.1 如何判断分治问题
一个问题是否合适使用分治解决,可以参考以下几点:
- 问题可以分解:原问题可以分解为规模更小、类似地子问题,以及能够以相同方式递归地进行划分。
- 子问题是相互独立的:子问题间没有重叠,互相没有关联,独立存在。
- 子问题的解可以合并:原问题的解通过合并子问题的解得到。
如此一来,归并排序显然是满足上面的3个条件的。 - 问题可以分解:递归地将数组(原问题)划分为两个子数组(子问题)。
- 子问题相互独立:每个子数组都是可以独立地进行排序
- 子问题地解可以合并:两个有序子树可以合并为一个有序数组。
void _MergeSort(int* a, int* tmp, int begin, int end)
{
//确定递归出口
if (begin >= end)
return;
int mid = (begin + end) / 2;//划分数组,将数组一分为二
//以下为分解逻辑
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
//以下为合并逻辑
int begin1 = begin,end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
//处理剩余元素
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
//将临时数组存放的数据重新复制到原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);//临时数组,存放合并时的数据
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
//归并排序的核心逻辑,再封装一个函数来实现
_MergeSort(a, tmp, 0, n - 1);
}
1.2 为什么通过分治可以提升效率
分治不仅可以有效地解决,算法问题,往往还可以提升算法效率。在排序算法中,快速排序、归并排序、堆排序相比较于选择排序、冒泡排序、插入排序更快,就是因为它们应用了分治的策略。
提问:为什么分治可以提升算法效率,它的底层逻辑是什么?为什么将大问题分解为多个子问题、解决子问题、将子问题的解合并为原问题的解、这几步的效率为什么就比直接解决原问题的效率更高?
回答:
1.2.1 操作数量的优化
以"冒泡排序"为例,其处理一个长度为n的数组需要O(N2)的时间。假设我们按照下图来操作,将数组从中点分为两个子数组,则划分需要O(N)时间,排序每个子数组需要O((N/2)2)时间,合并两个子数组需要O(N)的时间,总体时间复杂度为:
O(n+(n/2)^2+n) = O((n^2)/2+2*n)
接下来,我们计算以下不等式,其左边和右边分别为划分前和划分后的操作总数:
N^2 > (N^2)/2+2N
N^2-(N^2)/2-2N > 0
N(N-4) > 0
这意味着当N>4时,划分后的操作数量更少,排序效率应该更高,不过要注意的是这里划分后的时间复杂度仍然平阶O(N^2),只是复杂度中的常数项变小了。
进一步想,如果我们把子数组不断地再从中间划分为两个子数组,直到子数组只剩下一个元素时停下划分呢?这种思想就是"归并排序",时间复杂度为O(NlogN)
.
再去思考,如果我们再多设置几个划分点,将原数组平均划分为k个子数组呢?这种情况与"桶排序"非常类似非常适合排序海量数据,理论时间复杂度为O(N+K)
1.2.2 并行计算优化
我们知道,分治生成地子问题相互独立地,因此通常可以并向解决。也就是说,分治不仅可以降低算法时间复杂度,还有利于操作系统地并行优化。
并行优化再多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分利用计算机资源,从而显著减少总体的运行时间。
1.3 分治常见应用
- 寻找最近点对:该算法首先将点集分成两部分,然后分别找出两部分中最近的点对,最后找出跨越两部分的最近点对。
- 大整数乘法:例如Karastsuba算法,它将大整数乘法分解为几个较小的整数的乘法和加法。
- 矩阵乘法:例如Strassen算法,它将大矩阵分解为多个小矩阵的乘法和加法。
- 汉诺塔问题:汉诺塔问题可以通过递归解决,这是典型的分治策略应用。
- 求解逆序对:再一个序列中,如果前面的数字大于后面的数字,那么这连个数字构成一个逆序对。求解逆序对问题可以利用分治的思想,借助归并排序求解。
在另一方面,分治在算法和数据结构的设计中应用非常广泛。 - 二分查找:二分查找是将有序实在从中点索引处分为两部分,然后根据目标值与中间元素比较结果,决定排除哪一半的区间,并在剩余区间执行相同的二分操作。
- 归并排序:递归地将原数组划分为两个子数组,直到子数组只剩一个元素,从底到顶地将有序子数组进行合并,从而得到有序地原数组
- 快速排序:快速排序是选取一个基准值,然后把数字分为两个子数组,一个数组的元素比基准值小,另一个子数组比基准值大,再对这两部分较小相同的划分操作,直到子数组只剩下一个元素。
- 桶排序:推排序的基本思想是将数据分散到多个桶,然后最每个桶内的元素进行排序,最后将各个桶的元素以此取出,从而得到一个有序数组。
- 树:例如二叉搜索树,AVL树,红黑树,B树,B+树等,它们的查找、插入、删除等操作都可以视为分治策略的应用
- 堆:堆是一种特殊的完全二叉树,其各种操作,如插入、删除和堆化,实际上都隐含了分治的思想。
- 哈希表:虽然哈希表并不直接应用到分治,但某些哈希冲突解决方案间接地使用了分治策略,例如,链式地址中地长链表会被转化为红黑树,以提高查询效率。