C++中二叉堆排序詳解
1. 前言
什么是二叉堆?
二叉堆是有序的 完全二叉樹,在完全二叉樹的基礎(chǔ)上,二叉堆 提供了有序性特征:
二叉堆 的根結(jié)點上的值是整個堆中的最小值或最大值。
當根結(jié)點上的值是整個堆結(jié)構(gòu)中的最小值時,此堆稱為最小堆。最小堆中,任意節(jié)點的值大于父結(jié)點的值。
當根結(jié)點上的值是整個堆結(jié)構(gòu)中的最大值時,則稱堆為最大堆。最大堆中,任意節(jié)點的值小于父結(jié)點的值。
根據(jù)完全二叉樹的特性,二叉堆的父結(jié)點與子結(jié)點之間滿足下面的關(guān)系:
如果知道了一個結(jié)點的位置 i,則其左子結(jié)點在 2*i 位置,右子結(jié)點在 2*i+1 位置。
Tips: 前提是存在有子結(jié)點。
如果知道了一個結(jié)點的位置 i,則其父結(jié)點在 i除以 2 的位置。
Tips: 根結(jié)點沒有父結(jié)點。

如上圖所示:
值為 5 的結(jié)點在 2 處,則其左結(jié)點 12 的位置應(yīng)該在 2*2=4 處,而實際情況也是在 4 位置。其右子結(jié)點 13 的位置應(yīng)該在 2*2+1=5 的位置,實際位置也是在 5 位置。
值為 19 的結(jié)點現(xiàn)在 7 位置,其父結(jié)點的根據(jù)公式 7 除 2 等于 3(取整),應(yīng)該在 3 處,而實際情況也是在 3 處(位置在 3、 值為 8 的結(jié)點是其父結(jié)點)。
2 堆的數(shù)據(jù)結(jié)構(gòu)
2.1 二叉堆的抽象數(shù)據(jù)結(jié)構(gòu)
當談?wù)撃撤N數(shù)據(jù)結(jié)構(gòu)的抽象數(shù)據(jù)結(jié)構(gòu)時,最基本的 API 無非就是增、刪、改、查。
二叉堆的基本抽象數(shù)據(jù)結(jié)構(gòu):
Heap() :創(chuàng)建一個新堆。 insert(data): 向堆中添加新節(jié)點(數(shù)據(jù))。 getRoot(): 返回最小(大)堆的最?。ù螅┰?。 removeRoot() :刪除根節(jié)點。 isEmpty():判斷堆是否為空。 findAll():查詢堆中所有數(shù)據(jù)。
根據(jù)二叉堆的特性,順序存儲應(yīng)該成為堆的首選方案。
如有數(shù)列=[8,5,12,15,19,13,1],可以先創(chuàng)建一個一維數(shù)組。

數(shù)組第 0 位置初始為 0,從第 2 個位置也就是索引號為 1 的地方開始存儲堆的數(shù)據(jù)。如下圖,二叉堆中的數(shù)據(jù)在數(shù)組中的對應(yīng)存儲位置。

