锁定老帖子 主题:《算法导论》读书笔记5(线性时间排序)
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2010-01-06
最后修改:2010-01-07
算法导论的第八章是:线性时间排序
我们共分析过四个排序算法:插入排序,归并排序,堆排序以及快递排序,它们有一个共同点,就是都是基于比较的,都属于比较排序算法
通过决策树模型,可以知道比较排序算法的最坏情况下的下界是:nlog(n)
上面的话有点拗口,下面进行简单的分析
决策树,是一棵满二叉树,排序的过程就像从根走到叶的过程, 因为不是小于等于就是大于嘛,所以不是向左走,就是向右走,直到走到叶节点,排序完成。 那么比较的次数,在最坏的情况下,不会多于树的高度。
n的元素的排序,共有 n! 种 情况, 它们都会出现在决策树的叶子节点上 高度为 h 的二叉对, 叶子数目不会多于 2^h 个
因此 n! <= 2^h ----> h > lg(n!) 后者 相当于 nlg(n)
也就是说不管怎么样的决策树,它的高度不会小于 nlg(n)
而一个排序算法,就对应于一棵决策树, 因此在最坏的情况下,它的比较次数 h , 而不管算法设计多么棒 h >= lg(n!)
所以堆排序和合并排序都是渐近最优的。
快速排序在平均情况下,也是渐近最优的。
//-----
排序也可以不基于比较,因此就没有上述限制
计数排序
读了这一节,让我想到了《编程珠玑》(不是《编程珠玑2》) 中的一个问题, 大概是这样:
系统要对一个文件中的记录进行排序,排序后,输出到另一个文件 。 文件的每个记录都是一个小于7位的数字(背景是美国的电话号码), 记录不重复,记录数目非常多,大概也是近千万。
上面这个过程是一个大系统的一小部分,要求在最好十几秒内就要完成这个过程, 并且大概只有1M的内存空间可以使用。
首先, 1千万个数字是放不进1M的, 1M只能放一百万个数字, 所以不能使用普通的内存排序。 可以采用文件归并排序。 但速度肯定达不到要求。
但是1千万个(bit), 差不多是1M多一点, 如果用一个位表示一个数字, 那就有望在1M多一点的内在中保存1千万个数字。
该问题最后的解决方案大概是这样的(原文是伪代码,这里使用java实现,并且可以运行,因为可以运行的代码让程序员更兴奋)
等等,我们先产生这样的文件,否则又对什么进行操作呢?
public void prepareData() throws IOException { long begin = Calendar.getInstance().getTimeInMillis(); int n = 10000000; // 7个0; int[] data = new int[n]; // 这大概要用 40M内存, 在我们的机子上米关系 for (int i = 0; i < n; i++) { data[i] = i; } shuffle(data); // 打乱它 Writer writer = new BufferedWriter(new FileWriter("data.txt")); try { // 选其中的800万个输出 for (int i = 0; i < 8000000; i++) { writer.write("" + data[i] + "\n"); } } finally { writer.close(); } long end = Calendar.getInstance().getTimeInMillis(); System.out.printf("used: %f(s)", (end - begin) / 1000.0); }
我们首先按顺序产生一千万个数字,然后打乱它,最后输出到文件data.txt
其中,shuffle 在算法笔记2中有实现过,用的是ruby, 现在我们用java
private void shuffle(int[] data) { Random rand = new Random(); int n = data.length; for (int i = 0; i < n; i++) { int j = rand.nextInt(n); //swap int t = data[i]; data[i] = data[j]; data[j] = t; } }
在我的机子上大概用了9.3s时间, 产生了一个60.1M的文件。
下面就是排序并且输出到一个文件啦:)
public void hehe() throws IOException { long begin = Calendar.getInstance().getTimeInMillis(); BitSet bits = new BitSet(); BufferedReader reader = new BufferedReader(new FileReader("data.txt")); try { String s = null; while ((s = reader.readLine()) != null) { int k = Integer.parseInt(s); bits.set(k); } } finally { reader.close(); } Writer writer = new BufferedWriter(new FileWriter("out.txt")); try { for (int i = 0, n = bits.length(); i < n; i++) { if (bits.get(i)) { writer.write("" + i + "\n"); } } } finally { writer.close(); } long end = Calendar.getInstance().getTimeInMillis(); System.out.printf("used: %f(s)", (end - begin) / 1000.0); }
这在我的机子上用了11.2s, 可以说是读写有多快,它就有多快。
在上面的例子,我看到了计数排序的影子, 只不过这个 count = 1,所以简单地用位来实现
计数排序是这样的:
int[] countSort(int[] a, int k) { int[] c = new int[k]; // 准备好,对所有k个数字进行计数 for (int i = 0; i < c.length; i++) { c[i] = 0; } for (int i = 0, n = a.length; i < n; i++) { // 对数组中的数进行计数 c[a[i]]++; } // 到这里为止 c[i] 就是 i 这个数字出现的个数 for (int i = 1; i < c.length; i++) { c[i] = c[i] + c[i - 1]; } // 到这里为止 c[i] 就是 < i 数的个数 int[] b = new int[a.length]; for (int i = a.length - 1; i >= 0; i--) { b[c[a[i]] - 1] = a[i]; // 比a[i] 小的数有 c[a[i]] 个, 我们把它放到相应位置 c[a[i]]--; // 计数减一, 这样轮到下一次时,就放到前面 } return b; }
相应的测试代码是这样的:
@Test public void testCountSort() { Random rand = new Random(); int n = 5000000; int k = 10; int[] a = new int[n]; for (int i = 0; i < n; i++) { a[i] = rand.nextInt(k); } assertFalse(isSorted(a)); int[] b = countSort(a, k); assertTrue(isSorted(b)); }
从代码中可以看出,计数排序的复杂次为 ⊙(n), 是稳定排序,要使用同样多的内存空间
趁热打铁, 基数排序
直接上代码
int[] radixSort(int[] a, int d) { // 数据a中的数字不会长于d位 for (int i = 0; i < d; i++) { a = stableSort(a, i); // 从最右边开始,一位一位排 } return a; }
stableSort 怎么样呢? 我们使用上面的计数排序, 稍微进行小调整
int[] stableSort(int[] a, int k) { // 对a数组中的数的第k位进行计数排序 int[] c = new int[10]; // 数在[0, 10] for (int i = 0; i < c.length; ++i) { c[i] = 0; } for (int i = 0, n = a.length; i < n; i++) { int d = getIndex(a[i], k); c[d]++; // 统计第k位数 } for (int i = 1; i < c.length; i++) { c[i] = c[i] + c[i - 1]; } int[] b = new int[a.length]; for (int i = a.length - 1; i >= 0; i--) { int d = getIndex(a[i], k); b[c[d] - 1] = a[i]; c[d]--; } return b; }
getIndext很简单, 大学C语言经常做的,截取指定位数值
int getIndex(int a, int k) { while (a != 0 && k != 0) { a /= 10; k--; } return a % 10; }
最后还是测试,Test Test, 没有Test就是nothing!!
@Test public void testRadixSort() { Random rand = new Random(); int[] a = new int[100000]; // 一百万个 for (int i = 0; i < a.length; i++) { a[i] = rand.nextInt(10000000); // 不大于7位数 } int[] b = radixSort(a, 7); assertTrue(isSorted(b)); }
因为 基数排序 要经过d (数据长度)次排序, 每次使用计数排序, 计数排序的复杂度为 ⊙(n), d 相当于常量,和N无关, 所以基数排序也是 ⊙(n)
继续打铁, 桶排序
把一堆数据快速地扔进有序桶中,再对桶中的数据进行排序...
float[] bucketSort(float a[]) { // 0 < a[i] < 1,且分布较均匀 int n = a.length; List[] lists = new List[n]; // 准备好n个桶 for (int i = 0; i < n; i++) { lists[i] = new ArrayList(); } for (int i = 0; i < n; i++) { // 把每个数据放到相应的桶 lists[(int)(n * a[i])].add(a[i]); } for (int i = 0; i < n; i++) { // 现在每个桶中平均有一个数据 insertionSort(lists[i]); // 对它进行一次插入排序 } float b[] = new float[n]; // 输出 int k = 0; for (int i = 0; i < n; i++) { List list = lists[i]; for (int j = 0; j < list.size(); j++) { b[k++] = (Float) list.get(j); } } return b; }
void insertionSort(List list) { for (int i = 1; i < list.size(); i++) { float t = (Float) list.get(i); int j = i - 1; while (j >= 0 && (Float)list.get(j) > t) { list.set(j + 1, list.get(j)); j--; } list.set(j + 1, t); } }
最后还是测试:
@Test public void testBucketSort() { Random rand = new Random(); float[] a = new float[1000]; for (int i = 0; i < a.length; i++) { a[i] = rand.nextFloat(); } float[] b = bucketSort(a); assertTrue(isSorted(b)); }
对了,这里还有一个isSorted
原先的方法是针对int[] 的,现在是float[], 所以要重写一个, 仅仅这个参数不一样
问: 在java中这样的可以泛形吗?
public static boolean isSorted(int[] ary) { for (int i = 0; i < ary.length - 1; i++) { if (ary[i] > ary[i + 1]) { return false; } } return true; } public static boolean isSorted(float[] ary) { for (int i = 0; i < ary.length - 1; i++) { if (ary[i] > ary[i + 1]) { return false; } } return true; }
对java中的泛形仅仅停留在collection的使用上
对于c++,泛形的作用主要在于code generation, 而java中的擦除法泛形更多的功能是type check。
对java的泛形没有多作研究,不知道是否这样。
其实程序员不是很需要type check的,因为类型错误肯定会在test中给检测出来,而且我们也是思考着写程序的,几乎很少把类型给弄错了。当然强类型在编译优化,效率上肯定有一定的用处。但是我需要敲更少的键盘做更多的事。
啊呀,说算法,扯远了。
最后还有桶排序的复杂度没有说明。
桶排序的其它几个循环很清楚,都是线性的,主要是看下面这个循环:
for (int i = 0; i < n; i++) { // 现在每个桶中平均有一个数据 insertionSort(lists[i]); // 对它进行一次插入排序 }
虽然 insertionSort 是平方复杂度的, 但是 lists[i] 的数据是不会随着 n 的增长而增长的。 如果有n桶,桶中平均只有一个数。
所以上面这个循环也是线性复杂度的
所以桶排序的复杂度也是线性的
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 2233 次