C/C++ 入門核心算法大局觀:堆排序

觀看本系列博文提醒:

  1. 你將學會堆的原理算法實現
  2. 一個企業級應用:堆實現優先隊列
  3. 還有堆排序
  4. 最後還有一道檢測是否掌握堆算法作業

這已經是本系列博文的第三篇了,還沒看過第二篇博文: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;
}

運行截圖:
在這裏插入圖片描述

至此本系列入門核心算法大局觀:堆 到此完結!感謝觀看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章