代码改变世界

堆和堆排序(堆实现优先级队列)

2012-07-26 20:50  youxin  阅读(662)  评论(0编辑  收藏  举报

 

堆是一种特殊类型的二叉树,它具有2个性质:

1.每个节点的值大于等于其每个子节点的值

2该树完全平衡,最后一层的叶子都处于最左侧的位置

n个元素称为对,当且仅当它的关键字序列k1,k2,.....kn满足

 ki<=K2i, Ki<=K(2i+1); 1<=i<=floor(n/2);

或者反过来。

堆有最大堆和最小堆,最大最小堆等。可以直接跳到后面看。

注意,我们用数组表示堆时,根节点存放在H[1]中

#include<iostream>
#include<algorithm>

using namespace std;

//元素上移操作
/*数组h[]及被上移的元素下标i
输出,维持堆的性质的数组h[]
*/

template<class T>
void sift_up(T h[],int i)
{
    if(i!=1)
    {
        while(i!=1)
        {
            if(h[i]>h[i/2])
            {
                    swap(h[i],h[i/2]);
                    i=i/2;
            }
            else
                break;
            
        }
    }
}
                

/*下移操作
和儿子节点关键字大的进行比较
*/

template<class T>
void sift_down(T h[],int n,int i)
{

    if((2*i)<=n)
    {
        while((i=2*i)<=n)
        {
            if((i+1<=n)&&(h[i+1]>h[i]))
                i=i+1;
            if(h[i/2]<h[i])
                swap(h[i/2],h[i]);
            else
                break;
        }
    }
}

//删除元素
/*输入:数组h[],数组的元素个数n,被删除的元素下标
 输出:维持堆的性质的数组H[],及删除后的元素个数n
 */
//为了删除H[i],可用堆中最后一个元素取代h[i],然后根据被删除元素和取代
//元素 的矮小,确定是做下移还是上移操作
template<class T>
void deleteElem(T h[],int &n,int i)
{
    T x,y;
    x=h[i],y=h[n];
    n=n-1;
    if(i<=n)
    {
        h[i]=y;
        if(y>x)
            sift_up(h,i);
        else
            sift_down(h,n,i);
    }
}

/*删除关键字最大的元素,关键字最大的元素位于根节点,把根节点去掉*/
template<class T>
T delete_max(T h[],int &n)
{
    T x;
    x=h[1];
    deleteElem(h,n,1);
    return x;
}



//插入操作
/*为了把元素x插入堆中,只要把堆的大小增1后,把x放到堆的末端,然后对x上移即可。
*/
template<class T>
void insert(T h[],int &n,T x)
{
    n++;
    h[n]=x;
    sift_up(h,n);
}

//构造一个队
//输入数组h[],数组的元素个数n
//输出:n个元素的堆H[]
template<class T>
void make_heap1(T a[],T h[],int n)
{
    int i,m=0;
    for(i=0;i<n;i++)
        insert(h,m,a[i]);
}

//我们可以把数组本身构造成一个堆,调整过程是从最后一个树叶,找到它上面的分支节点,从这个
//节点开始做下移操作,一直到根节点为止,最后就成了一个堆*、
template<class T>
void make_heap(T a[],int n)
{
    int i;
    a[n]=a[0];
    for(i=n/2;i>=1;i--)
        sift_down(a,n,i);
}


int main()
{
    int a[100]={2,4,5,3,8,18,13,15,20,25};
    int h[100];
    make_heap1(a,h,10);


    for(int i=1;i<=10;i++)
        cout<<h[i]<<ends;
    cout<<endl;
    make_heap(a,10);
    for(int i=1;i<=10;i++)
        cout<<a[i]<<ends;
    cout<<endl;
    int n=10;
    delete_max(a,n);
    for(int i=1;i<=n;i++)
        cout<<a[i]<<ends;

}

注意上面两种构造堆方法不同,输出的堆不是一样的,但都是堆

结果:

堆的排序

   可以利用堆的性质,对数组A排序,假定A元素个数为n,根据最大堆的性质,根节点元素a[1]就是最大元素,此时,只要交换a[1]和a[n],则a[n]就成为数组关键字最大的元素。就相当于把a[n]从堆中删去,元素个数减1,而交换到h[1]的新元素,破坏了堆的结构,因此要对a[1]做下移操作,使其恢复堆的结果,经过这样交换后,a[1]----a[n-1]成为新的堆,其个数为n-1,反复进行这种操作,a[1]就是A中最小的元素了。堆排序代码:

template<class T>
void make_heap(T a[],int n)
{
    int i;
    a[n]=a[0];
    for(i=n/2;i>=1;i--)
        sift_down(a,n,i);
}

void heap_sort(int a[],int n)
{
     make_heap(a,n);
    for(int i=n;i>1;i--)
    {
        swap(a[1],a[i]);
        sift_down(a,i-1,1);
    }
}

注意我们传入的数组a[]的维度应该大于它的元素个数。

 -----------------------------------------------

算法导论:

Heapify

最大堆为例,伪代码:

MAX-HEAPIFY(A, i)

  l = LIFT(i)

  r = RIGHT(i)

  if l <= A.heapsize and A[l] > A[i]

    largest = l

  else largest = i

  if r <= A.heapsize and A[r] > A[largest]

    largest = r

  if largest != i

    exchage A[i] with A[largest]

    MAX-HEAPIFY(A, largest)

 怎么求时间复杂度?

用主定理求得T(n)=o(lgn),或者说,MAX-HEAPIFY作用域一个高度为h的节点所需的运行时间为O(h)。高度h为lgn

 

Build the heap

我们已经知道,当用数组存储了n个元素的堆时,叶子节点的下标为n/2+1,n/2+2.........n;(因为假设节点为i,只要i*2>n就说明

i为叶子节点。这样求的i=n/2+1)。叶子都可以看成只有一个元素的堆,过程build-max-heap对树中的每一个其他节点都调用一次

max-heapify.

BUILD-MAX-HEAP(A)  

   heap-size[A]=length(A);

   for i=length(A)/2 downto 1

         MAX-HEAPIFY(A,i)

上面的时间复杂度为O(n)。

可以在线性时间内,将一个无序数组建成一个最小堆

算法导论6-3-2:为什么i从n/2 down to 1,而不是从1 to n/2.

为了保证在调用Max-heapify(A,i)时,以Left[i]和right(i)为根的2颗二叉树都是最大堆。

因为自底至顶的循环方法可以首先让底部的元素满足堆的形式,最后让顶部元素满足堆的性质。如果自顶至低会有灾难性的后果,会有特殊情况使之树的局部不完备。

我的理解:开始时调用max-heapify(a,1)由于a[1]的左子树和右子树还不是堆,所以有问题。

 

堆排序算法:

  建立了最大堆后,因为数组中最大元素在根A[1],则可以通过它与A[n]互换来达到最终正确的位置。现在,如果从 堆中“去掉节点n”,

可以很容易将A[1..n-1]建成最大堆,原来根的子女仍是最大堆,而新的根元素可能违背了最大堆性质,这时调用MAX-HEAPIFY(A,1)就可以保持这一性质.

for i = A.length downto 2

    exchange A[1] with A[i]

    A.heap-size = A.heap-size - 1

    MAX-HEAPIFY(A, 1)

 

循环了n-1遍,每次调用后堆的size减1.为什么减1?

时间复杂度为O(nlgn).

代码:

#include<iostream>
using namespace std;

int heapSize;
 
void maxHeapify(int a[],int heapSize,int i)// 
{
    int l=2*i+1;//由于是从0开始,left为2*i+1,,right为2*i+2;
    int r=2*i+2;
    int largest;
    if(l<heapSize && a[l]>a[i])
        largest=l;
    else
        largest=i;

    if(r<heapSize && a[r]>a[largest])
        largest=r;
    
    if(largest!=i)
    {
        swap(a[i],a[largest]);
        maxHeapify(a,heapSize,largest);
    }
}

void buildMaxHeap(int a[],int  n)
{
    heapSize=n;//赋值
    for(int i=n/2-1;i>=0;i--)//这里要特别注意,由于是从0开始,不会n/2
    {
        maxHeapify(a,n,i);
    }
}
void heapSort(int a[],int n)
{
    heapSize=n;
    for(int i=n-1;i>=1;i--)
    {
        swap(a[0],a[i]);
        heapSize--;//很重要
        maxHeapify(a,heapSize,0);//第0个元素,也就是根节点
    }
}

#define aSize 10;

int main()
{
    int a[10]={4,1,3,2,16, 9,10,14,8,7};
    buildMaxHeap(a,10);
    for(int i=0;i<10;i++)
        cout<<a[i]<<ends;
    cout<<endl<<endl;
    heapSort(a,10);
    for(int i=0;i<10;i++)
        cout<<a[i]<<ends;
    cout<<endl;
     
}

由于我们从0开始,而不是从1开始,产生了很多陷阱,画红线的都是我写的时候不小心出错的地方。要特别注意:

数据来自算法导论P77:

 int a[10]={4,1,3,2,16, 9,10,14,8,7};

 

非递归maxheapify:

void adjust_max_heap(int *datas,int length,int i)
{
    int left,right,largest;
    int temp;
    while(1)
    {
        left = LEFT(i);   //left child
        right = RIGHT(i); //right child
        //find the largest value among left and rihgt and i.
        if(left <= length && datas[left] > datas[i])
            largest = left;
        else
            largest = i;
        if(right <= length && datas[right] > datas[largest])
            largest = right;
        //exchange i and largest
        if(largest != i)
        {
            temp = datas[i];
            datas[i] = datas[largest];
            datas[largest] = temp;
            i = largest;
            continue;
        }
        else
            break;
    }
}

 

最小堆只需要改以下判断条件:

void minHeapify(int a[],int heapSize,int i)
{
    int l=2*i+1;
    int r=2*i+2;
    int smallest;
    if(l<heapSize && a[l]<a[i])
        smallest=l;
    else
        smallest=i;
    if(r<heapSize && a[r]<a[smallest])
        smallest=r;

    if(smallest!=i)
    {
        swap(a[i],a[smallest]);
        minHeapify(a,heapSize,smallest);
    }
}


void minHeapify2(int *a,int n,int m) 
{ 
    int i=m; 
    int j=2*i+1; 
    int tmp=a[i]; 
    while(j<n) 
    { 
        if(j+1<n&&a[j]>a[j+1]) 
            j++; 
        if(a[j]>=tmp) 
            break; 
        else 
        { 
            a[i]=a[j]; 
            i=j; 
            j=2*i+1; 
        } 
    } 
    a[i]=tmp; 
} 

http://buptdtt.blog.51cto.com/2369962/864190

优先级队列:

优先级队列有两种:最大优先级队列和最小优先级队列,这两种类别分别可以用最大堆和最小堆实现。书中介绍了基于最大堆实现的最大优先级队列。一个最大优先级队列支持的操作如下操作:

insert(S,x)把元素x插入集合S

maximum(s) 返回S中具有最大关键字的元素

extract-max(S) 去掉并返回S中具有最大关键字的元素

increase-key(S,x,k) 将元素x的值增加到k(也就是使值增大为k),这里k值不能小于x原来的值。

 

heap-maximum 用了O(1)的时间

heap-maximum(A)

  return A[1];

 

extract_max(A)

Heap-Increase-Key(A,i,key):将节点i的值增加到key,这里key要比i节点原来的数大。

 

新增大的关键字与母亲比较,如果大于母亲则不断往上移动。

 

heap-insert实现了insert操作,这个程序首先加入一个关键字为负无穷大的叶节点来扩展最大堆,然后调用heap-increase,key来设置新节点的关键字的正确值,并保持最大堆的性质:

总之,一个堆可以在O(lgn)的时间内,支持大小为n的集合上的任意优先队列操作

#include<iostream>
using namespace std;

inline int parent(int i)
{
    return i>>1;
}
inline int left(int i)
{
    return i<<1;
}
inline int right(int i)
{
    return (i<<1)|1; ////位运算乘2后,结果是偶数所以最后一位一定是0, 所以|1将会把最后一位变成1,从而实现加1的效果
}
void maxHeapify(int a[],int heapSize,int i)// 
{
    int l=left(i);
    int r=right(i);
    int largest;
    if(l<=heapSize && a[l]>a[i])
        largest=l;
    else
        largest=i;

    if(r<=heapSize && a[r]>a[largest])
        largest=r;
    
    if(largest!=i)
    {
        swap(a[i],a[largest]);
        maxHeapify(a,heapSize,largest);
    }
}

void buildMaxHeap(int a[],int  n)
{
    int heapSize=n;//赋值
    for(int i=n/2;i>=1;i--)//这里要特别注意,由于是从0开始,不会n/2
    {
        maxHeapify(a,n,i);
    }
}

void heapSort(int a[],int n)
{
    int heapSize=n;
    for(int i=n;i>=2;i--)
    {
        swap(a[1],a[i]);
        heapSize--;
        maxHeapify(a,heapSize,1);
    }
}



int maximum(int A[])
{
    return A[1];
}
int extractMax(int A[],int heapSize)
{
    if(heapSize<1)
        return 0;
    int max=A[1];
    A[1]=A[heapSize];
    heapSize--;
    maxHeapify(A,heapSize,1);

    return max;
}

void increaseKey(int A[],int i,int key)
{
    A[i]=key;
    while(i>1 && A[parent(i)]<A[i])
    {
        swap(A[parent(i)],A[i]);
        i=parent(i);
    }
}

void maxHeapInsert(int A[],int heapSize,int key)
{
    heapSize++;
    A[heapSize]=-32768;
    increaseKey(A,heapSize,key);
}


int main()
{
    int a[15]={0,4,1,3,2,16, 9,10,14,8,7};//第0个为0仅仅是占位,不算,后面有10个元素
    buildMaxHeap(a,10);
    for(int i=1;i<=10;i++)
        cout<<a[i]<<ends;
    cout<<endl<<endl;
    //heapSort(a,10);
    for(int i=1;i<=10;i++)
        cout<<a[i]<<ends;
    cout<<endl;
    cout<<"maximu"<<maximum(a)<<endl;
    cout<<"extractMax:"<<extractMax(a,10)<<endl;
    for(int i=1;i<=9;i++)//extract后heapSize减1了
        cout<<a[i]<<ends;
    cout<<endl;
    increaseKey(a,6,16);
        for(int i=1;i<=9;i++)//extract后heapSize减1了
        cout<<a[i]<<ends;

}

 

习题:

6.5-8题目如下:请给出一个时间为O(nlgk)、用来将k个已排序链表合并为一个排序链表的算法。此处n为所有输入链表中元素的总数。(提示:用一个最小堆来做k路合并)。

 

更多:

http://www.cnblogs.com/Anker/archive/2013/01/23/2873422.html

http://www.cnblogs.com/dyingbleed/archive/2013/03/04/2941989.html