堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构。堆结构的二叉树存储有两种:
最大堆:每个父节点的都大于孩子节点;
最小堆:每个父节点的都小于孩子节点。
下来以大堆为例:
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;
}
else
break;
}
}
实现大堆的完整代码:
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;
}
else
break;
}
}
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;
}
else
break;
}
}
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;
}
else
break;
}
}
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;
}
else
break;
}
}
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 11
cout<<"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 16
h.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 19
h1.Push(9);
h1.PrintPriorityQueue(); //9 10 13 12 11 18 15 17 14 19 16
h1.Pop();
h1.PrintPriorityQueue();
}