當(dāng)前位置:首頁(yè) > IT技術(shù) > 編程語(yǔ)言 > 正文

堆、堆排序與索引堆
2022-04-19 11:18:59


完整代碼參見(jiàn)github

堆的概念

定義 堆就是一棵二叉樹(shù),每個(gè)節(jié)點(diǎn)包含一個(gè)鍵,不過(guò)還需要滿足以下兩個(gè)條件: (1)必須是完全二叉樹(shù),也就是說(shuō),樹(shù)的每一層都必須是滿的,除了最后一層最右邊的元素可能有所缺失 (2)堆特性(又稱為父母優(yōu)勢(shì),這里我們以最大堆為例),每一個(gè)節(jié)點(diǎn)都要大于或等于它的子節(jié)點(diǎn)(對(duì)于葉子節(jié)點(diǎn)我們認(rèn)為是滿足這個(gè)條件的) 堆、堆排序與索引堆_數(shù)據(jù)結(jié)構(gòu) 舉例說(shuō)明,上圖中只有第一棵樹(shù)是堆,第二棵樹(shù)違背了完全二叉樹(shù)條件,第三棵樹(shù)也不是堆,因?yàn)楣?jié)點(diǎn)5小于它的子節(jié)點(diǎn)6,堆特性條件不滿足。

需要注意的是,從根到某葉子節(jié)點(diǎn)的路徑上,鍵值的序列是遞減的(非遞增)的,但是同一層的節(jié)點(diǎn)之間或者不同層之間無(wú)父子(或祖先后代)關(guān)系的節(jié)點(diǎn)之間美哦與任何順序聯(lián)系!

堆的特征

堆、堆排序與索引堆_數(shù)組_02

堆的構(gòu)造

經(jīng)典的堆構(gòu)造往往采用數(shù)組的實(shí)現(xiàn)方式,并且下標(biāo)從1開(kāi)始(數(shù)組第一個(gè)元素閑置) 堆、堆排序與索引堆_子節(jié)點(diǎn)_03 堆的構(gòu)造方法通常有兩種,一種是元素逐個(gè)進(jìn)行插入,另一個(gè)是用heapify對(duì)整個(gè)數(shù)組進(jìn)行初始化。

1 插入構(gòu)造法

代碼如下:

#include <iostream>
#include <algorithm>
#include <cassert>

template <typename Item>
class MaxHeap {
private:
Item* data;
//堆的實(shí)際容量(不包括data[0])
int capacity;
//堆目前的元素?cái)?shù)量
int count;

void shiftUp(int index) {
while (index > 1) {
if (data[index] > data[index / 2])
std::swap(data[index],data[index / 2]);
index /= 2;
}
}

void shiftDown(int index) {
while (index * 2 <= count) {
int j =2 * index;
if (j + 1 <= count && data[j] < data[j + 1]) {
j++;
}

if (data[j] <= data[index]) {
break;
}

std::swap(data[j],data[index]);
index = j;
}
}

public:
MaxHeap(int capacity) {
this -> capacity = capacity;
data = new Item[capacity + 1];
count = 0;
}

~MaxHeap() {
delete [] data;
}

//插入元素
void insert(Item item) {
assert(count < capacity);
data[++count] = item;
//將item浮動(dòng)到合適的位置
shiftUp(count);
}

//彈出根元素
Item pop() {
assert(count > 0);
Item root = data[1];
data[1] = data[count];
count--;
shiftDown(1);
return root;
}

void print() {
for (int i = 1; i <= count; i++) {
std::cout << data[i] << " ";
}
}

//獲取堆的當(dāng)前大小
int size() {
return count;
}
//判斷堆是否為空
bool isEmpty() {
return count == 0;
}
};

將nnn個(gè)元素逐個(gè)插入到堆中,時(shí)間復(fù)雜度為O(nlogn)O(nlogn)O(nlogn)

接下來(lái)我們就可以直接利用最大堆的根節(jié)點(diǎn)最大的特點(diǎn)編寫堆排序

堆排序-版本一

