算法导论 第六章 二叉堆

前言

   通常的编程学习中,我们都会接触到堆这种树结构,本篇博客主要讨论最常见的二叉堆,并将其和斐波那契堆二项堆进行一些比较。


二叉堆的定义

    二叉堆顾名思义是形如二叉树的堆,而且是完全二叉树,不过其底层实现通常都是用的数组。因为如果将其从上到下,从左到右依次从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;
}






  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值