本文主要包括的内容:
- 堆的原理
- 建堆、调堆
- C++中 priority_queue (Java中也有一个 PriorityQueue,有必要再补上)
- 手动建堆的实例(C++类实现)
先吐个槽O(∩_∩)O:
网站上的编辑器我总是不太会用,排版总是让我很揪心,大概是写得太少了吧。慢慢习惯吧,希望每一篇文章的排版都能进步一点点。言归正传。
堆(heap)
是一种优先队列(priority queue)。
取出元素的顺序是按照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
特性:
- 结构性:用数组表示的完全二叉树。
- 有序性:任一节点的关键字是其子树所有节点的最大(小)值。
MaxHeap、MinHeap
主要操作:建堆、判空、判满、插入、删除堆顶元素。
直接生成堆的时间复杂性是O(nlog(n))。
在线性时间复杂度O(n)下建堆:
- 将N个元素按输入顺序存入,先满足完全二叉树(complete binary tree)的结构特性。
- 调整节点位置:从最后一个节点的父节点开始。
这边提供一份C语言的代码,主要关注它的原理:
/*----------- 建造最大堆 -----------*/
void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = H->Data[p]; /* 取出根结点存放的值 */
for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
Child = Parent * 2;
if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
Child++; /* Child指向左右子结点的较大者 */
if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性 */
/* 这里假设所有H->Size个元素已经存在H->Data[]中 */
int i;
/* 从最后一个结点的父节点开始,到根结点1 */
for( i = H->Size/2; i>0; i-- )
PercDown( H, i );
}
重要算法:上虑、下虑。
在C++中,提供了一种容器适配器(container adaptor)来实现这种数据结构,放在头文件 queue 中。
具体使用方式:
#include <queue>
using namespace std;
...
priority_queue<ElemType> priQue;
主要操作有:
bool empty() //Test whether container is empty.
int size() //Return size.
void push(elem) //Insert element.
void emplace(elem) //Construct and insert element.
void pop() //Remove top element.
elemType top() //Access top element.
priority_queue 默认按照严格弱序(strict weak ordering)排序规则生成最大堆,若要生成最小堆,看下面这个例子:
#include <iostream>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
int main(void) {
priority_queue<int> priQue;
for (int i = 0; i < 10; i++)
priQue.emplace(i);
while (!priQue.empty()) {
cout << priQue.top() << " ";
priQue.pop();
}
cout << endl << endl;
/*
第一个参数:元素类型;
第二个参数:底层容器类型,可以是vector或者deque;
第三个参数:比较器,可以自定义。
比如在 main 函数前写下:
struct greater {
bool operator() (int a, int b) {
return a > b;
}
};
在下面的一行代码中,把 greater<int> 改成 greater 即可,头文件 functional 也可删除。
*/
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int i = 0; i < 10; i++)
minHeap.push(i);
while (!minHeap.empty()) {
cout << minHeap.top() << " ";
minHeap.pop();
}
system("pause");
return 0;
}
但许多时候,常常有其他的需求。为此,我们就需要手动建一个堆,然后就可以在堆上增加一些函数,提供我们想要的功能。
写一个最小堆类:
typedef int ElemType;
class MinHeap {
public:
MinHeap(int capacity); //建堆,传入堆的容量。
~MinHeap();
bool isEmpty() const;
bool isFull() const;
bool insertElem(ElemType &elem);
ElemType deleteTop();
private:
ElemType *arr;
int capacity;
int len; //堆中元素个数,是堆的最后一个元素的索引。
};
/*
数组长度为堆容量+1。
0号元素作为“哨兵”,在最小堆中,设置为比所有元素都小的值。
1号元素作为根节点,那么父子节点下标有如下关系:
leftChild = parent * 2;
rightChild = parent * 2 + 1;
parent = Child/2;
*/
MinHeap::MinHeap(int capacity) {
arr = new ElemType[capacity + 1];
this->capacity = capacity;
len = 0;
arr[0] = INT_MIN; //定义在头文件 climits 里面
}
MinHeap::~MinHeap() {
delete[] arr;
arr = NULL;
}
bool MinHeap::isEmpty() const {
return 0 == len;
}
bool MinHeap::isFull() const {
return len == capacity;
}
bool MinHeap::insertElem(ElemType &elem) {
if (isFull()) return false;
/*
上滤。
如果父节点的数据值比elem大,那么把父节点的数据值拷贝到要插入的子节点中。
把子节点的下标更新为父节点,继续。
若父节点的数据值比elem小,那么elem就可以插入。
*/
int child = ++len;
for (; arr[child / 2] > elem; child /= 2)
arr[child] = arr[child / 2]; //上滤 child = parent
arr[child] = elem;
return true;
}
ElemType MinHeap::deleteTop() {
if (isEmpty()) return INT_MIN; //error
ElemType minItem = arr[1];
/*
下滤。
模拟将最后一个元素从根节点的位置向下层节点过滤的过程。
*/
ElemType tmp = arr[len--];
int parent = 1;
for (int child = 2 * parent; child <= len; child *= 2) { //如果有左孩子
if (child < len && arr[child] > arr[child + 1]) child++; //child指向数据值较小的子节点。
if (tmp <= arr[child]) break;
else arr[parent] = arr[child]; //下滤。
parent = child;
}
arr[parent] = tmp;
return minItem;
}
比如,如果要打印从一个给定索引到根节点的路径,就可以在 MinHeap 中增加一个函数,代码如下:
class MinHeap {
public:
void printPath(int index);
}
void MinHeap::printPath(int index) {
if (index > len) {
cout << "没有这个元素" << endl;
return;
}
cout << arr[index];
while (index > 1) {
index /= 2;
cout << " " << arr[index];
}
cout << endl;
}
好了,就写到这里吧。
慕友若有什么高见,可在下面回复。
祝开心。
热门评论
插入代码的时候,在第一行敲个回车,空出来,在第二行开始写,就不会自动缩进了。——做了个小笔记。