看懂堆排序——堆与堆排序(三)

  • 看懂堆排序——堆与堆排序(三)

    • 堆排序的基本思想
    • 代码详解
      • 父亲下标和孩子下标的关系
      • 打印数组的函数
      • 下滤函数
      • 构造堆的函数
      • 删除最大元函数
      • 排序主函数
    • 完整代码及运行截图
  • 参考资料

有了前面两篇文章的铺垫,

堆与堆排序(一)

堆与堆排序(二)

我们终于可以学习“堆排序”了。

假使给定一个数组a[N],其包含元素a[0],a[1],a[2],…,a[N-1],现要将其中元素按升序排列。如果利用堆这种数据结构,你会怎么做?

堆排序的基本思想

很自然地想到,首先把此数组构造成一个小根堆(利用原来的数组,原地构造),然后依次删除最小元素,直至堆为空。为了存储每次删除的最小元素,我们需要一个额外的数组,待堆为空的时候,把额外数组的内容拷贝到原来的数组。

这种方法可行吗?当然可行。但是需要一个额外的数组,当数组很大时,这个空间开销是非常可观的。避免使用额外的数组的聪明做法是意识到这样一个事实:在每次删除最小元素之后,堆的规模缩小了1. 因此,位于堆中最后的那个单元可以用来存放刚刚删去的元素。具体来说,堆排序的步骤如下:

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

代码详解

父亲下标和孩子下标的关系

因为待排序的数组一般都是从0开始,而不是从1开始,所以之前讨论的父节点和孩子节点之间的关系需要修改。

之前的是:

对于数组任一位置 i 上的元素,其左儿子在位置 2i 上,右儿子在2i+1上,它的父亲则在位置 ⌊i/2⌋⌊i/2⌋\lfloor i/2 \rfloor上。

现在的是

对于数组任一位置 i 上的元素,其左儿子在位置 2i+1 上,右儿子在2i+2上,它的父亲则在位置 ⌊(i−1)/2⌋⌊(i−1)/2⌋\lfloor (i-1)/2 \rfloor上。

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

  • B是它的父节点,B的下标是 1(= ⌊(3−1)/2⌋⌊(3−1)/2⌋\lfloor (3-1)/2 \rfloor),如图中黑色的线;
  • H是它的左孩子,H的下标是 7(= 2∗3+12∗3+12*3+1),如图中蓝色的线;
  • I是它的右孩子,I的下标是 8(= 2∗3+22∗3+22*3+2),如图中红色的线;

所以,我们的宏定义是:

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

打印数组的函数

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[0]的前面打印一个'*',那么可以这样调用(假设数组长度是10):

print_array_debug(a, 10, 0, '*');

如果不想用它的打印标记功能,则可以给pos传一个负数,给token随便什么值都行。比如

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

然后调用

print_array_debug(a, 10, DUMMY_POS, DUMMY_TOKEN);

下滤函数

对于给定的数列,我们首先要对其进行“堆化”,堆化的方法如下:

  1. 在初始化一棵包含 n 个节点的完全二叉树时,按照给定的顺序来放置键;

  2. 从最后一个父母节点开始,到根为止,检查这些父母节点的键是否满足父母优势。如果该节点不满足,就把该节点的键 K 和它子女的最大键进行交换,然后再检查在新的位置上,K 是否满足父母优势。这个过程一直继续到 K 满足父母优势为止(最终它必须满足,因为对每个叶子中的键来说,这条件是自动满足的)。

如果该节点不满足父母优势,就把该节点的键 K 和它子女的最大键进行交换,然后再检查在新的位置上,K 是否满足父母优势。这个过程一直继续到 K 满足父母优势为止——这种策略叫做下滤(percolate down)

// 下滤函数(递归解法)
// 假定以 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){       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_PROCEDUREprintf("build the max heap:\n");print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
#endiffor(int size=length; size>=2; --size) //当堆的大小为1时,停止{delete_max_to_end(a,size);
#ifdef PRINT_PROCEDURE      print_array_debug(a, ELMT_NUM, size-1, '|');
#endif}
}

完整代码及运行截图

#include <stdio.h>#define LEFT(i)             (2*i+1)
#define RIGHT(i)            (2*i+2)
#define PARENT(i)           ((i-1)/2)
#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){       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_PROCEDUREprintf("build the max heap:\n");print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
#endiffor(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);
}

假设文件名为heap_sort.c,编译:

gcc heap_sort.c -DPRINT_PROCEDURE

运行结果如下图:

图中竖线右边的是已经有序的元素,竖线左边是堆。

【本系列完】

参考资料

【1】《数据结构与算法分析(原书第2版)》(机械工业出版社,2004)
【2】《算法设计与分析基础(第3版)》(清华大学出版社,2015)

