蓝桥杯算法合集: 蓝桥杯算法合集(终极完结版)
算法Day4:分治法
分治法(divide&conquer)
思想: 分而治之
步骤: 先找重复性{最近重复性,最优重复性(动态规划) 分解成子问题(结构一样规模不同所以适用递归) 最后组合每个子问题的结果
(1)将原问题分解成若干个较小的子问题(子问题与原问题结构一样,只是规模不一样)
(2)子问题又不断分解成规模更小的子问题,直到不能再分解或可以轻易计算出子问题的解
(3)利用子问题的解推导出原问题的解
需要注意:子问题之间时相互独立的
应用:
快排、归并排序、最大子序列问题等
主定理:
解决规模为n的问题,分解为a个规模为n/b的子问题,然后在O(n^d)时间内将问题的解合并起来。
算法时间复杂度为T(n)=a*T(n/b)+O(n^d)(其中a>0,b>1,d>=0) |
---|
d>logb(a) 时间复杂度为O(n^d) |
d==logb(a) 时间复杂度为O(n^d*logn) |
d<logb(a) 时间复杂度为O(n^(logb(a))) |
比如归并排序时间复杂度为 T(n)=2T(n/2)+O(n) a=2,b=2.d=1 d==logb(a)
所以时间复杂度为O(nlogn)。关于常见递推式的时间复杂度,详见我之前写的博客:算法Day2:递归算法
有些问题采用分治法策略之后,性能会有所提升是因为对于同一个操作,数据规模的范围在不断缩小。
例如对于一般常规排序如冒泡/选择/插入排序 时间复杂度为O(n^2)
使用分治策略之后 将问题分解为数据规模为n/2的两个子问题
n/2 O(n^2 /4)
n/2 O(n^2 /4)
在加上合并子问题时的遍历操作O(n)
故使用分治策略之后时间复杂度为O(n)=O(n^2 /2)+O(n)
分治算法java模板:
data type divide_conquer(problem,param 1,param2){
//1.recursion terminator 终止条件/递归基
//问题转换为可以直接容易得出结果的规模或者说达到了树中的叶子节点
if problem is None{
print_result;
return;
}
//2.prepare data 预处理数据
data=prepare_data(problem);
subproblems=split_problem(problem,data);
//3.conquer subproblems 解决当前层的子问题
subresult1=self.divide_conquer(subproblem[0],p1……)
subresult2=self.divide_conquer(subproblem[1],p1……)
……
//4.process and generate the final result组合每个子问题的结果
result=process_result(subresult1,subresult2,……)
//5.reverse the current level satates
分治比递归多了一步:在drill down和reverse statue多了一步组合子问题结果
最大子序列和
问题描述(最大区段问题)
给定一个长度为n的整数序列,求它的最大连续子序列和
-2,1,-3,4,-1,2,1,-5,4 最大连续子序列和为4+(-1)+2+1=6
注意题目说最大没有说最长
package 分治法;
public class 最大连续子序列 {
/**解法一:暴力枚举法
* 时间复杂度 O(n^3)
* 空间复杂度 O(1)
* @param nums
* @return 返回子区段最大值
*/
public int maxSubArray(int[] nums) {
int max=Integer.MIN_VALUE;
//前两个for 切片 切出[begin,end]区间的子序列
for(int begin=0;begin<nums.length;begin++) {
for(int end=begin;end<nums.length;end++) {
//将该区段和统计起来
int sum=0;
for(int i=begin;i<=end;i++) {
sum+=nums[i];
}
//比较得出最大区段和
max=Math.max(max, sum);
}
}
return max;
}
/**解法二:带点优化的暴力枚举法
* 发现上面穷举时出现大量重复计算
* 如-2 1 -3 4 -1 2 1 -5 4
* 计算-2到2 之后计算-2到1 又重复计算了-2到d第二个2 这一段
* 可以将这一段结果保存利用下来从而减低复杂度
* 时间复杂度 O(n^2)
* 空间复杂度 O(1)
* @param nums
* @return 返回子区段最大值
*/
public int maxSubArray1(int[] nums) {
int max=Integer.MIN_VALUE;
//前两个for 切片 切出[begin,end]区间的子序列
for(int begin=0;begin<nums.length;begin++) {
int sum=0;
for(int end=begin;end<nums.length;end++) {
sum+=nums[end];
//比较得出最大区段和
max=Math.max(max, sum);
}
}
return max;
}
public int maxSubArray2(int[] nums) {
//判掉不合理情况
if(nums.length<1||nums==null) return 0;
return maxSubArray2(nums,0,nums.length);
}
/**maxSubArray2(int[] nums, int begin, int end)
* 功能是求出[begin,end)的最大子序列和
*分治法将[begin,end) 分为[begin,mid) [mid,end)
*最大子序列区段为[i,j]
* */
public int maxSubArray2(int[] nums, int begin, int end) {
if(end-begin<2) {//递归出口 当区段被切割到只有一个元素时 那么此时最大子序就是它本身
return nums[begin];//[begin,end)左闭右开 不能填nums[ends]
}
int mid=(begin+end)>>1;
//第一种情况[i,j)在左半段
//左半部分从mid-1开始从右往左扫
int leftSum=0;
int leftMax=Integer.MIN_VALUE;
for(int i=mid-1;i>=begin;i--) {
leftSum+=nums[i];
leftMax=Math.max(leftMax, leftSum);
}
//第二种情况[i,j)在右半段
//右半部分从mid开始从左往右扫
int rightSum=0;
int rightMax=Integer.MIN_VALUE;
for(int i=mid;i<end;i++) {
rightSum+=nums[i];
rightMax=Math.max(rightMax, rightSum);
}
//第三种情况[i,j)左半段和右半段都有
int max=leftMax+rightMax;
System.out.println(leftMax+" " +max+" "+rightMax);
return Math.max(max,
Math.max(maxSubArray2(nums,begin,mid),
maxSubArray2(nums, mid,end))
);
}
public static void main(String[] args) {
最大连续子序列 a=new 最大连续子序列();
int []nums=new int[]{-2,1,-3,4,-1,2,1,-5,4};
//int []nums=new int[]{3,1,-1,4,6,-8,9,-5,4};
System.out.println(a.maxSubArray(nums));
System.out.println(a.maxSubArray1(nums));
System.out.println(a.maxSubArray2(nums));
}
}
分治法解最大子序列和过程
归并排序
package 分治法;
public class 归并排序 {
/**
* 1.recursion terminator
* 2.process(prepare data,split your big problem do the current logic)
* 3.dirll down(subproblems),merge(subresults)
* 4.reverse the current level states if needed
*/
public void mergeSort(int[] nums,int begin,int end) {
//1.recursion terminator
if(begin<end) {
//2.process
//prepare data
int mid=begin+((end-begin)>>1);//中间元素 除以二是右移一位不是>>2 老是写错
//split your big problem
//System.out.println(nums[mid]);
//3.drill down
mergeSort(nums,begin,mid);
mergeSort(nums,mid+1,end);
//merge
merge(nums,begin,mid,end);
}
}
int []helper;
public void merge(int[] nums, int begin,int mid, int end) {
//应该在原数组nums进行修改 因为划分是一半一半地进行的
//下次扔进来的nums数组时是merge过的已经部分有序
//当左右区间划分到只剩下这个元素本身时 merge开始调用
//将a中已经分区部分元素拷贝到helper中
helper=new int[nums.length];
System.arraycopy(nums,begin,helper,begin,end-begin+1);
int left=begin;//左侧队伍头指针 指向待比较元素
int right=mid+1;//右侧队伍头指针 指向待比较元素
int current=begin;//新生成数组指针 指向待填入数据的位置
while(left<=mid&&right<=end) {
if(helper[left]<=helper[right]) {
nums[current++]=helper[left++];
}else {
nums[current++]=helper[right++];
}
}
//当左边还有元素剩余
while(left<=mid) {
nums[current++]=helper[left++];
}
for (int i : nums) {
System.out.print(i+" ");
}
System.out.println();
}
public static void main(String[] args) {
归并排序 a=new 归并排序();
//int []nums=new int[] {10,5,1,7,2,3,6,4,8,9};
int []nums=new int[] {1,2,3,4,5,6,7,8,9,10};
a.mergeSort(nums, 0, nums.length-1);
}
}
最后一次归并过程merge(nums,0,9)
快速排序
package 分治;
public class 快速排序 {
/**快速排序法 分治实现
* 重点在分区根据分区方法不同分为:单向扫描法 双向扫描法
* template:1.terminator 2.process(prepare data,split your big problem)
* 3.drill down ,merge 4.reverse the current level states
* quickSort(nums,begin,end)
* -->subproblem:
* 主元元素下标为mid 分为[begin,mid-1] [mid+1,end]
* -->subresult:
* quickSort(nums,begin,mid-1) quickSort(nums,mid+1,end)
*
*
*/
/**
* 对[begin,end]区间元素排序
*/
void quickSort(int []nums,int begin,int end) {
if(begin<end) {//terminator 当区间只有一个元素时
//求主元位置 分区
int position=partion_bilateral(nums,begin,end);
quickSort(nums,begin,position-1);
quickSort(nums,position+1,end);
}
}
//分区法一:单向扫描法
public int partion_single(int[] nums,int begin,int end) {
int pivot=nums[begin];
int sp=begin+1; //扫描指针 自左向右扫描待排序元素
int bigger=end;//右侧指针
while(sp<=bigger) {
if(nums[sp]<=pivot) {//扫描元素不大于主元
sp++;
}else {//扫描元素大于主元
swap(nums,sp,bigger);
bigger--;
}
}
swap(nums,bigger,begin);
// System.out.print(nums[bigger]+": ");
// for (int i : nums) {
// System.out.print(i+" ");
// }
System.out.println();
return bigger;
}
//分区法二:双向扫描法
public int partion_bilateral(int[] nums,int begin,int end) {
int pivot=nums[begin];
int left=begin+1; //左侧扫描指针 自左向右扫描待排序元素
int right=end;//右侧扫描指针 自右向左扫描待排序元素
//右侧指针扫到左侧指针左面则终止扫描
while(left<=right) {
//发现第一个大于主元的元素退出第一层while循环 left不动
//考略极端情况下 数组全部有序 left一直++到越界 出现访问过界
//所以应该先把left<=right放前面 简单与&& 回优先判掉left越界的情况 不进行nums[left]<=pivot的访问
while(left<=right&&nums[left]<=pivot) left++;
//发现第一个小于主元的元素退出第一层while循环 right不动
while(left<=right&&nums[right]>pivot) right--;
//两个指针还没错位时 交换指针的元素
if(left<right) {
swap(nums,left,right);
}
}
//扫描结束 右侧指针的位置就是主元应该待的位置
swap(nums,begin,right);
System.out.println(nums[right]);
return right;
}
public void swap(int[] array,int i,int j) {
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
public static void main(String[] args) {
int begin=0;
int []nums=new int[] {8,9,6,2,1,7,5,3,10,4};
//int []nums=new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int end=nums.length-1;
快速排序 a=new 快速排序();
a.quickSort(nums,begin,end);
for (int i : nums) {
System.out.print(i+" ");
}
}
}
在所选取的轴点左右元素数量比较均匀的情况下
同时也是最好时间复杂度:T(n)=2*(n/2)+O(n) 为O(n*logn)
在全部有序情况下,此时没有达到分区的效果 每次切出来的区段为一个元素
同时也是最坏时间复杂度;T(n)=T(n-1)+O(n) 为O(n^2)
空间复杂度:为递归调用次数logn*辅助空间O(1) O(logn)
由递推式推出时间复杂度参见我之前文章算法Day2——递归笔记
单向扫描法
另附:分治思想及快排双指针分区的应用