堆的基本实现及优先级队列问题

来源:互联网 发布:网络机顶盒破解 编辑:程序博客网 时间:2024/05/23 13:12

堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构。堆结构的二叉树存储有两种:

最大堆:每个父节点的都大于孩子节点;

最小堆:每个父节点的都小于孩子节点。

下来以大堆为例:

1.如何建堆?

1>首先我们选取vector来对堆中元素进行存储;

2>由于堆数据结构可被视为一棵完全二叉树,我们可以从最后一个非叶子节点起往上调整;但是须得注意,这个首先得满足左右子树都是大堆得情况

3>上调过程中,先让child指向左孩子,然后进行左,右孩子的比较,选出较大的一个,再与父节点比较,如果父节点比两孩子中较大那个值小,则需交换父节点与子节点,直至调整为大堆。


Heap(T* a,size_t size){     assert(a);     _a.reserve(size);     for (size_t i=0;i<size;++i)     {_a.push_back(a[i]);      }      //建堆,从第一个非叶子节点的父节点开始调整     for (int i=(size-2)/2;i>=0;--i)        {AdjustDown(i);     }}

void AdjustDown(size_t root){   size_t parent=root;   size_t child=parent*2+1;   //先让child指向左孩子   while (child<_a.size())   {//保证右孩子不为空if (child+1<_a.size() && _a[child+1]>_a[child]){   ++child;}if (_a[parent]<_a[child]){   std::swap(_a[parent],_a[child]);   parent=child;   child=parent*2+1;}else   break;}}


2.建好堆后如何在堆中进行push元素?

先将元素插入到数组最后一个位置,然后进行上调操作,直至满足大堆的特点。

3.pop元素?

1>我们采取先将堆中最后一个元素转移至第一个元素位置,也就是用最后一个元素覆盖掉第一个元素;

2>将最后那个pop掉;

3>从0位置处开始往下调整;

4>在向下调整时,需注意找公共父节点parent=(child-1)/2,以及循环条件


void Pop(){assert(!_a.empty());std::swap(_a[0],_a[_a.size()-1]);_a.pop_back();AdjustDown(0);}
void AdjustUp(size_t child){while (child>0){size_t parent=(child-1)/2;if (_a[parent]<_a[child]){std::swap(_a[parent],_a[child]);child=parent;}elsebreak;}}


实现大堆的完整代码:

Heap.h

#pragma once#include<iostream>#include<assert.h>#include<vector>using namespace std;template<class T>class Heap{public:Heap(T* a,size_t size){assert(a);_a.reserve(size);for (size_t i=0;i<size;++i){_a.push_back(a[i]);}//建堆,从第一个非叶子节点的父节点开始调整for (int i=(size-2)/2;i>=0;--i)   {AdjustDown(i);}}void Push(const T& d){_a.push_back(d);AdjustUp(_a.size()-1);}void Pop(){assert(!_a.empty());std::swap(_a[0],_a[_a.size()-1]);_a.pop_back();AdjustDown(0);}const T& Top(){assert(!_a.empty());return _a[0];}bool Empty(){return _a.empty();}size_t Size(){return _a.size();}void Print(){for (size_t i=0;i<_a.size();++i){cout<<_a[i]<<" ";}cout<<endl;}protected:void AdjustDown(size_t root){size_t parent=root;size_t child=parent*2+1;   //先让child指向左孩子while (child<_a.size()){//保证右孩子不为空if (child+1<_a.size() && _a[child+1]>_a[child]){++child;}if (_a[parent]<_a[child]){std::swap(_a[parent],_a[child]);parent=child;child=parent*2+1;}elsebreak;}}void AdjustUp(size_t child){while (child>0){size_t parent=(child-1)/2;if (_a[parent]<_a[child]){std::swap(_a[parent],_a[child]);child=parent;}elsebreak;}}protected:vector<T> _a;};

建小堆,其实思想都类似,所以可以采取一种方式,增强代码的复用性,即定义一个仿函数Less,larger实现数据的比较,从而实现父节点与子节点的交换,进而满足大小堆的特点。

#pragma once#include<iostream>#include<assert.h>#include<vector>using namespace std;template<class T>struct Less{bool operator()(const T& l,const T& r){return l<r;}};template<class T>struct Larger{bool operator()(const T& l,const T& r){return l>r;}};template<class T,class compare=Larger<T>>class Heap{public:Heap(T* a,size_t size){assert(a);_a.reserve(size);for (size_t i=0;i<size;++i){_a.push_back(a[i]);}//建堆,从第一个非叶子节点的父节点开始调整for (int i=(size-2)/2;i>=0;--i)   {AdjustDown(i);}}void Push(const T& d){_a.push_back(d);AdjustUp(_a.size()-1);}void Pop(){assert(!_a.empty());std::swap(_a[0],_a[_a.size()-1]);_a.pop_back();AdjustDown(0);}const T& Top(){assert(!_a.empty());return _a[0];}bool Empty(){return _a.empty();}size_t Size(){return _a.size();}void Print(){for (size_t i=0;i<_a.size();++i){cout<<_a[i]<<" ";}cout<<endl;}protected:void AdjustDown(size_t root){compare com;size_t parent=root;size_t child=parent*2+1;   //先让child指向左孩子while (child<_a.size()){//保证右孩子不为空,比较左右孩子if (child+1<_a.size() && com(_a[child+1],_a[child])){++child;}if (com(_a[child],_a[parent])){std::swap(_a[parent],_a[child]);parent=child;child=parent*2+1;}elsebreak;}}void AdjustUp(size_t child){while (child>0){compare com;size_t parent=(child-1)/2;if (com(_a[child],_a[parent])){std::swap(_a[parent],_a[child]);child=parent;}elsebreak;}}protected:vector<T> _a;};


测试:

#include"Heap.h"void Test1(){int a[] = {10,11,13,12,16,18,15,17,14,19};Heap<int> hp(a,sizeof(a)/sizeof(a[0]));     //默认大堆hp.Print();cout<<"Empty> "<<hp.Empty()<<endl;cout<<"Size> "<<hp.Size()<<endl;cout<<"Top> "<<hp.Top()<<endl;hp.Push(20);hp.Print();hp.Pop();hp.Print();}void Test2(){int a[] = {10,11,13,12,16,18,15,17,14,19};Heap<int,Less<int>> hp(a,sizeof(a)/sizeof(a[0]));hp.Print();cout<<"Empty> "<<hp.Empty()<<endl;cout<<"Size> "<<hp.Size()<<endl;cout<<"Top> "<<hp.Top()<<endl;hp.Push(9);hp.Print();hp.Pop();hp.Print();}int main(){//Test1();Test2();system("pause");return 0;}


下面谈谈堆的建立,Push,Pop时间复杂度问题:

1.在建堆的过程,时间复杂度为O(n*lgn);

2.在Push时,时间复杂度为O(lgn);

3.在Pop时,时间复杂度为O(lgn).

再引入一个优先级队列的问题,队列本来是“先进先出”的,但是在一些特殊情况下排在队中间的需要比对头元素先出去,这就是优先级问题。

以前解决的方法是:1.在优先位置插入元素,时间复杂度为O(n);在队头删除元素,时间复杂度O(1).

                                  2.在队尾插入元素,时间复杂度O(1);先找到优先出队列元素,再Pop,时间复杂度为O(n).

这两种方法,总的来说Push,Pop时间复杂度都为O(n).

学习堆之后,我们解决这个问题就可以更高效了,堆的Push,Pop总的时间复杂度才O(lgn).相比上面的方法,在处理大量数据时这种方法很占优势。

在堆得基本操作实现后,优先级队列可实现为:

template<class T,class compare=Larger<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();}bool Empty(){return _h.Empty()==0;}size_t Size(){return _h.Size();}const T& Front(){return _h.Top();}void PrintPriorityQueue(){_h.Print();}protected:Heap<T,compare> _h;};

测试用例:

void Test3(){int a[] = {10,11,13,12,16,18,15,17,14,19};PriorityQueue<int,Larger<int>> h(a,sizeof(a)/sizeof(a[0]));h.PrintPriorityQueue();  //19 17 18 14 16 13 15 12 10 11cout<<"Empty> "<<h.Empty()<<endl;cout<<"Size> "<<h.Size()<<endl;cout<<"Top> "<<h.Front()<<endl;h.Push(20);               //20 19 18 14 17 13 15 12 10 11 16h.PrintPriorityQueue();h.Pop();h.PrintPriorityQueue();PriorityQueue<int,Less<int>> h1(a,sizeof(a)/sizeof(a[0]));h1.PrintPriorityQueue();   //10 11 13 12 16 18 15 17 14 19h1.Push(9);               h1.PrintPriorityQueue();    //9 10 13 12 11 18 15 17 14 19 16h1.Pop();h1.PrintPriorityQueue();}





















0 0
原创粉丝点击