看懂堆排序——堆与堆排序(三)相关推荐

  1. 一文看懂Python 爬虫 进阶(三)

    一文看懂Python 爬虫 进阶(三) 文章目录 一文看懂Python 爬虫 进阶(三) **猫眼电影(xpath)** **链家二手房案例(xpath)** **百度贴吧图片抓取** 这篇几乎都是代 ...

  2. 一文看懂阿里产业AI:三驾马车、一个飞轮

    曾震宇 新智元 昨天   新智元报道    演讲人:曾震宇 编辑:肖琴 [新智元导读]产业AI作为核心引擎,驱动数据和业务发展的飞轮,推动业务价值快速增长.阿里云数据智能总经理曾震宇解读了产业AI的关 ...

  3. 一张图看懂java 堆和栈

    JAVA的JVM的内存可分为3个区:堆(heap).栈(stack)和方法区(method) 栈区: 每个线程包含一个栈区,栈中只保存方法中(不包括对象的成员变量)的基础数据类型和自定义对象的引用(不 ...

  4. 教你如何看懂体检报告

    九张图教你看懂体检报告 4.肿瘤三项 5.血糖 6.肝功 7.血脂 8.尿液 9.血常规 转载自人们日报~~ 建议注意饮食,多运动~~  祝安康~~

  5. 算法学习笔记17:堆、堆排序

    目录 堆和堆排序:为什么说堆排序没有快速排序快 如何理解"堆" 如何实现一个堆 1. 往堆中插入一个元素 2. 删除堆顶元素 如何基于堆实现排序 1. 建堆 2. 排序 解答开篇 ...

  6. 白话经典算法系列之七 堆与堆排序

     堆排序与高速排序,归并排序一样都是时间复杂度为O(N*logN)的几种常见排序方法.学习堆排序前,先解说下什么是数据结构中的二叉堆. 二叉堆的定义 二叉堆是全然二叉树或者是近似全然二叉树. 二叉堆满 ...

  7. 建堆 java_堆排序就这么简单

    一.堆排序介绍 来源百度百科: 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种.可以利用数组的特点快速定位指定索引的元素.堆分为大根堆和小根堆,是完 ...

  8. linux 堆地址,堆与堆排序_Linux编程_Linux公社-Linux系统门户网站

    堆排序与 二叉堆的定义 二叉堆是完全二叉树或者是近似完全二叉树. 二叉堆满足二个特性: 1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值. 2.每个结点的左子树和右子树都是一个二叉堆( ...

  9. 堆的C语言实现——堆与堆排序(二)

    堆的C语言实现--堆与堆排序(二) 堆的C语言实现--堆与堆排序(二) 头文件 初始化函数 下滤函数1(递归) 堆构造函数1(自底向上,递归) 下滤函数2(非递归,交换法) 下滤函数3(非递归,空穴法 ...

最新文章

  1. JDK1.8 中的双冒号::是什么语法?
  2. VO 2 具体的过程
  3. Linux系统Sudo基本用法
  4. java 整数 字节数组_将整数转换为字节数组(Java)
  5. Unable to instantiate org.apache.hadoop.hive.ql.metadata.SessionHiveMetaStoreClient 1
  6. reactor模式:多线程的reactor模式
  7. Js正则表达式数字或者带小数点的数字
  8. 【信号】函数kill、raise、abort、alarm
  9. 大数据学习(1)-大数据概述
  10. xcodebuild构建时报错unknown error -1=ffffffffffffffff Command /bin/sh failed with exit code 1
  11. 自学python能学成吗-自学Python能学会吗 零基础怎么学
  12. 如何看数据库是否处在force_logging模式下
  13. xtrabackup之Innobackupex全备数据库
  14. P2P终结者和反P2P终结者如何使用
  15. 计算机模拟水循环的过程,袋装水模拟做科学小实验水循环(步骤图解)
  16. 我有酒,你有故事吗?
  17. python字典统计单词个数_python字典统计单词个数
  18. CISP证书价值​NISP证书价值|CISP和NISP含金量如何
  19. redis学习--三种特殊数据类型,GEO地理位置,HyperLogLog,BitMap
  20. 2075 Problem G 点菜问题

热门文章

  1. NYOJ 679 贪婪的商店
  2. hdu 1203 I NEED A OFFER!
  3. k8s之wsl2+kind+docker desktop搭建kubevela测试集群
  4. 基于各种基础数据结构的SPFA和各种优化
  5. npm和node.js升级
  6. Kotlin学习笔记-----函数的定义
  7. HttpContext.Cache属性
  8. HDU 5828 Rikka with Sequence (线段树+剪枝优化)
  9. Spring 事情具体详尽的解释
  10. SQL的四种连接-左外连接、右外连接、内连接、全连接