1.5排序——堆排序:二叉堆和排序

我们有意调整了排序的顺序,最后讲这个堆排序。不是因为它很难,而是它涉及到了基本的数据结构知识。

# 以下py版摘自百度百科
def big_endian(arr,start,end):    
    root=start    
    child=root*2+1 #左孩子    
    while child<=end:
    #孩子比最后一个节点还大,也就意味着最后一个叶子节点了,就得跳出去一次循环,已经调整完毕     
        if child+1<=end and arr[child]<arr[child+1]:
        #为了始终让其跟子元素的较大值比较,如果右边大就左换右,左边大的话就默认           
            child+=1            
        if arr[root]<arr[child]:
        #父节点小于子节点直接交换位置,同时坐标也得换,这样下次循环可以准确判断:是否为最底层,
        #是不是调整完毕                
            arr[root],arr[child]=arr[child],arr[root]                
            root=child                
            child=root*2+1            
        else:               
        break
         
def heap_sort(arr): #无序区大根堆排序    
    first=len(arr)//2 - 1    
    for start in range(first,-1,-1):
    #从下到上,从左到右对每个节点进行调整,循环得到非叶子节点        
        big_endian(arr,start,len(arr)-1) #去调整所有的节点    
    for end in range(len(arr)-1,0,-1):        
        arr[0],arr[end]=arr[end],arr[0] #顶部尾部互换位置        
        big_endian(arr,0,end-1) #重新调整子节点的顺序,从顶开始调整    
    return arr

堆,又名“优先队列”,是一个带有优先级(就是一定顺序)的队列,而队列则是一种基础数据结构,它的特点是“先进先出”,i.e.,先进入队列的元素,在做移除操作时会被首先移除出队列。其实,优先队列这种队列,并没有说最先进入的会被最先移除,因为带有一定的顺序,首先进入的元素也会根据要求放到合适的位置,而不是最前端,而删除时,假设是默认删除,会删除最上方的元素。

我们的堆,一般情况下都是二叉堆,就是由完全二叉树构成的对象。二叉树是一种树形结构,每个父节点只有2个子节点;而完全二叉树则是说,在排下一个子节点时,必须保证这一层之前所有的位置都有节点。

这样的结构就会有以下几个特点,

  1. 假设一个元素的编号是 i,一个元素的2个子节点——左孩子和右孩子分别是 2i 和 2i+1。
  2. 其父节点的编号是 i/2,是向下取整的。

所以,当我们构建一个(最小)二叉堆时,我们首先需要一个基本数据结构——描述堆的struct或者class:

template <typename T>
struct Heap
{
    int Capacity;  //描述堆的能力,i.e.,堆一共能放几个数
    int Size;  //堆目前放了几个数
    T *elements;  //一个放置元素的数组
};

而我们在使用的时候,可以这样定义一下:

typedef struct Heap *PriorityQueue;

就会有一个指向此优先队列的指针。

而初始化这个优先队列也很简单,给指向优先队列的指针一个空间,给放置元素的数组开辟一个空间,给结构Heap一个初始值:

template<typename T>
PriorityQueue Initialize( int MaxSize )
{
    PriorityQueue H;
    H = malloc( sizeof( struct Heap ) );  //开辟指针的空间
    if ( H == NULL )
        Error( "out of space" );  //空间开辟失败
    H->Elements = malloc( ( MaxSize + 1 ) * sizeof( T ) );  //把最前边的作为哨兵
    if ( H == NULL )
        Error( "out of space" );
    //Heap初始化
    H->Capacity = MaxSize;
    H->Size = 0;
    H->Elements[0] = Min;  //这个Min是什么大家可以自己定义,比如INT_MIN
    return H;
}

这个多一位的“哨兵”有2个好处,一个是左右孩子可以直接取2i和2i+1(不然的话第0位的2倍没有意义),第二个是在执行元素的插入操作时可以作为结束循环的判断。

那咱们就写一下如何Insert元素吧,因为堆是有顺序的,所以插入时必须要考虑如何把它放到合适的位置,一般,咱们先在堆的末端添加一个空位置,然后判断空位置放此元素是不是合理的,不合理的话就把此位置的父节点下移,然后尝试父节点的位置放置此元素是否合理,直到根节点:

template <typename T>
void insert( T x, PriorityQueue H )
{
    int i;
    if( Queue已经满了 )
    {
        Error;
        return;
    }
    for( i = ++H->Size; H->Elements[i/2] > x; i /= 2)  //i从一个新位置(size+1)开始
    //这样在父节点的移动时就可以避免繁琐的swap例程,只用依次放入;而循环条件也很简单,
    //只要父元素大于它,就把父元素下移,继续寻找父元素
        H->Elements[i] = H->Elements[i/2];
    H->Elements[i] = x;
}

