分治算法
- 用于设计算法的一种常用技巧–分治算法(divide and conquer)。分治算法由两部分组成:
- 分(divide):递归然后借机较小的问题(基础情况除外)
- 治(conquer):然后从子问题的解构建原问题的解
- 分治算法一般在主题逻辑中都至少含有两个递归调用的例程,而正文中只有一个递归调用的例程不算是分治算法。一般坚持子问题是不想交的(即不重叠)
前几章中分治算法
- 之前我的文章中,我们依据看到有几个分支算法,比如
- 排序算法中的快速排序
- 二叉树中的树的遍历
案例分析
最大子序列求和问题
- 给定(包含有负数)的整数A1, A2, A3,…AN,求解该集合中子序列的和的最大值∑k=ij\sum_{k=i}^j∑k=ijAk
- 此问题最简单的方式暴力双循环,在这种情况下算法逻辑简单,但是时间复杂度达到了O(N^2),如下代码实现:
/*** @author liaojiamin* @Date:Created in 16:38 2021/1/29*/
public class MaxSumRec {public static int[] getArrayData(int size) {int[] arrayData = new int[size];Random random = new Random();for (int i = 0; i < size; i++) {int temp = random.nextInt(50);if (temp > 25) {arrayData[i] = temp;} else {int value = temp - 2 * temp;arrayData[i] = value;}}return arrayData;}/*** fun1 双循环 时间复杂度O(N^2)* */public static int getMaxSumRec(int[] arrayData){if(arrayData == null || arrayData.length <= 0){return -1;}if(arrayData.length == 1){return arrayData[0] > 0 ? arrayData[0] : -1;}int max = 0;for (int i = 0; i < arrayData.length; i++) {int sum = 0;for(int j=i; j< arrayData.length; j++){sum +=arrayData[j];if(sum > max){max = sum;}}}return max;}public static void main(String[] args) {int[] arraydata = getArrayData(20);for (int i = 0; i < arraydata.length; i++) {System.out.print(arraydata[i] + ", ");}System.out.println();System.out.println(getMaxSumRec(arraydata));}
}
- 但是显然这不是最优的解法,对应这个存在复杂度为O(NlogN)的解法,我们如下描述。
- 我们采用分治算法(divide-and-conquer)策略。他的思想是将问题分成两个大致相等的子问题,然后递归的对它们求解,这是分的部分,治的部分将两个子问题的解补充到一起并做少量的附加工作,然后得到整个问题的解法。
- 如上案例中,我们最大子序列可能分布在三个地方:
- 整个序列在输入数组的前半部分:可以递归求解
- 整个序列在输入数组的后半部分:可以递归求解
- 整个序列在输入数组的中间部分:需要先求出前半部分的最大和(其中包含最后一个元素),在求后半部分最大和(其中包含首个元素),将两个和相加
- 我给出如下算法实现:
/*** @author liaojiamin* @Date:Created in 16:38 2021/1/29*/
public class MaxSumRec {public static int[] getArrayData(int size) {int[] arrayData = new int[size];Random random = new Random();for (int i = 0; i < size; i++) {int temp = random.nextInt(50);if (temp > 25) {arrayData[i] = temp;} else {int value = temp - 2 * temp;arrayData[i] = value;}}return arrayData;}/*** 分治算法* */public static int getMaxSumRec_1(int[] arrayData){return getMaxSumRecDivide(arrayData, 0, arrayData.length -1);}public static int getMaxSumRecDivide(int[] arrayData, int left, int right){if(left == right){return arrayData[left] > 0 ? arrayData[left] : -1;}int center = (left + right)/2;int maxLeft = getMaxSumRecDivide(arrayData, left, center);int maxRight = getMaxSumRecDivide(arrayData, center + 1, right);int maxLeftBordeSum =0;int leftBordeSum = 0;for(int i = center; i>= left; i--){leftBordeSum+=arrayData[i];if(maxLeftBordeSum < leftBordeSum){maxLeftBordeSum = leftBordeSum;}}int maxRightBordeSum = 0;int rightBordeSum = 0;for(int i = center+1; i<=right;i++){rightBordeSum+=arrayData[i];if(maxRightBordeSum < rightBordeSum){maxRightBordeSum = rightBordeSum;}}return Math.max(Math.max(maxLeft, maxRight), maxLeftBordeSum+maxRightBordeSum);}public static void main(String[] args) {int[] arraydata = getArrayData(20);for (int i = 0; i < arraydata.length; i++) {System.out.print(arraydata[i] + ", ");}System.out.println();System.out.println(getMaxSumRec_1(arraydata));}
}
- 如上算法的运行次数分析如下:
- 令T(N)是求解大小为N的最大子序列的问题所花费的世界。
- 如果N=1,则只需要执行最基础情况环肥常数时间量,1个单位时间,得到T(1) = 1;
- 如果N>1,则必须运行两个递归调用,以及之后的循环,和最后的最大值判断
- 因为分治算法保证每个子问题不重叠,那么我们在循环中必然是每个元素范文一次那么我们for循环总共解除到A_0~A_n-1 每一个元素,因此花费O(N)时间,毫无疑问
- 接着看递归部分:我们假设N是偶数,那么两个递归的基数是一样的,我们每次递归得到原来基数数据的一半,那么每一个子递归总的花费时间是T(N/2),(此处可以用数学归纳法证明,N被无限二分达到基数为1 的次数,此处略)
- 因此递归共花费2T(N/2)个时间单位,
- 如上总数花费时间:2T(N/2) + O(N)
T(1) = 1
T(N) = 2T(N/2) +O(N)
- 得到如上的数学公式,为简化计算,我们用N代替O(N)项,由于T(N)最终还是要用大O表示,因此这么做不影响答案
- 此处我们只进行递推,不进行数学证明,依据以上公式,T(N) = 2T(N/2)+N,且T(1) = 1
- 那么我们得到 T(2) = 2T(1) + 2 = 4 = 22, T(4) = 2T(2) + 4 = 12 = 43 … T(16) = 2T(8) + 16 = 80 = 16*5
- 若N=2^k,则 T(N) =N*(k+1) = N*LogN +N = O(NlogN)
最大子序列和投机方法
- 一个循环解决此问题,如下实现:
public static int getMaxSumRec_2(int[] arrayData){int maxSum = 0, thisSum = 0;for (int i = 0; i < arrayData.length; i++) {thisSum+=arrayData[i];if(thisSum > maxSum){maxSum = thisSum;}else if (thisSum < 0){//如果之前项累计不大于0, 则情况之前项和,从小计数thisSum = 0;}}return maxSum;}
再谈快排
- 快速排序是经典的分支算法应用,我们在之前的排序归纳文章中已经给出过具体的算法分析以及实现,基本算法都是如下几个步骤:
- 如果S集合中元素个数是0或者1,则返回
- 取S中任何一个元素V,称为枢纽元(pivot)
- 将S-{V}(S中其他元素)划分为两个不想交的集合:S1 = { X \in S-{V} |X<=V | } 和 S2 = { X \in S-{V} |X>=V | }
- 返回{quickSort(S1)} 后跟v,继续返回 quickSort(S2)
- 以上算法中针对枢纽元的选择上并没有详细处理,因此这就成了一种设计决策,一部分好的实现方法是将这种情形尽可能完美的解决。直观的看,我们洗碗能将集合中的一半关键字分配到S1 中,另外一半分配到S2,很想二叉树。
枢纽元选择
- 虽然不管枢纽元选择的那个元素,最终都能完成排序,但是有些选择明显更优秀。
- 错误的选择方式:
- 我们之前的算法中直接选择的第一个元素作为枢纽元,因为当时的算法说明中数据来源是随机生成数组成的数列,那么这种情况是可以接受的。当时如果输入的数列是已有序的数列,或者反序列,那么这样的枢纽元选取会产生一个最差情况的分割,因为所有的元素不是被分配到S1就是S2,并且这种情况会发生在递归排序的所有调用中。时间复杂度O(N^2)
- 另外一种方法是选取前两个互异的关键字中的较大者作为枢纽元,不过这种值选取第一个元素作为枢纽元具有相同的问题,
- 一种安全的做法:
- 随机选择枢纽元,一般来说这种策略非常安全,除非随机数发生器有问题,因为随机的枢纽元不可能总在接连不断的产生劣质的分割,另一方面,随机数的生成开销比较大,根本减少不了算法其余部分的平均运行时间。
- 三数中值分割法(Median-of-Three Partitioning) :一组N个数的中值是滴N/2 个最大的数。枢纽元最好的选择数是数组的中值。但是,这很难算出并且明显减慢快排的速度,这样的中值的估计量我们可以通过随机选取三个数并用他们的中值作为枢纽元而得到。而实际上,随机性并没有这么大的帮助,因此一般的做法是使用左端,右端和中间位置的三个元素的中值作为枢纽元。
分割策略
- 有几种分割策略用于实践,我们给出的分割策略已经被证明能够给出好的结果。我们将枢纽元与最后的元素交换,使得枢纽元离开要被分割的数据段。i从第一个数据开始,j从倒数第二个元素开始。我们用如下动图
- 上图用的交换法,当i在j左边的时候,我们将i右移,移过哪些小于枢纽元的元素,并且建j左移,移过哪些大于枢纽元的元素,当i和j停止时候,i指向一个大元素,j指向一个小元素。如果i在j左边,那么将交换这两个元素。最后效果是将大元素推向右边,小元素推向左边。最后如果i 到了j 的右边,并且停在第一个最大的元素时候,我们停止交换i, j ,并且将pivot与i 交换。
- 在最后一个步骤当枢纽元与i 所指向的元素交换的时候,我们知道,在位置position < i 的每一个元素都必须小于枢纽元,类似position > i 的元素都大于枢纽元。
- 必须考虑的一个重要情况,如何处理等于枢纽元情况,i ,j 应该做相同的动作,否则分割将会偏向一方。
- 极端情况,所有数据相等,那么我们每次都需要交换,虽然没有意义,但是i ,j, 将会在中间交错,时间复杂度O(NlogN)
- 实际情况,几乎不会存在都相同情况,所以我们让i, j,都停止,并交换。
小数组
- 对于很小的数组(N <= 20),快速排序不如插入排序快
快速排序实现
/*** @author liaojiamin* @Date:Created in 12:06 2021/2/1*/
public class DivideAndConquerGreat {public static void main(String[] args) {int[] beginArrayData = getArrayData(30);System.out.println("------------------");int[] arrayData = quickSort(beginArrayData);for (int i = 0; i < arrayData.length; i++) {System.out.println(arrayData[i]);}}public static int[] quickSort(int[] arrayData) {if (arrayData == null || arrayData.length <= 1) {return arrayData;}return quickSort(arrayData, 0, arrayData.length - 1);}public static int[] quickSort(int[] arrayData, int left, int right) {if(Math.abs(left - right) <= 20){insertionSort(arrayData, left, right);}else {if (left < right) {int position = swap(arrayData, left, right);quickSort(arrayData, left, position - 1);quickSort(arrayData, position + 1, right);}}return arrayData;}/*** 快排主体实现*/public static int swap(int[] arrayData, int left, int right) {int position = median3(arrayData, left, right);int i = left ;int j = right - 1;while (i < j) {while (i < j && arrayData[i] <= position) {i++;}while (i < j && arrayData[j] >= position) {j--;}if (i < j) {swapElement(arrayData, i, j);}}//position初始位置是right-1swapElement(arrayData, i, right - 1);return i;}/*** 数据交换*/public static void swapElement(int[] arrayData, int i, int j) {int temp = arrayData[i];arrayData[i] = arrayData[j];arrayData[j] = temp;}/*** 三数中值获取*/public static int median3(int[] arrayData, int left, int right) {int center = (left + right) / 2;if (arrayData[center] < arrayData[left]) {swapElement(arrayData, center, left);}if (arrayData[right] < arrayData[left]) {swapElement(arrayData, right, left);}if (arrayData[right] < arrayData[center]) {swapElement(arrayData, right, center);}swapElement(arrayData, center, right - 1);return arrayData[right - 1];}/*** 插入排序*/public static int[] insertionSort(int[] arraydata, int left, int right) {if (arraydata == null || arraydata.length <= 1) {return arraydata;}for (int i = 0; i <= right; i++) {for (int j = i; j > left; j--) {if (arraydata[j - 1] > arraydata[j]) {int temp = arraydata[j - 1];arraydata[j - 1] = arraydata[j];arraydata[j] = temp;}}}return arraydata;}/*** 随机生成数列*/public static int[] getArrayData(int size) {int[] arrayData = new int[size];Random random = new Random();for (int i = 0; i < size; i++) {int temp = random.nextInt(10);if (temp > 0) {arrayData[i] = temp;} else {int value = temp - 2 * temp;arrayData[i] = value;}System.out.println(arrayData[i]);}return arrayData;}
}
上一篇:数据结构与算法–贪婪算法2