2.2 基礎(chǔ) API 實現(xiàn)
設(shè)計一個 Heap 類封裝對二叉堆的操作方法,類中方法用來實現(xiàn)最小堆。
#include <iostream>
using namespace std;
/*
* 堆類
*/
template<typename T>
class Heap{
private:
//數(shù)組
T heapList[100];
//實際大小
int size=0;
public:
/*
*構(gòu)造函數(shù)
*/
Heap(){
}
/*
*返回根結(jié)點的值
*/
T getRoot();
/*
*刪除根結(jié)點
*/
T removeRoot();
/*
*遞歸刪除
*/
T removeRoot_();
void removeRootByRecursion(int parentIdx );
/*
*初始化根結(jié)點
*/
void setRoot(T val);
/*
*添加新結(jié)點,返回存儲位置
*/
int insert(T val);
/*
*堆是否為空
*/
bool isEmpty();
/*
* 遞歸插入
*/
int insert_(T val);
int insertByRecursion(int pos);
/*
*輸出所有結(jié)點
*/
void findAll() {
for(int i=0; i<=size; i++)
cout<<this->heapList[i]<<"\t";
cout<<endl;
}
}; Heap 類中的屬性詳解:
heapList:使用數(shù)組存儲二叉堆的數(shù)據(jù),初始時,列表的第 0 位置初始為默認值 0。
Tips: 為什么要設(shè)置列表的第 0 位置的默認值為 0?
這個 0 也不是隨意指定的,有其特殊數(shù)據(jù)含義:用來描述根結(jié)點的父結(jié)點編號或者說根結(jié)點沒有父結(jié)點。
size:用來存儲二叉堆中數(shù)據(jù)的實際個數(shù)。
Heap 類中的方法介紹:
isEmpty:檢查是不是空堆。邏輯較簡單。
/*
*當 size 為 0 時,堆為空
*/
template<typename T>
bool Heap<T>::isEmpty(){
return Heap::size==0;
}setRoot:創(chuàng)建根結(jié)點。保證根節(jié)點始終存儲在列表索引為 1 的位置。
/*
*初始化根結(jié)點
*/
template<typename T>
void Heap<T>::setRoot(T val) {
if( Heap<T>::heapList[1]==0 )
Heap<T>::heapList[1]=val;
Heap<T>::size++;
}getRoot:如果是最大堆,則返回二叉堆的最大值,如果是最小堆,則返回二叉堆的最小值。
/*
*返回根結(jié)點
*/
template<typename T>
T Heap<T>::getRoot() {
if( !Heap<T>::isEmpty )
return Heap<T>::heapList[1];
}Tips: 使用數(shù)組存儲二叉堆數(shù)據(jù)時,根結(jié)點始終保存在索引號為 1 的位置。
前面是幾個基本方法,現(xiàn)在實現(xiàn)添加新結(jié)點,編碼之前,先要知道如何在二叉堆中添加新結(jié)點:
2.3 上沉算法
添加新結(jié)點采用上沉算法。如下演示上沉算法的實現(xiàn)過程。

把新結(jié)點添加到已有的二叉堆的最后面。如下圖,添加值為 4 的新結(jié)點,存儲至索引號為 7 的位置。

查找新結(jié)點的父結(jié)點,并與父結(jié)點的值比較大小,如果比父結(jié)點的值小,則和父結(jié)點交換位置。如下圖,值為 4 的結(jié)點小于值為 8 的父結(jié)點,兩者交換位置。

交換后再查詢是否存在父結(jié)點,如果有,同樣比較大小、交換,直到到達根結(jié)點或比父結(jié)點大為止。值為 4 的結(jié)點小于值為 5 的父結(jié)點,繼續(xù)交換。交換后,新結(jié)點已經(jīng)達到了根結(jié)點位置,整個添加過程可結(jié)束。觀察后會發(fā)現(xiàn),遵循此流程添加后,沒有破壞二叉堆的有序性。

編碼實現(xiàn) insert 方法
/*
*添加新結(jié)點
*/
template<typename T>
T Heap<T>::insert(T val) {
//存儲在最后一個位置
int pos= ++Heap<T>::size;
Heap<T>::heapList[pos]=val;
int temp=0;
//上沉算法
while(1) {
//找到父結(jié)點位置
int parentIdx= pos / 2;
if(parentIdx==0)
//出口一,沒有父結(jié)點
break;
if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
//出口二:大于父結(jié)點
break;
else {
//和父親結(jié)點交換
temp=Heap<T>::heapList[pos];
Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
Heap<T>::heapList[parentIdx]=temp;
pos=parentIdx
}
}
}測試向二叉堆中添加數(shù)據(jù)。
int main(int argc, char** argv) {
//實例化堆
Heap<int> heap;
//初始化根結(jié)點
heap.setRoot(5);
//檢查根結(jié)點是否創(chuàng)建成功
int rootVal=heap.getRoot();
cout<<"根結(jié)點的值:"<<rootVal<<endl;
//添加值為 12和值為 13 的 2個新結(jié)點,檢查添加新結(jié)點后整個二叉堆的有序性是否正確。
heap.insert(12);
heap.insert(13);
cout<<"測試一:"<<endl;
heap.findAll();
return 0;
}輸出結(jié)果:


添加值為 1 的新結(jié)點,并檢查二叉堆的有序性。
int main(int argc, char** argv) {
//省略……
//添加值為 1 的結(jié)點
heap.insert(1);
cout<<"測試二:"<<endl;
heap.findAll();
return 0;
}

繼續(xù)添加值為 15、19、8 的 3 個新結(jié)點,并檢查二叉堆的狀況。
int main(int argc, char** argv) {
//省略……
heap.insert(15);
heap.insert(19);
heap.insert(8);
cout<<"測試三:"<<endl;
heap.findAll();
return 0;
}

