1. 概述

堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别称为大顶堆和小顶堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。

堆结构是一类很重要的数据结构,特别是在实现优先队列方面。堆结构实际上都是树(或森林),不过按节点的存储方式又可以分为两种

  1. 顺序存储
    这种情况下,必须保持是完全树。 (在插入,删除后及时调整)
    可以通过下标运算找到父节点,字节点。
  2. 链接存储
    如果是二叉树,一般就用LeftChild,RightChild 两个指针。
    否则一般用环链表表示一个 节点的子女。

数据结构 存储方式 定义与性质 支持的操作 及 复杂度(分摊)
最大堆 顺序存储 完全二叉树,最大树 插入
O(log n)
删除最大元素 O(log n)
最小最大堆 顺序存储

完全二叉树,树的各层交替为最小层和最大层

插入
O(log n)
删除最大元素 O(log n)
删除最小元素 O(log n)
双堆 顺序存储 完全二叉树
或者为空,或者同时满足:
1.根结点不含元素
2.左子树是最小堆
3.右子树为最大堆
4.若右子树不为空,设i 为左子树中的任意结点,
j为右子树的对应结点.
若j不存在,则令j为右子树中对应i的父结点的结点。
必有结点i的key 小于 结点j的key值
插入
O(log n)
删除最大元素 O(log n)
删除最小元素 O(log n)
左偏树
(最小/最大)
链接存储 1.是左偏树
每一个内部结点x 有
shortest(LeftChild(x))
>=shortes(RightChild(x))
2.是最小/最大树
插入
O(log n)
删除最小/最大元素 O(log n)
合并 O(log n)
二项式堆
(最小/最大)
链接存储 最小/最大二项式树的集合.
度为k的二项式树(记作Bk)定义为:
若k=0,该树只有1个结点.
若k>0,该树的根的度为k,其子树为B0,B1,…..,Bk-1
插入 O(1)
删除最小/最大元素 O(log n)
合并 O(1)
斐波纳契堆
(最小/最大)
链接存储 最小/最大树的集合.
二项式堆是斐波纳契堆的特殊情况.
使用双环链表,并在结点中加入parent和childcut.
关键是瀑布修剪
插入 O(1)
删除最小/最大元素 O(log n)
合并 O(1)
减少指定结点key值
O(1)
删除指定结点元素
O(log n)

2. 堆的基本操作

堆是一棵完全二叉树,高度为O(lg n),其基本操作至多与树的高度成正比。在介绍堆的基本操作之前,先介绍几个基本术语:

A:用于表示堆的数组,下标从1开始,一直到n

PARENT(t):节点t的父节点,即floor(t/2)

RIGHT(t):节点t的左孩子节点,即:2*t

LEFT(t):节点t的右孩子节点,即:2*t+1

HEAP_SIZE(A):堆A当前的元素数目


下面给出其主要的四个操作(以大顶堆为例):

1.一个是他是一个数组(当然你也可以真的用链表来做。)。
2.他可以看做一个完全二叉树。注意是完全二叉树。所以他的叶子个数刚好是nSize / 2个。
3.我使用的下标从1开始,这样好算,如果节点的位置为i,他的父节点就是i/2,他的左孩子结点就是i*2,右孩子结点就是i*2+1,如果下标从0开始,要复杂一点。
4.他的父节点一定不比子节点小(我所指的是最大堆)。


由这些性质就可以看出堆得一些优点:
1.可以一下找到最大值,就在第一个位置heap[1].
2.维持堆只需要log(2,n)(n是数据个数)的复杂度,速度比较快。他只需要比较父与子之间的大小关系,所以比较次数就是树的高度,而他是一个完全二叉树,所以比较次数就是log(2,n)。

代码如下:

  binheap.h
typedef int ElementType;
#ifndef BINHEAP_H_INCLUDED
#define BINHEAP_H_INCLUDED
struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;
PriorityQueue Initialize( int MaxElements );
void Destroy( PriorityQueue H );
void MakeEmpty( PriorityQueue H );
void Insert( ElementType X, PriorityQueue H );
ElementType DeleteMin( PriorityQueue H );
ElementType FindMin( PriorityQueue H );
int IsEmpty( PriorityQueue H );
int IsFull( PriorityQueue H );
#endif // BINHEAP_H_INCLUDED
  
binheap.c
#include "binheap.h"
#include "../lib/fatal.h"
#include <stdlib.h>
#define MinPQSize 10
#define MinData -32767
struct HeapStruct
{
    int Capacity;
    int Size;
    ElementType *Elements;
};
PriorityQueue Initialize( int MaxElements )
{
    PriorityQueue H;
    if( MaxElements < MinPQSize )
        Error( "Priority queue size is too small" );
    H = malloc( sizeof( struct HeapStruct ) );
    if( H ==NULL )
        FatalError( "Out of space!!!" );
    
    H->Elements = malloc( ( MaxElements + 1 ) * sizeof( ElementType ) );
    if( H->Elements == NULL )
        FatalError( "Out of space!!!" );
    H->Capacity = MaxElements;
    H->Size = 0;
    H->Elements[ 0 ] = MinData;
    return H;
}
void MakeEmpty( PriorityQueue H )
{
    H->Size = 0;
}

