分治法的基本思想_递归应用(一)——分治算法,排序

   1c4be3587b517009ed8832483548b4ce.png1b77d472e34828665e967189068f0127.png点击上方蓝字关注,更多惊喜等着你5af6907b5cc8709508ba460fc3cd64d4.png290215a364b01b5e8986bc90ca3790f5.png     前面讲解了递归的基本概念,今天我们利用递归的基本思想来了解一下分治算法,分治算法递归的实现方式在大多数算法书籍上都有伪代码,而且网络上也有各种语言的实现版本。本文的主要目的是带大家理解为什么可以使用递归,为什么想到使用递归的方式来解决排序问题。     大多数书籍和网络上的文章,都是在解释分治算法本身,主要是在讲如何利用递归实现排序。但在我我看来,最重要的是理解为什么我们可以使用递归来排序,这样当我们遇到其他业务场景时,可以判断是否可以使用递归来解决问题。即回到最初的问题,什么时候我们会考虑使用递归?     今天我们要讲的是分治算法,看看维基百科对分治算法的解释:

在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范式。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)。

另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以数学归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。

    中华文化5000年,早在很久以前,就有“分而治之,各个击破”的兵法。对于数学表达式,可以把复杂的表达式通过化简得到更加简洁的表达式;人类解决复杂问题的时候,常常把复杂问题分解成多个简单易于求解的问题。再比如:

9ecba4b68c829f1b6d3ea7faa2119688.png

国产动画《葫芦娃》中的蛇精都知道,7个葫芦娃的大山一举拿下比较困难,所以选择各个击破......

    所以,分治算法,通常把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解。那这个和递归有什么关系呢?根据前面红色文字的解释,分治需要把问题分为两个或多个相同或相似的子问题,递归,回忆一下上一篇文章,这不就正是递归的应用场景吗?需要做相同或者相似的事情,这时我们就可以考虑使用递归。因为平常而言,我们都是要循环来解决相同或相似的事情,正常来说,使用递归完成的方法都可以使用循环完成,但是某些时候,递归有更简洁的代码,更易于阅读,更易使用语言实现,反之使用循环实现起来却非常麻烦,所以,递归也是我们必须要掌握的内容。

    现在,我们知道了,在什么样的情况下可以考虑使用递归,那么,现在我们回到具体例子,看看如何通过分治思想,利用递归的方式,实现排序呢?

    在一堆无序的数字中,对其进行排序。显然,这是一个相对复杂的问题,即使不使用编程语言实现,在现实生活中我们也比较难处理,但是,如果只有两个数,叫我们比较大小,那就比较简单了。如果一堆无序的数字,我们能够把他们两个两个的比较,最后把两两比较的结果合并成最终结果,那么就可以比较简洁地实现排序了。

    分治法的思想:把负责问题分解成两个或多个相同或相似的问题,而这个分解过程,正好可以使用递归来完成。这就是为什么我们可以使用分治法结合递归实现排序,切不可反过来,不要仅仅是为了明白分治法和递归的排序代码,这个代码其实意义不大(网上随便一搜都是一大堆),对我们有意义的是:在什么场景下,我们会考虑使用分治法和递归的思想解决问题。这个对我们以后的发展,才是有意义的。

    在明确了上面的思想之后,我们还是需要使用代码完成这个排序算法。这就是理论分析结合实际操作的时候了:

   首先,我们要把排序这个问题分解成若干个规模较小,相对独立,与原问题形式相同的子问题。当子问题规模较小且易于解决时,则直接解。否则,递归地解决各子问题。最后,将各子问题的解合并为原问题的解。

    现在的问题是,如何将排序这个问题分解成子问题?前面我们说到了,比较两个数的大小,就比较简单了呀。所以要想进行排序,就得把所有数据都得到。于是,我们可以把要排序的这个一堆数据,分成对等的2堆数据,以此类推,直到无法再继续分解,即只有一个数时为止,于是乎,可以有以下递归函数:

static void merge_sort(int *data, int start, int end){    int mid = start + (end - start) / 2;//奇数时,mid为正中,偶数时,mid为靠近左侧的数    if (start >= end) {        return ;    }    merge_sort(data,start,mid);//左分解    merge_sort(data,mid + 1,end);//右分解    merge(data,start,mid,end);//左右合并}

其中,递归的退出条件是不能再继续分解了,即start >= end;start代表的排序起始位置,end代表结束位置。比如一个需要排序的数组长度为100,start则为0,end为99。我们通过start 和 end,把需要排序的数分解成两份,直到不能再继续分解为止。merge_sort函数是递归函数,通过start和mid分解,已经mid+1和end分解,会将原始需要排序的数据2等分直到无法继续分解。为了方便大家理解并节约我制作PPT的时间,我手绘了一个插图:

3866e8ca12cff7cf5b57f540e5611350.png

需要排序的数字为:3 9 6 2 1 5 8 7在分解结束之后,得到就是3 9 6 2 1 5 8 7然后就是合并,合并的时候,需要将小的数据放在左边(升序方式),最后合并出来的数据就是排序之后的。编码阶段中最难的也就在于合并了,如何合并?分解比较简单,递归思想,前面说过,不要陷入递归的细节,它只是我们的工具,相信递归可以完成工作。关于合并,我们来看看插图:

96bf4851fdf3bc1d234639cb705cb7e8.png

3和9,合并成,3,9;6和2合并成2,6。因为我们这里的例子采用升序排列,所以6,2变2,6了,这是一个数字的合并,比较简单。那3,9,2,6如何合并成2,3,6,9呢?我们会先把左边的3和右边的2比较,谁小,谁就放在左边,然后继续使用前面的较小者和另一边未比较的数比较,这里比较抽象一点,那么这里是什么意思呢?以具体例子来说,这里3比2大,所以把2放在最前,然后继续使用3和6比较,此时3 更小,继续放在2后面,然后拿9和6比较,6 更小,继续放在3后面,最后再把9放在6后面,就合并成了2,3,6,9。注意,由于我们是升序排列,合并时一定是从左到右增大的,这也是为什么6,2会变成2,6。读者这里一定要自己推算一下,如果这是一道数学题,而不是编程题,上面插图那样的数字,你会怎么去排序?我相信你回答了这个问题,就可以写出相应的代码。很多文章和书籍都是直接给出合并代码,但是这样对于初次学习分治法的人来说非常不友好,在完成前面的推导之后,我们来看合并的代码:

static void merge(int *data, int start, int mid, int end){    int *data_buf = (int *)malloc((end - start + 1) * sizeof(int));    int i = start;    int j = mid + 1;    int k = 0;     while (i <= mid && j <= end) {         if (data[i] <= data[j]) {             data_buf[k++] = data[i++];         } else {             data_buf[k++] = data[j++];         }     }    //左边剩余填充(如果有)    while (i <= mid) {        data_buf[k++] = data[i++];    }    //右边剩余填充(如果有)    while (j <= end) {        data_buf[k++] = data[j++];    }  //把缓冲区数据复制给原数组    for (i = 0; i < k; i++) {         data[start + i] = data_buf[i];    }  free(data_buf);}

9-15行,第一个循环,就是找左边和右边最小的数,谁小就把这个数放在缓冲区data_buf中,还是以3,9(左)     2,6(右)为例;这个循环做的是什么呢?判断3是否小于等于2,显然不满足,此时把2放在缓冲里面,并且,右边的索引增加1;下一次循环到来,判断3是否小于等于6,满足,把3继续放在缓冲区里面,此时左边的索引增加1;下一次循环来到,比较9是否小于等于6,不满足,将6放在缓冲里面,同时循环不会再继续执行,此时还有一个9没有放在缓冲区中,所以16-19行的循环,就是把左边剩余的9填充在缓冲区中;而20-23行的循环就是把右边剩余的数填充在缓冲区,因为可能是左边有剩余,也可能是右边有剩余。比如:

6ef7c6ad84f2cbfe707cca7ad04f7ee9.png

刚才我们分析了3,9,2,6;现在分析1,5,7,8,按照前面的分析方法,1,5都会被填充在缓冲区中,剩余的7,8就需要20-23行的循环就是把右边剩余的数填充在缓冲区。最后,24-27行,是把缓冲区的数据复制到原始数组,最后原始数组的数据就是排序之后的了。    至此,利用分治法和递归思想的排序代码就完成了,通过本文,我们收获的是: 1.理解什么场景下我们可以考虑使用递归和分治法的思想来解决问题; 2.锻炼递归和分治法解决实际问题的思维,在大公司中,通常会有一些这样类型的面试试题,这也是为找工作或者换工作做铺垫,同时也是提高自身编码能力的一种锻炼,业精于勤荒于嬉,没有绝对的天才和吊车尾,编码和锻炼身体一样,只有坚持才能看到成效。 code demo(读者可以实现降序排序):
#include #include static void merge(int *data, int start, int mid, int end){    int *data_buf = (int *)malloc((end - start + 1) * sizeof(int));    int i = start;    int j = mid + 1;    int k = 0;     while (i <= mid && j <= end) {         if (data[i] <= data[j]) {             data_buf[k++] = data[i++];         } else {             data_buf[k++] = data[j++];         }     }    //左边剩余填充(如果有)    while (i <= mid) {        data_buf[k++] = data[i++];    }    //右边剩余填充(如果有)    while (j <= end) {        data_buf[k++] = data[j++];    }    for (i = 0; i < k; i++) {         data[start + i] = data_buf[i];    }    free(data_buf);}static void merge_sort(int *data, int start, int end){    int mid = start + (end - start) / 2;//奇数时,mid为正中,偶数时,mid为靠近左侧的数    if (start >= end) {        return ;    }    merge_sort(data,start,mid);    merge_sort(data,mid + 1,end);    merge(data,start,mid,end);}int main (void){    int arry[] = {3,9,6,2,1,5,8,7};    int size = sizeof(arry)/sizeof(arry[0]);    //升序排列    merge_sort(arry,0,size - 1);    for (int i = 0; i < size; i++) {        printf("%d ", arry[i]);    }    return 0;}
44434c0ed49bee303aa9dd49fd80bf86.png欢迎扫关注B站(C语言教程,强烈推荐的视频教程):https://space.bilibili.com/5782182 69a4e644e8864a6d26eabd1305056051.png 8f3fee183f7c13b5d0210d07ede76d07.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值