上沉算法同樣可以使用遞歸實現(xiàn)。
/*
*遞歸實現(xiàn)插入
*/
template<typename T>
int Heap<T>::insert_(T val) {
//存儲在最后一個位置
int pos= ++Heap<T>::size;
Heap<T>::heapList[pos]=val;
//調(diào)用
Heap<T>::insertByRecursion(pos);
}
template<typename T>
int Heap<T>::insertByRecursion(int pos) {
//找到父結(jié)點位置
int parentIdx= pos / 2;
if(parentIdx==0)
//出口一,沒有父結(jié)點
return pos;
if( Heap<T>::heapList[pos]>Heap<T>::heapList[parentIdx] )
//出口二:大于父結(jié)點
return pos;
else {
//和父親結(jié)點交換
int temp=Heap<T>::heapList[pos];
Heap<T>::heapList[pos]=Heap<T>::heapList[parentIdx];
Heap<T>::heapList[parentIdx]=temp;
//遞歸
Heap<T>::insertByRecursion(parentIdx);
}
}2.4 下沉算法
介紹完添加方法后,再來了解一下,如何使用下沉算法刪除二叉堆中的結(jié)點。
二叉堆的刪除操作從根結(jié)點開始,如下圖刪除根結(jié)點后,空出來的根結(jié)點位置,需要在整個二叉堆中重新找一個結(jié)點充當新的根結(jié)點。

二叉堆中使用下沉算法選擇新的根結(jié)點:
找到二叉堆中的最后一個結(jié)點,移到到根結(jié)點位置。如下圖,把二叉堆中最后那個值為 19 的結(jié)點移到根結(jié)點位置。

最小堆中,如果新的根結(jié)點的值比左或右子結(jié)點的值大,則和子結(jié)點交換位置。如下圖,在二叉堆中把 19 和 5 的位置進行交換。
Tips: 總是和最小的子結(jié)點交換。

交換后,如果還是不滿足最小二叉堆父結(jié)點小于子結(jié)點的規(guī)則,則繼續(xù)比較、交換新根結(jié)點直到下沉到二叉堆有序為止。如下,繼續(xù)交換 12 和 19 的值。如此反復(fù)經(jīng)過多次交換直到整個堆結(jié)構(gòu)符合二叉堆的特性。