void Insert( ElementType X, PriorityQueue H )
{
    int i;
    if( IsFull( H ) )
    {
        Error( "Priority queue is full" );
        return;
    }
    for( i = ++H->Size; H->Elements[ i / 2 ] > X; i /= 2 )
        H->Elements[ i ] = H->Elements[ i / 2 ];
    H->Elements[ i ] = X;
}
ElementType DeleteMin( PriorityQueue H )
{
    int i, Child;
    ElementType MinElement, LastElement;
    if( IsEmpty( H ) )
    {
        Error( "Priority queue is empty" );
        return H->Elements[ 0 ];
    }
    MinElement = H->Elements[ 1 ];
    LastElement = H->Elements[ H->Size-- ];
    for( i = 1; i * 2 <= H->Size; i = Child )
    {
        
        Child = i * 2;
        if( Child != H->Size && H->Elements[ Child + 1 ] < H->Elements[ Child ] )
            Child++;
        
        if( LastElement > H->Elements[ Child ] )
            H->Elements[ i ] = H->Elements[ Child ];
        else
            break;
    }
    H->Elements[ i ] = LastElement;
    return MinElement;
}
ElementType FindMin( PriorityQueue H )
{
    if( !IsEmpty( H ) )
        return H->Elements[ 1 ];
    Error( "Priority Queue is Empty" );
    return H->Elements[ 0 ];
}
int IsEmpty( PriorityQueue H )
{
    return H->Size == 0;
}
int IsFull( PriorityQueue H )
{
    return H->Size == H->Capacity;
}
void Destroy( PriorityQueue H )
{
    free( H->Elements );
    free( H );
}




2.1 Heapify(A,n,t)

该操作主要用于维持堆的基本性质。假定以RIGHT(t)和LEFT(t)为根的子树都已经是堆,然后调整以t为根的子树,使之成为堆。

void Heapify(int A[], int n, int t) 
  
{ 
  
  int left = LEFT(t); 
  
  int right = RIGHT(t); 
  
  int max = t; 
  
  if(left <= n)     max = A[left] > A[max] ? left : max; 
  
  if(right <= n)     max = A[right] > A[max] ? right : max; 
  
  if(max != A[t]) 
  
  { 
  
    swap(A, max, t); 
  
    Heapify(A, n, max); 
  
  } 
  
}

2.2  BuildHeap(A,n)

该操作主要是将数组A转化成一个大顶堆。思想是,先找到堆的最后一个非叶子节点(即为第n/2个节点),然后从该节点开始,从后往前逐个调整每个子树,使之称为堆,最终整个数组便是一个堆。

void BuildHeap(int A[], int n) 
  
{ 
  
  int i; 
  
  for(i = n/2; i<=n; i++) 
  
  Heapify(A, n, i); 
  
}


2.3 GetMaximum(A,n)

该操作主要是获取堆中最大的元素,同时保持堆的基本性质。堆的最大元素即为第一个元素,将其保存下来,同时将最后一个元素放到A[1]位置,之后从上往下调整A,使之成为一个堆。

void GetMaximum(int A[], int n) 
  
{ 
  
  int max = A[1]; 
  
  A[1] = A[n]; 
  
  n--; 
  
  Heapify(A, n, 1); 
  
  return max; 
  
}


2.4  Insert(A, n, t)

向堆中添加一个元素t,同时保持堆的性质。算法思想是,将t放到A的最后,然后从该元素开始,自下向上调整,直至A成为一个大顶堆。

void Insert(int A[], int n, int t) 
  
{ 
  
  n++; 
  
  A[n] = t; 
  
  int p = n; 
  
  while(p >1 && A[PARENT(p)] < t) 
  
  { 
  
    A[p] = A[PARENT(p)]; 
  
    p = PARENT(p); 
  
  } 
  
  A[p] = t; 
  
  return max; 
  
}


3.  堆的应用

3.1  堆排序

堆的最常见应用是堆排序,时间复杂度为O(N lg N)。如果是从小到大排序,用小顶堆;从大到小排序,用大顶堆。

3.2  在O(n lg k)时间内,将k个排序表合并成一个排序表,n为所有有序表中元素个数。

【解析】取前100 万个整数,构造成了一棵数组方式存储的具有小顶堆,然后接着依次取下一个整数,如果它大于最小元素亦即堆顶元素,则将其赋予堆顶元素,然后用Heapify调整整个堆,如此下去,则最后留在堆中的100万个整数即为所求 100万个数字。该方法可大大节约内存。

3.3 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k))

4. 总结

堆是一种非常基础但很实用的数据结构,很多复杂算法或者数据结构的基础就是堆,因而,了解和掌握堆这种数据结构显得尤为重要。

5. 参考资料

(1)经典算法教程《算法导论》



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值