分治:本质上就是分而治之,本质上来说就是将一个大问题转化成若干个相同或者是相似的小问题,然后再这些子问题的基础上继续进行划分相同类型的子问题,直到划分到某一个位置开始,这个子问题能够被彻底的解决,向上进行返回;
一)颜色划分:
75. 颜色分类 - 力扣(LeetCode)
算法原理:
使用三指针算法解决此问题:
index索引是用来遍历整个数组的
left索引:标记0区域的最右侧
right索引:标记2区域的最左侧,最开始的那个2的下标
[0,left]全部都是0
[left+1,index-1]全部都是1
[index,right-1]全部都是待扫描的元素
[right,n)全部是等于2的元素
分类讨论:nums[index]进行分类讨论
class Solution { public void swap(int i,int j,int[] array){ int temp=array[i]; array[i]=array[j]; array[j]=temp; } public void sortColors(int[] array) { int left=-1; int right=array.length; int index=0; while(index
二)快速排序:
1)之前学习过的快排是将整个数组分成两部分,左边的区域全部是小于等于基准元素的,右边的区域全部是大于基准元素的,但是假设数组已经处于完全有序的情况下,那么基准元素会被分配到最右边,接下来再次将基准值的右边进行排序,此时时间复杂度就达到了O(N^2)
使用数组分三块的思想实现快排,此时我们还是使用上面的颜色划分的思想来进行实现快排
2)但是数组元素全部是相同的情况下,时间复杂度也是有可能达到O(N^2)
1)中间的那一段区域[left+1,right+1]的这一段区域就不需要再管了,中间这一段区域一定是合适的区域
2)当数据重复的时候,这道题的算法的时间复杂度是更高的,假设当数组中的元素全部是key的时候,我们经过三指针算法以后,是将这个数组划分成三块的,那么中间的这些元素全部是等于key的,此时左右区间是不存在的,此时递归直接结束,如果使用三指针算法,当数组都是重复元素,直接进行操作一次,快排就直接结束了,相比于数组划分成两块就更快
class Solution { public static void swap(int[] array,int i,int j){ int temp=array[i]; array[i]=array[j]; array[j]=temp; } public static int[] Sort(int left,int right,int[] array) { int temp=array[left]; int leftindex=left-1; int rightindex=right+1; int index=left; while(index
temp){ rightindex--; swap(array,rightindex,index); }else{ leftindex++; swap(array,leftindex,index); index++; } } return new int[]{leftindex,rightindex}; } public static void QuickSort(int left,int right,int[] array){ if(right<=left){ return; } int[] temp=Sort(left,right,array); int leftIndex=temp[0]; int rightIndex=temp[1]; QuickSort(left,leftIndex,array); QuickSort(rightIndex,right,array); } public static void main(String[] args) { int[] array={10,8,19,100,8,102}; QuickSort(0,array.length-1,array); System.out.println(Arrays.toString(array)); } } 2) 为什么说这个算法效率更快呢?
假设现在数组中的元素都是重复的元素,将数组划分成三段数组就直接排完序了,要是采用划分成两端的方式,时间复杂度就是O(N^2)
3)用随机的方式选取基准元素:必须是等概率性的返回数组中的随机的一个数字
三)数组中第K大的元素
215. 数组中的第K个最大元素 - 力扣(LeetCode)
一)堆排序:使用优先级队列(集合类容器)进行堆排序
class Solution { public int findKthLargest(int[] nums, int k) { //创建一个大小是k的小堆 PriorityBlockingQueue
queue=new PriorityBlockingQueue<>(k, new Comparator () { @Override public int compare(Integer o1, Integer o2) { return o1-o2; } }); for(int i=0;i top){ queue.poll(); queue.add(nums[i]); } } } return queue.poll(); } } 二)手动创建一个大堆,进行弹出堆顶元素N-1次即可
class Solution { public int poll(int[] array,int len){ int result=array[0]; int temp=array[0]; array[0]=array[len-1]; array[len-1]=temp; len--; createBigHeat(array,0,len); return result; } public void createBigHeat(int[] array,int parent,int len){ int child=2*parent+1; while(child
array[parent]){ int temp=array[child]; array[child]=array[parent]; array[parent]=temp; parent=child; child=2*parent+1; }else{ break; } } } public int findKthLargest(int[] array, int k) { //1.首先建立一个大堆 int len=array.length; for(int i=(array.length-1-1)/2;i>=0;i--){ createBigHeat(array,i,len); } //2.建立完大堆之后,出数组中的元素 int data=0; for(int i=0;i 三)使用快速选择算法来解决此问题(时间复杂度是O(N))
先进行选取一个基准元素key,然后按照基准元素key将数组划分成三部分
如果可以确定这个数是在三个区间中的哪一个区间,那么剩下的两个区间其实是不需要进行考虑的,那么我们如何来进行确定第k大的元素是落在左边区域还是右边区域还是在中间的区域呢?首先要先计算出各个区间内的元素;
1)要求的是第K大的元素,首先应该考虑的是元素最大的区域,c>=k,第K大的元素一定会落在[right,n-1),只是需要在array数组中的[right,n-1)这段区间内继续去寻找第K大的元素
2)我们可以约定一下,只要到了第二种情况,那么第一种情况一定是不成立的,如果最终第K大的元素是落在b区间内,b+c>=k了,此时就不需要再次进行递归函数了,直接进行返回即可,此时直接返回key,如果运气好的话,一次快排就可以直接找到结果
3)如果此时第一种情况和第二种情况不成立,那么此时应该在[0,left]区间内寻找结果,此时要找的就不是第K大的元素了,那么此时要找的就不是第K大的元素了,要找的是第k-b-c大的元素;
class Solution { //1.这个函数是交换两个位置的元素 public void swap(int[] array,int i,int j){ int temp=array[i]; array[i]=array[j]; array[j]=temp; } //2.先经历一次快排划分区间 public int[] SortAndFind(int[] array,int left,int right){ //1.先进行选取随机的一个元素 Random random=new Random(); int randomIndex=random.nextInt(right-left+1); int resultIndex=randomIndex+left; int temp=array[resultIndex];//基准元素选取完成 //2.根据基准元素使数组分三块 int index=left; int leftIndex=left-1; int rightIndex=right+1; while(index
=k)//处于大于temp的区间 return QuickSort(array,result[1],right,k); else if(c+b>=k) return array[result[1]-1];//处于等于temp的区间 return QuickSort(array,left,result[0],k-b-c);//处于小于temp的区间 } public int findKthLargest(int[] nums, int k) { return QuickSort(nums,0,nums.length-1,k); } }
四)数组中最小的k个数
剑指 Offer 40. 最小的k个数 - 力扣(LeetCode)
不讲武得:直接排序,找到前k个数
一)集合类容器+优先级队列O(N*logK)
class Solution { public int[] getLeastNumbers(int[] array, int k) { int[] result=new int[k]; if(k==0) return result; PriorityBlockingQueue
queue=new PriorityBlockingQueue<>(k, new Comparator () { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } }); for(int i=0;i array[i]){ queue.poll(); queue.add(array[i]); } } } for(int i=0;i 二)快速选择算法(时间复杂度O(N))
1)先按照基准值将数组划分成三块,分别是小于key,等于key和大于key,假设[0,left]有a个元素,[left,right-1)有b个元素,[right,N-1)有c个元素
2)现在我们要进行查找的元素是第k小的元素
2.1)if(a>=k)那么现在只是需要在[0,left)区间找到第key小的元素即可
如果在[0,left)区间之内无法找到第k小的元素
2.2)如果第一种情况不存在,if(a+b>=k)那么直接返回元素array[left]
2.3)else 直接去[right,n-1)区间内去进行查找第k-a-b小的元素
class Solution { //这个函数是交换两个位置的元素 public void swap(int[] array,int i,int j){ int temp=array[i]; array[i]=array[j]; array[j]=temp; } public int[] SortAndFind(int[] array,int left,int right){ Random random=new Random(); int randomIndex=random.nextInt(right-left+1); int resultIndex=randomIndex+left; int temp=array[resultIndex];//基准元素选取完成 int leftIndex=left-1; int rightIndex=right+1; int index=left; while(index
=k){ QuickSort(array,left,result[0],k); }else if(a+b>=k){ return; }else{ QuickSort(array,result[1],right,k-a-b); } } public int[] getLeastNumbers(int[] array, int k) { QuickSort(array,0,array.length-1,k); int[] ret=new int[k]; //相当于是把数组中的前key个元素放到了最前面 for(int i=0;i
五)归并排序:宏观思想
912. 排序数组 - 力扣(LeetCode)
归并排序的流程充分的体现了分而治之的思想,大体流程主要分成两部:
分:将数组一分为二分成两部分,一直分解到数组的长度是1,使整个数组的排序过程被分为左半部分排序+右半部分排序
治:将两个较短的有序数组合并成一个较长的有序数组,一直合并到最初的长度
1)重复子问题:先找到中间结点的值,将整个数组划分成两部分,把左区间排一下序,让右区间排一下序,然后在合并两个有序数组
2)在本层先把数组分成两块,先找到中间值,先让mid左边的数组的数有序,再让mid右边的数有序,在合并两个区间内的数组元素,而在让左边的数组有序的过程中,还是在左边的数组中找到一个基准值,不断地进行划分合并;
3)现根据中点划分区间,先把左右区间排个序
4)然后合并两个有序数组,递归的出口就是当数组只剩下一个元素的时候,直接返回,其实本质上快排和归并的结束条件都是当数组只剩下一个元素的时候,直接返回即可
5)归并排序,非常类似于二叉树的后序遍历,先搞左子树,在搞右子树,最后来搞根节点,而归并排序,是先将左边的区间排序,再将右边的区间排序,最后要将整个区间排序
6)而对于快速排序来说,是先将数组划分成两部分,先将左边排序,再将右边区间排序,其实本质上就是相当于是二叉树的前序遍历,先搞根节点,在搞左子树,最后搞右子树
六)数组中的逆序对
剑指 Offer 51. 数组中的逆序对 - 力扣(LeetCode)
解法1:暴力枚举两层for循环,将所有的二元组枚举出来,最后判断一下是否是逆序对即可,就是可以先固定一个数,然后可以在这个数后面的这段区间内去寻找有多少数比他小即可
解法2:使用归并排序的思想来解决这个问题:
1)如果我想要求出整个数组的逆序对,首先将数组按照中间点划分成两部分
就是先找到数组的中间元素将数组划分成两部分,左半部分和右半部分,首先只看数组左半部分的这一段区间,找到a个逆序对,然后只看数组右半部分的这一段区间,找到b个逆序对,最后从数组的左半部分区间跳出一个数,右半部分区间跳出一个数,再来看看能不能构成逆序对,最终的逆序对的个数就是N=a+b+c
2)先在左边挑出a个逆序对,再从右边挑出b个逆序对
再从左边挑一个数,再从右边挑选一个数,看看是否能够组成逆序对,左右组成逆序对的个数使c个,那么数组一共的逆序对的个数就是N=a+b+c,本质上还是一个暴力枚举
3)现在再来想一下:
首先还是将数组划成两部分:
1)先在左半部分挑完以后,找到a个逆序对,然后针对于左半部分的元素进行排序
2)然后在右半部分挑完b个逆序对,然后针对于右半部分的元素进行排序
3)当我们分别在左右两边枚举完所有的逆序对然后进行排序之后,继续执行上面的第三步,然后在左边找一个数,右边找一个数,然后再进行组合,此时是否将左半部分的元素进行排序和右半部分的元素进行排序并不重要,只进行关心的是在前半段区间内找到一个数之后,在后半段区间内只需要找到比这个数小的元素的个数即可,根本不需要关心后半段区间内的元素是否有序,或者是说固定右半段区间内的元素,只需要找到前段区间内有多少元素是比当前元素大的个数,至于前半段区间是否有序,我是不会关心的,所以当对两段区间进行排序的时候,是不会影响一左一右的挑法的
1)下面正式使用归并排序的思想来解决一下这个问题:升序排序
时间复杂度:O(N*logN)
先搞定一左一右:
策略1:找到一个数,找出该数之前,有多少个数比我大,就是如果我想要看某一个数之前,有多少个数比我大的时候,那么在上面这个图中,只是需要盯着s2这个指针来看就可以了,只需要找到左边的区间里面有多少个数比nums[s2]大即可,接下来的分析过程可以自己花两个有序数组来模拟一下这个过程;
2)拓展:如果说整个数组是降序排序的,那么可以解决这个问题吗,再来分析一下,我们的原始策略依旧是不变的,找到该数之前一共有多少个数比我大即可
1)如果整个数组是降序排序的话,那么如果通过mid指针将整个数组划分分成两部分,左半部分的区间绿色部分的元素都是比nums[S1]大的,右半部分的区域绿色部分的元素都是比nums[s2]大的
3)策略2:找出该数之后有多少个数比我小,只是关心第一个区间的第一个元素:
可以自己画图分析一下,升序是不可以的
1)if(cur1>cur2),那么此时cur2连同cur2后面半段区间的元素都是比cur1小的
此时ret+=right-cur2+1,此时cur1指针向后移动,cur1++,这行和归并排序的策略一模一样的
2)if(cur1
3)if(cur1==cur2)此时cur2++即可
class Solution { public int MerageSort(int [] nums,int left,int right,int mid){ int s1=left; int e1=mid; int ret=0; int s2=mid+1; int e2=right; int[] array=new int[right-left+1]; int index=0; while(s1<=e1&&s2<=e2){ if(nums[s1]>nums[s2]){ ret+=e1-s1+1; array[index]=nums[s2]; s2++; index++; }else{ array[index]=nums[s1]; s1++; index++; } } while(s1<=e1){ array[index]=nums[s1]; s1++; index++; } while(s2<=e2){ array[index]=nums[s2]; s2++; index++; } for(int i=0;i
=right) return 0; int ret=0;//统计一下本层中逆序对出现的个数 int mid=(left+right)/2; ret+=Sort(nums,left,mid); ret+=Sort(nums,mid+1,right); ret+=MerageSort(nums,left,right,mid); return ret; } public int reversePairs(int[] nums) { return Sort(nums,0,nums.length-1); } }
1)递归的终止条件:left>=right,说明此时数组中没有元素或者是数组中只存在一个元素,那么此时数组是没有任何逆序对的,此时就返回0即可;
2)在逆序对的处理过程中,是找找左边有多少个逆序对,将左边区间顺便排一下序,找找右边有多少个逆序对,将右边区间顺便排一下序,这两步找左边有多少个逆序对和合并有序数组的逻辑是相同的,找数组逆序对的过程本质上是合并两个有序数组的过程
class Solution { public int MerageSort(int [] nums,int left,int right,int mid){ int s1=left; int e1=mid; int ret=0; int s2=mid+1; int e2=right; int[] array=new int[right-left+1]; int index=0; while(s1<=e1&&s2<=e2){ if(nums[s1]>nums[s2]){ ret+=e2-s2+1; array[index]=nums[s1]; s1++; index++; }else{ array[index++]=nums[s2++]; } } while(s1<=e1){ array[index]=nums[s1]; s1++; index++; } while(s2<=e2){ array[index]=nums[s2]; s2++; index++; } for(int i=0;i
=right) return 0; int ret=0;//统计一下本层中逆序对出现的个数 int mid=(left+right)/2; ret+=Sort(nums,left,mid); ret+=Sort(nums,mid+1,right); ret+=MerageSort(nums,left,right,mid); return ret; } public int reversePairs(int[] nums) { return Sort(nums,0,nums.length-1); } }
七)计算右侧小于当前元素的个数:
315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)
1)首先看完题目之后这个题是和求逆序对的操作是一模一样的,当我们将整个数组进行计算完成之后,整个数组元素的和就是逆序对的总数,但是这个题和上一个题不一样的地方就是,上一道题求的是总的逆序对的数量,而这道题求的是当这个元素是左元素的时候,后面区间有多少元素比我小,用数组来返回,那么我们该如何来解决这个数组的问题呢?
算法原理:这个题的解决思路就是在当前元素的后面找一下有多少元素比当前元素小,而是使用的是降序排序
2)所以此时应该进行解决的问题就是当找到一个数的右边有多少个元素比当前的这个元素小的时候,要找到nums当前这个元素的原始下标是多少?
class Solution { List
result=new ArrayList<>();//保存最终结果 int[] ret;//保存最终结果 int[] indexs;//下标数组 public void Sort(int[] nums,int left,int right,int mid){ int[] array=new int[right-left+1]; int[] tempIndex=new int[right-left+1]; //数组排序是从大到小的 int s1=left; int e1=mid; int s2=mid+1; int e2=right; int index=0; while(s1<=e1&&s2<=e2){ if(nums[s1]<=nums[s2]){ array[index]=nums[s2]; tempIndex[index]=indexs[s2];//此时下标数组要同步进行修改 s2++; index++; }else{ array[index]=nums[s1]; ret[indexs[s1]]+=e2-s2+1; tempIndex[index]=indexs[s1]; s1++; index++; } } //处理剩余的数组排序问题 while(s1<=e1){ array[index]=nums[s1]; tempIndex[index]=indexs[s1]; s1++; index++; } while(s2<=e2){ array[index]=nums[s2]; tempIndex[index]=indexs[s2]; s2++; index++; } for(int i=0;i =right) return; int mid=(left+right)/2; MerageSort(nums,left,mid); MerageSort(nums,mid+1,right); Sort(nums,left,right,mid); } public List countSmaller(int[] nums) { this.ret=new int[nums.length]; this.indexs=new int[nums.length]; //初始化indexs数组 for(int i=0;i
八)翻转对:
493. 翻转对 - 力扣(LeetCode)
解法1:暴力枚举
固定一个数,在这个数后面找到符号要求的数,那么时间复杂度就是O(N^2)
解法2:归并排序+分治
1)选取一个中间点mid,将整个数组划分成两部分,可以先求出左边的翻转对的个数计为a,在求出右边的翻转对的个数记为b,然后求出一左一右的反转对的个数是c个,那么整个数组的反转对的个数就是a+b+c个,逆序对那道题因为可以和归并排序完美地进行契合,是因为逆序对的比较是一比一进行比较,如果cur1是左边区间的指针,cur2是右边区间的指针,那么我们只是需要进行比较nums[cur1]的值和nums[cur2]的值即可,判断他们的大小关系即可,而我们归并排序的比较方式和逆序对操作的比较方式是一模一样的
2)但是这个题不一样,我们要进行比较的是nums[i]>2*nums[j],我们就不能按照归并排序的流程来求得我们的翻转对了,但是我们却可以在归并排序之前来求得我们的翻转对,照样是根据两个数组有序的特性,因为利用两数组有序的特性就可以利用O(N)的时间复杂度来求得这一层的翻转对的个数了,那么我们该如何利用数组有序的特性,快速的求出翻转对呢?
策略1:计算当前元素的后面 ,有多少元素的两倍比我小(降序排列),一定要进行画图理解
当我们统计出翻转对的个数以后,当合并两个有序数组的时候,要按照降序排列来进行合并
class Solution { public int MerageSort(int[] nums,int left,int right,int mid){ //1.先统计翻转对的个数 int s1=left; int e1=mid; int s2=mid+1; int e2=right; int ret=0; while(s1<=e1&&s2<=e2){ if(nums[s1]/2.0<=nums[s2]){ s2++; }else{ ret+=right-s2+1; s1++; } } //2.然后再来进行合并两个有序数组,整个数组是降序排序 s1=left; e1=mid; s2=mid+1; e2=right; int index=0; int[] array=new int[right-left+1]; while(s1<=e1&&s2<=e2){ if(nums[s1]<=nums[s2]){ array[index++]=nums[s2++]; }else{ array[index++]=nums[s1++]; } } while(s1<=e1){ array[index++]=nums[s1++]; } while(s2<=e2){ array[index++]=nums[s2++]; } for(int i=0;i
=right) return 0; int mid=(left+right)/2; int ret=0; ret+=Sort(nums,left,mid); ret+=Sort(nums,mid+1,right); ret+=MerageSort(nums,left,right,mid); return ret; } public int reversePairs(int[] nums) { return Sort(nums,0,nums.length-1); } } 策略2:计算当前元素的前面,有多少元素的一半比我大(升序排列)
class Solution { public int MerageSort(int[] nums,int left,int right,int mid){ //1.先统计翻转对的个数 int s1=left; int e1=mid; int s2=mid+1; int e2=right; int ret=0; while(s1<=e1&&s2<=e2){ if(nums[s1]/2.0>nums[s2]){ ret+=mid-s1+1; s2++; }else{ s1++; } } //2.然后再来进行合并两个有序数组,整个数组是降序排序 s1=left; e1=mid; s2=mid+1; e2=right; int index=0; int[] array=new int[right-left+1]; while(s1<=e1&&s2<=e2){ if(nums[s1]>=nums[s2]){ array[index++]=nums[s2++]; }else{ array[index++]=nums[s1++]; } } while(s1<=e1){ array[index++]=nums[s1++]; } while(s2<=e2){ array[index++]=nums[s2++]; } for(int i=0;i
=right) return 0; int mid=(left+right)/2; int ret=0; ret+=Sort(nums,left,mid); ret+=Sort(nums,mid+1,right); ret+=MerageSort(nums,left,right,mid); return ret; } public int reversePairs(int[] nums) { return Sort(nums,0,nums.length-1); } } 总结:这道题和之前的题的区别就是不能利用合并两个有序数组的逻辑来进行计算出翻转对,这道题是先进行计算翻转对的个数,最后再来合并两个有序数组,因为判断条件是不同的