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

chen09 2010-03-19 11:49:49
blog地址:

《算法导论的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实现:


import java.util.Arrays;
import java.util.Comparator;

public class MergeSort {
public static <T> void merge(T[] a, int p, int q, int r,
Comparator<? super T> c) {

T[] left = Arrays.copyOfRange(a, p, q);
T[] right = Arrays.copyOfRange(a, q, r);

int indexLeft = 0;
int indexRight = 0;

for (int i = p; i < r; i++) {
if (indexLeft >= left.length) {
break;
}
if (indexRight >= right.length) {
System
.arraycopy(left, indexLeft, a, i, left.length
- indexLeft);
break;
}
if (c.compare(left[indexLeft], right[indexRight]) < 0) {
a[i] = left[indexLeft++];
} else {
a[i] = right[indexRight++];
}
}
}

public static <T> void mergeSort(T[] a, int p, int r,
Comparator<? super T> c) {
if (p + 1 < r) {
int q = (p + r) / 2;
mergeSort(a, p, q, c);
mergeSort(a, q + 1, r, c);
merge(a, p, q, r, c);
}
}

public static <T> void mergeSort(T[] a, Comparator<? super T> c) {
mergeSort(a, 0, a.length, c);
}

public static void main(String[] args) {
// merge's test
System.out.println("merge method test");
Integer[] tempMerge = new Integer[] { 1, 4, 7, 11, 14, 17, 2, 4, 6, 8,
10, 20, 30, 40 };
merge(tempMerge, 0, 6, tempMerge.length, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});

for (int i : tempMerge) {
System.out.print(i + " ");
}
System.out.println();

// mergeSort's test
System.out.println("mergeSort method test");
Integer[] tempMergeSoft = new Integer[] { 5, 2, 4, 6, 1, 3, 2, 6 };
mergeSort(tempMergeSoft, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
for (int i : tempMergeSoft) {
System.out.print(i + " ");
}
System.out.println();
}
}


输出:
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,而不是伪代码的错(不优雅的“凭空增加一个数组元素,往里面赋值无限大”除外)。

...全文
566 16 打赏 收藏 转发到动态 举报
写回复
用AI写文章
16 条回复
切换为时间正序
请发表友善的回复…
发表回复
chen09 2010-03-23
  • 打赏
  • 举报
回复
[Quote=引用 15 楼 leehomwong 的回复:]

顶一个~
[/Quote]

thanks。今明2天有更新。
24K純帥 2010-03-22
  • 打赏
  • 举报
回复
顶一个~
BearKin 2010-03-22
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 chen09 的回复:]
引用 11 楼 bearkin 的回复:

不运行下是很难理解啊..分治,解决,合并 恩恩..


的确,小时候刚学快速排序时,俺也是云里雾里的。
[/Quote]

小时候 你埋汰我。。。。
chen09 2010-03-22
  • 打赏
  • 举报
回复
[Quote=引用 11 楼 bearkin 的回复:]

不运行下是很难理解啊..分治,解决,合并 恩恩..
[/Quote]

的确,小时候刚学快速排序时,俺也是云里雾里的。
BearKin 2010-03-22
  • 打赏
  • 举报
回复
而且刚才发现了个CSDN的BUG 不过不知道是不是啊 通过全部动态我看到的chen09的信息 我点的明明是插入排序 他给我跳到分治算法了..
BearKin 2010-03-22
  • 打赏
  • 举报
回复
不运行下是很难理解啊..分治,解决,合并 恩恩..
chen09 2010-03-21
  • 打赏
  • 举报
回复
.
invail 2010-03-21
  • 打赏
  • 举报
回复
mark 新手啊 看的云里来雾里去的
chen09 2010-03-21
  • 打赏
  • 举报
回复
[Quote=引用 7 楼 longq121 的回复:]

高手就是高手,我都看不下去了。。。
真希望你们能够教导我。
[/Quote]

的确《算法导论》的进度真的是很快。现在才第一章,讲一些算法的概念而已。
就讲到分治,已经要用到递归的了。

如果觉得难,那就运行运行代码,跟踪一下,有助理解。
  • 打赏
  • 举报
回复
不卖关子了,代码源于《Java 数值方法》一书。

public class Test {

public static void main(String[] args) {
double n1 = pow(2, 6);
double n2 = pow(2, -6);
System.out.println(n1);
System.out.println(n2);
}

public static double pow(double base, int exponent) {
if (exponent < 0)
return 1 / pow(base, -exponent);
double power = 1;
while(exponent > 0) {
if((exponent & 1) == 1) {
power *= base;
}
base *= base;
exponent >>= 1;
}
return power;
}
}
chen09 2010-03-20
  • 打赏
  • 举报
回复
[Quote=引用 1 楼 bao110908 的回复:]
public static double pow(double base, int exponent);
计算一个 double 的 int 次方,以 O(lgN) 复杂度实现。
[/Quote]

这题目出得很好!大家可以想一想。

答案可以参考我一个月前的一个blog

ECPP——利用有限域上的椭圆曲线,精确判定素数的算法

那篇里面有个用java实现的模幂算法,复杂度为O(lgN)。把“模”相关的去掉,剩下的“幂”,也就是多少次方了。

那就是分治算法了,我把求椭圆曲线的次方的函数贴在下面,看看能否给大家以提示:


1. static public Point multiply(Point p, int x, Line l) throws EllipticException {
2. mapResult.clear();
3. // mapResult.put(1, p);
4. return multiply(p, x, l, mapResult);
5. }
6. static private Point multiply(Point p, int x, Line l, Map<Integer, Point> mapResult)
7. throws EllipticException {
8. if (x == 1)
9. return p;
10. if (mapResult.containsKey(x))
11. return mapResult.get(x);
12. int half = x / 2;
13. while (half > 0) {
14. try {
15. Point point = plus(multiply(p, half, l, mapResult), multiply(p, x - half, l,
16. mapResult), l);
17. mapResult.put(x, point);
18. return point;
19. } catch (EllipticException ee) {
20. half--;
21. }
22. }
23. throw new EllipticException("can't multiply " + p + "*" + x + " mod " + l.p);
24. }


请注意15行,典型的分治算法。
  • 打赏
  • 举报
回复
Arrays.copyOfRange 方法是 JDK 6 中新增的。
  • 打赏
  • 举报
回复
算法学到了,要会运用。

比如下面这些很不起眼,甚至可能是不屑一顾的:

public static double pow(double base, int exponent);
计算一个 double 的 int 次方,以 O(lgN) 复杂度实现。

辗转相除法求两个数的最大公约数大家肯定都会,那如何求三个数的最大公约数呢?
chen09 2010-03-20
  • 打赏
  • 举报
回复
自己顶一下
longq121 2010-03-20
  • 打赏
  • 举报
回复
高手就是高手,我都看不下去了。。。
真希望你们能够教导我。
chen09 2010-03-20
  • 打赏
  • 举报
回复
那俺就来解释一下吧。

if((exponent & 1) == 1)
可以这么想象,把exponent当成2进制数,那么这个判断就是判断当前最低位是不是1?如果是0,就不用累乘。

exponent >>= 1;
就是除以2了

62,621

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