template <typename T>
template heapSort1(T arr[],int n){
MaxHeap<T> maxHeap = MaxHeap<T>(n);
for (int i = 0;i < n;i++){
maxHeap.insert(arr[i]);
}
for (int i = n - 1;i >= 0;i--){
arr[i] = maxHeap.pop();
}
}

Heapify將數(shù)組轉(zhuǎn)換為堆結(jié)構(gòu)

之前我們都是將元素逐個(gè)加入到數(shù)組中,然后利用shiftUp進(jìn)行堆結(jié)構(gòu)的整理,但更好的做法是直接在原數(shù)組中將數(shù)組整理為堆結(jié)構(gòu),這個(gè)過(guò)程叫做Heapify

我們將原來(lái)不滿足堆結(jié)構(gòu)的數(shù)組以完全二叉樹(shù)的結(jié)構(gòu)進(jìn)行表示,如下圖所示:

堆、堆排序與索引堆_數(shù)組_04 通過(guò)觀察,可以發(fā)現(xiàn),所有的葉子節(jié)點(diǎn)都滿足堆結(jié)構(gòu),從第一個(gè)不是葉子節(jié)點(diǎn)的節(jié)點(diǎn)(索引為5的節(jié)點(diǎn)22)開(kāi)始不滿足堆結(jié)構(gòu),因此,從該節(jié)點(diǎn)開(kāi)始我們不斷進(jìn)行shiftDown操作,直到根節(jié)點(diǎn),這樣我們就完成了對(duì)數(shù)組進(jìn)行堆結(jié)構(gòu)整理的操作。 我們將該過(guò)程整理成MaxHeap類的另一個(gè)構(gòu)造方法,代碼如下:

//利用數(shù)組進(jìn)行堆的初始化
MaxHeap(Item arr[],int n) {
data = new Item[n + 1];
this -> capacity = n;
this -> count = n;

for (int i = 0; i < n; i ++) {
data[i + 1] = arr[i];
}

//從第一個(gè)不是葉子節(jié)點(diǎn)的節(jié)點(diǎn)開(kāi)始向上,逐次使用shiftDown方法
for (int i = count / 2; i > 0 ; i --) {
shiftDown(i);
}
}

用heapipy方法,時(shí)間復(fù)雜度為O(n)O(n)O(n)

堆排序-版本二

template <typename T>

template heapSort2(T arr[],int n){
MaxHeap<T> maxHeap = MaxHeap<T>(arr,n);
for (int i = n - 1;i >= 0;i--){
arr[i] = maxHeap.pop();
}
}

堆排序-最終版

//堆排序算法--最終版 
//在原數(shù)組中進(jìn)行堆排序,不占用額外空間
//首先對(duì)原數(shù)組進(jìn)行heapify
//然后將arr[0]和最后一個(gè)元素進(jìn)行交換,重新對(duì)第一個(gè)元素進(jìn)行heapify,以此類推

//注意:索引從0開(kāi)始
//左子樹(shù):2 * i + 1
//右子樹(shù):2 * i + 2
//父節(jié)點(diǎn):(i - 1) / 2

#include <iostream>
#include <cassert>

template <typename T>
void __shiftDown(T arr[],int n,int index) {
while (2 * index + 1 < n) {
int j = 2 * index + 1;
if (j + 1 < n && arr[j] < arr[j + 1])
j++;
if (arr[index] > arr[j]) {
break;
}
std::swap(arr[index],arr[j]);
index = j;
}
}

template <typename T>
void heapSort(T arr[] ,int n) {
//從第一個(gè)不是葉子節(jié)點(diǎn)的元素開(kāi)始向上,對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行shiftDown
for (int i = (n - 1) / 2; i >= 0; i--) {
__shiftDown(arr, n, i);
}

for (int i = n - 1; i > 0 ; i --) {
std::swap(arr[i], arr[0]);
__shiftDown(arr, i, 0);
}
}

索引堆

什么是索引堆?

