堆与堆排序

堆与堆排序

这里的堆,不是内存管理中说的堆栈的堆,前者的堆,准确的说是二叉堆,是一种类似于完全二叉树的数据结构;后者的堆是一种类似于链表的数据结构。

完全二叉树

堆的结构性质

二叉堆在逻辑结构上是一颗完全二叉树。

对于一个有 N 个节点的完全二叉树,我们可以为每个节点指定一个索引,其实就是按照层序遍历的方式,如下图。

对于一个有 N 个节点的完全二叉树,索引值和元素是一一对应的,我们可以先用一个数组表示,而不需要指针,索引值就是数组的下标,数组元素的值就是节点的关键字。

如图,完全二叉树和数组的对应关系。

img

有一个规律:对于数组任一位置 i 上的元素,其左儿子在 2 i + 1 2i+1 2i+1 位置上,右儿子在 2 i + 2 2i+2 2i+2 位置上,父亲则在 ( i − 1 ) / 2 (i-1)/2 (i1)/2 位置上。

以节点 D 为例,D的下标是 3,

  • B 是它的父节点,B 的下标是 1 = ( 3 − 1 ) / 2 1 = (3 - 1) / 2 1=(31)/2,图中黑色的线。
  • H 是它的左孩子,H 的下标是 7 = 2 ∗ 3 + 1 7 = 2 * 3 + 1 7=23+1,图中蓝色的线。
  • I 是它的右孩子,I 的下标是 8 = 2 ∗ 3 + 2 8 = 2 * 3 + 2 8=23+2,图中红色的线。

堆序性质

二叉堆一般分为两种:最大堆和最小堆。

  • 最大堆:也叫做大根堆。每一个节点的关键字,都大于或等于它的孩子的值。

    img

  • 最小堆:也叫做小跟堆,每一个节点的关键字,都小于或等于它的孩子的值。

    img

**堆序特性:**以大根堆为例,在任何从跟到某个叶子的路径上,键值的序列是递减的(如果有相同的键值存在,则是非递增的)。但是,键值之间不存在从左到右的次序,也就是说,同一层节点之间,不存在任何关系,更一般的来说,也就是同一节点的左右子树,之间不存在任何关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mrDLHgR-1662559297273)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1644755459996.png)]

  1. 一颗有 n 个节点的完全二叉树。它的高度等于 ( l o g 2 n ) + 1 (log_2 n)+1 (log2n)+1
  2. 堆的根总是包含了堆的最大元素。
  3. 堆的一个节点,及该节点的子孙,也是一个堆。
  4. 可以用数组来实现堆,方法是从上到下,从左到右记录堆的元素。
  5. 在上述方法中,父母节点的键,将会占据前 n / 2 n/2 n/2 个位置,叶子节点的键将会占据后 ( n + 1 ) / 2 (n+1)/2 (n+1)/2 个位置。

**最后一个父母节点:**一个堆有 N 个元素,判断一个节点是不是父母节点,可以判断有没有孩子,如果有孩子,那么 2 i + 1 2i+1 2i+1 一定小于等于 N-1,如果 2 i + 1 2i+1 2i+1 大于 N-1,那么它一定是叶子节点,在它之后的节点,也一定是叶子节点。

所以,得不等式, 2 i + 1 > N − 1 2i+1>N-1 2i+1>N1,取 i 的最小值,就是第一个叶子节点的位置,i 的最小值就是, i = N / 2 i=N/2 i=N/2,最后一个父母节点的值也就是 i = N / 2 − 1 i=N/2-1 i=N/21

构造堆

方法一:自底向上构造堆

构造大根堆:假设有一颗完全二叉树。

  1. 从最后一个父节点开始,检查是否满足父母优势,如果不满足,就将父母节点和子节点中最大值进行交换,然后再检查在新的位置上,是否满足父母优势。循环这个过程,直到这个节点满足父母优势。(对叶子节点来说,父母优势自动满足)
  2. 在对当前这个父节点为根的子树,完成堆化之后,再对前一个父节点(就是数组中此节点的前一个节点)按同样的操作完成堆化。直到对树的根完成这个操作,堆构造完毕。

上述方法称为:下滤。示意图和代码,见附录。

方法二:自顶向下构造堆

通过把新的键连续插入事先构造好的堆,来构造一个堆(效率较低),可以称为自顶向下构造堆。

  1. 首先,将一个键值为 K 的新节点附加在最后一个叶子节点的后面。(也就是数组最后)
  2. 然后,拿 K 和它父母的键作比较,如果 K 小于等于它的父母,算法停止。否则交换这两个键,并把 K 和它的新父母作比较。
  3. 重复 2,一直到 K 不大于它的父母,或者 K 成为树根为止。

