跳至主要内容

算法 | 堆排序(heap sort)

   
注:本文只讨论最大堆排序,最小堆排序同理



一 . 堆
 
堆(heap)可以定义为一棵完全二叉树,并且这棵树的每一个节点都大于或等于它的子女节点。

 最大堆长这样子:




堆的特性

  1. 一棵n节点的完全二叉树,其高度为$$\lfloor \log_{2}n\rfloor$$
  2. 堆的每一个节点都大于等于它的子女节点。
  3. 可以用数组来实现堆。留空H[0],然后在H[1]到H[n]中存在堆元素。留空H[0]的目的是为了让之后的堆排序更加的方便。
  4. 所有父母节点的键会在数组的前$$\lfloor n/2 \rfloor$$ 个位置中,而叶子节点会占据后面的$$\lceil n/2 \rceil$$个位置。
  5. 在数组中,对于每一个位于父母位置i的键来说,它的子女会位于2i和2i+1
  6. 对于一个位于位置i的键来说它的父母将会位于$$\lfloor i/2 \rfloor$$

如何生成堆

自底向上堆构造(bottom-up heap construction):

对每个父母节点进行父母优势检查:
  1. 从最后的父母节点开始,到根为止,检查这些节点的键是否满足父母优势要求。
  2. 如果该节点不满足,就把该节点的键K与它子女的最大键进行交换。
  3. 然后再检查新位置上,K是不是满足父母的优势要求。
效率:
最坏情况下,每个位于树第i层的节点都要移动到树的最底层h,因为移动一层需要两次键值比较,因此移动到h层需要 2(h-i) 次键值比较,所以总键值比较次数为:




自顶向下堆构造(top-down heap construction):

把新的键连续插入到已经预先构造好的堆:

  1. 把包含k附加到堆的最后一个叶节点的后面。
  2. 拿k与它的父母节点作比较,如果k刚好小于它的父母节点,则算法停止。
  3. 否则,交换k和其父母节点的位置
  4. 重复2,3步骤直到k不大于它的父母,或者到达了根为止。

效率:
因为包含n个节点的树的高度大约是 log n ,因此每次插入的时间效率属于 O(log n)
总时间效率叠加即可。




二. 堆排序

堆排序的思路很简单:
  1. 把数组构造成堆,此时根是最大值。
  2. 把根从堆中“移除”,即把根节点和最后一个叶节点对换,然后把换到第一位的叶节点用堆排序安排至恰当的位置。
  3. 重复2步骤直到堆中的元素全部 "删除" 完为止。

堆排序的完整代码:



三. 堆排序的效率

  1. 第一阶段,用自底向上的方式进行堆构造,时间效率属于 O(n)
  2. 第二阶段,每次消去根节点,又重新构造堆,最差情况下需要的键值比较次数记为 C(n). 有下面不等式:

$$C(n)\leq2\lfloor log_{2}(n-1)\rfloor+\lfloor log_{2}(n-2)\rfloor+……+\lfloor log_{2}1\rfloor
\leq 2\sum_{i=1}^{n-1}log_{2}i \leq 2\sum_{i=1}^{n-1}log_{2}(n-1) = 2(n-1)log_{2}(n-1) \leq 2nlog_{2}n$$

        因此第二阶段中C(n)属于 O(nlog n)
        因此总的时间效率为 O(nlog n)

实际上,堆排序的最差效率和平均效率都属于O(nlog n),因此堆排序的时间效率和归并排序的时间效率属于同一类。
但是!堆排序是原地排序,不需要额外的存储空间,合并排序需要消耗至少一个数组的空间。

评论

此博客中的热门博文

Javascript | 从swap函数思考js中的参数传递

事情是这样的,有一天,林逍遥同学在学js的时候,想要用js写一个swap函数,她是这么写的: 但是结果输出来之后,她发现事情并没有这么简单:a还是等于1,b还是等于2. 她赶紧翻了一下《Javascript高级程序设计》,发现里面有一段话: ECMAScript中所有函数的参数都是按值传递的。  传递基本类型的值时,被传递是值会被复制给一个局部变量(即命名参数)。 传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反应在函数的外部。  原来如此,已知a,b是两个基本类型的变量,当把a,b传入swap函数的时候,会生成两个新的局部变量,分别存储a,b的值,也相当于它们的副本,虽然值跟a,b是一样的,但在内存中的地址却是不同的,所以在swap函数里面做的改变并不会影响a,b的值,也就谈不上交换了。 综上所述,不可能写一个swap函数来交换两个基本类型的值。 不过对于引用类型的值(数组,对象),却是可以通过函数改变它们的值的。 当把person复制给xy时,同样也会把存储在person中的值复制一份给xy,不过,person存储的值并不是一个对象,而是这个对象的地址(也就是指针),因此复制的其实是地址值。复制操作结束后,person和xy将指向内存中的同一个地址。 所以在xy上做的改变。也会反映到person上面。 同样地,在传递参数的时候,相当于临时创建了一个局部变量,把person的地址也复制给了这个局部变量,因此在函数内部做的改变会反应到person上。 但是,在函数中对参数赋新值,不会影响到函数外部。 给参数赋值一个新的对象,相当于把参数指向另一块地址,所以不会对person产生影响。 总结:js中参数的传递都是按值传递,不过基本类型变量传递的是变量值,引用类型变量传递的是地址。 题外话:如何用一句话代码交换a,b的值? b={x:a,y:(a=b)}.x; js文化真是博大精深哪!

Javascript | Javascript中的this关键字

先来看一段代码: 第一个问题,为什么this.arg是undefined? 因为this指向的是调用该函数的对象,而不是函数。无论在window或者对象o中,都没有定义arg这个变量。 在严格模式下运行这个函数,会 出现Uncaught TypeError: Cannot read property 'arg' of undefined错误。  第二个问题,为什么第一个this是指向window,第二个是Object?我们知道js中,函数调用有4种方式: fn()  obj.fn()  fn.call(obj)和foo.apply(obj)  new  fn()  fn(a) 相当于 fn.call(undefined)  本来在这种调用模式下,打印出的this应该是undefined才对,但是在浏览器中有一条规定: 如果你传的 context 不是一个对象,那么 window 对象就是默认的 context。   (这条规则在 Node.js 和 strict 模式下会稍微不一样,不过那不是我们现在要讨论的) 所以上面的例子中第一个this打印出来是window。 obj.fn(a) 相当于 fn.call(obj) 因此在这种情况下,fn的this参数绑定的是对象obj。 因此第二个this打印出来是Object,即对象o。 fn.call(obj) 不用说,绑定的就是obj。 new fn()  绑定的就是新创建的对象。 理解这四种情况之后,就不会对this感到迷茫了。 当你不知道this指向哪个对象时,就把函数转化为function.call()的方式。 最后再来一个例子: 知道为什么this指向window,为什么getAge()返回的是NaN了吧?