您的位置:首页 > 其它

白话算法(4) 谁升起,谁就是太阳

2010-04-19 08:33 337 查看
  正如优秀文学的秘密并不会因为《哈姆雷特》或《曼斯菲尔德庄园》的存在而一劳永逸地昭然若揭,建筑大师奥托·瓦格纳或西古德·列维伦茨的作品也丝毫无力减少低劣建筑的增生。艺术的杰作至今仍像是凭运气偶然产生的,而艺术家仍然像成功点燃了一把火的洞穴人,并没有搞清楚他们是如何做到的,更遑论将他们取得的成就的原理讲与他人知晓了。艺术天分就像是一场绝妙的焰火,瞬间穿透漆黑的暗夜,使观者油然而生敬畏之情,可转眼间就烟消云散,只留下无尽的黑暗与向往。
——阿兰·德波顿 《幸福的建筑》

选择排序


  选择排序的思路是,每次从输入的数组s中拿出一个最小的元素x,放到数组L的尾部,这样一直迭代下去,直到把s掏空时,L就是排好序的s。

 s)
{
// 有序数组 L = s[0..i]
for (int i = 0]选择排序的性能[/b]

  选择排序的最坏情况时间代价为Θ(n2)。这是因为R是无序的,所以要找到最小的元素就必须把R中所有元素都遍历一遍。要想提高性能,自然是要把R先搞成某种有序(这个“有序”指的不是“有顺序”而是指“有一定规律”)的形式,籍此来提高查找的性能。堆排序算法就是先把那个无序的系统搞成一种叫做的有序形式,并且每次拿出一个最小(或最大)元素后,再次保持它的堆的性质。

堆排序

  是一棵二叉树,并且每个节点都大于等于该节点的子节点(称为大根堆),或者每个节点都小于等于该节点的子节点(称为小根堆)。聪明的你一定想到了,对于大根堆来说,s[0]就是最大的元素,每次输出它就行了。
  可以用数组来表现一颗二叉树。



通过对数组的下标的计算,就可以得到一个节点的父节点、左孩子、右孩子的下标。

// 返回下标为nodeIndex的节点的父节点的下标
static int Parent(int nodeIndex)
{
return (nodeIndex-1) / 2;
}

// 返回下标为nodeIndex的节点的左孩子节点的下标
static int Left(int nodeIndex)
{
return (nodeIndex+1) * 2 - 1;
}

// 返回下标为nodeIndex的节点的右孩子节点的下标
static int Right(int nodeIndex)
{
return (nodeIndex + 1) * 2;
}


注:在《算法导论》里,数组的下界为1。但是在.Net里,数组的默认下界为0。虽然也可以创建下界为1的数组,但是效率会大打折扣,所以上面的计算树节点下标的方法还是基于下界为0的数组。

建堆

  第一步,自然是要把数组处理成堆。我们无法一下子做的这点,所以和插入排序一样,要使用增量的方法。先处理最后一个非叶子节点为根的子树——它最多只有3个节点——可以很容易让它符合堆的性质,然后依次处理倒数第2个非叶子节点为根的子树、倒数第3个非叶子节点为根的子树……麻烦的地方在于,这样下去总会遇到这样的情况:左孩子和右孩子可能不再是叶子节点,而是一棵树,所以我们的处理方法不能只考虑子节点是叶子节点的情况。虽然数据结构由数组变成了更为复杂的树,但是就像前面几篇曾经反复讨论过的,计算机之所以能够以有限的代码处理不确定数量的输入,一个常用的思路就是,将输入分解成具有相似结构和特性(或者说规律)的子集,增量地处理。Heapify()很好地诠释了什么叫“具有相似的结构和特性”——它每次面临的情况都是“左右孩子已经是堆,只有根节点可能小于它的左右孩子节点”。

 s)
{
// 从最后一个非叶子节点开始,直到根节点,调用Heapify()
for (int i = s.Length / 2 - 1]// 排序数组s
static void HeapSort(int[] s)
{
BuildHeap(s); // 先将s处理成大根堆
for (int heapEndIndex = s.Length-1; heapEndIndex >= 1; heapEndIndex--)
{
// 未排序的系统(也就是堆)L=s[0..heapEndIndex],已排序的系统R=s[heapEndIndex+1..n-1]
// 每次从堆里拿出最大的一个元素放到R的起始位置,然后再把L处理成堆
swap(s, 0, heapEndIndex);
Heapify(s, 0, heapEndIndex - 1);
}
}


堆排序的性能

  由于每次从堆里查找最大元素的时间代价为Θ(log n),所以堆排序的时间代价提高到了Θ(n log n)。
  

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: