算法导论笔记第6章 堆和堆排序

堆排序结合了插入排序和归并排序的有点:它空间复杂度是O(1), 时间复杂度是O(nlgn).

要讲堆排序,先讲数据结构“堆”

堆:

  堆是用数组来存放一个完全二叉树的数据结构。假设数组名是A,树的根节点存放在A[1]。它的左孩子存放在A[2],右孩子存放在A[3]

  即:对于某个下标位i的节点,它的左孩子是A[2i],  右孩子是A[2i+1].  父节点是A[i/2]

PARENT(i)
   return ?i/2?

LEFT(i)
   return 2i

RIGHT(i)
   return 2i + 1

这个结论很简单也很好记忆,只是有一个小问题:算法导论的数组下标是从1开始的。而现实中大部分流行语言的数组下标却是从0开始的。这里有一个小技巧是将A[0]元素保留不使用,就可以规避掉这个问题。

不过在C++的标准库STL,或者是Golang的container/heap/heap.go里,并没有使用这个小技巧。公式调整为
PARENT(i)
   return ?(i-1)/2?

LEFT(i)
   return 2i + 1

RIGHT(i)
   return 2i + 2
不建议记这个公式,会造成混乱。只需要知道有这么一回事就行。

堆有两个应用:
  1. 堆排序时使用的是最大堆。
  2. 优先级队列使用的是最小堆。

堆的基本函数有:

max_heapify。它是保持最大堆性质的关键函数。运行时间是o(lgn);

build_max_heap 以线性时间运行,可以在无序的输入数组基础上构建出最大堆

heapsort 运行时间是O(nlgn), 可以对一个数组进行原地排序。

max_heap_insert, heap_extract_max, heap_increase_keyheap_maximum运行时间为O(lgn),可以让堆结构作为优先队列使用。

书上递归版本的max_heapify

#define PARENT(i) ((i)/2)
#define LEFT(i) (2*(i))
#define RIGHT(i) (2*(i)+1)
void max_heapify(int* A, int heap_size, int i) {
  int l = LEFT(i);
  int r = RIGHT(i);
  int largest = 0;
  if ((l < heap_size) && (A[l] > A[i])) {
    largest = l;
  }else {
    largest = i;
  }

  if ((r < heap_size) && A[r] > (A[largest])) {
    largest = r;
  }

  if (largest != i) {
    int temp = A[i];
    A[i] = A[largest];
    A[largest] = temp;
    max_heapify(A,heap_size,largest);
  }
}

在算法的每一步里,从元素A[i], A[LEFT(i)], A[RIGHT(i)]中找出最大的。并将其下标存放在largest中。如果A[i]是最大的,则以为跟的子树已经是最大堆,程序结束。

否则i的某个子节点中有最大元素,则交换 A[i]和A[largest],从而使i及其子女满足堆性质。下标largest的节点在交换后的值是A[i],以该节点为根的字数又有可能违反最大堆性质,因此要对子树递归调用max_heapify。递归调用的次数是树的高度。而完全二叉树的高度是lgn, 所以该算法的时间复杂度是O(lgn)



void build_max_heap(int *A, int heap_size) {
  int i;
  for (i = heap_size / 2; i >0; i ++) {
    max_heapify(A, heap_size, i);
  }
}

为了证明算法是正确的,我们用循环不变式来分析一下: 循环中不变的量是每一次迭代开始时,节点i+1, i+2,...,n都是一个最大堆的根。

  1. 初始化:在第一轮循环迭代之前,i=(n/2)。 节点(n/2)+1, (n/2) + 2...,n都是叶节点,也是平凡最大堆的根。
  2. 保持:节点i的子节点的编号均比i大。于是根据循环不变式这些子节点都是最大堆的根。着也是调用函数max_heapify以是节点i成为最大堆的根的前提条件。此外,max_heapify的调用保持了节点i+1,i+2,。。。。。n成为最大堆的根的性质。在循环中递减,记为下一次迭代重新建立了循环不变式。
  3. 终止:过程终止时,i=0根据循环不变式,我们知道节点1,2,...n中,每个都是最大堆的根,节点1就是一个最大堆的根。

原文地址:https://www.cnblogs.com/jackson-zhou/p/8337892.html

时间: 2024-11-09 18:22:22

算法导论笔记第6章 堆和堆排序的相关文章

算法导论笔记 第8章 线性时间排序

任何比较排序在最好情况下都要经过Ω(nlgn),即比较排序的下界为Ω(nlgn). 合并排序和堆排序都是渐进最优的. 要突破Ω(nlgn),就要进行非比较排序.计数排序.基数排序和桶排序都有非比较的一些操作来确定排序顺序,它们可以达到线性运行时间. 这三种排序都是以空间换时间.应用的不广,先不细看了. 原文地址:https://www.cnblogs.com/jackson-zhou/p/8419798.html

【算法导论】第五章

开始学习算法导论,看书+笔记+做课后题目+做OJ 计划是每天一个小时看书+写笔记 挑些课后题目来做,然后一道OJ ---------------------------------------- 今天看随机算法与概率分布,又复习了一下概率论 - - 讲到了两个随机算法:其中一个是随机分布优先度,然后按照优先度排列,能证明每一种排列的概率是1/n! ,符合随机性. 第二中是交换法,for i <- 1 to n swap (a[i] , a[random(i,n)]) 也证明了随机性. 这章的收获

MIT算法导论笔记

详细MIT算法导论笔记 (网络链接) 第一讲:课程简介及算法分析 第二讲:渐近符号.递归及解法

算法导论第十二章__二叉搜索数

package I第12章__二叉搜索树; //普通二叉树 public class BinaryTree<T> { // -----------------------数据结构--------------------------------- private int height = 0; private Node<T> rootNode; class Node<T> { T t; int key; Node left; Node right; public Node

散列表(算法导论笔记)

散列表 直接寻址表 一个数组T[0..m-1]中的每个位置分别对应全域U中的一个关键字,槽k指向集合中一个关键字为k的元素,如果该集合中没有关键字为k的元素,则T[k] = NIL 全域U={0,1,…,9}中的每个关键字都对应于表中的一个下标值,由实际关键字构成的集合K={2,3,5,8}决定表中的一些槽,这些槽包含指向元素的指针,而另一些槽包含NIL 直接寻址的技术缺点非常明显:如果全域U很大,则在一台标准的计算机可用内存容量中,要存储大小为|U|的一张表T也许不太实际,甚至是不可能的.还有

算法导论第十九章 斐波那契堆

<算法导论>第二版中在讨论斐波那契堆之前还讨论了二项堆,但是第三版中已经把这块的内容放到思考题中,究极原因我想大概是二项堆只是个引子,目的是为了引出斐波那契堆,便于理解,而且许多经典的算法实现都是基于斐波那契堆,譬如计算最小生成树问题和寻找单源最短路径问题等,此时再把二项堆单独作为一章来讲显然没有必要.类似的堆结构还有很多,如左倾堆,斜堆,二项堆等,下次我打算开一篇博客来记录下它们的异同点. 一.摊还分析(第十七章) 这些高级的数据结构的性能分析一般是基于一个技术——摊还分析,可以理解成一种时

算法导论第十五章动态规划

概述: 1.动态规划是通过组合子问题的解而解决原问题的. 2.动态规划适用于子问题不是独立的情况,也就是各子问题的包含公共的子子问题. 3.动态规划对每个子问题只求解一次,将其结果保存在一张表中. 4.动态规划的设计步骤:a.描述最优解的结构b.递归定义最优解的值c.按自底向上的方式计算最优觖的值d.由计算出的结构构造一个最优解 15.1钢条切割 钢条切割问题:给定定长的钢条和价格表,求切割方案,使得收益最大.如果n英寸的钢条的价格足够大,则不需要切割. 代码如下: //朴素递归求解钢条切割收益

《算法导论》第六章 练习题 Exercise

6.1-1 在高度为 h 的堆中,元素最多有 2h+1 - 1 个,最少有 2h  个.注意算法导论里的高度是指深度,从 0 开始而不是从 1 开始. 6.1-2 这很好想,但是不好证明. 由已知高度为 h 的堆,它的元素个数满足 2h   <= n <= 2h+1 - 1 ,解出 lg(n+1) - 1 <= h <= lgn ,但是它不够"合理",因为当 n = 2h+1-1 时,n 等于 2的幂 - 1,此时 lg(n+1) -1 = ?lgn? ,所以 

算法导论第十二章 二叉搜索树

一.二叉搜索树概览 二叉搜索树(又名二叉查找树.二叉排序树)是一种可提供良好搜寻效率的树形结构,支持动态集合操作,所谓动态集合操作,就是Search.Maximum.Minimum.Insert.Delete等操作,二叉搜索树可以保证这些操作在对数时间内完成.当然,在最坏情况下,即所有节点形成一种链式树结构,则需要O(n)时间.这就说明,针对这些动态集合操作,二叉搜索树还有改进的空间,即确保最坏情况下所有操作在对数时间内完成.这样的改进结构有AVL(Adelson-Velskii-Landis)