美文网首页
1.5排序——堆排序:二叉堆和排序

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

作者: 吃个小烧饼 | 来源:发表于2017-07-23 01:59 被阅读66次

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

堆,又名“优先队列”,是一个带有优先级(就是一定顺序)的队列,而队列则是一种基础数据结构,它的特点是“先进先出”,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稍微有所区别。

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

相关文章

  • 二叉树的应用

    1 排序二叉树和堆 用途树结构关系存储方式应用(大根)堆排序完全二叉树根>左子树,根>右子树数组堆排序,取topk...

  • java堆排序

    什么是堆排序:图解堆排序堆排序:利用了堆这种数据结构堆数据结构:特殊的完全二叉树,因为具有以下的特点:1)每个结点...

  • Java排序算法 - 堆排序

    堆排序 堆排序是基于堆这种数据结构的一种排序算法,通过每一次弹出堆顶元素,实现排序。预备知识: 堆是一棵完全二叉树...

  • 排序算法(六)堆排序

    排序算法(六 )堆排序 1.算法思路  堆排序(Heap-Sort)是一种基于二叉堆的排序算法。即将一个无序序列构...

  • 排序算法08:优先队列与堆排序

    堆排序一种是基于二叉堆的排序。本文将从优先队列讲起,循序渐进的实现堆排序。这也是《算法》第四版上讲解堆排序的大致章...

  • 堆排序

    堆排序 堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法. 堆是一个近似完全二叉树的结构, ...

  • PHP算法系列教程(三)-堆排序

    PHP算法系列教程(三)-堆排序 介绍 要介绍堆排序我们就要先了解什么是堆. 什么是堆 堆(二叉堆)可以视为一棵完...

  • 堆&堆排序

    堆排序 堆排序是一种原地的、时间复杂度为 O(nlogn) 的排序算法。 什么是堆 堆是一个完全二叉树(除了最后一...

  • 算法和数据结构

    1. 排序 快速排序: 冒泡排序: 插入排序: 希尔排序: 堆排序:堆是具有以下性质的完全二叉树:每个节点的值都...

  • 堆排序

    堆排序 堆:heap 一种数据结构 完全二叉树(每个父节点最多只有两个子节点左节点和右结点) 常用来做堆排序和二叉...

网友评论

      本文标题:1.5排序——堆排序:二叉堆和排序

      本文链接:https://www.haomeiwen.com/subject/gfvykxtx.html