上述方法称为上滤,示意图,代码,见附录。

堆排序的基本思想:

  1. 为给定的序列创建一个堆。
  2. 交换堆的第一个元素a[0]和最后一个元素和a[n - 1]
  3. 堆的大小减 1(–n)。如果 1 == n,算法停止,否则对 a[0] 进行下滤。
  4. 重复 2~3 步。

对大跟堆,堆的最顶端元素总是最大的,所以如上述方法,交换第一个元素和最后一个元素,然后检查树根的父母优势,采用上述的下滤函数重新构造出一个堆。

关键点在于如何删除最大键,示意图和代码,见附录。

在每次删除最大元素之后,堆的规模减小了 1,因此,位于最后的那个空间,可以用来存放,刚刚删去的最大值。

附录:

一:自底向上构造堆+下滤

递归+交换:
示意图:

有 10 个键: 4, 1, 3, 2, 16, 9, 10, 14, 8, 7

对应的完全二叉树:

img

依照上面公式,最后一个父母节点位置:10 / 2 = 5,所以我们从 5 号节点开始对这个完全二叉树进行堆化。

示意图:

这里写图片描述

这里写图片描述

img

这里写图片描述

这里写图片描述

这里写图片描述

代码:
// 对于给定的位置 i 节点
#define LEFT(i)    (2*i+1)
#define RIGHT(i)   (2*i+2)
#define PARENT(i)  ((i-1)/2)
// 下滤函数
void percolate_down_recursive(int arr[], int t)
{   
    int left = LEFT(t);
    int right = RIGHT(t);
    int max = t;            //都是索引,不是值
    if(left <= NUM){     //索引t左孩子存在
        max = arr[left] > arr[max] ? left : max;
    }
    if(right <= NUM){   //索引t右孩子存在
        max = arr[right] > arr[max] ? right : max;
    }
    if(max != t){
        swap(arr + t, arr + max);   // 交换函数
        percolate_down_recursive(arr, max);
    }
}
非递归+空穴:

非递归+交换也可以。

示意图:

这里以 1 号节点为例。实际对树进行堆化时,还是按照自底向上的方法,从最后一个父节点开始,采用下滤策略。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

代码:
void percolate_down_no_swap(int arr[], int t)
{   
	int key = arr[t];   //要处理的键值,一个根节点
    int max_idx = -1;   //比较大的索引
    int heap_ok = 0;    //父母优势不满足标志,初始值不满足
    while(!heap_ok && LEFT(t) <= NUM){    //有孩子必定先有左孩子
        max_idx = LEFT(t);      //假设左孩子键值大
        if(LEFT(t) < NUM){       //存在右孩子
            if(arr[LEFT(t)] < arr[RIGHT(t)]){     //找出最大值
                max_idx = RIGHT(t);
            }
        }
        if(key >= arr[max_idx]){     //父母优势满足,跳出
            heap_ok = 1;
        }
        else{
            arr[t] = arr[max_idx];
            t = max_idx;            //下一轮,孩子的子树 父母优势判断,
        }
    }
    arr[t] = key;
}

一:自顶向下构造堆+上滤

依然使用4, 1, 3, 2, 16, 9, 10, 14, 8, 7这列键 。

示意图:

img

这里写图片描述

这里写图片描述

img

这里写图片描述

这里写图片描述

这里写图片描述

代码:
void insert(element_t x, priority_queue max_heap)
{
    if( is_full(max_heap) ){
        printf("priority queue is full, insert failed\n");
        return;
    }
    // 把新节点附加在当前堆的最后一个叶子后面
    max_heap->elements[ ++max_heap->size ] = x; 
    int cur = max_heap->size; // cur 指向新的节点
#ifdef MAX_HEAP_SENTINEL
    while( x > max_heap->elements[ cur/2 ] ) // 把新节点的键值和它的父母比较
#else
    // 当 cur == 1 时,说明新节点已经被交换到了树根的位置,此时应该跳出循环
    while( (cur != 1) && (x > max_heap->elements[ cur/2 ]) )
#endif
    {
        // 交换新节点和它的父母
        swap(max_heap->elements + cur/2, max_heap->elements + cur);
        cur /= 2;   // cur继续指向新节点
    }
}
// swap操作比较费时,上面的函数可以优化一下
void insert_no_swap(element_t x, priority_queue max_heap)
{
    if( is_full(max_heap) ){
        printf("priority queue is full, insert failed\n");
        return;
    }
    // 在当前堆的最后一个叶子后面创建一个空穴,用cur指向空穴
    int cur = ++max_heap->size; 
#ifdef MAX_HEAP_SENTINEL
    while( x > max_heap->elements[ cur/2 ] ) // 新节点的键值和空穴的父母作比较
#else
    while( (cur != 1) && (x > max_heap->elements[ cur/2 ]) )
#endif
    {
        // 父母向下移动一层,填补空穴,原父母的位置成为新的空穴
        max_heap->elements[cur] = max_heap->elements[cur/2];
        cur /= 2;   
    }
    max_heap->elements[cur] = x; //把新节点填入空穴
}