我觉得我已经在code里把操作叙述的很清楚了,这里就不再多说,不过只说一点(还是要说啊),这个循环条件语句可以用简单的大小判断,就是因为我们把0位置设为了哨兵(一个很小的值),这样即使我们需要插入的值是最小值,当它插入到根节点的位置后,就必然大于0位置的值(1/2=0),从而结束循环。

有插入就有删除,我们实现一种删除,就是删除最小的元素:堆顶元素。其他的删除可以参考它:

template<typename T>
T DeleteTop( PriorityQueue H )
{
    int i, int child;
    T minElement, lastElement;
    if( empty( H ) )
    {
        Error;  //空
        return H->Elements[0];  //返回第0个元素
    }
    minElement = H->Elements[1];
    lastElement = H->Elements[H->Size--];  //赋给最后一个元素,同时size减1,因为要删除一个元素
    for( i = 1; i * 2 <= H->Size; i = child)  
    //条件是(左)儿子在范围内,因为如果左孩子不在那么右孩子必然不在
    {
        child = i * 2;
        if( child != H->size && H->elements[child + 1] < H->elements[child])
        //这里我们没有明显检测右孩子是否存在,但是其实用了child != H->size来控制,这里的!=
        //其实就是<,只有它不是最后一位,那么就保证了child+1位置的存在
            child++;
        if( lastElements>H->elements[child] )
            H->elements[i] = H->elements[child];
        else
            break;
    }
    H->elements[i] = lastElements;
    return minElements;
}

有了这个删除堆顶(最小)元素的例程,我们就可以做删除任意元素的操作:把想要删除的元素赋值为最小元素,即给它一个小于堆顶元素的值,然后Insert到堆顶,最后执行DeleteTop就好了。

我们写了很多关于二叉堆的操作,其实和马上要写的堆排序代码并不太一致,至少不需要掌握复杂的插入删除,但它是我们理解堆排序的必要知识。

现在我们来看堆排序就很简单了,它的主要操作就是:

  1. 拿数据建堆。
  2. 按照规则删堆(解散堆),因为只删除堆顶的,故最后得到的是一个有序序列。

假设我们还是需求一个非减序列,那么相应的,我们最好建一个最大堆,因为最大堆的top delete之后可以放到堆尾,这样的排序不需要额外的空间,可以说是相当成功了。

作为一个可以用的排序,我觉得应该从数组的第0位开始(废话,就这么一个数组做原地排序,你不从第0位开始从哪开始啊?),也因为是从第0位开始的,左孩子就不能是2i了(上边讲过),得是2i+1。那么,我们写写?

#define LeftChild(i) (2*(i)+1)
template<typename T>
void heap( T a[], int i, int n )
{
    int child;
    T tmp;
    for( tmp = a[i]; LeftChild(i) < n; i = child )
    {
        child = LeftChild(i);
        if( child != n - 1 && a[child+1] > a[child] )
            child++;
        if( tmp < a[child] )  //建立最大堆,那么就是parent小于child时,child上移
            a[i] = a[child];
        else
            break;  //找到了合适的位置,停止循环
    }
    a[i] = tmp;
}

这个建堆的基础操作,被我们拿来既用作建堆,也用做删堆,当然在这里更好的称呼是排序:建堆就不用讲了,就是从最后一位元素开始逐渐建立小堆,然后合并成大堆;而排序的过程就是把top元素放入堆尾,对其他元素做重建堆:

template<typename T>
void HeapSort( T a[], int n )
{
    int i;
    for( i = n/2; i >= 0; i-- )
        heap( a, i, n );  //建堆
    for( i = n - 1; i > 0; i--)  //一个微小的点:i>0,不需要=,因为最后一项不用排序
    {
        swap( a[0], a[i] );  //交换第0项和最后一项后,排除最后一项建堆
        heap( a, 0, i );
    }
}

以上2段代码只有2个需要注意的地方,一是HeapSort()建堆的过程中,从i=n/2;开始,而不是很多代码的i=n-1;,因为最后一层并没有什么可建的,完全是浪费时间;二是在HeapSort()执行heap()时,最后一个参数是数组的数量,也就是数组的最后一项加1,因为在heap()中的循环条件决定了我们是拿n来比较的,这和其他排序的例程中使用n-1稍微有所区别。

目前,我们已经把所有经典的排序都过了一遍,顺便还讲了递归式的证明,以及二叉堆,在一般的算法课上,这大概需要不到一个月的时间,希望大家喜欢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,030评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,198评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,995评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,973评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,869评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,766评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,967评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,599评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,886评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,901评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,728评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,504评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,967评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,128评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,445评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,018评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,224评论 2 339

推荐阅读更多精彩内容