您的位置:首页 > 其它

算法笔记五:利用堆结构来对数据进行排序

2014-10-17 17:34 525 查看
算法思想
//将数组build成最大堆格式,这样,数组的第一个元素一定是最大的
//有了这个前提,就可以从后往前迭代:将最后一个元素与第一个元素交换,这样最大值就排到了最后面
//然后对第一个元素开头的堆结构进行重建堆,使其依然满足第一个元素为最大的元素

//关键就在于建堆的逻辑

//堆数据结构:给定一个节点的下标i,其左子节点的位置为2*i+1(i右移一位+1),右子节点的位置为2*i加2(右移一位+2)
//最大堆:父节点的值data[i]一定不小于子节点的值
//涉及的几个操作:

//堆调整:给定一个节点,假设其左子节树和右子树都满足最大堆的性质,那么,如果当前节点和左右子节点不满足堆性质,就要进行调整,调整时,要么和左子节点进行交换,要么和右子节点进行交换
//一旦发生交换后,就会破坏被交换的那个子节点的堆性质,然后需要做的就是递归对子节点进行同样的堆调整操作

//建堆:这里,我们可以利用堆调整的操作,自底而上的构建堆,由子节点位置的计算公式可以得出,数组的n/2处,后面的一定都是叶子节点,所以我们从n/2处开始往回走,直到第一个元素,对这些每个非叶子节点,自下而上的进行堆性质维护,因为自下而上,所以每次都满足我们建堆的假设条件:左右子树都满足堆的性质,所以最终就完成了整个堆的构建

//有了堆,排序就好办,数组的第一个元素是最大的,将他和数组的最后一个元素调换,这样最大的就放到最后去了,然后对堆的根节点重新进行调整

//其他的几个操作:

//增大一个节点的值:因为是增大该节点,所以,其节点本身的堆属性并不会受影响
//而唯一需要做的就是,连续向上和父节点进行比较,如果比父节点大,则交换,直到其比父节点的值要小

//插入一个节点
//也很简单,先扩容,然后插入一个无穷小的值到最后,这时候一定是不违反堆性质的,然后执行上面的增大值的操作将这个无穷大的值增大到实际值即可

//其实就是扩容-->赋值-->执行该节点的增大操作

这里的一个问题就是,其是对原有问题的分解,不属于分治思想,但我们依然可以分析其执行的代价:
整个排序由两部分组成:
1、建堆
2、取最大值(取完之后又要对未取完的数据进行建堆)
其中,取最大值的代价,就是两个元素进行交换的代价,是一个常量值,而且最多执行n次,所以我们可以忽略它
所以,其根本还是取决于建堆的代价

堆调整:其执行的次数,最大值就是要调整树的高度的次数,其值等于lgn = h(树的高度)

建堆:假定每个要调整的子树,其最坏调整代价都是lgn(除根节点处的所有子树,都会小于这个值),
那么一共会有少于总数n的有限个子树需要调整,总代价为n*lgn ,这里的数学公式比较的复杂,但是可以告诉大家其答案为n,也就是不会超过整棵树的元素的个数,读者可以多画几棵树来实际推测验证下。

取最大值:最大值,是遍历数组最后一个元素,与第一个元素交换,然后再重建调整堆,直到交换到数组只剩2个元素,所以,一共的交换的次数为n-1,而每次交换后的调整堆的代价最坏也就是lgN,所以总代价就是(n-1)*lgn,再加上我们前面分析的建堆的代价为lgn,得出最坏的代价为(n-1)*lgn + n

结论:其最坏的代价为(n-1)*lgn + n(这里教科书上将其渐进的等于nlgn)

空间代价:
空间上只有在调整堆时,对两个元素交换使用了一个临时存储空间,所以空间代价还是非常小的

算法实现
#ifndef __p1__HeapSortV2__
#define __p1__HeapSortV2__

#include <stdio.h>

//将数组build成最大堆格式,这样,数组的第一个元素一定是最大的
//有了这个前提,就可以从后往前迭代:将最后一个元素与第一个元素交换,这样最大值就排到了最后面
//然后对第一个元素开头的堆结构进行重建堆,使其依然满足第一个元素为最大的元素

//关键就在于建堆的逻辑

//堆数据结构:给定一个节点的下标i,其左子节点的位置为2*i+1(i右移一位+1),右子节点的位置为2*i加2(右移一位+2)
//最大堆:父节点的值data[i]一定不小于子节点的值
//涉及的几个操作:

//堆调整:给定一个节点,假设其左子节树和右子树都满足最大堆的性质,那么,如果当前节点和左右子节点不满足堆性质,就要进行调整,调整时,要么和左子节点进行交换,要么和右子节点进行交换
//一旦发生交换后,就会破坏被交换的那个子节点的堆性质,然后需要做的就是递归对子节点进行同样的堆调整操作

//建堆:这里,我们可以利用堆调整的操作,自底而上的构建堆,由子节点位置的计算公式可以得出,数组的n/2处,后面的一定都是叶子节点,所以我们从n/2处开始往回走,直到第一个元素,对这些每个非叶子节点,自下而上的进行堆性质维护,因为自下而上,所以每次都满足我们建堆的假设条件:左右子树都满足堆的性质,所以最终就完成了整个堆的构建

//有了堆,排序就好办,数组的第一个元素是最大的,将他和数组的最后一个元素调换,这样最大的就放到最后去了,然后对堆的根节点重新进行调整

//其他的几个操作:

//增大一个节点的值:因为是增大该节点,所以,其节点本身的堆属性并不会受影响
//而唯一需要做的就是,连续向上和父节点进行比较,如果比父节点大,则交换,直到其比父节点的值要小

//插入一个节点
//也很简单,先扩容,然后插入一个无穷小的值到最后,这时候一定是不违反堆性质的,然后执行上面的增大值的操作将这个无穷大的值增大到实际值即可
//其实就是扩容-->赋值-->执行该节点的增大操作

class HeapSortV2 {

public:
//获取父节点
int getParentNode(int i){
return ( i - 1 ) >> 1;
}
//获取左子节点
int getLeftChildNode(int i){
return (i << 1) + 1;
}
//获取右子节点
int getRightChildNode(int i){
return (i << 1) + 2;
}

void changeTwoItems(int * data,int i,int j){
int tmp = *(data + i);
*(data + i) = *(data + j);
*(data + j) = tmp;
}

//求父节点和两个子节点的最大的那个节点
int getLargestNode(int * data,int node,int size){
int leftNode = getLeftChildNode(node);
int rightNode = getRightChildNode(node);

//node没有左子节点
if(leftNode >= size){
return node;
}
//有左子树,没有右子树
if(rightNode >= size){
if(*(data + node) >= *(data + leftNode)){
return node;
}else{
return leftNode;
}
}

//左子树和右子树都有,则求哪个节点是最大的节点
int largsetNode = node;
if(*(data + node) >= *(data + leftNode)){
if(*(data + node) >= *(data + rightNode)){
largsetNode = node;
}else{
largsetNode = rightNode;
}
}else{
if(*(data + leftNode) >= *(data + rightNode)){
largsetNode = leftNode;
}else{
largsetNode = rightNode;
}
}
return largsetNode;
}

//调整给定节点的堆的性质,假定其左子树和右子树都已经满足堆的性质
void adjustHeap(int * data,int node,int size){
//求得最大的节点
int largsetNode = getLargestNode(data,node,size);
if(largsetNode != node){
//交换
changeTwoItems(data,node,largsetNode);
//继续调整那个被改变了的节点
adjustHeap(data,largsetNode,size);
}

}

//将数组build成最大堆
void buildHeap(int * data,int size){
int middle = size / 2 ;
for (; middle >=0 ; middle--) {
adjustHeap(data,middle,size);
}
}

//增大一个节点的值
void increaseNode(int * data,int node){
if(node == 0){
return;
}
int parentNode = getParentNode(node);
if(*(data + node) > *(data + parentNode)){
changeTwoItems(data,node,parentNode);
increaseNode(data, parentNode);
}
}
//增大一个节点的值
void increateNodeValue(int * data,int node,int newValue){
*(data + node) = newValue;
increaseNode(data, node);
}

//执行堆排序
void sortByHeap(int * data,int size){
//建堆
buildHeap(data,size);

//增大值的测试:第80个元素的值增大1002
increateNodeValue(data,8,*(data + 8) + 1002);

//有了堆,排序就好办,数组的第一个元素是最大的,将他和数组的最后一个元素调换,这样最大的就放到最后去了,然后对堆的根节点重新进行调整
for (int i= size -1; i>0; i--) {
changeTwoItems(data,i,0);
adjustHeap(data,0,i );
}
}

//排序入口
void do_sorting(int * data,int size){
sortByHeap(data, size);
}

};

#endif /* defined(__p1__HeapSortV2__) */


算法总结:
时间代价表现优异,n + n*lgn
空间代价上,仅仅在需要进行调整时,开辟一个用来存储两个元素交换的临时的一个存储空间,代价非常小。
还有一个结论就是,在一个已经建好的堆上执行所有的优先级队列的操作,其时间代价均为lgn!比如:取出一个最大值,插入一个值,调整一个值的大小==
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: