堆&&堆排序&&N個數中找出K個最大值&&優先順序佇列
學習二叉樹後,有一個東西需要我們來關注下,就是堆,對於堆,來說我們可以把堆看作一顆完全二叉樹。這裡我們也可以叫做二叉堆。
二叉堆滿足二個特性:
1.父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值。
2.每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。
另外,需要關注的就是有一個大堆和一個小堆。
大堆就是父節點的數值大於任何一個子節點的數值。
小堆就是父節點的數值小於任何一個子節點的數值。
1.堆
接下來,我們進行堆的建立:
首先我們要清楚,堆的儲存結構,在這裡作為一顆完全二叉樹,我們可以使用陣列來儲存堆,然後調整陣列的下標,這樣就可以抽象出一顆完全二叉樹。
我們接下來使用STL中的vector來代替陣列。
然後這裡我們需要它們父節點和子節點的關係:
父節點=(子節點-1)*2;
子節點=父節點*2+1;
1.1堆的建立
這樣我們就可以根據上述的關係實現找到某個節點的父親節點或者孩子節點。
比如,我們給出一個數組:
int a[] = { 10, 16, 18, 12, 11, 13, 15, 17, 14, 19 };
那麼我們就相當於建立了這樣的一顆完全二叉樹
這就是這顆完全二叉樹的關係。
接下來我們需要進行建堆。所謂建堆,其實就是我們所說的進行這顆完全二叉樹的調整,根據我們想要大堆還是小堆,進行調整。
調整的方法是,從第倒數第一個非葉子節點進行調整,與它的兩個葉子節點進行比較,調整。這個就是堆中的所謂的向下調整法。在這裡要注意調整的時候要取出葉子節點中的最值節點(即最小值或者最大值)。然後和父節點進行比較。
void _AdjustDown(int root)
{
assert(!_a.empty());
size_t parent = root;
size_t child = parent * 2 + 1;
while (parent<_a.size())
{
Compare com;
if (child + 1<_a.size()
&& com(_a[child + 1], _a[child]))
//在這裡對兩個子節點進行判斷。看應該取出哪一個
{
++child;
}
if (child<_a.size() && com(_a[child], _a[parent]))
//進行子節點和父節點之間的判斷調整
{
std::swap(_a[child], _a[parent]);
parent = child;//調整完後的原父節點依然要和下面的節點進行繼續調整。
child = parent * 2 + 1;
}
else
{
break;
}
}
}
這就是重要的向下調整法,為了方便對於大小堆的操控,我們引入仿函式,通過仿函式返回值,控制比較的條件。
template<typename T>
//當需要小堆時:
class Less
{
public:
bool operator ()(const T& a, const T& b)
{
return a < b;
}
};
template<typename T>
//當需要大堆時
class Greater
{
public:
bool operator ()(const T& a, const T& b)
{
return a > b;
}
};
通過仿函式,我們只需要傳入一個模板引數,然後通過建立的物件就可以實現控制是大堆還是小堆。
當我們每次對整個堆進行增加以後,我們都可以採用向下調整法,進行調整,這樣就可以調整出新的滿足要求的堆。
建堆的時間複雜度:O(N*logN)
1.2堆的插入
所以,堆的插入演算法也就簡單的實現了。
因為我們這裡使用的是vector,所以我們直接使用vector 的演算法進行新增。
插入演算法就是把一個節點插入到堆的最後,然後進行向上調整演算法。
向上調整法就是從一個節點開始,進行與他的父節點進行比較。
然後根據大堆或者小堆進行調整。
void _AdjustUp(size_t child)
{
assert(!_a.empty());
while (child>0)
{
Compare com;
size_t parent = (child - 1) / 2;
if (com(_a[child], _a[parent]))
{
std::swap(_a[child], _a[parent]);
child = parent;
}
else
{
break;
}
}
}
void Push(const T& d)
{
_a.push_back(d);
_AdjustUp(_a.size() - 1);
}
插入的時間複雜度:O(logN)
1.3堆的刪除
堆的刪除演算法,堆可以進行pop(),這裡就是對堆頂進行pop(),
在這裡的演算法就是我們需要交換堆頂和最後一個堆節點,然後進行堆的向下調整演算法。
void Pop()
{
assert(!_a.empty());
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
_AdjustDown(0);
}
刪除的時間複雜度:O(logN)
然後附上堆實現的完整程式碼:
//堆
#pragma once
#include<iostream>
#include<cstdlib>
#include<cassert>
#include<vector>
using namespace std;
template<typename T>
class Less
{
public:
bool operator ()(const T& a, const T& b)
{
return a < b;
}
};
template<typename T>
class Greater
{
public:
bool operator ()(const T& a, const T& b)
{
return a > b;
}
};
//大堆
template<typename T, typename Compare = Greater<T>>
class Heap
{
public:
Heap(T *a, size_t size)
{
_a.reserve(size);
for (size_t i = 0; i < size; i++)
{
_a.push_back(a[i]);
}
for (int j = (_a.size() - 2) / 2; j >= 0; j--)
{
_AdjustDown(j);
}
}
void Pop()
{
assert(!_a.empty());
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
_AdjustDown(0);
}
void Push(const T& d)
{
_a.push_back(d);
_AdjustUp(_a.size() - 1);
}
const T& Top()
{
assert(!_a.empty());
return _a[0];
}
size_t Size()
{
return _a.size();
}
bool empty()
{
return _a.empty();
}
protected:
void _AdjustDown(int root)
{
assert(!_a.empty());
size_t parent = root;
size_t child = parent * 2 + 1;
while (parent<_a.size())
{
Compare com;
if (child + 1<_a.size()
&& com(_a[child + 1], _a[child]))
{
++child;
}
if (child<_a.size() && com(_a[child], _a[parent]))
{
std::swap(_a[child], _a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void _AdjustUp(size_t child)
{
assert(!_a.empty());
while (child>0)
{
Compare com;
size_t parent = (child - 1) / 2;
if (com(_a[child], _a[parent]))
{
std::swap(_a[child], _a[parent]);
child = parent;
}
else
{
break;
}
}
}
protected:
std::vector<T > _a;
};
2.堆排序
堆的一個重要的應用就是堆排序。堆排序是一種選擇排序,是利用堆這種二叉樹的性質進行的排序。
初始時把要排序的n個數的序列看作是一棵順序儲存的二叉樹(一維陣列儲存二叉樹),調整它們的儲存序,使之成為一個堆,將堆頂元素輸出,得到n 個元素中最小(或最大)的元素,這時堆的根節點的數最小(或者最大)。然後對前面(n-1)個元素重新調整使之成為堆,輸出堆頂元素,得到n 個元素中次小(或次大)的元素。依此類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。稱這個過程為堆排序。
堆排序的重要的思路就是當排升序的時候需要用到大堆,當拍降序的時候需要用到小堆
。
所以堆排序需解決兩個問題:
1. 如何將n 個待排序的數建成堆;
2. 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成為一個新堆。
例如:
int arr[] = { 5,6,3,2,1,4 };
2.1堆排序之建堆
在這裡,我們就可仿照前面的思路,把這排序的N個數建一個堆,然後執行向下調整演算法。
//建堆
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i,size);
}
2.2堆排序之排序
建完堆以後,這個時候我們在這個堆中進行排序就好了,排序的思想就是把第一個節點和最後一個節點進行交換,然後對除了最後一個節點之外的進行向下調整。這個就是讓最後一個數作為有序序列,然後其他作為無序序列,從無序序列中招出需要的有序數,進行繼續的排序,所以說堆排序就是選擇排序。
//排序演算法
size_t end = size - 1;
while (end > 0)
{
swap(arr[0], arr[end]);
AdjustDown(arr, 0, end);
end--;
}
完整堆排序程式碼:
#include<iostream>
#include<cassert>
#include<cstdlib>
using namespace std;
template<typename T>
void AdjustDown(T* arr, size_t root,size_t count)
{
assert(arr);
size_t parent = root;
size_t child = parent * 2 + 1;
while (parent<count)
{
if (child + 1 < count&&arr[child + 1]<arr[child])
{
++child;
}
if (child<count&&arr[child] < arr[parent])
{
std::swap(arr[child], arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
template<typename T>
void HeapSort(T* arr, size_t size)
{
assert(arr);
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, i,size);
}
size_t end = size - 1;
while (end > 0)
{
swap(arr[0], arr[end]);
AdjustDown(arr, 0, end);
end--;
}
}
void test1()
{
int arr[] = { 5,6,3,2,1,4 };
HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
}
int main()
{
test1();
system("pause");
return 0;
}
接下來是兩道關於堆的應用。
3.N個數中找出K個最大的值
這道題就是相關海量資料的一道題。我們利用堆來解決這一道題,首先我們來構建K個數的堆,然後調整這個堆,最後把剩下的數拿出來和這個堆中的數進行比較調整。
在這裡要注意的是找最大的值用小堆,找最小的值用大堆
因為核心思想是你剩下的N-K個數和堆頂中的最值相比較,然後調整。
示例程式碼:
#define _CRT_SECURE_NO_WARNINGS 1
#define N 10000
#define K 10
#include<iostream>
#include<cstdlib>
#include<cassert>
using namespace std;
template<typename T>
void Adjustdown(T *top, size_t root)
{
assert(root < K);
size_t parent = root;
size_t child = parent * 2 + 1;
while (parent<K)
{
if (child + 1 < K&&top[child + 1] < top[child])
{
child++;
}
if (child<K&&top[child]<top[parent])
{
std::swap(top[child], top[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
template<typename T>
void GetKNum(T *arr, T *top)
{
assert(K < N);
for (size_t i = 0; i < K; i++)
{
top[i] = arr[i];
}
for (int j = (K - 2) / 2; j >= 0; j--)
{
Adjustdown(top, j);
}
for (size_t i = K; i < N; i++)
{
if (arr[i]>top[0])
{
std::swap(arr[i], top[0]);
Adjustdown(top, 0);
}
}
}
template<typename T>
void print(T *top)
{
for (size_t i=0; i < K; i++)
{
cout << top[i] << " ";
}
cout << endl;
}
void test1()
{
int arr[N] = { 0 };
int top[K] = { 0 };
for (size_t i = 0; i < N; i++)
{
arr[i] = i;
}
GetKNum(arr, top);
print(top);
}
int main()
{
test1();
system("pause");
return 0;
}
最後計算一下時間複雜度:
建K個數的堆:KlogK
進行N-K個數的比較操作調整:(N-K)logK
所以最後的時間複雜度:O(N*logK)
4.優先順序佇列
堆的另外一個應用就是優先順序佇列,我們可以用堆來建立物件來管理這個優先順序佇列。
對於優先順序通過給的模板引數來控制。
然後這樣就可以根據優先順序進行調整出你想要的。
#pragma once
#include"heap.h"
#include<iostream>
#include<cstdlib>
#include<cassert>
using namespace std;
template<typename T,typename Compare=Greater<T>>
class PriorityQueue
{
public:
PriorityQueue(T* a,size_t size)
:_h(a,size)
{
}
void Push(const T& d)
{
_h.Push(d);
}
void Pop()
{
_h.pop();
}
size_t Size()
{
return _h.Size();
}
const T& Top()
{
return _h.Top();
}
bool empty()
{
return _h.empty();
}
protected:
Heap<T, Compare> _h;
};