一、描述
堆其实就是一个数组,它被看做是一个近似完全二叉树,树上的每个元素的每个节点对应数组中的一个元素。除了最底层没有填充完,该树是完全的,而且是从左向右填充。
表示堆的数组包括两个属性:
A.length 通常表示数组元素的个数
A.heap_size 通常表示有多少个堆元素存储在数组中
也就是说 A[1 … A.length] 可能都存有数据,但是只有 A[1 … A.heap_size] 存放的是堆的有效元素。
这里:
0 <= A.heap_size <= A.length
这里,树根为 A[1]
给定一个下标我们就能很容易计算出它的父节点、左孩子、右孩子的下标
PARENT(i)
return i/2
LEFT(i)
return 2*i
RIGHT(i)
return 2*i + 1
这里一般可以用宏或者 inline 内联函数来实现。
二、最小堆与最大堆
最大堆中,最大堆性质指的是:除了根节点之外的所有节点都要满足
A[PARENT(i)] >= A[i]
最小堆中,最小堆性质指的是:反过来
A[PARENT(i)] <= A[i]
最小堆通常用于构造优先队列
如果把堆看成一棵树,我们定义一个堆中的节点的高度:
该节点到叶子节点最长简单路径上边的数目 ;进而我们可以将堆的高度定义为根节点的高度
那么堆是一个包含 n 个节点的完全二叉树,其高度就是
O( lg(n) )
其实堆上面的一些操作,其时间至多和堆的高度成正比,即 o(log(n))
基本操作如下:
MIN_HEAPIFY : O(lg(n)),它自上而下维护(这里讨论最小堆)堆的性质
BUILD_MIN_HEAP : 线性时间复杂度,从无序输入数据(数组)中构造一个最小堆
HEAP_SORT : 堆排序,时间复杂度O( n*lg(n) ),对一个数组进行原址排序
MAX_HEAP_INSERT / HEAP_EXTRACT_MIN / HEAP_DECREASE_KEY / HEAP_MINIMUM : 时间复杂度都是O(lg(n)),功能是利用堆实现一个优先队列。
三、维护堆的性质
MIN_HEAPIFY 是维护堆性质的关键。输入是数组 A 和下标 i
调用它的时候,我们假定根节点 LEFT( i ) RIGHT( i ) 的二叉树都是最小堆,这时 A[ i ] 可能大于其孩子,违背了最小堆的性质。 MIN_HEAPIFY 通过让 A[ i ] 的值逐级下降,从而使得以 i 为根节点的子数遵循最小堆的性质。
伪代码如下:
// suppose that the left & right children are already meet min-heap
// modify the location of i inorder to meet the requirement
//
MIN_HEAPIFY(A, i)
{
left = LEFT(i)
right = RIGHT(i)
if (left < A.heap_size && A[left] < A[i])
minimal = left
else
minimal = i
if (right < A.heap_size && A[right] < A[i])
minimal = right
if (minimal != i) {
exchange A[i] with A[minimal]
MIN_HEAPIFY(A, minimal) //recurse
}
}
//
//
MIN_HEAPIFY_LOOP(A, i)
{
left = LEFT(i)
right = RIGHT(i)
if (left < A.heap_size && A[left] < A[i])
minimal = left
else
minimal = i
if (right < A.heap_size && A[right] < A[i])
minimal = right
while (minimal != i) { // loop instead of recurse
exchange A[i] with A[minimal]
i = minimal
l = LEFT(i)
r = RIGHT(i)
if (l < A.heap_size && A[l] < A[i])
minimal = l
else
minimal = i
if (r < A.heap_size && A[r] < A[i])
minimal = r
}
}
第一个版本是递归版,第二个用普通的循环代替递归操作。
时间复杂度:
T( n ) = O( lg n )
四、建堆
我们可以用自底向上的方法利用过程 MIN_HEAPIFY 把一个大小为 n = A.length 的数组 A[ 1 … n ]转换为最大堆。
观察可以知道,字数组中 A[ (n/2)+1 … n ] 这些元素都是树的叶子节点,每个叶子节点可以看做是只包含一个元素的堆。
因此,build 堆的过程从最底下的第一个非叶子节点开始就可以了,然后自底向上 到根节点,不断调整以当前节点为根的子树的最小(或者最大)堆性质:
// A is the input data array
// from n/2 , bottom to top
//
BUILD_MIN_HEAP(A)
{
A.heap_size = A.length
for i = (A.length / 2) to 1
MAX_HEAPIFY(A, i)
}
时间复杂度:
O( n )
五、堆排序算法
初始的时候,堆排序算法利用 BUILD_MIN_HEAP 将输入 A[ 1 … n ] 建立为最小堆,n = A.length
数组中最大元素在 A[ 1 ] 之中,通过把它与 A[ n ] 进行互相交换,可以让元素放到正确的位置上去,然后去掉这个点 n (可以通过减少 A.heap_size)来实现。
交换到 A[ 1 ] 的节点可能不符合最小(或者最大)堆的性质,因此我们调整 MIN_HEAPIFY(A , 1) ,从而在 A [ 1 … n-1 ] 上再次构造一个最小(大)堆
堆排序算法不断重复这个过程,直到堆的大小从 n - 1 降到 2
HEAPSORT(A)
{
BUILD_MIN_HEAP(A);
for i = A.length downto 2
{
exchange A[1] with A[i] //A[1] store the minimal/largest
A.heap_size = A.heap_size - 1
MIN_HEAPIFY(A, 1)
}
}
时间复杂度:
O( n * lg(n) )
六、优先队列
优先队列是一种用来维护由一组元素构成的集合S的数据结构,其中每个元素都有一个相关值,称作‘关键字’
一个“最小优先队列”支持以下操作:
INSERT(S, x) : 将元素x插入结合S之中
MINIMUM(S) : 返回S中具有最小关键字的元素
EXTRACT_MIN(S) : 去掉并返回S中的具有最小关键字的元素
DECREASE_KEY(s, x, k) : 将元素 x 的关键字减少到 k ,这里是针对最小堆, k <= x 才行。
如果是最大堆,这里应该是增加关键字, k >= x
应用点:
“最大优先队列”的应用有很多,其中一个就是在共享计算机系统的作业调度中,记录将要执行的各个作业以及他们之间的相对优先级。当一个作业完成或者被中断后,调度器调用 EXTRACT_MAX 从所有等待的作业中,取出具有最高优先级的作业来执行。在任何时候都能够调用 INSERT 将一个新作业加入到队列中来。
相应的,“最小优先队列”可以被用于基于事件驱动的模拟器。队列中保存要模拟的时间,每个事件都有一个发生事件作为其关键字。事件必须按照发生的时间顺序进行模拟,因为某一个事件的模拟结果可能会触发对其他事件的模拟。在每一步中,模拟程序调用 EXTRACT_MIN 来选择下一个要模拟的事件。新的事件产生时,模拟器通过调用 INSERT 将它插入到最小优先队列中。
HEAP_MINIMUM(A)在 O(1)时间获得最小值:
#
# priority queue: based on minimal-heap
#
// get the minimal element of the array A
//
HEAP_MINIMAL(A)
{
return A[1]
}
HEAP_EXTRACT_MIN 实现 EXTRACT_MIN 操作。它与 HEAP_SORT的 for 循环部分很相似。
时间复杂度: O( lg n )
// get the minimal, delete it from the origin data set
//
HEAP_EXTRACT_MIN(A)
{
if (A.heap_size < 1)
error: heap underflow
min = A[1]
A[1] = A[A.heap_size]
A.heap_size = A.heap_size - 1
MIN_HEAPIFY(A, 1) // adjust 维持堆的性质
return min
}
HEAP_DECREASE_KEY 实现 DECREASE_KEY 的操作
在优先队列中,我们希望减少/增加 关键字的优先队列元素由对应的数组下标 i 来标识。这一操作需要首先将元素 A[ i ] 的关键字更新为新的值。而减少/增加 关键字的值之后,可能会违反堆的性质,从当前节点到树的根节点的路径上(自下而上),需要不断为新增的关键字寻找适当的插入位置,不断与它的父节点元素进行比较和交换,直到它符合最小/最大堆的性质为止。
// in minimal-heap, key should be :
// key <= i
// otherwise, in max-heap, key >= i
// replace the formal one, ajust it from bottom to top
// to keep the requirement of min/max-heap
//
HEAP_DECREASE_KEY(A, i, key)
{
if (key > A[i])
error: key is greater than current key
A[i] = key
while (i > 1 && A[PARENT(i)] > A[i]) {
exchange A[i] with A[PARENT(i)]
i = PARENT(i)
}
}
时间复杂度也是 : O( lg n )
MIN_HEAP_INSERT 能够实现 INSERT 操作。
它的输入是要被插入到最小堆 A 中的新元素的关键字, MIN_HEAP_INSERT 首先增加一个关键字为(正无穷大)的叶子节点来扩展最小堆,(如果是给最大堆插入,就增加一个负无穷大的叶子节点)
然后调用 HEAP_DECREASE_KEY 为新节点设置对应的关键字,同时调整后保持最小堆的性质
时间复杂度是: O( lg n )
// key is the new element to insert in A
// we could add a new element at the tail
// initialze it with infinity(negative infinity) of min(max) heap
// change the key of it
//
MIN_HEAP_INSERT(A, key)
{
A.heap_size = A.heap_size + 1
A[A.heap_size] = infinity in min-heap(or negative infinity in max-heap)
HEAP_DECREASE_KEY(A, A.heap_size, key)
}
ADDITION 补充:
算法导论习题 6.5-8:
HEAP_DELETE( A, i ) 操作能够将节点 i 从堆 A 之中删除。对于一个包含 n 个元素的堆,设计一个能够在 O(lg n) 时间内完成的操作:
分析:
// design the operation delete(A, i)
// to delete the exact element i (means A[i]) in heap A
//
// time requirement should be O( log(n) )
//
HEAP_DELETE(A, i)
{
if (i > A.heap_size)
error: i is out of range
key = A[A.heap_size]
A.heap_size = A.heap_size - 1
if (key < A[i]) {
// min-heap should decrease, decrease will adjust from
// bottom to top
// max-heap should increase here (key > A[i])
HEAP_DECREASE_KEY(A, i, key) //往上调整
}
else {
//key >= A[i]
A[i] = key
MIN_HEAPIFY(A, i) //往下调整
}
}
算法导论习题 6.5-9:
请设计一个时间复杂度为 O(n * lg k)的算法,它能够将 k 个有序链表合并为一个有序链表,这里 n 是指所有输入链表包含的总的元素的个数(提示:用最小堆来完成 k 路合并)
分析:
我们利用最小堆的性质和操作,k个已经有序的链表,我们将每个链表的头元素放到最小堆中进行维护,然后每次选择最小的元素出来,根据所选节点所在的链表是否为空进行下一步操作即可。
这样,选择 n 个元素,每次选择可能导致 O(lg n)最小堆性质调整的时间。符合条件了。
实现:
step 1: 取每个链表的首元素,构造为一个含有 k 个元素的最小堆 (看原来链表的排序方式决定)
step 2: 若堆不为空,将根元素记录到已经排序的结果之中(新的结果链表)
step 3: 判断根节点所在的链表,若链表为空则goto step 4,否则goto step 5
step 4: 删除堆的根节点,调整之后 goto step 2
step 5: 将根节点替换为原根节点所在链表的下一个元素,调整堆,goto step 2
七、实现
伪代码实现:
#
# preparation
#
PARENT(i)
return (int)(i/2)
#define PARENT(i) (i)>>2
LEFT(i)
return (i * 2)
#define LEFT(i) (i)<<2
RIGHT(i)
return (i * 2) + 1
#define RIGHT(i) ((i)<<2)+1
#
# operation
#
// suppose that the left & right children are already meet min-heap
// modify the location of i inorder to meet the requirement
//
MIN_HEAPIFY(A, i)
{
left = LEFT(i)
right = RIGHT(i)
if (left < A.heap_size && A[left] < A[i])
minimal = left
else
minimal = i
if (right < A.heap_size && A[right] < A[i])
minimal = right
if (minimal != i) {
exchange A[i] with A[minimal]
MIN_HEAPIFY(A, minimal) //recurse
}
}
//
//
MIN_HEAPIFY_LOOP(A, i)
{
left = LEFT(i)
right = RIGHT(i)
if (left < A.heap_size && A[left] < A[i])
minimal = left
else
minimal = i
if (right < A.heap_size && A[right] < A[i])
minimal = right
while (minimal != i) { // loop instead of recurse
exchange A[i] with A[minimal]
i = minimal
l = LEFT(i)
r = RIGHT(i)
if (l < A.heap_size && A[l] < A[i])
minimal = l
else
minimal = i
if (r < A.heap_size && A[r] < A[i])
minimal = r
}
}
// A is the input data array
// from n/2 , bottom to top
//
BUILD_MIN_HEAP(A)
{
A.heap_size = A.length
for i = (A.length / 2) to 1
MAX_HEAPIFY(A, i)
}
// as a result, array A will maintain the sort result
// time requirement : O(n * log(n))
//
HEAPSORT(A)
{
BUILD_MIN_HEAP(A);
for i = A.length downto 2
{
exchange A[1] with A[i] //A[1] store the minimal/largest
A.heap_size = A.heap_size - 1
MIN_HEAPIFY(A, 1)
}
}
#
# priority queue: based on minimal-heap
#
// get the minimal element of the array A
//
HEAP_MINIMAL(A)
{
return A[1]
}
// get the minimal, delete it from the origin data set
//
HEAP_EXTRACT_MIN(A)
{
if (A.heap_size < 1)
error: heap underflow
min = A[1]
A[1] = A[A.heap_size]
A.heap_size = A.heap_size - 1
MIN_HEAPIFY(A, 1) // adjust
return min
}
// in minimal-heap, key should be :
// key <= i
// otherwise, in max-heap, key >= i
// replace the formal one, ajust it from bottom to top
// to keep the requirement of min/max-heap
//
HEAP_DECREASE_KEY(A, i, key)
{
if (key > A[i])
error: key is greater than current key
A[i] = key
while (i > 1 && A[PARENT(i)] > A[i]) {
exchange A[i] with A[PARENT(i)]
i = PARENT(i)
}
}
// key is the new element to insert in A
// we could add a new element at the tail
// initialze it with infinity(negative infinity) of min(max) heap
// change the key of it
//
MIN_HEAP_INSERT(A, key)
{
A.heap_size = A.heap_size + 1
A[A.heap_size] = infinity in min-heap(or negative infinity in max-heap)
HEAP_DECREASE_KEY(A, A.heap_size, key)
}
// design the operation delete(A, i)
// to delete the exact element i (means A[i]) in heap A
//
// time requirement should be O( log(n) )
//
HEAP_DELETE(A, i)
{
if (i > A.heap_size)
error: i is out of range
key = A[A.heap_size]
A.heap_size = A.heap_size - 1
if (key < A[i]) {
// min-heap should decrease, decrease will adjust from
// bottom to top
// max-heap should increase here (key > A[i])
HEAP_DECREASE_KEY(A, i, key)
}
else {
//key >= A[i]
A[i] = key
MIN_HEAPIFY(A, i)
}
}
min_heap.h
#include <iostream>
// preparation
//
#define PARENT(i) (i/2)
#define LEFT(i) (i*2)
#define RIGHT(i) (i*2)+1
//preparation end
//
#define DTYPE int
#define MAX 50
#define NEGATIVE_INFINITY -999999
#define INFINITY 999999
class Heap {
public:
Heap();
virtual ~Heap();
bool Initialize(DTYPE *source, const int len);
/*
* after sort
* max-heap: A: 1 2 3 4 ...
* min-heap: A: 5 4 3 2 ...
*/
void HeapSort();
/*
* basement
*/
void Min_Heapify(int i);
void Min_Heapify_Loop(int i);
void Build_Min_Heap(); // call this after call Initialize()
void exchange(const int a, const int b);
void print_A();
/*
* priority-queue operation based on min-heap
*/
DTYPE Heap_Minimal(); //get minimal
DTYPE Heap_Extract_Min(); //get min, delete it
void Heap_Decrease_Key(int i, DTYPE key);
void Min_Heap_Insert(DTYPE key);
bool Heap_Delete(int i);
private:
DTYPE *A;
int heap_size;
int length;
};
min_heap.cc
#include "./min_heap.h"
Heap::Heap()
{
A = new DTYPE[MAX + 1];
heap_size = 0;
length = 0;
}
Heap::~Heap()
{
delete []A;
heap_size = 0;
length = 0;
}
bool Heap::Initialize(DTYPE *source, const int len)
{
/*
if (len > this->length) {
std::cerr<<"source is too long.."<<std::endl;
return false;
}
*/
this->length = len;
for (int i = 1; i <= len; i++)
A[i] = source[i];
this->heap_size = len;
return true;
}
// time requirement : O(n * log(n))
//
// should invoke Initialize() before this.
//
// after this, A[] will be sorted
// heap_size will equal to 0
// length is also the length
//
void Heap::HeapSort()
{
Build_Min_Heap();
int len = this->length;
for (int i = len; i >= 2; i--) {
exchange(i, 1);
this->heap_size--;
Min_Heapify(1);
}
}
/*
* basement
*/
void Heap::Min_Heapify(int i)
{
int left = LEFT(i);
int right = RIGHT(i);
std::cout<<"i="<<i<<", left="<<left<<", right="<<right<<std::endl;///
int minimal = i;
if (left <= this->heap_size && A[left] < A[minimal])
minimal = left;
if (right <= this->heap_size && A[right] < A[minimal])
minimal = right;
if (minimal != i) {
std::cout<<"before exchange: A[i]="<<A[i]<<", A[minimal]="<<A[minimal]<<std::endl;
exchange(i, minimal);
std::cout<<" exchange: A[i]="<<A[i]<<", A[minimal]="<<A[minimal]<<std::endl;
Min_Heapify(minimal);
}
}
void Heap::Min_Heapify_Loop(int i)
{
int j = i;
int left = LEFT(i);
int right = RIGHT(i);
int minimal = j;
if (left <= this->heap_size && A[left] < A[minimal])
minimal = left;
if (right <= this->heap_size && A[right] < A[minimal])
minimal = right;
while (minimal != j) {
exchange(minimal, j);
j = minimal;
int l = LEFT(j);
int r = RIGHT(j);
if (l <= this->heap_size && A[l] < A[j])
minimal = l;
if (r <= this->heap_size && A[r] < A[minimal])
minimal = r;
}
}
// call this after call Initialize()
// handle the input data set A[]
// from A.length/2 down to node
// min_heapify() each node
//
void Heap::Build_Min_Heap()
{
this->heap_size = this->length;
//std::cout<<"heap_size = "<<heap_size<<", length = "<<length<<std::endl;
int loop_len = static_cast<int>(this->length / 2);
//std::cout<<"loop len = "<<loop_len<<std::endl;
for (int i = loop_len; i >= 1; i--) {
std::cout<<"i = "<<i<<std::endl; //
Min_Heapify_Loop(i);
}
}
// a, b is the superscript of A
// exchange the element A[a] with A[b]
void Heap::exchange(const int a, const int b)
{
DTYPE temp = A[a];
A[a] = A[b];
A[b] = temp;
}
void Heap::print_A()
{
for (int i = 1; i <= this->length; i++)
std::cout<<A[i]<<" ";
std::cout<<std::endl;
}
/*
* priority-queue :
* operation based on minimal-heap
*/
//get minimal
DTYPE Heap::Heap_Minimal()
{
if (this->heap_size < 1) {
std::cerr<<"heap is empty.."<<std::endl;
return -1;
}
return A[1];
}
//get min, delete it
DTYPE Heap::Heap_Extract_Min()
{
if (this->heap_size < 1) {
std::cerr<<"heap is empty.."<<std::endl;
return -1;
}
int min = A[1];
A[1] = A[this->heap_size];
this->heap_size = this->heap_size - 1;
Min_Heapify(1);
return min;
}
// in minimal-heap, key should :
// key <= i
//
// min-heap: decrease key
// max-heap: increase key
//
void Heap::Heap_Decrease_Key(int i, DTYPE key)
{
if (key > A[i]) {
std::cerr<<"key > A["<<i<<"].."<<std::endl;
return ;
}
A[i] = key;
while (i > 1 && A[PARENT(i)] > A[i]) {
exchange(i, PARENT(i));
i = PARENT(i);
}
}
// insert at the tail
// min-heap: initialize with INFINITY
// max-heap: initialize with NEGATIVE_INFINITY
//
void Heap::Min_Heap_Insert(DTYPE key)
{
this->heap_size = this->heap_size + 1;
A[this->heap_size] = INFINITY;
Heap_Decrease_Key(this->heap_size, key);
}
// design the function delete(A, i)
// delete the exact element i (means A[i]) in heap A
//
// time requirement : O( log(n) )
//
bool Heap::Heap_Delete(int i)
{
if (i > this->heap_size) {
std::cerr<<"i is out of range.."<<std::endl;
return false;
}
int key = A[this->heap_size];
this->heap_size = this->heap_size - 1;
//based on min-heap
if (key < A[i]) {
// max-heap, this could be increase_key()
Heap_Decrease_Key(i, key);//adjust from bottom to top
} else {
A[i] = key;
Min_Heapify(i);//adjust from top to bottom
}
return true;
}
test.cc
#include "./min_heap.h"
int main()
{
//input data
int N;
scanf("%d", &N);
if (N > MAX) {
std::cout<<"N > MAX.."<<std::endl;
return -1;
}
DTYPE source[MAX + 1];
for (int i = 1; i <= N; i++) {
int temp;
scanf("%d", &temp);
source[i] = temp;
}
//end input
Heap _obj = Heap();
if (_obj.Initialize(source, N) == false) {
std::cout<<"Initialize error.."<<std::endl;
return -1;
}
_obj.Build_Min_Heap();
_obj.print_A();
return 0;
}