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

算法思想

//将数组build成最大堆格式,这样,数组的第一个元素一定是最大的

//有了这个前提,就可以从后往前迭代:将最后一个元素与第一个元素交换,这样最大值就排到了最后面

//然后对第一个元素开头的堆结构进行重建堆,使其依然满足第一个元素为最大的元素


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


//堆数据结构:给定一个节点的下标i,其左子节点的位置为2*i+1(i右移一位+1),右子节点的位置为2*i2(右移一位+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!比如:取出一个最大值,插入一个值,调整一个值的大小==

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值