在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范式。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)。
另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以数学归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。
中华文化5000年,早在很久以前,就有“分而治之,各个击破”的兵法。对于数学表达式,可以把复杂的表达式通过化简得到更加简洁的表达式;人类解决复杂问题的时候,常常把复杂问题分解成多个简单易于求解的问题。再比如:
国产动画《葫芦娃》中的蛇精都知道,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的时间,我手绘了一个插图:
需要排序的数字为:3 9 6 2 1 5 8 7在分解结束之后,得到就是3 9 6 2 1 5 8 7然后就是合并,合并的时候,需要将小的数据放在左边(升序方式),最后合并出来的数据就是排序之后的。编码阶段中最难的也就在于合并了,如何合并?分解比较简单,递归思想,前面说过,不要陷入递归的细节,它只是我们的工具,相信递归可以完成工作。关于合并,我们来看看插图: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行的循环就是把右边剩余的数填充在缓冲区,因为可能是左边有剩余,也可能是右边有剩余。比如:
刚才我们分析了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;}
欢迎扫关注B站(C语言教程,强烈推荐的视频教程):https://space.bilibili.com/5782182