欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

堆、堆的操作、优先级队列

程序员文章站 2024-02-11 20:38:58
...

●将完全二叉树的结构特点套用在一个一维数组上,堆的实质是一个一维数组。
什么样的一维数组才称为堆?
●将数组中的元素按照完全二叉树的方式排列起来(如图):
堆、堆的操作、优先级队列
使其满足任一结点的数值均小于(大于)等于它的左右孩子结点的数值,位于堆顶结点的数值最小(最大),从根结点到每个结点的路径上数组元素组成的序列都是递增(递减)的
就称这个一维数组为堆。
若位于堆顶结点的数值最大,则为大堆
若位于堆顶结点的数值最小,则为小堆
堆存储与下标为0开始的一维数组中,因此在堆中给定下标为i的结点时:
如果i=0,则结点i是根结点,没有双亲结点;否则结点i的双亲结点为结点(i-1)/2
如果2*i+1<=n-1,则结点i的左孩子为结点2*i+1,否则结点i无左孩子
如果2*i+2<=n-1,则结点i的右孩子为结点2*i+2,否则结点i无右孩子

堆的创建

给出堆的结构体:

#define MAXSIZE 50
typedef int(*Compare)(HDataType, HDataType);//函数指针,用来确定要创建大堆还是小堆
typedef int HDataType;
typedef struct Heap
{
    HDataType* _array;//存放数据的一维数组
    int _capacity;//容量
    int _size;//已经使用的大小
    Compare _com;//创建大堆还是小堆
}Heap;

●首先要以完全二叉树的结构套用所给定的一维数组,0号位置为根结点,1为根结点的左孩子,2为根结点的右孩子……
●将二叉树调整为最小(大)堆的原理:

从最后一个非叶子结点开始调整,一直到根结点为止,将每个结点及其子树调整到满足最小堆的性质即可

int Less(HDataType Left, HDataType Right)//若要创建小堆,则就将此函数的地址(函数名)作为参数传递给创建堆的函数
{
    return Left < Right;
}
int Greater(HDataType Left, HDataType Right)//若要创建大堆,则就将此函数的地址(函数名)作为参数传递给创建堆的函数
{
    return Left>Right;
}


//创建堆
void CreateHeap(Heap* hp, HDataType* array, int size, Compare com)
{
    int i = 0;
    int Root = 0;
    if (NULL == hp)
    {
        assert(0);
        return;
    }
    //指明堆的调整方式(大堆/小堆)
    hp->_com = com;

    //给堆开辟空间
    hp->_capacity = MAXSIZE;
    hp->_array = (HDataType*)malloc(sizeof(HDataType)*hp->_capacity);
    if (NULL == hp->_array)
    {
        assert(0);
        return;
    }
    //将数组赋给堆
    hp->_size = size;
    for (i = size - 1; i >= 0; i--)
    {
        hp->_array[i] = array[i];
    }
    //com指明要创建一个小堆
    //整理堆
    Root = (size - 2) / 2;
    for (; Root >= 0; --Root)
    {
        _AdjustDown(hp, Root);
    }
}
int Less(HDataType Left, HDataType Right)
{
    return Left < Right;
}
int Greater(HDataType Left, HDataType Right)
{
    return Left>Right;
}


//小/大堆的向下调整函数(向下调整法)
void _AdjustDown(Heap* hp, int Root)
{
    int Parent = 0;
    int Child = 0;
    Parent = Root;
    Child = Parent * 2 + 1;

    while (Child < hp->_size)
    {
        //判断他的右孩子是否存在
        if (Child + 1 <= hp->_size - 1)
        {
            //找出两个孩子中较小的一个
            if (Child+1<hp->_size && hp->_com(hp->_array[Child + 1] , hp->_array[Child]))
                Child += 1;
        }

        //较小的孩子与双亲节点比较,如果双亲节点较大就交换
        if (hp->_com(hp->_array[Child], hp->_array[Parent]))
        {
            Swap(&hp->_array[Parent], &hp->_array[Child]);
        }
        Parent = Child;
        Child = Parent * 2 + 1;
    }
}

给堆中插入元素

●检测堆中是否还有可用空间,若无则需给堆增容
●将要插入的数据作为一个叶子节点,插在堆的末尾
●从插入的结点的双亲结点开始向上调整堆,直至根结点