與普通堆只存儲(chǔ)元素不同,索引堆是將元素和其索引同時(shí)存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)(多用一個(gè)數(shù)組來(lái)保存元素的索引)。

為什么需要索引堆?

我們以普通堆為例進(jìn)行說(shuō)明,普通堆構(gòu)造之前的數(shù)據(jù)存儲(chǔ)如下圖所示 堆、堆排序與索引堆_數(shù)組_05

我們用數(shù)組存儲(chǔ)堆的元素,隨后我們將數(shù)組進(jìn)行堆構(gòu)造 堆、堆排序與索引堆_#include_06

我們看到,數(shù)組以堆的形式進(jìn)行了重構(gòu),但這在某些特殊情形下可能會(huì)帶來(lái)一些麻煩。

比如,我們存儲(chǔ)的元素是一篇上萬(wàn)字的文章,那么對(duì)文章進(jìn)行交換位置的開(kāi)銷是相當(dāng)大的;再比如,原始數(shù)組的元素表示的是計(jì)算機(jī)的進(jìn)程,其數(shù)組索引表示的該進(jìn)程的id號(hào),當(dāng)我們按照堆對(duì)其進(jìn)行重構(gòu)之后,我們無(wú)法確定排列之后的元素(進(jìn)程)的id號(hào)是多少,因此諸如改變某個(gè)id為3的進(jìn)程的優(yōu)先級(jí)的這種操作便不太靈活(除非我們對(duì)存儲(chǔ)的元素進(jìn)行數(shù)據(jù)結(jié)構(gòu)封裝,使其同時(shí)存儲(chǔ)id號(hào),但同樣還是帶來(lái)了元素交換的效率問(wèn)題),這時(shí)候我們的索引堆就派上了用場(chǎng)。

索引堆的構(gòu)造

這里我們?nèi)匀灰宰畲蠖褳槔?/p>

索引堆底層用兩個(gè)數(shù)組進(jìn)行表示,一個(gè)用來(lái)存儲(chǔ)元素的索引(數(shù)組從索引1開(kāi)始存儲(chǔ)),另一個(gè)(數(shù)組從索引1開(kāi)始存儲(chǔ))用來(lái)存儲(chǔ)元素,我們對(duì)存儲(chǔ)索引的數(shù)組進(jìn)行堆的構(gòu)造(對(duì)int類型的數(shù)據(jù)進(jìn)行交換是很高效的),存儲(chǔ)元素的數(shù)組我們不改變其存儲(chǔ)的順序,圖示如下:

堆、堆排序與索引堆_數(shù)組_07

我們用heapify對(duì)索引數(shù)組進(jìn)行堆構(gòu)造,完成之后的圖示如下:

堆、堆排序與索引堆_數(shù)組_08

代碼如下:

#include <iostream>
#include <algorithm>
#include <cassert>

template <typename Item>
class IndexMaxHeap{
private:
//存儲(chǔ)元素的數(shù)組
Item* data;
//存儲(chǔ)元素索引的數(shù)組
int* indexes;
//當(dāng)前堆中元素個(gè)數(shù)
int count;
//堆容量
int capacity;
void shiftUp(int i){
while(i > 1){
if (data[indexes[i]] > data[indexes[i / 2]]){
std::swap(indexes[i],indexes[i / 2]);
}
i /= 2;
}
}

void shiftDown(int i){
int j = 0;
while (2 * i <= count){
j = 2 * i;

if (j + 1 <= count && data[indexes[j]] < data[indexes[j + 1]])
j += 1;

if (data[indexes[i]] >= data[indexes[j]])
break;

std::swap(indexes[i],indexes[j]);
i = j;
}
}

public:
IndexMaxHeap(int capacity){
data = new Item[capacity + 1];
indexes = new int[capacity + 1];
this -> capacity = capacity;
this -> count = 0;
}
~IndexMaxHeap(){
delete [] data;
delete [] indexes;
}

int size(){
return count;
}
//注意,對(duì)用戶而言,i是從0開(kāi)始的,但我們存儲(chǔ)是從1開(kāi)始,編程實(shí)需要注意
void insert(int i,Item item){
assert(count < capcacity);
assert(i > 0 && i + 1 < capcacity);

data[++i] = item;
indexes[++count] = i;
shiftUp(count);
}

Item pop(){
assert(count > 0);
Item root = data[indexes[1]];
indexes[1]=indexes[count];
count--;
shiftDown(1);
return root;
}

//返回最大元素的索引
int popMaxIndex(){
assert(count > 0);
//注意,對(duì)用戶來(lái)說(shuō)從0開(kāi)始
int root = indexes[1] - 1;
indexes[1] = indexes[count];
std::swap(indexes[1],indexes[count]);
count--;
shiftDown(1);
return root;
}

Item getItem(int i){
return data[i + 1];
}
//修改索引為i的item
//O(n)
void change(int i,Item newItem){
data[++i] = newItem;
//接下來(lái)我們需要找到newItem(即data[i]在indexes中的位置)
//indexes[j] = i,j表示的就是data[i]在堆中的位置
//將i嘗試shiftUp操作或者shiftDown
for (int j = 1;j < count; j++){
if (indexes[j] == i ){
shiftUp(j);
shiftDown(j);
}
}

}
}

