大小堆的實現(C++)
過了一段時間了,還是記錄一下,最近感覺記憶力下降的很快啊。
大小堆的構造
首先得知道大小堆是個什麼,大小堆是一種二叉樹,它的要求比起平衡樹來說是簡單一些的,只有一個,就是所有父節點必須大於自己的子節點,但是沒有什麼左孩子必須小於右孩子這種。到這裡其實大小堆的作用很容易看出,最大堆就是根節點最大,最小堆就是根節點最小,可以用於排序。
作為測試,這裡使用整形陣列作為初始化資料,二叉樹可以直接使用c++單迴圈實現二叉樹的前,中,後序,層次遍歷中的結構,現在我們的任務是將一個無序的陣列轉為大小堆。如何做呢,首先我們將這個陣列先按層次遍歷順序對映到一棵二叉樹上,然後再進行排序。
層次遍歷這裡就不說了,前面也有,當有了一棵無序的二叉樹時,這裡有一個效率比較高的排序方式,這裡就把遍歷那篇的圖拿過來說明一下
這裡的數字我們既把它看做序號也把它看做節點值,很容易看出這是一個最小堆,我們現在要將其變為最大堆。我們很容易想到,肯定是需要將子節點與父節點進行比較,然後交換,但是如果是從根節點開始向下比較的話明顯是不明智的。因為其並不能知道底下哪個是最大,而二叉樹的特性是父子節點比較是容易的,兄弟比較是比較複雜的,而剛才的方案必定需要左右子樹最大值比較。所以這裡使用自底向上的方案,我們使用一個棧來存放所有父節點,剛好我們又是層次遍歷生成樹,所以出棧的順序正好是從最後一個父節點往根節點。
這裡的情況是4號節點第一個出棧,與左孩子比較,交換,4號節點此時為8,再與右孩子比較,交換,此時4號節點為9,結束。此時489這棵子樹已經是最大堆了,3號節點出棧,和剛才一樣,然後2號節點出棧,與左孩子比較,交換,這時4號節點為2,這時遞迴4號節點也就是左孩子,4號節點與其左孩子比較,交換,此時4號節點為4,4號節點再與右孩子比較,交換,此時4號節點值為8,最後2號節點與右孩子比較,不動,至此2號節點的子樹按層次遍歷為98524,這棵子樹現在也是最大堆了。然後當最後一個節點也就是根節點出棧,再執行一遍剛才的遞迴,整棵樹就是最大堆了。我們可以算算一共進行了幾次比較,大概是14次的樣子,這已經是很壞的情況了,所以我們可以認為時間複雜度為線性的,當然複雜度不是這麼分析的。
enum SWAPMODE { MAX, MIN }; //根據陣列建立堆,flag使用列舉值MIN或者MAX BinaryTreeNode<int> * InitHeap(std::vector<int> v, int flag); //交換兩個節點的值 void Swap(BinaryTreeNode<int> *p1, BinaryTreeNode<int> *p2); //遞迴排序 void Swap(BinaryTreeNode<int> *node, int flag); //插入節點 void InsertHeap(BinaryTreeNode<int> *root, int data, int flag); //獲取指定節點的父節點指標 BinaryTreeNode<int> * GetParent_heap(BinaryTreeNode<int> *root,BinaryTreeNode<int> *node); //獲取最後一個插入節點的父節點 BinaryTreeNode<int> * GetLastParent(BinaryTreeNode<int> *root); //刪除堆的根節點並返回節點值 int DeleteHeap(BinaryTreeNode<int> * root, int flag);
以上是標頭檔案定義。下面是構造堆
BinaryTreeNode<int>* InitHeap(std::vector<int> v, int flag)
{
BinaryTreeNode<int> *p = new BinaryTreeNode<int>(); //
BinaryTreeNode<int> *root = p; //儲存根節點
std::queue<BinaryTreeNode<int> *> q; //定義佇列用於層次遍歷
std::stack<BinaryTreeNode<int> *> s; //定義棧用於儲存父節點
q.push(p);
int n = v.size();
for (size_t i = 0; i < n; i++)
{
p = q.front();
q.pop();
p->data = v[i];
if ((i + 1) * 2 <= n) { //左孩子
p->left = new BinaryTreeNode<int>();
q.push(p->left);
s.push(p);
if ((i + 1) * 2 + 1 <= n) { //右孩子
p->right = new BinaryTreeNode<int>();
q.push(p->right);
}
}
}
while (!s.empty())
{
BinaryTreeNode<int> *_p = s.top(); //依次彈出父節點進行遞迴排序
s.pop();
Swap(_p, flag);
}
return root;
}
void Swap(BinaryTreeNode<int>* p1, BinaryTreeNode<int>* p2)
{
int temp;
temp = p1->data;
p1->data = p2->data;
p2->data = temp;
}
void Swap(BinaryTreeNode<int>* node, int flag)
{
if (node == nullptr) {
return;
}
if (flag == MAX) {
if (node->left != nullptr) {
if (node->left->data > node->data) {
Swap(node, node->left);
Swap(node->left, flag);
}
if (node->right != nullptr) {
if (node->right->data > node->data) {
Swap(node, node->right);
Swap(node->right, flag);
}
}
}
}
else if (flag == MIN) {
if (node->left != nullptr) {
if (node->left->data < node->data) {
Swap(node, node->left);
Swap(node->left, flag);
}
if (node->right != nullptr) {
if (node->right->data < node->data) {
Swap(node, node->right);
Swap(node->right, flag);
}
}
}
}
}
使用隨機陣列測試結果如下
這裡樹的顯示是另一塊的函式,程式碼比較多,因為是測試用的所以寫的比較凌亂,有機會可以單獨整理一下寫一下,主要是利用樹的層次遍歷獲取對映到陣列的下標,然後利用二叉樹的特性,最後一層為2^(k-1)個節點,然後就能向上推算出每層所佔輸出空間。
堆的插入
首先明確我們要做的事,插入一個節點,保持樹結構不被破壞,也就是所有父節點大於其子節點的原則。我們依然可以將其分為,首先插入一個節點到最後,然後遞迴排序其父節點。還是拿上面的圖來說,現在這棵樹已經是最小堆了,這時我們插入一個值為0的節點。首先我們將其插入到5號節點的左孩子位置,後面我們需要一個函式來專門尋找最後的一個空位,首先看最後一個父節點,如果父節點孩子滿了,就找序號最前的一個葉子節點,這裡5號就是。
插入後就是排序了,有了前面,這裡就是照葫蘆畫瓢了,直接遞迴排序父節點,所以,還需要一個函式來專門找指定節點的父節點,這裡我是直接返回父節點,如果做優化的話應該可以返回到根節點的父節點棧,這樣就不用每次都找了。
//堆的插入操作
void InsertHeap(BinaryTreeNode<int>* root, int data, int flag)
{
BinaryTreeNode<int> *p = new BinaryTreeNode<int>(); //新節點
p->data = data;
BinaryTreeNode<int> *last = GetLastParent(root); //獲取最後插入位置
if (last != nullptr) {
if (last->left != nullptr) //如果左孩子不為空則插入否則插右孩子
last->right = p;
else
last->left = p;
}
else
throw std::exception("插入失敗");
BinaryTreeNode<int> *pp = last;
if (flag == MAX) {
while (pp!=nullptr && pp->data<p->data)
{
Swap(pp, MAX);
p = pp;
pp = GetParent_heap(root, p); //不斷向上排序
}
}
else if (flag == MIN) {
while (pp!=nullptr && pp->data>p->data)
{
Swap(pp, MIN);
p = pp;
pp = GetParent_heap(root, p);
}
}
}
BinaryTreeNode<int>* GetParent_heap(BinaryTreeNode<int>* root, BinaryTreeNode<int>* node)
{
BinaryTreeNode<int> *p = root;
std::queue<BinaryTreeNode<int> *> q; //依然是層次遍歷思想
q.push(p);
while (!q.empty())
{
p = q.front();
q.pop();
if (p->left != nullptr) {
if (p->left == node) {
return p;
}
q.push(p->left);
if (p->right != nullptr) {
if (p->right == node) {
return p;
}
q.push(p->right);
}
}
}
return nullptr;
}
BinaryTreeNode<int>* GetLastParent(BinaryTreeNode<int>* root)
{
BinaryTreeNode<int> *p = root;
std::queue<BinaryTreeNode<int> *> q;
std::stack<BinaryTreeNode<int> *> s;
BinaryTreeNode<int> *_p = nullptr;
int count = 0;
q.push(p);
while (!q.empty())
{
p = q.front();
q.pop();
if (p->left != nullptr) {
q.push(p->left);
s.push(p);
if (p->right != nullptr)
q.push(p->right);
}
else if(count==0)
{
_p = p; //記錄第一個葉子節點
count++;
}
}
BinaryTreeNode<int> *pp = s.top();
s.pop();
if (pp->right != nullptr)
return _p; //如果最後一個父節點孩子滿了則返回第一個葉子節點
else
return pp;
}
測試一下
堆的刪除
同插入一樣,我們將其分為刪除與排序,為了節省排序時間,我們將最後一個節點的值賦給根節點,然後刪除最後一個節點,然後只需對根節點做一次遞迴排序便可,因為這時根節點的左右子樹依然是最大堆。
int DeleteHeap(BinaryTreeNode<int>* root, int flag)
{
if (root == nullptr) {
throw std::exception("樹為空");
}
BinaryTreeNode<int> *p = root;
BinaryTreeNode<int> *_p = nullptr;
std::queue<BinaryTreeNode<int> *> q;
int ret = root->data;
q.push(p);
while (!q.empty()) //走一趟層次遍歷後指標指向序號最後一個
{
p = q.front();
q.pop();
if (p->left != nullptr) {
q.push(p->left);
if (p->right != nullptr) {
q.push(p->right);
}
}
}
_p = GetParent_heap(root, p); //獲取它的父節點
if (_p != nullptr) {
if (_p->left == p) {
_p->left = nullptr;
}
else
{
_p->right = nullptr;
}
root->data = p->data; //將其值賦給根節點
delete p;
}
Swap(root, flag); //排序
return ret;
}