您的位置:首页 > 其它

【算法导论】堆排序

2015-09-11 12:38 295 查看
堆排序,我们必须先了解一下那里的桩。这堆实际上是在二叉树,这是不是很形象,从头看起来完全二叉树,它是一个三角形,它也可以被看作是一个“堆”。

一、数组转换成堆

那么首先要解决的问题就是给数组排序,怎样转换成二叉树的?转换方法如图:

数组 int a[],包括元素a[0]。a[1],a[2]。a[3].....等等。

转换成二叉树:



图还是比較形象的,事实上就是依次从堆顶向下排,可是有一个原则,就是上一层没有排满的时候,下一层不会有元素。有限排满上一层,才考虑下一层。

二、怎样进行堆排序

那么第二个问题来了,我们怎么才干对其进行排序呢。

在考虑这个问题之前,我们须要先进行一些前序工作:

1、已知一个堆中的元素a[i]。它的父节点是谁?左右子节点呢(假设有)?

通过观察不难得出。假设像我们上图这样,数组序号从0開始排起的话。元素a[i]的父节点(当然i不等于0时)是a[(i+1)/2 - 1],而左右子节点各自是a[2*i+1]和a[2*i+2];

2、引入大根堆的概念。

大根堆:除根节点以外的全部及诶单a[i]都要满足:a[PARENT(i)]>=a[i],这里parent(i)表示i的父节点的下标值;显然假设我们将一个树变成了一个大根堆,我们就能够得到一个当前树的最大值,也就是这个堆的顶部节点的值,由大根堆的性质可知。

3、对于一个左右子树已经是大根堆的根节点,如何将以该节点为根的二叉树转换成大根堆?

显然,假设根节点比左右子节点要大,那么已经是大根堆

假设根节点比左右子节点要小。或者比当中一个要小?我们就须要将最大的那个子节点与根节点交换位置;可是这还没有完,由于原根节点交换到它的直接子节点位置之后,还不一定会满足大根堆的条件限制,假设不满足。我们就须要继续刚才的工作;

看看以下的代码:

/**
* 在左右子树都是大根堆的情况下,形成新的大根堆
*/
void maxHeapIndex(int a[], int root, int maxSize) {
//得到左子节点的下标
int l = leftChildIndex(root);
//得到右子节点的下标
int r = rightChildIndex(root);
//先默认最大节点的下标值为当前根节点
int largest = root;

//假设左节点存在且左节点的值大于根节点,则标记最大值为左节点
if (l <= maxSize && a[l] > a[root]) {
largest = l;
}

//假设左节点存在且右节点的值大于根节点,则标记最大值为右节点
if (r <= maxSize && a[r] > a[largest]) {
largest = r;
}

//经过标记推断。假设发现最大值确实不是根节点,则进行交换值,且进行递归调用看看交换之后的根节点是否满足大根堆性质
if (root != largest) {
swap(a + root, a + largest);
maxHeapIndex(a, largest, maxSize);
}
}

/*
* 交换
*/
void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
/*
* 得到左子节点的下标
*/
int leftChildIndex(int i) {
return (i + 1) * 2 - 1;
}

/*
* 得到右子节点的下标
*/
int rightChildIndex(int i) {
return (i + 1) * 2;
}


4、堆中节点的下标关系。

经过第3点,已经搞定了在左右子树都是大根堆的情况下。形成一个新的大根堆;那么我们怎样让一颗没有处理过的堆来形成一个大根堆呢?怎样找到左右子树都是大根堆的情况呢?找不到,那么不如先不要左右子树,那就是叶子节点。叶子节点没有左右子树,换言之它们就是天然的单节点大根堆;所以叶子节点的父节点都是满足3所如果的条件的。那么对于一个堆而言,叶子节点的父节点的下标的范围是多少呢?

对于一个满节点二叉树而言,假设有h层。比方上图中到a[15]处,有三层;对于h层的满节点二叉树,它的节点个数是2*2^h-1。2^h即2的h次幂。叶子节点的个数为2^h,而除叶子节点之外的其它节点的个数为2^h-1个。我们不难得出,假设数组下标从0開始算起的话。除叶子节点以外的其它节点的下标范围应该是[0,2^h-2]。

所以我们仅仅须要从2^h-2位置的节点開始,也就是倒数第二层的最右边的节点開始。

而对于高为h层。节点数为n的堆而言,h和n有例如以下关系:h=不小于lgn的最接近的整数。知道了上面这些,我们就能够从2^h-2位置開始一直到0位置一直循环运行第3点来实现形成一个大根堆:

/**
* 生成一个大根堆,思路:
* 从倒数第二层的最右边的节点開始。依次调用maxHeapIndex
*
*/
void maxHeapBuild(int a[], int len) {
//得到高度
int h = log2(len);

//得到可用的最大的下标值
int max = (int) (pow(2, h)) - 2;

//循环调用
for (int i = max; i >= 0; i--) {
maxHeapIndex(a, i, len - 1);
}
}

/**
* 得到log2为底的值
*/
int log2(int i) {
//ceil函数返回一个不大于某值的整数
return (int) floor(log(i+1) / log(2));
}


三、堆排序过程

经过上面四步,我们已经能够得到一个大根堆了,接下来的工作就是怎样利用大根堆来进行排序。

对于大根堆而言。我们唯一能够确定的是,它的顶部的那个元素一定是当前堆中最大的元素。于是我们能够每次将顶部的元素和最右边的叶子节点也就是最后一个叶子节点进行交换。然后拿出这个最大值。而且将堆的节点数减一,然后又一次对顶点也就是刚才那个交换到顶点位置的叶子节点进行上面3、中的操作。来又一次让新堆变成一个大根堆,然后我们依次循环运行上面的操作。直到取出堆中的全部元素。这样,我们从開始到最后取出的元素是依照从大到小的顺序。也就完毕了排序。

这一步的代码非常easy:

/*
* 堆排序。思路:
* 先进行一次maxHeapBuild。使得root节点为最大的值
* 然后将root节点和最后一个节点的值进行交换
* 然后将最后一个节点断开(maxsize-=1)
* 然后进行maxheapify,由于这时候根节点的左右子树依然是大根堆。所以仅仅须要maxheapify而不须要maxHeapBuild
* 这样从n-1開始循环到2的时候就可以
*/

void maxHeapSort(int a[], int maxSize, int tmp[]) {
maxHeapBuild(a, maxSize); //n

for (int i = maxSize; i > 0;) { //循环n-1次
swap(a, a + i);
tmp[i] = a[i];
i--;

if (i == 0) {
tmp[0] = a[0];
} else {
maxHeapIndex(a, 0, i); //logn 运行了n-2次
}
}

for (int i = 0; i <= maxSize; i++) {
a[i] = tmp[i];
}
}


四、堆排序时间复杂度

对于堆排序算法,我们简单的看一下它的时间复杂度,一次maxHeapBuild的时间复杂度是n,然后循环n-1的maxHeapIndex时间复杂度为nlogn,循环赋值操作复杂度为n,所以算起来。时间复杂度为nlogn。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: