专题:分治法

  • 分治法(Divide and Conquer)

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

    分治法的精髓:
      分--将问题分解为规模更小的子问题;
      治--将这些规模更小的子问题逐个击破;
      合--将已解决的子问题合并,最终得出“母”问题的解;
    分治法的作用,自然是让程序更加快速地处理问题。比如一个n的问题分解成两个n/2的问题,并由两个人来完成,效率就会快一些。当然单线程的程序的分治法,就是把n的问题剔除掉可以省略的步骤,从而提高程序运行的速度。
                                                            
  • 二分法(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,虽然我一直按照着它的思想来写,但是不同语言的实现确实有些小小的区别。至今我还描述不出来,但是这给我提了个醒,学算法一定要做题!不仅地把经典算法的实现老老实实写出来,还要认真地体会它的细节,不停在脑子里画出程序的执行结构图,使之形成习惯。

 

 
posted @   天目山电鳗  阅读(6988)  评论(1编辑  收藏  举报
编辑推荐:
· 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训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示