`
bencode
  • 浏览: 109221 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

《算法导论》读书笔记3(堆排序)

阅读更多

第二部分,排序和顺序统计学

 

在笔记一中, 我们实现了两个排序算法:插入排序归并排序

 

第六章是堆排序。现在就是第六章。

 

这里的堆,不是堆栈的堆,那个一般是指一块动态分配的内存:)

 

这里的堆是一个数据结构,它是一个二叉树(二叉堆),可以存在数组中

 

像这样:

 

           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)时间内完成...

    MIT 算法导论 课堂笔记

    排序是将一组数据按特定顺序排列的过程,常见的排序算法有冒泡排序、插入排序、选择排序、快速排序、归并排序和堆排序等。搜索算法则涉及在数据结构中查找特定元素,如线性搜索、二分搜索以及哈希表搜索。 四、图...

    算法导论试题及答案

    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算法等,用于解决最短路径、最小生成树...

    [麻省理工学院-算法导论].Introduction.to.Algorithms.-.Lecture.Notes 算法导论-课堂笔记 讲义

    以上只是《算法导论》部分内容的概述,实际的课堂笔记和讲义会包含更丰富的例子、习题和解析,帮助读者深入理解和掌握这些概念。通过学习这些内容,可以提升对算法和数据结构的理解,为解决实际问题打下坚实基础。

    算法导论系列读书笔记之七

    此外,书中还可能讨论其他排序算法,如归并排序、堆排序等,以及它们与快速排序的对比和选择。 通过阅读和理解这一章的内容,读者不仅可以掌握快速排序这一实用算法,还能提升对分治策略的理解,这对于解决更复杂的...

    算法导论系列读书笔记之九

    在“算法导论系列读书笔记之九”中,我们聚焦于其中的两个重要概念:中位数和顺序统计学。 中位数是统计学中的一个关键概念,它是将一组数据从小到大排列后位于中间位置的数值。对于奇数个数据,中位数是中间的那个...

    算法导论教师手册

    - **讲座笔记**:分别介绍了几种经典的排序算法(如堆排序、快速排序、线性时间排序等)和常用的数据结构(如哈希表、二叉搜索树、红黑树等),并讨论了它们的应用场景和优缺点。 - **解决方案**:对每种算法和数据...

    MIT公开课——算法导论教材

    排序算法是处理数据的关键,包括冒泡排序、选择排序、插入排序、快速排序、归并排序以及堆排序等多种方法。这些排序算法各有优劣,适用于不同的数据结构和场景,理解它们的工作原理和性能分析对于优化代码和提高程序...

    算法导论课后习题与思考题答案合集

    根据提供的文件信息,本文将围绕《算法导论》一书的课后习题与思考题答案合集的知识点展开详细说明。首先,《算法导论》是计算机科学领域内一本极具权威性和实用性的教科书,由Thomas H. Cormen、Charles E. ...

Global site tag (gtag.js) - Google Analytics