前言
通常的编程学习中,我们都会接触到堆这种树结构,本篇博客主要讨论最常见的二叉堆,并将其和斐波那契堆与二项堆进行一些比较。
二叉堆的定义
二叉堆顾名思义是形如二叉树的堆,而且是完全二叉树,不过其底层实现通常都是用的数组。因为如果将其从上到下,从左到右依次从1开始编号,正好可以填满数组的前部分,更重要的是实现简单高效,编号此时即为数组索引。如下图即为一个最大堆:
对于第i个节点,其父母索引为:i / 2;
其左孩子索引为:2i,右孩子索引为:2i + 1。
二叉堆的特性和用途
堆有最大堆和最小堆两种,前者中父节点关键字总是比左右孩子大,同时左子堆和右子堆也是这样;后者反之。
堆最大的用于是用来实现优先级队列,根据最大堆和最小堆之分,则可以有最小优先级队列和最大优先级队列
此外,还可以用来实现堆排序,本篇博客不讨论。
二叉堆的操作
1、insert:向堆中插入一个元素,并保持堆的特性;
2、maximum:返回堆中最大关键字;
3、extractMax:抽取堆中的最大关键字,并保持堆特性;
3、increaseKey:增加某一节点的关键字值,并保持堆的特性。
inset
算法流程:
1、现在堆末尾新增一个关键字为无穷小的元素;
2、然后调用increaseKey过程对其进行自下而上的调整,直至保持了堆的特性。
时间复杂度O(lgn)。
maximum
直接返回堆根部的元素关键字
extractMax
算法流程:
1、记下根节点关键字,并将最末尾节点复制到根,同时将堆大小减1;
2、比较该节点及其左右孩子,找出最大值节点,互相交换;
3、以交换后的节点为新根,即子堆,继续上述过程;
4、直到该节点本身就是最大值节点,或者已经成为叶子,即没有孩子为止。
上述过程自上而下迭代下来,由于树高为O(lgn),因而时间复杂度为O(lgn)。稍后直接给出源代码。
increaseKey
该算法的参数是堆中某节点的句柄(指针或者整数标识等)和新关键字
算法流程:
1、比较该节点的关键字和新关键字的大小,如果新关键字不大于就关键字,则拒绝修改,终止程序,否则转2;
2、修改掉旧关键字后,将该节点和父节点关键字值比较,如果大于则互相交换,并继续向上迭代;
3、如果不大于或者已经成为根,则终止程序,对特性得以保持。
上述过程自下而上进行,由于树高为O(lgn),因而时间复杂度为O(lgn)。稍后该处源代码。
二叉堆实现诸诸细节
一、堆中节点的结构
底层数据结构我们采用的是vector数组,而其节点结构则如下所示:
template <typename Key, typename Handle = void*>
struct node
{//堆结点
Key key;//堆关键字,通常是应用对象的优先权
Handle obj_handle;//与该关键字关联的句柄,指向外部应用对象,可能是指针,不可变
node(const Key &k, const Handle &h_) :key(k), obj_handle(h_){}
node() :key(), obj_handle(){}<pre class="cpp" name="code"> Handle getHandle()const { return obj_handle; }
};
正如《算法导论》所讲,堆结点通常只需要两个元素,一个是优先级(key),另一个就是外部对象的句柄,通常是指针,这样可以很方便于和外部对象作用,稍后举例。
在这个结构中,Handle的默认类型时void*,通常情况下应用时是必须指明确切类型的,但是如果堆中只是存储单一关键字,则可以不必指定。
二、堆结构
template <typename Key,typename Handle = void*,class EditObjHandle = doNothing<Handle>,class CompareKey = greater<Key>>
class heap//类型参数分别为关键字类型,句柄类型,编辑堆结点句柄所对应的外部对象的句柄,比较器
{//二叉堆,默认最大堆
private:
typedef node<Key, Handle> node;
vector<node> h;
int size;//堆大小
EditObjIndex editIndex;
CompareKey compare;
};
类型参数中需要着重介绍的是第三个参数EditObjHandle——在保持堆的特性的时候,我们需要移动堆中的节点,那么外部对象所关联的节点索引必然改变,因而通过这个函数对象我们可以轻易地通过obj_handle使索引保持一致,这个函数对象由使用者自行提供。
默认得类型时doNothing<Handle>,它的实现如下:
template <typename Handle>
struct doNothing :public binary_function < Handle, int, void >
{//默认的外部对象在堆中的句柄修改函数对象
void operator()(const Handle &hd, int h)
{//不作任何事
return;
}
};
是的,它不作任何事情,因而如何改变外部对象的堆中节点索引必须由自己定义。这个默认定义的好处就是,在只存储单一关键字时可以使用它,无需指定。
最后一个类型参数用以表示堆中节点按何种策略排序,即实现最小堆还是最大堆,我们默认实现的是最大堆。
三、堆节点和外部对象的关系
外部对象通常是这样的形式,如下:
template <typename Value>
struct object
{//测试对象
int node_index;//在堆中的索引
int priority;//优先权
Value v;//值
object(int pri, const Value& vv) :heap_handle(0), v(vv), priority(pri){}//初始,句柄默认为0
};
heap_handle是其所关联的堆结点的索引;priority是这个对象的优先级,也就是堆结点的关键字域key;v是其的值。初始时heap_handle为0(表示空),在之后的对特性调整中会得到正确的维护。
它们之间的交互关系如下:
其中的EditObjIndex一般可以是如下形式:
template <typename Value>
struct editObjIdx :public binary_function < object<Value>*, int , void >
{//自定义的修改对象的堆中句柄函数对象
void operator()(object<Value> *p, int hh)
{
p->node_index = hh;
}
};
二叉堆、斐波那契堆、二项堆各可合并堆操作时间的比较,注:二叉堆有些操作没有予以实现
下面,我们给出最终的实现源代码,注释详实。
#include<iostream>
#include<vector>
#include<functional>
using namespace std;
//#define MINIMUM_HEAP_H
#ifdef MINIMUM_HEAP_H //如果定义了该值
const static int EXTREAM = 0x7fffffff;//则表明实现的是最小堆
#else
const static int EXTREAM = 0x80000000;
#endif
template <typename Key, typename Handle = void*>
struct node
{//堆结点
Key key;//堆关键字,通常是应用对象的优先权
Handle obj_handle;//与该关键字关联的句柄,指向外部应用对象,可能是指针,不可变
node(const Key &k, const Handle &h_) :key(k), obj_handle(h_){}
node() :key(), obj_handle(){}
Handle getHandle()const { return obj_handle; }
};
template <typename Handle>
struct doNothing :public binary_function < Handle, int, void >
{//默认的外部对象在堆中的句柄修改函数对象
void operator()(const Handle &hd, int h)
{//不作任何事
return;
}
};
template <typename Key,typename Handle = void*,class EditObjIndex = doNothing<Handle>,class CompareKey = greater<Key>>
class heap//类型参数分别为关键字类型,句柄类型,编辑堆结点句柄所对应的外部对象的句柄,比较器
{//二叉堆,默认最大堆
private:
typedef node<Key, Handle> node;
vector<node> h;
int size;//堆大小
EditObjIndex editIndex;
CompareKey compare;
public:
//四种不同的构造函数
heap() :h(), editIndex(), compare(),size(0){}
heap(const EditObjIndex &eh) :editIndex(eh), compare(), h(), size(0){}
heap(const CompareKey &comp) :compare(comp), editIndex(), h(),size(0){}
heap(const EditObjIndex &eh, const CompareKey &comp) :editIndex(eh), compare(comp), h(), size(0){}
bool empty()const { return size == 0; }
size_t length()const { return size; }
Key maximum()const { return h[0].key; }//如果是最小堆,则返回的是最小关键字
void insert(const Key&,const Handle& = Handle());
Key extractMax();
void increaseKey(int, const Key&);
};
template <typename K,typename H,class EOH,class CK>
K heap<K, H, EOH, CK>::extractMax()
{//抽取最大值,并调整堆
K k = h[0].key;
h[0] = h[--size];//将最后一个元素放到堆根部
editIndex(h[size].obj_handle, 0);//修改该元素关联的外部对象在堆中的句柄
int left = 1, right = 2, i = 0, largest = i;
while (left < size)
{//从上至下开始调整堆
if (left < size && compare(h[left].key, h[largest].key))
largest = left;
if (right < size && compare(h[right].key, h[largest].key))
largest = right;//确定最大值
if (largest == i) break;//如果本身最大,则退出,不再调整
else
{//否则
std::swap(h[largest], h[i]);//交换两元素
//并修改这两个元素关联的外部对象的堆中句柄
editIndex(h[largest].obj_handle, largest);
editIndex(h[i].obj_handle, i);
}
left = 2 * largest + 1, right = 2 * largest + 2,i = largest;//修改,以继续迭代
}
return k;//返回最大值
}
template <typename K,typename H,class EOH,class CK>
void heap<K, H, EOH, CK>::increaseKey(int index, const K &k)
{//增加指定元素的关键字值,如果是最小堆,该函数即表示减小关键字值
if (!compare(k, h[index].key))
{//若新关键字不适合
#ifdef MINIMUM_HEAP_H
cout << "Error:greater key" << endl;
#else
cout << "Error:less key" << endl;
#endif
return;
}
h[index].key = k;
int parent = (index - 1) / 2;
while (parent >= 0 && compare(k,h[parent].key))//若比父节点大
{//自下而上调整堆
std::swap(h[index], h[parent]);
editIndex(h[parent].obj_handle, parent);
editIndex(h[index].obj_handle, index);
if (parent == 0) break;//若已到根部
else
{//否则继续迭代
index = parent;
parent = (index - 1) / 2;
}
}
}
template <typename K,typename H,class EOH,class CK>
void heap<K, H, EOH, CK>::insert(const K &k,const H &hd = H())
{//插入元素,参数分别为外部对象的权值和唯一性标识
if (size == h.size()) h.resize(2 * size + 1);//如果vector已满
++size;
editIndex(hd, size - 1);
if (size == 1)//若是第一个元素
h[size - 1] = node(k, hd);
else
{//否则
h[size - 1] = node(EXTREAM, hd);//若是
increaseKey(size - 1, k);
}
}
//----------------------------------------测试---------------------------------------
template <typename Value>
struct object
{//测试对象
int node_index;//在堆中的的索引
int priority;//优先权
Value v;//值
object(int pri, const Value& vv) :node_index(0), v(vv), priority(pri){}//初始,索引默认为0
};
template <typename Value>
struct editObjIdx :public binary_function < object<Value>*, int , void >
{//自定义的修改对象的堆中索引函数对象
void operator()(object<Value> *p, int hh)
{
p->node_index = hh;
}
};
int main()
{
heap<int, object<int>*, editObjIdx<int>> h;
vector<object<int>*> vec;//存储这些obj
for (int i = 0; i <= 10; i += 2)
{//obj的权值为2 * i,值为i * i
object<int> *p = new object<int>(2 * i, i * i);
vec.push_back(p);
h.insert(2 * i, p);
}
for (int i = 1; i <= 10; i += 2)
{
object<int> *p = new object<int>(2 * i, i * i);
vec.push_back(p);
h.insert(2 * i, p);
}
/*while (!h.empty())
cout << h.extractMax() << endl;
cout << endl;*/
for (size_t i = 0; i <= h.length(); i += 3)
{//增加某些对象的权值
vec[i]->priority += 100;
h.increaseKey(vec[i]->node_index, vec[i]->priority);
}
while (!h.empty())
cout << h.extractMax() << endl;
for (int i = 0; i != vec.size(); ++i)
delete vec[i];
/*heap<int> h;//只有关键字的堆,此时Handle和EditObjIndex等均为默认值,没有所谓的句柄
for (int i = 0; i != 10; ++i)
h.insert(i);
while (!h.empty())
cout << h.extractMax() << endl;*/
getchar();
return 0;
}