《算法导论的Java实现》 1_3.1 分治算法

第一章 算法概念
1.3 算法设计
1.3.1 分治算法

伪代码:
MERGE(A, p, q, r)
 1  n1 ← q - p + 1
 2  n2 ← r - q
 3  create arrays L[1 ‥ n1 + 1] and R[1 ‥ n2 + 1]
 4  for i ← 1 to n1
 5       do L[i] ← A[p + i - 1]
 6  for j ← 1 to n2
 7       do R[j] ← A[q + j]
 8  L[n1 + 1] ← ∞
 9  R[n2 + 1] ← ∞
10  i ← 1
11  j ← 1
12  for k ← p to r
13       do if L[i] ≤ R[j]
14             then A[k] ← L[i]
15                  i ← i + 1
16             else A[k] ← R[j]
17                  j ← j + 1

这个函数的功能是,将A[p … r]排顺序,假设A[p … q]和A[q + 1 … r]已经排好顺序(p≤q<r—)。
我手上的影印本《算法导论》里,≤和<很难区分清楚,所以不知道是否中文版本身就有问题,看上去都一样。但实际上,这种边界条件非常重要的,所以,必须参照英文原版。

显然,这只是分治算法的一部分。大方向上来说,分治算法一般分为:分治,解决,合并。
上面的MERGE(A, p, q, r)就是“合并”。

而这段分治排序法的合并部分,在其他场合也一样有用。那就是对已经排序的数列进行合并。由于没有双重循环,所以其时间复杂度为Θ(n) 。循环体最多被执行n次(n = r - p + 1)。

伪代码:
MERGE-SORT(A, p, r)
1 if p < r
2   then q ← ┕(p + r)/2┙
3        MERGE-SORT(A, p, q)
4        MERGE-SORT(A, q + 1, r)
5        MERGE(A, p, q, r)

这就是“分治”和“解决”了。递归的好处,或者说这里的分治的好处就是:程序清晰。上面这段代码太让人赏心悦目了。q ← ┕(p + r)/2┙,设定分治点;A[p … r]分成A[p … q]和A[q + 1 … r]两部分,然后调用MERGE(A, p, q, r)进行“合并”。

Java实现:



输出:
merge method test
1 2 4 4 6 7 8 10 11 14 17 20 30 40
mergeSort method test
1 2 2 3 4 5 6 6

coding后感
1,很有意思的是,java.util.Collections的“public static <T> void sort(List<T> list, Comparator<? super T> c)”,里面调用的java.util.Arrays类里面的函数,正是“private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off)”,这是个private函数,外面不可见,参数low,high,off和《算法导论》里的参数p,q,r,意思是不同的,特别是off是偏移量,而q是中间值;《算法导论》里面的MERGE(A, p, q, r)是只是合并,分治和解决在MERGE-SORT(A, p, r)里面,而Arrays是放在的mergeSort里面,全部搞定的。这点区别还是要注意一下的。
2,为了练手,看到Arrays里面有mergeSort后,故意不参考它,只是根据《算法导论》的伪代码写下的程序,结果当然是差不多。 看来我一个人也抵得上Josh Bloch、Neal Gafter、John Rose他们仨了(Arrays类的作者,2006年),呵呵。
3,上一篇写“插入排序”的时候,我说“插入排序本身是没有任何实用价值的”。可能是我错了。因为Arrays里面也有插入排序,上次我没有看到。看来插入排序也是有实用的地方,就现在这个分治排序也是有用到的地方的,凡排序必云“快速排序”,是不行的。
4,合并函数merge,其实还是很有用的。不只是这个分治算法里可用,更普遍的情况,对2个(甚至多个)有序组数的再排序,可以把两个已排好序的数组,直接连在一起后,调用merge。所以我特地加了段测试merge函数的代码放在main的最前面。
5,Java的数组从0开始是到“长度-1”为止这个特点,再次使我比较困扰。比方说mergeSort里面的if文,伪代码的是“ p < r”,如果Java里面也写“p < r”,结果是不变的,一样可以正常排序。但是我debug时发现,如果用p < r,当分治到只有2个元素是,还会再分,直到分治到只有一个元素,而合并时就是没有元素的数组和只有一个元素的数组进行合并,平白多了一层递归,和一次没有用的合并。改成“p + 1 < r”后,就没有这个问题了,分治到2个元素为止,2个元素时返回(退出一层递归),交个merge函数去合并。
6,我在上面即用了System的arraycopy函数,又用了Arrays的copyOfRange,我的本意是不想用java.util包的东西的(接口除外),希望不熟悉API的初学者也可以看懂所有的代码。但是细看copyOfRange后,发现里面也就是新建一个数组,调用System的arraycopy函数来复制,还是很容易理解的。现在这篇东西主要讲分治,所以copyOfRange的细节就不讲述了,有兴趣的人可以自己去看看,还是很好玩的。
7,结合5,6两点,Java的数组的上下限,System的arraycopy的参数的意义(主要是位置参数和长度参数),Arrays的copyOfRange的参数的意义(复制的是由from和to参数定义的前闭后开区间),这3样混在一起,会发现,伪代码和Java实现的样子,看上去会很不同。
8,和前面的插入排序一样,这里分治排序Java的实现里面,是直接改变被排序的数组里面的内容的。
9,MERGE伪代码里面的1到7行,在Java实现里面就只需要2句话,API真是强大啊(或者说会这么用API的人,真是强大啊。哈哈。)。MERGE伪代码里面的8,9行,一点也不优雅,被俺好不犹豫的舍弃了。后面的for循环,做的事儿是一样的,但是我的代码比伪代码要更高效一点。不需要循环整个被排序区间,只要循环到左右两段被排序区间的任意一段遍历完成就行。遍历完后,如果是左段先结束,那么什么都不用做,排序就已经好了,因为右段本身就是已经排好的;如果是右段先结束,那么左段还剩下的元素要紧跟着,复制到已经排好的数组元素的后面。当然,Java更高效的原因是存在数组复制的API,而不是伪代码的错(不优雅的“凭空增加一个数组元素,往里面赋值无限大”除外)。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值