观看本系列博文提醒:
- 你将学会堆的原理 和 算法实现;
- 一个企业级应用:堆实现优先队列;
- 还有堆排序;
- 最后还有一道检测是否掌握堆算法的作业。
这已经是本系列博文的第三篇了,还没看过第二篇博文:C/C++ 入门核心算法:堆的企业级应用 之 堆实现优先队列 的朋友可以点击下面链接去了解一下。
https://editor.csdn.net/md/?articleId=105602667
我们下面所讲的案例完全是依照本系列第一篇博文来将的,所以强烈建议大家先去看本系列的第一篇博文 入门核心算法大局观:堆 ,然后再看这篇博文。
https://blog.csdn.net/cpp_learner/article/details/105599877
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素.
(选择排序工作原理 - 第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零) 。
如下图:
之前的博文讲的是堆顶出堆后,堆顶元素也就没了,现在呢,我们将他和最后一个元素替换位置,然后堆的长度减一,这样堆就无法访问到它了,最后再进行堆的重新排序,又可以形成最大堆了。
当堆中还剩下一个元素时,元素就已经排好序了,如上图中排好序是这样的:
82, 86, 87, 92, 93, 95, 99
升序排序,如果再代码中将堆的节点判断搞相反的话,就可以弄成降序排序。
也即是下面代码中的这条代码:
#define IsEstimate(a, b) (a < b) // 判断两数大小
只需要改变一下宏判断的关系元素符就行了。
好了,原理讲到这里了,下面开始码代码。
定义
typedef int DateType;
#define IsEstimate(a, b) (a < b) // 判断两数大小
typedef struct _heap {
DateType *arr;
int size; // 当前存储个数
int capacity; // 当前存储容量
}Heap;
定义一个单纯的堆结构体。
#define IsEstimate(a, b) (a < b) // 判断两数大小
:用于判断两个数据的大小。
定义算法所需要的函数:
bool initHeap(Heap &heap, DateType *arr, int size); // 初始化堆
static void buildHeap(Heap &heap); // 建堆
static void heapDown(Heap &heap, int index); // 堆节点下移
void heapSort(Heap &heap); // 排序
比之前的少了很多,因为是排序,所以我们不需要插入和删除的操作。
下面是算法的具体实现:
建堆
void heapDown(Heap &heap, int index) {
int parent, child;
int cur = heap.arr[index];
for (parent = index; parent * 2 + 1 < heap.size; parent = child) {
child = parent * 2 + 1; // 计算出左子节点的小标
// 不管有没有右子节点,此条if判断都可以得到最大的子节点下标
if (child + 1 < heap.size && IsEstimate(heap.arr[child], heap.arr[child + 1])) {
child++;
}
if (IsEstimate(heap.arr[child], cur)) { // 如果父节点大于子节点,那么无需执行任何操作
break;
} else {
heap.arr[parent] = heap.arr[child]; // 进行头尾交换数据
heap.arr[child] = cur;
}
}
}
void buildHeap(Heap &heap) { // 从最后一个节点的父节点开始往回遍历
for (int i = heap.size / 2 - 1; i >= 0; i--) {
heapDown(heap, i);
}
}
bool initHeap(Heap &heap, DateType *arr, int size) {
if (!arr) {
return false;
}
// 堆指向了数组,那么就是堆可以随遍调整数组中的顺序
/*******************************************/
heap.arr = arr; // 将数组指针赋值给堆的指针(无需再分配内存)
/*******************************************/
heap.capacity = size;
heap.size = size;
if (size > 0) { // 直到堆中还剩下一个数据为止
buildHeap(heap);
}
}
这里有一个重点:heap.arr = arr; // 将数组指针赋值给堆的指针(无需再分配内存)
这步操作,使得堆可以直接操作数组中的元素,实现更加高效的排序。
排序:
void heapSort(Heap &heap) {
if (heap.size < 1) {
return;
}
while (heap.size > 0) { // 直到堆中还剩下一个数据为止
int value = heap.arr[0]; // 保存头部的值
heap.arr[0] = heap.arr[heap.size - 1]; // 将尾部的值赋值给头部
heap.arr[heap.size - 1] = value; // 将头部的值赋值给尾部(实现两数交换)
heap.size--; // 长度减一,将原先头部的值排除在外(即堆无法访问到它)
heapDown(heap, 0); // 将头节点的值排序一遍
}
}
函数执行完后,数组中的值也就已经排好序了。
下面我们来测试一下:
#include <iostream>
#include <Windows.h>
using namespace std;
typedef int DateType;
#define IsEstimate(a, b) (a < b) // 判断两数大小
typedef struct _heap {
DateType *arr;
int size; // 当前存储个数
int capacity; // 当前存储容量
}Heap;
bool initHeap(Heap &heap, DateType *arr, int size); // 初始化堆
static void buildHeap(Heap &heap); // 建堆
static void heapDown(Heap &heap, int index); // 堆节点下移
void heapSort(Heap &heap); // 排序
void heapDown(Heap &heap, int index) {
int parent, child;
int cur = heap.arr[index];
for (parent = index; parent * 2 + 1 < heap.size; parent = child) {
child = parent * 2 + 1; // 计算出左子节点的小标
// 不管有没有右子节点,此条if判断都可以得到最大的子节点下标
if (child + 1 < heap.size && IsEstimate(heap.arr[child], heap.arr[child + 1])) {
child++;
}
if (IsEstimate(heap.arr[child], cur)) { // 如果父节点大于子节点,那么无需执行任何操作
break;
} else {
heap.arr[parent] = heap.arr[child]; // 进行头尾交换数据
heap.arr[child] = cur;
}
}
}
void buildHeap(Heap &heap) { // 从最后一个节点的父节点开始往回遍历
for (int i = heap.size / 2 - 1; i >= 0; i--) {
heapDown(heap, i);
}
}
bool initHeap(Heap &heap, DateType *arr, int size) {
if (!arr) {
return false;
}
// 堆指向了数组,那么就是堆可以随遍调整数组中的顺序
/*******************************************/
heap.arr = arr; // 将数组指针赋值给堆的指针(无需再分配内存)
/*******************************************/
heap.capacity = size;
heap.size = size;
if (size > 0) { // 当数组中有数据时
buildHeap(heap);
}
}
void heapSort(Heap &heap) {
if (heap.size < 1) {
return;
}
while (heap.size > 0) { // 直到堆中还剩下一个数据为止
int value = heap.arr[0]; // 保存头部的值
heap.arr[0] = heap.arr[heap.size - 1]; // 将尾部的值赋值给头部
heap.arr[heap.size - 1] = value; // 将头部的值赋值给尾部(实现两数交换)
heap.size--; // 长度减一,将原先头部的值排除在外(即堆无法访问到它)
heapDown(heap, 0); // 将头节点的值排序一遍
}
}
int main(void) {
DateType arr[] = { 23, 54, 24, 88, 45, 8, 1, 9, 55 };
Heap heap;
cout << "堆排序前:" << endl;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << arr[i] << " ";
}
cout << endl;
// 初始化堆
initHeap(heap, arr, sizeof(arr) / sizeof(arr[0]));
// 排序(升序)
heapSort(heap);
cout << endl << "堆排序后:" << endl;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
运行截图:
当我们将宏比较修改为:
#define IsEstimate(a, b) (a > b) // 判断两数大小
小于号比较的话
数组中的值已经变成降序排序了。
总结:
至此,堆排序已经讲完了,不知道大家学会没有呢?没学会的话,推荐先去看本系列第一篇博文,然后再来看本篇博文,相信你们一定可以理解的。
下面给出本系列博文最后一道作业:检测是否掌握堆算法的作业。
快速查找无序集合中前 N 大(小)的记录。
如果能独立做出来的话,说明你已经掌握堆算法啦!
下面是作业答案:
#include <iostream>
#include <Windows.h>
// 快速查找无序组合中的前N大(小)的记录
using namespace std;
typedef int DateType;
#define IsEstimate(a, b) (a < b) // 判断两数大小
typedef struct _heap {
DateType* arr;
int size; // 当前存储个数
int capacity; // 当前存储容量
}Heap;
bool initHeap(Heap& heap, DateType* arr, int size);
static void buildHeap(Heap& heap);
static void heapDown(Heap& heap, int index);
void heapSort(Heap& heap, int n); // 出队前n个元素
void heapDown(Heap& heap, int index) {
int parent, child;
int cur = heap.arr[index];
for (parent = index; parent * 2 + 1 < heap.size; parent = child) {
child = parent * 2 + 1; // 计算出左子节点的小标
// 不管有没有右子节点,此条if判断都可以得到最大的子节点下标
if (child + 1 < heap.size && IsEstimate(heap.arr[child], heap.arr[child + 1])) {
child++;
}
if (IsEstimate(heap.arr[child], cur)) {
break;
}
else {
heap.arr[parent] = heap.arr[child];
heap.arr[child] = cur;
}
}
}
void buildHeap(Heap& heap) {
for (int i = heap.size / 2 - 1; i >= 0; i--) {
heapDown(heap, i);
}
}
bool initHeap(Heap& heap, DateType* arr, int size) {
if (!arr) {
return false;
}
heap.arr = arr;
heap.capacity = size;
heap.size = size;
if (size > 0) {
buildHeap(heap);
}
}
void heapSort(Heap& heap, int n) {
if (heap.size < 1) {
return;
}
if (n > heap.size) {
return;
}
while (n > 0) { // 判断前n个数值
int value = heap.arr[0]; // 保存头部的值
heap.arr[0] = heap.arr[heap.size - 1]; // 将尾部的值赋值给头部
heap.arr[heap.size - 1] = value; // 将头部的值赋值给尾部(实现两数交换)
heap.size--; // 长度减一,将原先头部的值排除在外(即堆无法访问到它)
cout << "值:" << value << endl;
n--;
heapDown(heap, 0); // 将头节点的值排序一遍
}
}
int main(void) {
DateType arr[] = { 23, 54, 24, 88, 45, 8, 1, 9, 55 };
Heap heap;
cout << "堆初始化前数组中的值:" << endl;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << arr[i] << " ";
}
cout << endl << endl;
// 初始化堆
initHeap(heap, arr, sizeof(arr) / sizeof(arr[0]));
cout << "堆初始化后数组中的值:" << endl;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << arr[i] << " ";
}
cout << endl << endl;
cout << "堆排序后出堆前三个大的元素:" << endl;
// 查找前n个元素
heapSort(heap, 3);
initHeap(heap, arr, sizeof(arr) / sizeof(arr[0]));
cout << endl << endl;
system("pause");
return 0;
}
运行截图:
至此本系列入门核心算法大局观:堆 到此完结!感谢观看!