分治
- 一.分治
- 二.经典应用案例
- 三.快速排序
- (1)颜色分类
- (2)排序数组
- (3)数组中第K个最大的元素
- 四.归并排序
- 1.排序数组
- 2.交易逆序对总数
- 3.计算右侧小于当前元素的个数
- 4.翻转对
一.分治
分治算法是一种通过将复杂问题分解为多个结构相似的子问题,递归求解后再合并结果来解决原问题的算法设计范式。其核心思想可概括为三个步骤:
-
分(Divide):将原问题分解为若干规模较小的子问题。
-
治(Conquer):递归或直接求解子问题。
-
合(Combine):将子问题的解合并为原问题的解。
二.经典应用案例
-
归并排序(Merge Sort):将数组分为两半分别排序,再合并有序子数组。
-
快速排序(Quick Sort):通过枢轴划分子数组,递归排序后合并。
-
二分查找(Binary Search):每次将搜索区间减半,定位目标值。
-
Strassen矩阵乘法:通过分块矩阵运算,将时间复杂度从 O(n3)优化至 O(n2.81 )。
-
最近点对问题:分解点集,分别求解后合并邻近区域的解。
三.快速排序
(1)颜色分类
颜色分类
思路:将数组分为三个小数组,第一个数组的数据为0的区域,第二个数组的数据为1的区域,第三个数组的数据为2的数据。简单地说,就是通过三个指针分成四个区域,[0,left]是0区域,[left + 1,i-1]是1的区域,[i,right-1]是带扫描区域,[right,n-1]是2的区域
分为四个区域之后,将中间区域作为key值进行比较,当扫描区域的值小于key值,则先让left+1后,修改此时left和i位置上的数据,再让i进行++;如果此时扫描的数据和key相等,则直接i++,继续扫描后面的区域。
如果此时扫描的值大于key值,先让right-1后,再进行i和right位置上的值进行交换,需要注意的是,此时i不能++,因为此时数据换了之后,无法确定此时i位置换后的数据等于key,所以应该再扫描一次i位置的数据,再进行一次比较,和上述的方法一样不再赘述。
还需要注意的是,left的下标是-1,right的下标是n,补药记成left的下标为0,right的下标为n-1
自我心得:一个新的排序算法思路,最重要的是理解下图中的扫描位置后交换位置细节。
class Solution {public void sortColors(int[] nums) {int left = -1,right = nums.length, i = 0;while(i < right) {if(nums[i] == 0) {swap(nums,++left,i++); }else if(nums[i] == 1) {i++; }else {swap(nums,--right,i);} }}public void swap(int[] nums,int x ,int y){int tmp = nums[x];nums[x] = nums[y];nums[y] = tmp;}
}
(2)排序数组
排序数组
思路:
使用的是快排的递归思路,只不过其中的partation是使用三指针,也就是上一道题中的颜色分类中的思想进行划分。
细节:
我认为最重要的是进行递归的时候,要区分l和left,r和right,这样写代码时更容易理解,其中left和right则就是颜色分类中的思想中的left和right
还需要注意的是,这里的key值不像颜色分类中的key值直接给了具体的数字,所以需要通过随机值获取一个数据,在算法导论中概率随机数的随机值需要的时间复杂度更小,这个随机值的取法需要注意其中Random括号中的细节,要记住加上偏移量l
自我反思:要知道每次排序后,排序的区域是哪一块,而不是直接凭感觉写,更不是背记住代码,还需要知道取随机数时,要注意是r-l后还要+1,但是具体的情况下看你如何进行取值,使用r-l+1是为了等概率的获取[0,n - 1]的下标,再加上偏移量L,就能够选取一个中间的key值(个人理解,有误请指出)
class Solution {public int[] sortArray(int[] nums) {qsort(nums,0,nums.length-1);return nums;}public void qsort(int[] nums,int l,int r) {//递归出口if(l >= r) return;//数组分三块int left = l - 1,right = r + 1, i = l;int key = nums[new Random().nextInt(r - l + 1) + l];//执行完后,[left + 1, right - 1]排序完成while(i < right) {if(nums[i] < key) swap(nums,++left,i++);else if(nums[i] == key) i++;else swap(nums,--right,i);}//区域分为[l,left] [left + 1, right - 1] [right, r]qsort(nums,l,left);//比当前key值小的区域排序qsort(nums,right,r);//比当前key值大的区域排序}public void swap(int[] nums, int x, int y) {int tmp = nums[x];nums[x] = nums[y];nums[y] = tmp;}
}
(3)数组中第K个最大的元素
数组中第K个最大的元素
思路:通过前面学习的颜色分类+排序数组中的思想进行查找第N个大的数,这里不能使用top-k问题的堆解法是因为超时了,top-k的时间需要N*logN。与上两道题不同的是,这里需要的是一个数值,所以简单分析一下。
数组会被分为3块,此时每一块都进行计算其中元素的个数(从左到右依次为a,b,c),因为是升序的排列,所以只需要判断k和a,b,c的三者大小即可,第一种情况则是c >= k的时候,此时只需要在大于key值的数组中寻找,如果此时b + c >= k,此时就是为key值的数组,此时直接返回key就行了。如果此时b + c < k,就直接在小于key值的数组中寻找第k - b - c大的值
需要注意的是,这里可以不需要递归出口,因为不是对其全部进行排列,只是通过k三个区域数组的长度来进行寻找需要的第K大的数。还需要注意的是,此时找到第K大的数时,不需要做其他的返回,因为找打第K大的数值时,它就是key值。
反思:写题的时候不知道key值的返回是通过找到第K大的值为key的值时返回的,做题时的深入不够多,一直在思考找到第K大所在的区域后,通过第K大数的k值也就是第K大数的下标进行计算出它在原数组中的位置来进行返回,我感觉是不可行的,因为好像越界了。还是要去多思考每个返回带来的效果,而不是再次的凭感觉写。
class Solution {public int findKthLargest(int[] nums, int k) {return qsort(nums,0,nums.length-1,k);}public int qsort(int[] nums, int l, int r, int k) {if(l >= r) return nums[l];int left = l - 1, right = r + 1, i = l;int key = nums[new Random().nextInt(r - l + 1) + l];while(i < right) {if (nums[i] > key) {swap(nums,--right,i);}else if (nums[i] == key) {i++;}else {swap(nums,++left,i++);}} int b = right - 1 - left, c = r - right + 1;if(c >= k) {return qsort(nums,right,r,k); }else if (b + c >= k) {return key;}else {return qsort(nums,l,left,k - b - c);}}public void swap(int[] nums, int x, int y) {int tmp = nums[x];nums[x] = nums[y];nums[y] = tmp;}
}
四.归并排序
1.排序数组
排序数组
思路:
简单的复习一下归并排序的写法,归并排序通过中间点将数组分成两块区域,再将中点的左右两侧通过递归将数组排序成有序数组后,再将整个数组进行合并后,再通过临时数组进行还原数组的操作。
细节:
不断地通过递归将数组分为两块区域,直到数组只有一个数据时,此时则返回,并且先进行完左边的递归后,再进行右边的递归,两处的递归执行完后,对这两块有序数组进行合并,合并的方式通过双指针的方式按照大小的顺序合并,并且使用临时数组进行存储后,再将数据还原给原数组。
还需要注意的是,合并的时候选择数组中小数据进行合并后,在两块数组区域比大小的循环中,会存在有一块数组先达到数组的长度后退出循环,此时就需要在写一个循环判断是哪一块数组还没合并完,将没有合并完的数组拼接到临时数组上。
class Solution {int tmp[];public int[] sortArray(int[] nums) {tmp = new int[nums.length];//归并mergesort(nums,0,nums.length - 1);return nums;}public void mergesort(int[] nums, int left, int right) {if(left >= right) return;//1.根据中间点分成两块区域int mid = (right + left) / 2;//2.进入递归排序mergesort(nums, left, mid);mergesort(nums, mid + 1, right);//3.合并两个有序数组int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right) {tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];} //遍历完还剩下的数组while(cur1 <= mid) tmp[i++] = nums[cur1++];while(cur2 <= right) tmp[i++] = nums[cur2++];//4.覆盖数据for (int j = left; j <= right; j++) {nums[j] = tmp[j - left];}}}
2.交易逆序对总数
交易逆序对总数
思路:逆序数需要用前面的数字和后面小的数字比较,如果前面的数字大,则逆序数+1,反之则逆序数不变。类似于这样的场景可以通过归并将数组不断划分成两个区域进行比较,于是便可以使用归并排序。
可以使用两个策略:
策略一:右边区域的数组块中指定数据后,在左边区域的数组块中找到比右边区域数组块大的数据就行,又因为左边区域数组块进行归并排序时就进行了升序的排序操作,此时在左边区域数组块中的数据的mid - cur1+ 1个数据都比右边数组块指定的数据大,也有对应个数的逆序数。
策略二:左右两块区域通过降序进行排列,此时将左边区域的数据中指定一个后,在右边区域块中找到一个比左边区域块小的数据后,就会有right - cur2 + 1 个数的数据比左边区域的指定数据小。
注意:
在cur1和cur2的为下标的数组值进行比较时,相等的情况出现时,应该让被指定数据的区域数组块进行下标向右移动的操作,不然是不行的。
还需要注意的是,在策略1中只能通过升序来进行计数,如果通过降序就会出现,当cur1下标向右时,只能使用策略二的方式来进行计数,按照策略一中指定左边区域数组块作为指定数据区就没办法进行计数了。如果非要使用cur1 - left + 1进行计数的话,那么此时前面计算过的区域都被重复计算了,如果通过ret++的行为进行计数,是不行的,因为你会循环遍历右边区域的数组块。
反思:
在递归时不能习惯性的将left传入0,而是要注意传入left就应该传入left而不是习惯传入0,这样的错误还是太低级了,不太注意细节,还需要多多仔细的掌握算法。
class Solution {int[] tmp;int ret = 0;public int reversePairs(int[] record) {tmp = new int[record.length];mergesort(record, 0, record.length - 1);return ret;}public void mergesort(int[] record, int left, int right) {if(left >= right) return ;//进行分块int mid = (left + right) / 2;//递归排序mergesort(record, left, mid);mergesort(record, mid + 1, right);int cur1 = left, cur2 = mid + 1, i = 0;//合并并计算逆序对(使用升序)while(cur1 <= mid && cur2 <= right) {if(record[cur1] <= record[cur2]) {tmp[i++] = record[cur1++];}else {tmp[i++] = record[cur2++];ret += mid - cur1 + 1;}}//合并并计算逆序对(使用降序)while(cur1 <= mid && cur2 <= right) {if(record[cur1] <= record[cur2]) {tmp[i++] = record[cur2++];}else {tmp[i++] = record[cur1++];ret += right - cur2 + 1;}}while(cur1 <= mid) tmp[i++] = record[cur1++];while(cur2 <= right) tmp[i++] = record[cur2++];//还原到原数组for(int j = left; j <= right; j++) {record[j] = tmp[j - left];}}
}
3.计算右侧小于当前元素的个数
计算右侧小于当前元素的个数
思路:
这里需要上述第2题中讲到的策略二的方法,降序进行找值。但是不同的是,返回的数据是一个链表每个原始元素下标对应满足题意的个数(也就是这个原始元素下标的右侧有几个元素的值比它小,记录下比它小的元素的个数)。这里就增加了难度,因为进行归并排序时,原数组中原始元素的下标肯定是发生变化了,此时需要搞一个index数组记住原始元素的下标。
index数组,这个数组本身的下标就是原始元素的下标,而index这个数组下标对应的元素就是原始元素排序后的下标,就可以让原始元素的下标和index下标对应的元素进行绑定。
注意:
还原数据之前,index数组也需要创建一个临时数组进行对原始元素的下标进行更新。
反思:
没有搞清楚index下标中的数据和数组本身的下标哪个代表什么,没有搞明白,理解的时候非常吃力,根据自己画图几次后,就理解到了里面的原理,总结就是还得多加练习,这个题又做了2个小时。
class Solution {int[] tmp;//更新降序排好的数组位置int[] tmpIndex;//更新原数组中元素现在所处位置的新下标int[] index;//记录原始数组下标位置的数组,该数组中的数据代表原始数组中元素现在的位置int[] ret;public List<Integer> countSmaller(int[] nums) {ret = new int[nums.length];tmp = new int[nums.length];tmpIndex = new int[nums.length];index = new int[nums.length];//先把原始数组下标的位置记录到数组中for (int i = 0; i < nums.length; i++) {index[i] = i;}List<Integer> l = new ArrayList<>();mergesort(nums, 0, nums.length - 1);for (int x : ret) {l.add(x);}return l;}public void mergesort(int[] nums, int left, int right) {if (left >= right) return;//进行分区域int mid = (left + right) / 2;//左右递归排序并计数mergesort(nums, left, mid);mergesort(nums, mid + 1, right);//进行计数操作int cur1 = left, cur2 = mid + 1, i = 0;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2]) {tmp[i] = nums[cur2];tmpIndex[i++] = index[cur2++];//因为元素进行排序后就会导致原数组的元素的下标发生改变,为了记住原数组的下标//就通过index数组中的数据将原数组中的元素的现在的下标记录下来,所以在元素进行交换位置时,也需要进行对index数据的改变}else {tmp[i] = nums[cur1];ret[index[cur1]] += right - cur2 + 1;//由于index数组自己的下标就是原数组元素对应的下标//则通过cur1这个原始元素的下标就可以在index中找到此时这个原始元素下标现在的下标是什么//知道下标后就在ret中进行计算,原始元素下标这个位置的右侧比它小的原始数据有几个。tmpIndex[i++] = index[cur1++];//也要对index数组的数据进行临时保存,方便后续对index数组元素的还原操作}}//剩余的也添加到临时数组中while (cur1 <= mid) {tmp[i] = nums[cur1];tmpIndex[i++] = index[cur1++];}while (cur2 <= right) {tmp[i] = nums[cur2];tmpIndex[i++] = index[cur2++];}//还原操作for (int j = left; j <= right; j++) {nums[j] = tmp[j - left];index[j] = tmpIndex[j - left];}}
}
4.翻转对
翻转对
思路:
这道题和上面的题不同的是,计算翻转对时,不会在进行左右数组合并的时候计算翻转对数。这道题是先将翻转对数计算完后,再进行数组的合并,并且策略一和二都可以使用只是需要注意其中使用的细节。
细节:
里面会需要使用到乘以2倍,或者除以2,这里需要注意的是,使用乘时,会达到int类型的最大值。因为int类型的值达到最大值时,此时进行加或者乘操作就会导致实际数字减小。
因为int类型的范围是:-2147483648~2147483647,此时如果值为2147483647,加1或者乘2,都会变成比2147483647小的数,此时判断的逻辑就错误了。
如果出现这样的情况,可以使用long类型先获取int类型的值后,再对此时的值进行乘以2操作就不会造成超过范围了。
还有除以2的时候要使用2.0才是浮点数,不然不会出现小数,也会对判断进行干扰。
class Solution {int[] tmp;int ret = 0;public int reversePairs(int[] nums) {if(nums.length < 2) {return 0;}tmp = new int[nums.length];mergesort(nums, 0, nums.length - 1);return ret;}public void mergesort(int[] nums, int left, int right) {if (left >= right) return;//找中点分块int mid = (left + right) / 2;//左右递归进行排序mergesort(nums, left, mid);mergesort(nums, mid + 1, right);int cur1 = left, cur2 = mid + 1, i = 0;// //计算翻转对(升序计算)// while (cur1 <= mid && cur2 <= right) {// double n1 = nums[cur1] / 2.0;// if(n1 > nums[cur2]) {// ret += mid - cur1 + 1;// cur2++;// }else {// cur1++;// }// }// //合并两个数组(升序)// cur1 = left;// cur2 = mid + 1;// while (cur1 <= mid && cur2 <= right) {// if (nums[cur1] > nums[cur2]) {// tmp[i++] = nums[cur2++];// }else {// tmp[i++] = nums[cur1++];// }// }// //剩余的未合并完的数组// while (cur1 <= mid) tmp[i++] = nums[cur1++];// while (cur2 <= right) tmp[i++] = nums[cur2++];//计算翻转对(降序计算)while (cur1 <= mid && cur2 <= right) {long n1 = nums[cur1];long n = nums[cur2];long n2 = 2 * n;if(n1 > n2) {ret += right - cur2 + 1;cur1++;}else {cur2++;}}//合并两个数组(降序)cur1 = left;cur2 = mid + 1;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] > nums[cur2]) {tmp[i++] = nums[cur1++];}else {tmp[i++] = nums[cur2++];}}//剩余的未合并完的数组while (cur1 <= mid) tmp[i++] = nums[cur1++];while (cur2 <= right) tmp[i++] = nums[cur2++];//还原for (int j = left; j <= right; j++) {nums[j] = tmp[j - left];}}
}