//在堆中插入一个元素
void InsertHeap(Heap* hp, HDataType data)
{
    int Parent = 0;
    int Child = 0;
    assert(hp);
    if (hp->_size >= hp->_capacity)
        return;

    //插入时,首先要检测堆中是否还有可用空间
    _CheckCapacity(hp);

    //将要插入的数据连接在堆的末尾
    hp->_array[hp->_size] = data;
    hp->_size++;

    //使用向上调整法调整堆
    Child = hp->_size - 1;
    while (Child > 0)
    {
        _AdjustUp(hp, Child);
        Child = ((Child - 1) >> 1);
    }
}


//向上调整法
void _AdjustUp(Heap* hp, int Root)
{
    int Parent = 0;
    int Child = 0;
    //让child指向新插入的指针
    Child = Root;
    Parent = ((Child - 1) >> 1);

    //调整堆
    while (0 <= Parent)
    {
        if (hp->_com(hp->_array[Child], hp->_array[Parent]))
        {
            Swap(&hp->_array[Child], &hp->_array[Parent]);
        }
        Child = Parent;
        Parent = ((Parent - 2) >> 1);
    }
}


//检测堆中是否还有可用空间
void _CheckCapacity(Heap* hp)
{
    assert(hp);
    if (hp->_capacity == hp->_size)
    {
        realloc(hp->_array, sizeof(HDataType)*(hp->_capacity * 2));//增容时,默认将容量增加到原容量的两倍
        if (NULL == hp->_array)
        {
            assert(0);
            return;
        }
    }
}

删除堆顶元素

●用堆的最后一个叶子结点覆盖堆顶结点
●释放堆的最后一个叶子结点
●重新进行堆调整

//删除堆顶元素 
//直接删除堆顶的元素,然后把堆中最末尾的元素放在堆顶,再进行堆调整
void DeleteHeapTop(Heap* hp)
{
    assert(hp);
    if (EmptyHeap(hp))
    {
        assert(0);
        return;
    }
    //用堆末尾的元素覆盖堆顶的元素
    hp->_array[0] = hp->_array[hp->_size - 1];
    hp->_size--;

    //进行堆调整
    _AdjustDown(hp, 0);//因为其他位置原先已经是调整好的堆,改动了根结点,所以只需从根结点开始向下调整
}

销毁堆

●释放所开辟的空间
●销毁所有数值

//销毁堆
void DestroyHeap(Heap* hp)
{
    assert(hp);
    free(hp->_array);
    hp->_array = NULL;
    hp->_capacity = 0;
    hp->_size = 0;
    hp->_com = NULL;
}

堆的应用

堆排序

●先将数据调整为一个小(大)堆
●交换堆顶元素与堆末尾的元素(将最小(大)的数据放置在堆尾)
●将堆的size-1,即将堆尾的元素不在视为堆的一份子
●循环此过程,直至堆中只剩下一个元素

//堆排序
//1.先交换堆顶元素与末尾元素(即已经把最大的元素放在末尾)
//2.下次调整就不包括堆中的最后一个元素
void SortHeap(Heap* hp)
{
    int Parent = 0;
    int Child = 0;
    int size = 0;
    assert(hp);
    size = hp->_size;

    while (hp->_size > 0)
    {
        //调整堆
        Parent = (hp->_size - 2) / 2;
        Child = Parent * 2 + 1;
        while (0 <= Parent)
        {
        _AdjustDown(hp, Parent);
        Parent = ((Parent - 1) >> 1);
        }
        //PrintHeap(*hp);
        //交换堆顶元素和末尾元素
        Swap(&hp->_array[0], &hp->_array[hp->_size - 1]);

        //吐出最后一个元素
        --(hp->_size);
    }
    hp->_size = size;
}


//交换节点所存储的值
void Swap(HDataType* first, HDataType* second)
{
    HDataType tmp = 0;
    tmp = *first;
    *first = *second;
    *second = tmp;
}

用堆解决Top-K问题

eg:100亿个数据中找出最大的前K个数
●用数据的前10个元素创建一个小堆
●遍历剩下的元素,若大于堆顶元素,则用它覆盖原本的堆顶元素,再调整堆,若小于,则遍历下一个元素,当遍历完成时,堆中的所有元素即就是所要求的元素