堆排序:

删除最大键:

假设依据上面的方法,构造好了一个大根堆。对应数组是: [16,14,10,8,7,3,9,1,4,2]

示意图:

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

代码:
element_t delete_max(priority_queue max_heap)
{
    if( is_empty( max_heap ) ){
        printf("priority queue is empty, return [0]\n");
        return max_heap->elements[0];
    }
    if( max_heap->size == 1){
        max_heap->size = 0;
        return max_heap->elements[1];
    }
    else{   
        element_t max_elmt = max_heap->elements[1];
        // 得到最后一个元素,并且堆的大小减一
        element_t last_elmt = max_heap->elements[max_heap->size--];
        // 把最后一个元素移动到树根的位置
        max_heap->elements[1] = last_elmt;
        // 调整树根
        percolate_down_no_swap(max_heap->elements,max_heap->size ,1);   
        return max_elmt;
    }
}

完整代码:

#include <stdio.h>

#define LEFT(i)             (2*i+1)
#define RIGHT(i)            (2*i+2)
#define PARENT(i)           ((i-1)/2)

#define LAST_PARENT(n)           (((n)/2) - 1)     // yang

#define ELMT_NUM            10
#define DUMMY_POS           -1
#define DUMMY_TOKEN         '\0'

typedef int element_t;

void print_array_debug(int a[], int len, int pos, char token)
{
    for(int i=0; i<len; ++i)
    {
        if( i == pos )
        {
            printf("%c %d ", token, a[i]); //打印元素值和记号
        }
        else
        {
            printf("%3d ",a[i]); //正常打印
        }
    }
    printf("\n\n");
}

//交换*a和*b
void swap(int* a, int* b) 
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 下滤函数(递归解法)
// 假定以 LEFT(t) 和 RIGHT(t) 为根的子树都已经是大根堆
// 调整以 t 为根的子树,使之成为大根堆。
// 节点位置为 [0], [1], [2], ..., [n-1]
void percolate_down_recursive(int a[], int n, int t) 
{   
    int left = LEFT(t);
    int right = RIGHT(t);   
    int max = t; //假设当前节点的键值最大

    if(left < n)  // 说明t有左孩子    
    {
        max = a[left] > a[max] ? left : max;
    }

    if(right < n)  // 说明t有右孩子  
    {
        max = a[right] > a[max] ? right : max;
    }

    if(max != t)
    {   
        swap(a + max, a + t); // 交换t和它的某个孩子,即t被换到了max位置
        percolate_down_recursive(a, n, max); // 递归,继续考察t
    }
}

// 自底向上建堆,下滤法
void build_max_heap(element_t a[], int n) 
{   
    int i;
    // 从最后一个父母节点开始下滤,一直到根节点
    //for(i = PARENT(n); i >= 0; --i)        // yang
    for(i = LAST_PARENT(n); i >= 0; --i)
    {       
        percolate_down_recursive(a, n, i);
    }
}

//把最大元素和堆末尾的元素交换位置,堆的规模减1,再下滤根节点
void delete_max_to_end(int heap[], int heap_size)
{
    if(heap_size == 2) // 当剩下2个节点的时候,交换后不用下滤
    {
        swap( heap + 0, heap + 1 );     
    }
    else if(heap_size > 2)
    {
        swap( heap + 0, heap + heap_size - 1 );
        percolate_down_recursive(heap, heap_size-1, 0);
    }
    return;
}

void heap_sort(int a[], int length)
{
    build_max_heap(a,length);
#ifdef PRINT_PROCEDURE
    printf("build the max heap:\n");
    print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
#endif
    for(int size=length; size>=2; --size)
    {
        delete_max_to_end(a,size);
#ifdef PRINT_PROCEDURE      
        print_array_debug(a, ELMT_NUM, size-1, '|');
#endif
    }
}

int main(void)
{
    int a[ELMT_NUM]={4,1,3,2,16,9,10,14,8,7}; //10个
    printf("the array to be sorted:\n ");   
    print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);

    heap_sort(a,ELMT_NUM);

    printf("sort finished:\n ");
    print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
}

程序运行结果截图:

在这里插入图片描述img

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值