removeoot 方法的具體實現(xiàn):
/*
* 下沉算法,刪除結(jié)點
*/
template<typename T>
T Heap<T>::removeRoot() {
if(Heap<T>::size==0)return NULL;
T root=Heap<T>::heapList[1];
if(Heap<T>::size==1) {
Heap<T>::size--;
return root;
}
//堆中最后一個結(jié)點移動根結(jié)點
Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
Heap<T>::size--;
//下沉算法
int parentIdx=1;
//子結(jié)點值
T minChild;
//子結(jié)點位置
int idx;
while(1) {
//左結(jié)點位置
int leftIdx=parentIdx*2;
//右結(jié)點位置
int rightIdx=parentIdx*2+1;
if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) {
//記錄較小的結(jié)點值和位置
minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
} else if( leftIdx<=Heap<T>::size) {
minChild=Heap<T>::heapList[leftIdx];
idx=leftIdx;
} else if( rightIdx<=Heap<T>::size ) {
minChild=Heap<T>::heapList[rightIdx];
idx=rightIdx;
}else{
//沒有子結(jié)點
break;
}
//是否交換
if( Heap<T>::heapList[parentIdx]>minChild ) {
Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
Heap<T>::heapList[parentIdx]=minChild;
parentIdx=idx;
} else {
break;
}
}
return root;
} 測試在二叉堆中刪除結(jié)點:
int main(int argc, char** argv) {
//省略……
cout<<"測試刪除一:"<<endl;
heap.removeRoot();
heap.findAll();
return 0;
}
可以看到最后二叉堆的結(jié)構(gòu)和有序性都得到了完整的保持。
"下沉算法" 同樣可以使用遞歸實現(xiàn)。
/*
*遞歸實現(xiàn)下沉算法
*/
template<typename T>
T Heap<T>::removeRoot_() {
if(Heap<T>::size==0)return NULL;
//根結(jié)點值
T root=Heap<T>::heapList[1];
//
if(Heap<T>::size==1) {
Heap<T>::size--;
return root;
}
//堆中最后一個結(jié)點移動根結(jié)點
Heap<T>::heapList[1]=Heap<T>::heapList[Heap<T>::size];
Heap<T>::size--;
//調(diào)用
Heap<T>::removeRootByRecursion(1);
return root;
}
template<typename T>
void Heap<T>::removeRootByRecursion(int parentIdx ) {
//子結(jié)點值
T minChild;
//子結(jié)點位置
int idx;
//左結(jié)點位置
int leftIdx=parentIdx*2;
//右結(jié)點位置
int rightIdx=parentIdx*2+1;
if( leftIdx<=Heap<T>::size && rightIdx<=Heap<T>::size ) {
//記錄較小的結(jié)點值和位置
minChild=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?Heap<T>::heapList[leftIdx]:Heap<T>::heapList[rightIdx];
idx=Heap<T>::heapList[leftIdx]<Heap<T>::heapList[rightIdx]?leftIdx:rightIdx;
} else if( leftIdx<=Heap<T>::size) {
minChild=Heap<T>::heapList[leftIdx];
idx=leftIdx;
} else if( rightIdx<=Heap<T>::size ) {
minChild=Heap<T>::heapList[rightIdx];
idx=rightIdx;
} else {
//沒有子結(jié)點
return;
}
//是否交換
if( Heap<T>::heapList[parentIdx]>minChild ) {
Heap<T>::heapList[idx]=Heap<T>::heapList[parentIdx];
Heap<T>::heapList[parentIdx]=minChild;
//遞歸
Heap<T>::removeRootByRecursion(idx);
} else {
return;
}
}3. 堆排序
堆排序指借助堆的有序性對數(shù)據(jù)進行排序。
需要排序的數(shù)據(jù)以堆的方式保存。 然后再從堆中以根結(jié)點方式取出來,無序數(shù)據(jù)就會變成有序數(shù)據(jù) 。
如有數(shù)列=[4,1,8,12,5,10,7,21,3],現(xiàn)通過堆的數(shù)據(jù)結(jié)構(gòu)進行排序。
int main(int argc, char** argv) {
//實例化堆
Heap<int> heap;
int nums[] = {4,1,8,12,5,10,7,21,3};
int size=sizeof(nums)/4;
// 創(chuàng)建根節(jié)點
heap.setRoot(nums[0]);
// 其它數(shù)據(jù)添加到二叉堆中
for (int i=1; i<size; i++) {
heap.insert(nums[i]);
}
cout<<"堆中數(shù)據(jù):"<<endl;
heap.findAll();
// 獲取堆中的數(shù)據(jù)
for(int i=0; i<size; i++ ) {
nums[i]= heap.removeRoot();
heap.findAll();
}
for(int i=0; i<size; i++)
cout<<nums[i]<<"\t";
return 0;
}輸出結(jié)果:

本例中的代碼還有優(yōu)化空間,本文試圖講清楚堆的使用,優(yōu)化的地方交給有興趣者。
4. 后記
在樹結(jié)構(gòu)上加上一些新特性要求,樹會產(chǎn)生很多新的變種,如二叉樹,限制子結(jié)點的個數(shù),如滿二叉樹,限制葉結(jié)點的個數(shù),如完全二叉樹就是在滿二叉樹的“滿”字上做點文章,讓這個''滿"變成"不那么滿"。
在完全二叉樹上添加有序性,則會衍生出二叉堆數(shù)據(jù)結(jié)構(gòu)。利用二叉堆的有序性,能輕松完成對數(shù)據(jù)的排序。
到此這篇關(guān)于C++中二叉堆排序詳解的文章就介紹到這了,更多相關(guān)C++ 二叉堆排序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c++使用Easyx圖形庫實現(xiàn)飛機大戰(zhàn)
本文詳細講解了c++使用Easyx圖形庫實現(xiàn)飛機大戰(zhàn),文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12
VC6.0如何創(chuàng)建以及調(diào)用動態(tài)鏈接庫實例詳解
作為客戶與后臺的中介,為了更好的調(diào)節(jié)兩方的關(guān)系,我明智滴選擇了webservice以及動態(tài)鏈接庫。在與客戶c++使動態(tài)鏈接庫方式,而與后臺java,使用webservice來交流溝通2013-01-01
c++ 獲取數(shù)字字符串的子串數(shù)值性能示例分析
這篇文章主要為大家介紹了c++ 獲取數(shù)字字符串的子串數(shù)值示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11