//用堆解决Top_k问题
//先用数据的前K个元素建立一个小堆
//剩余的数据与堆顶元素比较  
//如果大于堆顶元素就覆盖堆顶元素然后重新调整堆
//如果小于就继续查看下一个元素
//循环此过程直至数据遍历完成
void TopKByHeap(HDataType* array, int K, int size)
{
    int i = 0;
    //初始化堆
    Heap hp;
    InitHeap(&hp, Less);

    //建立有K个节点的小堆
    while (hp._size < K)
    {
        InsertHeap(&hp, array[i]);
        i++;
    }

    //调整后续元素
    while (i < size)
    {
        //如果后续元素大于堆顶元素
        if (array[i]>hp._array[0])
        {
            //覆盖堆顶元素
            hp._array[0] = array[i];

            //调整堆
            _AdjustDown(&hp, 0);
        }
        i++;
    }
    printf("Top-K:");
    PrintHeap(hp);//打印堆
}

优先级队列

优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。

//堆的初始化
void InitHeap(Heap* hp, Compare com)
{
    assert(hp);
    hp->_array = NULL;
    hp->_size = 0;
    hp->_capacity = MAXSIZE;
    hp->_array = (HDataType*)malloc(sizeof(HDataType)*hp->_capacity);
    hp->_com = com;
}


//优先级队列的初始化
void PriorityQueueInit(PriorityQueue* q, Compare com)
{
    assert(q);

    //初始化优先级队列中的堆
    InitHeap(&q->_hp, com);
}

//在堆中插入一个元素
void InsertHeap(Heap* hp, HDataType data)
{
    int Parent = 0;
    int Child = 0;
    assert(hp);
    if (hp->_size >= hp->_capacity)
        return;

    //插入时,首先要检测堆中是否还有可用空间
    _CheckCapacity(hp);

    //将要插入的数据连接在堆的末尾
    hp->_array[hp->_size] = data;
    hp->_size++;

    //使用向上调整法调整堆
    Child = hp->_size - 1;
    while (Child > 0)
    {
        _AdjustUp(hp, Child);
        Child = ((Child - 1) >> 1);
    }
}


//向队列中插入元素
void PriorityQueuePush(PriorityQueue* q, HDataType data)
{
    assert(q);
    InsertHeap(&q->_hp, data);
}


//直接删除堆顶的元素,然后把堆中最末尾的元素放在堆顶,再进行堆调整
void DeleteHeapTop(Heap* hp)
{
    assert(hp);
    if (EmptyHeap(hp))
    {
        assert(0);
        return;
    }
    //用堆末尾的元素覆盖堆顶的元素
    hp->_array[0] = hp->_array[hp->_size - 1];
    hp->_size--;

    //进行堆调整
    _AdjustDown(hp, 0);
}


//删除优先级最高的元素
void PriorityQueuePop(PriorityQueue* q)
{
    assert(q);
    DeleteHeapTop(&q->_hp);
}

//获取堆中元素个数
int SizeHeap(Heap* hp)
{
    assert(hp);
    return hp->_size;
}


//获取优先级队列元素个数
int PriorityQueueSize(PriorityQueue* q)
{
    assert(q);
    return SizeHeap(&q->_hp);
}

//检测一个堆是否为空堆
int EmptyHeap(Heap* hp)
{
    assert(hp);
    return 0 == hp->_size;
}


//检测优先级队列是否为空
int PriorityEmpty(PriorityQueue* q)
{
    assert(q);
    return EmptyHeap(&q->_hp);
}


//获取优先级队列的堆顶元素
HDataType PriorityQueueTop(PriorityQueue* q)
{
    assert(q);
    return (q->_hp)._array[0];
}

//销毁堆
void DestroyHeap(Heap* hp)
{
    assert(hp);
    free(hp->_array);
    hp->_array = NULL;
    hp->_capacity = 0;
    hp->_size = 0;
    hp->_com = NULL;
}


//销毁优先级队列
void PriorityQueueDestroy(PriorityQueue* q)
{
    assert(q);
    DestroyHeap(&q->_hp);
}