這里我們?cè)赾lass中加入了一個(gè)比較常用的操作change,將data中索引為i的元素改為newItem,我們采用遍歷indexes的做法來(lái)找到indexes的下標(biāo)j,這個(gè)j表示的是我們更改的newItem的索引在indexes中的存儲(chǔ)位置,即 ??data[index[j]] = newIndex?? 這樣一來(lái)change操作的時(shí)間復(fù)雜度就是O(n)O(n)O(n)

能不能有更快的操作方法呢?

reverse表

堆、堆排序與索引堆_數(shù)組_09

如上圖,rev數(shù)組就是index(就是indexes)數(shù)組的一個(gè)reverse表(當(dāng)然反過(guò)來(lái)說(shuō)也是正確的),其實(shí)就是索引和元素值的調(diào)換而已,

index[i] = j;
reverse[j] = i;

但經(jīng)過(guò)這樣存儲(chǔ),我們可以通過(guò)O(1)O(1)O(1)的時(shí)間復(fù)雜度找到index中的索引。

舉個(gè)例子: 假如我更改了data數(shù)組中下標(biāo)為4的元素13的值為100,那么為了維持堆的性質(zhì),data數(shù)組可以不變,我們必須對(duì)index數(shù)組進(jìn)行重構(gòu),那我們就需要知道100的下標(biāo)(4)在index中的存儲(chǔ)位置j,然后對(duì)j進(jìn)行??shiftUp???或??shiftDown??試探(總有一個(gè)會(huì)有效)。

如果我們不用reverse表的方法,我們需要從頭遍歷index數(shù)組,直到我們遇到??index[j] == 4???,此時(shí)我們得到 ??j=9??。

如果我們采用reverse表的方式,我們要找4在index中的存儲(chǔ)位置,直接利用??reverse[4]??就可以得到,時(shí)間復(fù)雜度為O(1)O(1)O(1)

詳細(xì)代碼見(jiàn)ReverseIndexMaxHeap.h

###最后一點(diǎn)優(yōu)化 我們上面寫的代碼中,getItem(int i)?和change(int i,Item newItem)?函數(shù)其實(shí)有一點(diǎn)小瑕疵,如果我們輸入的參數(shù)i并沒(méi)有存儲(chǔ)在indexes數(shù)組中,那么我們的程序就會(huì)出錯(cuò)

因此我們需要編寫一個(gè)函數(shù)來(lái)檢驗(yàn)i?是否在堆(indexes)中,這里我們用到了reverse數(shù)組 詳細(xì)代碼見(jiàn)?ReverseIndexMaxHeap.h

bool isContains(int i){
assert(i + 1 >=1 && i + 1 <=capacity);
return reverse[i + 1] != 0;
}

本文摘自 :https://blog.51cto.com/u

開(kāi)通會(huì)員,享受整站包年服務(wù)立即開(kāi)通 >