第二部分,排序和顺序统计学
在笔记一中, 我们实现了两个排序算法:插入排序和归并排序。
第六章是堆排序。现在就是第六章。
这里的堆,不是堆栈的堆,那个一般是指一块动态分配的内存:)
这里的堆是一个数据结构,它是一个二叉树(二叉堆),可以存在数组中
像这样:
16
/ \
14 10
/ \ / \
8 7 9 3 -> 16 14 10 8 7 9 3 2 4 1
/ \ /
2 4 1
根要比左右两个结点要大,并且左右子树也是一个堆--> 是一个递归定义
可以用这样的代码来表示上述定义
boolean isHeap(int[] a, int i) {
int n = a.length;
if (i >= n) {
return true;
}
int left = 2 * i + 1; // 左节点
int right = 2 * i + 2; // 右节点
if (left < n && a[left] > a[i]) { // 比左右都要大,否则不行
return false;
}
if (right < n && a[right] > a[i]) {
return false;
}
return isHeap(a, left) && isHeap(a, right); // 左右子树也是堆
}
然后写一个测试运行一下:
@Test
public void testIsHeap() {
int[] a = { 16, 14, 10, 8, 7, 9, 3, 2, 4, 1 }; // 就是上述图示数据
assertTrue(isHeap(a, 0));
}
显然, 根节点就是最大的。那么堆排序就是基于这个特性:每次选出"最大的", 再建,再选, 直到排好:)
算法像这样:
void heapSort(int[] a) {
buildHeap(a); // 首先,建一个堆, 此时a[0] 就是最大的元素
for (int i = a.length - 1; i > 0; i--) { // 每次"选出"最大的元素, 共需要进行 n - 1 次
swap(a, 0, i); // 在这之后, i 以及后面的元素都是有序的
heapify(a, 0, i - 1); // 根元素由于被交换过,所以需要进行一次“调整”,
// 让他再变成堆
}
}
算法很简单, 但是编译还是不通过的:) 因为有三个方法还没有完成。
swap:交换数组元素, 这个很容易,属于"模板代码" (PS:晚上喝了好些杨梅酒, 希望不要错)
void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
再来 heapify, 这个比较容易些:)
// 此时 i 的左右子树都是堆 (j 后面的元素不管,不属于堆), 我们要把他调整成堆
void heapify(int[] a, int i, int j) {
int left = i * 2 + 1;
int right = i * 2 + 2;
if (left > j) { // 没有左子树? 那就好了
return;
}
int large = left; // large 是大的这个
if (right <= j && a[left] < a[right]) {
large = right;
}
if (a[i] < a[large]) { // 根元素比 large 小? 交换根和 large
swap(a, i, large);
heapify(a, large, j); 此时 large 树 又可能不是堆了, 那就继续努力:)
}
}
建堆是这样滴
void buildHeap(int[] a) {
int n = a.length;
for (int i = n / 2 - 1; i >= 0; --i) { // n / 2 - 1, 就是最后一个有子节点的元素
heapify(a, i, n - 1); // 偶们从这开始,不断调整,直到整个数组变成堆
}
}
现在完工了, 然后再写一个测试代码测试它---证明比较累, 无情的测试相对容易些
可以测试空数组,一个元素,两个元素, 一万个元素的随机数组, 测试一万次。 都green, 就米问题了:)
@Test
public void testHeapSort() {
for (int i = 0; i < 10000; i++) {
int[] array = genRandAry(i);
heapSort(array);
assertTrue(isSorted(array));
}
}
以上 genRandAry 以及 isSorted , 见“算法笔记1”
程序要跑起来,算法要写出来。。否则变成纯思维,很纠结
照惯例,要看看算法的复杂度。对于排序而言, 就是看看比较次数,因为排序的主要操作就是比较(这里的排序是基于比较的)
下面,我们要修改heapSort, 现在不是排序,而是求heapSort的比较次数
由于heapify是基本操作, 先来分析它:)
目标是,要把下面方法重构成 计算比较次数
void heapify(int[] a, int i, int j) {
int left = i * 2 + 1;
int right = i * 2 + 2;
if (left > j) {
return;
}
int large = left;
if (right <= j && a[left] < a[right]) { // 这里是一次元素比较
large = right;
}
if (a[i] < a[large]) { // 这里又是一次比较
swap(a, i, large); // 这个不管它
heapify(a, large, j); // 这个? 递归调用而已
}
}
所以, 上面的方法可以重构成这样
int heapify2(int[] a, int i, int j) { // modify the return type to int
int left = i * 2 + 1;
int right = i * 2 + 2;
if (left > j) {
return 0; // 没有子树,比较次数为0
}
int times = 2; // added
int large = left;
if (right <= j && a[left] < a[right]) {
large = right;
}
if (a[i] < a[large]) {
swap(a, i, large);
times += heapify2(a, large, j); // modified
}
return times; // added
}
下面要删掉无关的代码, large 就是 left(因为和排序没关系) ,left 就是 i * 2 + 1
所以:
int heapify2(int[] a, int i, int j) {
// int left = i * 2 + 1;
// int right = i * 2 + 2;
if (i * 2 + 1 > j) {
return 0;
}
int times = 2;
int large = i * 2 + 1;
// if (right <= j && a[left] < a[right]) {
// large = right;
// }
// if (a[i] < a[large]) {
// swap(a, i, large);
times += heapify2(a, large, j);
// }
return times; // added
}
}
整理一下:
int heapify2(int[] a, int i, int j) {
if (i * 2 + 1 > j) {
return 0;
}
return 2 + heapify2(a, i * 2 + 1, j);
}
和 a 无关
int heapify2(int i, int j) {
if (i * 2 + 1 > j) {
return 0;
}
return 2 + heapify2(i * 2 + 1, j);
}
比较容易看出,以上heapify2可以优化为迭代版本, 预示着heapify 代码也可以使用迭代 代替 递归。
如果对n个数进行调整,此时 i = 0, j = n
int heapifyN(int n) {
return heapify2(0, n);
}
在算法笔记二中,我们有一个工具可以用来看看复杂度,现在试一下:)
往charts目录中加一个文件: HeapifyN.rb
module Charts
class HeapifyN
def name
'heapfy(n)'
end
def fun(x)
return nil unless x >= 0
x = x.to_i
heapfy(0, x);
end
def heapfy(i, j)
if i * 2 + 1 > j
return 0
end
return 2 + heapfy(i * 2 + 1, j)
end
end
end
运行,就能绘出如下图
绿的就是 heapify(n) , 蓝的是 log(x) 黄的是 log10(x)
所以 它的复杂度和 log(n) 相当的
下面是buildHeap, 它的代码是这样的:
void buildHeap(int[] a) {
int n = a.length;
for (int i = n / 2 - 1; i >= 0; --i) {
heapify(a, i, n - 1);
}
}
重构成求比较次数,差不多是这样:
int buildHeap2(int n) {
int times = 0;
for (int i = 0; i < n / 2; i++) {
times += heapify2(i, n - 1);
}
return times;
}
OK, 这个复杂度不会大于 nlog(n), 因为heapify2 是 log(n), 循环了 n / 2 次, 但是 是否为 nlog(n)呢?(因为有一半元素不需要 heaify, 而且每次heapify 的规模也不一样)
我们再来画画图:)
蓝线是 nlog(n), 黑线是 buildHeap。
书中经过数学推导,证明其复杂度是 O(n), 图中黑线很“直”,所以应该是线性的
还有最后一个heapSort, 这个是多少呢? 不过肯定比 buildHeap 要大,因为一开始就要buildHeap.
然后还要进行 n-1次heapify。
还是再画图吧:)
其实画buildHeap时就比较累了, 当横坐标为2000时,等了好几秒。 现在 heapSort 就更慢了, 横坐标为1000时也等了好久:) 不过可以对计算结果进行适当的缓存,就可很大提高计算的速度:)
- 大小: 4.6 KB
- 大小: 6.2 KB
- 大小: 8.5 KB
分享到:
相关推荐
《算法导论》系列读书笔记之六主要涵盖了优先级队列、堆排序以及大根堆和最大堆等重要概念。这些知识点在计算机科学与技术领域,尤其是数据结构和算法分析中占据着核心地位。下面将对这些内容进行深入的探讨。 ...
作为“算法导论系列读书笔记之三”,本文将主要探讨第三章的内容,这一章通常聚焦于排序与选择算法,这些是数据处理的基础,对理解和优化程序性能至关重要。 在第一章和第二章中,我们可能已经接触到了基本的数据...
通过阅读和理解《算法导论》的这些章节,我们可以掌握基础的算法思想,提升问题解决能力,并为后续深入学习打下坚实基础。学习和实践这些算法,不仅可以提升编程技能,也有助于培养分析和解决复杂问题的能力。
2. **排序与搜索**:排序算法如冒泡排序、插入排序、选择排序、快速排序、归并排序和堆排序,它们各有优劣,适用于不同的场景。搜索算法包括线性搜索和二分搜索,其中二分搜索在有序数组中的效率更高。 3. **图算法...
同时,书中也讲解了更高级的排序算法,如堆排序和基数排序,以及各种排序算法的复杂性分析。 其次,图算法在实际问题中有着广泛的应用,如最短路径问题(Dijkstra算法和Floyd-Warshall算法)、最小生成树(Prim算法...
这份"算法导论授课教案学习笔记"是针对该书的深入学习资源,包括了教学教案、课后作业及解答,对于正在学习算法的学生来说,无疑是一份极其宝贵的参考资料。 教程部分可能涵盖以下知识点: 1. **算法基础**:介绍...
《麻省理工算法导论读书笔记》是一份深入学习算法理论和实践的宝贵资源,源自世界顶级学府麻省理工学院的教学资料。这份笔记涵盖了算法分析、设计策略、数据结构等多个核心主题,对于想要深入了解算法的朋友们极具...
《算法导论》详细讲解了各种排序算法,包括冒泡排序、插入排序、选择排序、归并排序、快速排序和堆排序。其中,快速排序以其高效性和通用性而著名,而堆排序则利用了二叉堆的数据结构,能够在O(n log n)时间内完成...
排序是将一组数据按特定顺序排列的过程,常见的排序算法有冒泡排序、插入排序、选择排序、快速排序、归并排序和堆排序等。搜索算法则涉及在数据结构中查找特定元素,如线性搜索、二分搜索以及哈希表搜索。 四、图...
1. **排序算法**:包括冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序等,以及它们的时间复杂度和适用场景。 2. **搜索算法**:二分查找、广度优先搜索(BFS)、深度优先搜索(DFS)等,以及在图和树...
《麻省理工算法导论全套笔记》是一份深入学习算法的宝贵资料,源自世界顶级学府麻省理工学院(MIT)的课程。这份笔记涵盖了广泛的算法主题,旨在帮助读者掌握算法设计、分析以及实现的核心概念。以下是根据提供的...
文档《算法导论英文笔记和答案(较全)》是由Thomas H. Cormen、Clara Lee和Erica Lin编写的,它是与《算法导论》(第二版)一书配套的辅助材料。本书的英文版由The MIT Press和McGraw-Hill Higher Education联合...
### 算法导论的笔记与答案 #### 一、引言 《算法导论》是一本在计算机科学领域非常著名的教材,由Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest和Clifford Stein共同编写。这本书广泛地被用作大学本科...
1. **排序与搜索算法**:如快速排序、归并排序、堆排序、二分查找等,这些都是基础且实用的算法。 2. **图算法**:包括Dijkstra算法、Floyd-Warshall算法、Prim算法和Kruskal算法等,用于解决最短路径、最小生成树...
以上只是《算法导论》部分内容的概述,实际的课堂笔记和讲义会包含更丰富的例子、习题和解析,帮助读者深入理解和掌握这些概念。通过学习这些内容,可以提升对算法和数据结构的理解,为解决实际问题打下坚实基础。
此外,书中还可能讨论其他排序算法,如归并排序、堆排序等,以及它们与快速排序的对比和选择。 通过阅读和理解这一章的内容,读者不仅可以掌握快速排序这一实用算法,还能提升对分治策略的理解,这对于解决更复杂的...
在“算法导论系列读书笔记之九”中,我们聚焦于其中的两个重要概念:中位数和顺序统计学。 中位数是统计学中的一个关键概念,它是将一组数据从小到大排列后位于中间位置的数值。对于奇数个数据,中位数是中间的那个...
- **讲座笔记**:分别介绍了几种经典的排序算法(如堆排序、快速排序、线性时间排序等)和常用的数据结构(如哈希表、二叉搜索树、红黑树等),并讨论了它们的应用场景和优缺点。 - **解决方案**:对每种算法和数据...
排序算法是处理数据的关键,包括冒泡排序、选择排序、插入排序、快速排序、归并排序以及堆排序等多种方法。这些排序算法各有优劣,适用于不同的数据结构和场景,理解它们的工作原理和性能分析对于优化代码和提高程序...
根据提供的文件信息,本文将围绕《算法导论》一书的课后习题与思考题答案合集的知识点展开详细说明。首先,《算法导论》是计算机科学领域内一本极具权威性和实用性的教科书,由Thomas H. Cormen、Charles E. ...