觀看本系列博文提醒:
- 你將學會堆的原理 和 算法實現;
- 一個企業級應用:堆實現優先隊列;
- 還有堆排序;
- 最後還有一道檢測是否掌握堆算法的作業。
這已經是本系列博文的第三篇了,還沒看過第二篇博文: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;
}
運行截圖:
至此本系列入門核心算法大局觀:堆 到此完結!感謝觀看!