专题:分治法
- 分治法(Divide and Conquer)
作为五大算法之一的分治法,可算是最早接触的一种算法。分治法,与其说是一种算法,不如将其称为策略来的更贴切一些。算法的思想就是将大问题分成小问题,并解决小问题之后合并起来生成大问题的解。

- 二分法(Bisection)
二分法针对于有序集合的处理来说显得比按序遍历来得更快一些。记得有次我读到一篇处理问题的博客:有一个城市到另一个城市之间的电杆不通电了,该如何排查?作为常用思维大概就是逐个去查了,如果能确定两地间的电杆是串联的节点,那么二分法显然是比较有效率的,因为它跳过了很多不必要排查的电杆。
我们一般使用二分法去寻找一个流中的特定的数。比如查找有序数列中是否存在一个数,或者使用二分法求一个数的根号。
1.找数字
假设我们需要在1-10000里面找一个数200,使用逐个搜索的方法,我们会消耗200步。如果计入小数的画,恐怕就大大超过200这个消耗了。
假如使用二分法:
第一步我们找到1-10000中间的那个数:5000。它大于200,所以200应该在1-4999这个区间内,这样我们就丢掉了后5000个数。
第二步我们找到2500,也比200要大,200在1-2500这个区间内。
第三步找到1250这个数,也比200大。
第四步找到750。
第五步找到375。
第六步找到167,它比200要小了,说明200在167-375之间。
第七步找到271,它在167-271之间。
第八步找到219,它在167-219之间。
第九步找到193,它在193-219之间。
第十步找到206,它在193-206之间。
第十一步找到199,它在199-206之间。
第十二步找到202,它在199-202之间。
第十三步找到200。
在n=10000的这个问题来讲,从200步到13步是一个很大的改进。如果我在这里写200步,那我肯定是傻了。
使用二分法找数组中的一个值的下标,如果没有找到则返回-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | int index=- 1 ; public void BisectionFind( int a[], int l, int r, int target){ if (l<r){ int mid=(l+r)/ 2 ; if (a[mid]<target) BisectionFind(a,mid+ 1 ,r,target); else if (a[mid]>target) BisectionFind(a, l, mid- 1 , target); else { index=mid; return ; } } } public static void main(String[] args) { SqrtDemo sq= new SqrtDemo(); int a[]={ 5 , 13 , 19 , 21 , 37 , 56 , 64 , 75 , 80 , 88 , 92 }; sq.BisectionFind(a, 0 , a.length- 1 , 21 ); System.out.println(sq.index); } |
2.求根号
对于我来说,最早接触的有序数列大概就是数轴了,要在数轴上找到一个根号的具体值,就是从数轴上找一个数乘以自己看是否在所求数字的周围。如果精确度可以接受的话,那么就采用这个值为这个数的根号的近似值。
这回我们舍弃的是半个数轴,半个数轴按照精确度的不同,它会产生不同的复杂度。所以二分法的效率,远远高于按序查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public double sqrt1( double number, double precision){ double up=(number> 1 ?number: 1 ); double down= 0 ; double n; int time= 0 ; while ( true ){ n=(down+up)/ 2 ; if (n*n-number<precision && n*n-number>= 0 ) break ; else if (n*n-number>precision) up=n; else if (n*n-number< 0 ) down=n; time++; System.out.println(n); } System.out.println( "time=" +time); return n; } |
1 2 | SqrtDemo sq= new SqrtDemo(); System.out.println(sq.sqrt1( 10 , 0.001 )); |
上面两种问题,它们能确定这个问题的解就在序列的内部,所以它们在执行的时候都转换成了寻找子问题的解。在不断分割问题的过程中,问题的复杂度急剧下降,效率大大地提高了。
- 快速排序(QuickSort)
经典的分治法案例,在乱序数组中做到了O(nlogn)的效率,对冒泡法(O(n*n))的一个很大的改进。
快速排序的步骤:1.寻找一个基准元素
2.从右向左寻找大于(小于)基准元素的值
3.从左向右寻找小于(大于)基准元素的值
4.使得基准元素左边都是小于(大于)它的元素,右边都是大于(小于)它的元素
5.递归的处理基准元素左边与右边的模块
C++版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int Partition1( int a[], int i, int j){ int start=i; int end=j; int x=a[i]; while (start<end){ while (a[end]>=x && end>start) end--; swap(a[start],a[end]); while (a[start]<=x && end>start) start++; swap(a[start],a[end]); } cout<< "中心位置:" <<start<<endl; return start; } void quickSort1( int a[], int p, int r){ if (p<r) { int x=Partition1(a, p, r); quickSort1(a, p, x-1); quickSort1(a, x+1, r); } } |
Java版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public int Partition( int a[], int p, int r){ int start=p; int end=r; int x=a[p]; while (start<end){ while (start<end && a[end]>=x) end--; if (start<end) a[start++]=a[end]; while (start<end && a[start]<=x) start++; if (start<end) a[end--]=a[start]; } a[start]=x; return start; } public void quickSort( int a[], int i, int j){ if (i<j){ int p=Partition(a, i, j); quickSort(a, i, p- 1 ); quickSort(a, p+ 1 , j); } } |
Python版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | def Partition(a,p,r): x=a[p] i=p j=r while ( 1 ): while ( 1 ): if (a[i]<=x and i<len(a)- 1 ): i=i+ 1 else : break while ( 1 ): if (a[j]>=x and j> 0 ): j=j- 1 else : break if (i>=j): break else : a [j], a [i] = a [i], a [j] a[p]=a[j] a[j]=x return j def quickSort(a,i,j): if (i<j): p=Partition(a,i,j) quickSort(a,i,p- 1 ) quickSort(a,p+ 1 ,j) def PartitionDemo(a,p,r): x=a[p] start=p end=r while start<end : while start<end and a[end]>=x : end-= 1 while start<end and a[start]<x : a[start]=a[end] start+= 1 a[end]=a[start] a[start]=x return start def quickSortDemo(a,i,j): if (i<j): q=PartitionDemo(a,i,j) quickSortDemo(a,i,q- 1 ) quickSortDemo(a,q+ 1 ,j) a=[ 9 , 5 , 2 , 4 , 7 , 3 , 6 , 8 , 15 , 18 , 11 , 13 ] quickSortDemo(a, 0 ,len(a)- 1 ) print a |
- 归并排序(MergeSort)
作为经典排序算法,使用分治策略,归并排序无疑是最能体现分治思想并且易于理解的一种算法。它在排序之前先将序列分割成最短为1的小数组。当长度为1的时候,数组无疑是有序的。合并的时候就如树形结构逆着生成根节点一样,子问题排序、合并,最终生成一个有序的数组。
C++版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void merge( int a[], int b[], int p, int mid, int r){ int i=p; int j=mid+1; int t=p; while (i<=mid && j<=r){ if (a[i]<a[j]) b[t++]=a[i++]; else if (a[j]<=a[i]) b[t++]=a[j++]; } if (i!=mid) while (t<=r) b[t++]=a[j++]; else while (t<=r) b[t++]=a[i++]; for (i=p;i<=r;i++) a[i]=b[i]; } void MergeSort( int a[], int b[], int start, int end){ if (start<end){ int mid=(start+end)/2; MergeSort(a,b,start,mid); MergeSort(a,b,mid+1,end); merge(a,b,start,mid,end); } } |
Java版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public void merge( int a[], int b[], int start, int mid, int end){ int i=start; int j=mid+ 1 ; int k=start; while (i<=mid && j<=end){ if (a[i]<=a[j]) b[k++]=a[i++]; else b[k++]=a[j++]; } while (i<=mid) b[k++]=a[i++]; while (j<=end) b[k++]=a[j++]; for (i=start;i<=end;i++) a[i]=b[i]; } public void MergeSort( int a[], int b[], int start, int end){ if (start<end) { int mid=(start+end)/ 2 ; MergeSort(a,b,start,mid); MergeSort(a,b,mid+ 1 ,end); merge(a,b,start,mid,end); } } |
- 总结
分治法作为一个比较重要的算法,思想的理解来说还是比较简单的。但是它写起代码来确实有些许抽象。从C++到java再到python,虽然我一直按照着它的思想来写,但是不同语言的实现确实有些小小的区别。至今我还描述不出来,但是这给我提了个醒,学算法一定要做题!不仅地把经典算法的实现老老实实写出来,还要认真地体会它的细节,不停在脑子里画出程序的执行结构图,使之形成习惯。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人