(二叉)堆是一个数组,可以看成为一个近似的完全二叉树。树上的每一个节点对应数组中的一个元素,除了最底层外,其他层是完全充满的,而且是从左到右填充。表示堆的数组A包括两个属性,A.length表示数组元素的个数,A.heapsize表示有多少个堆元素存储在该数组中。
树的根节点是A[1],给定一个节点的下标i,可以得到它的父节点,左孩子和右孩子下标:
//父节点,下标向下取整
int parent(int i)
{return i / 2;}
//左孩子节点
int left(int i)
{return 2 * i;}
//右孩子节点
int right(int i)
{return 2 * i + 1;}
二叉堆可以分成两种形式:最大堆和最小堆。在最大堆中,除了根节点之外的所有节点 i 都要满足A[parent(i)] >= A[i],也就是说某个节点的值至多和其父节点一样大。在最小堆中正好相反。
堆的性质:1.高度为h的堆中,元素个数最少为2^(n), 最多为2^(n+1)-1;
2.含有n个元素的堆的高度为lgn(以2为底)向下取整。
3.当用数组表示存储n个元素的堆时,叶子节点的下标分别为(n/2+1),(n/2+2),…,n(均为向下取整)。因为,最后一个节点的父节点为(n/2),其后所有节点均为叶子节点。
维护堆的性质
maxHeapify用于维护堆的性质,通过让A[i]同左右孩子对比,使得小值在最大堆中逐级下降。
void maxHeapify(int A[], int heapSize, int i)
{
int L = left(i);
int R = right(i);
int largest;
if (L<=heapSize && A[L]>A[i])
largest = L;
else largest = i;
if (R<=heapSize && A[R] > A[largest])
largest = R;
int temp;
if (largest != i)
{
temp = A[largest];
A[largest] = A[i];
A[i] = temp;
maxHeapify(A, heapSize, largest);
}
}
在交换后,下标为largest的节点的值时原来的A[i],以其为根节点的子树可能会违反最大堆的性质,需要还需递归调用。这个函数的事件复杂度为O(lg n),根据堆性质,对于一个树高为h的节点来说,时间复杂度为O(h)。
建堆
自底向上建堆。
void buildMaxHeap(int A[], int heapSize)
{
int i;
for (i = heapSize / 2; i > 0; i--)
maxHeapify(A, heapSize, i);
}
下标 i 从最后一个根节点开始,也就是(heapSize/2)。自底向上建堆是为了保证以当前节点 i 的左右孩子为根节点的子树是最大堆。
堆排序
时间复杂度为O(n * lg n)
void heapSort(int A[], int heapSize)
{
buildMaxHeap(A, heapSize);
int temp;
for (int i = heapSize; i > 1; i--)
{
temp = A[i];
A[i] = A[1];
A[1] = temp;
maxHeapify(A, i - 1, 1);
}
}
将 A[1] 同 A[heapSize] 交换,在将heapSize减一,然后调整到最大堆。
优先队列
优先队列:一种用来维护由一组元素构成的集合S的数据结构,其中的每一个元素都有一个相关的值,称为关键字,最大优先队列操作包括:
insert(A,x):把元素插入到集合A中;
maximum(A):返回A中具有最大关键字的元素
extractMax(A):去掉并返回A中最大关键字的元素
increaseKey(A,x,k):将元素x的关键字值增加到k,假设k的值不小于x的原关键字值
最大优先队列:记录将要执行的各个作业以及它们之间的相对优先级。当一个作业完成或者被中断,调度器将调用extractMax(A)从所有的等待作业中,选出具有最高优先级的作业来执行。在任何时候,调度器可以调用insert(A,x)把一个新作业加入到队列中来
最小优先队列:可以用于基于事件驱动的模拟器,队列中保存要模拟的事件,每个事件都有一个发生时间作为其关键字。事件必须按照发生的时间顺序进行模拟,因为某一事件的模拟结果可能会触发对其他事件的模拟。在每一步,模拟程序调用extractMin(A)来选择下一个要模拟的事件。当一个新事件产生时,模拟器通过调用insert将其插入最小优先级对列中
优先队列可以用堆来实现。我们需要确定那个对象对应一个给定的优先队列元素。再用堆来实现优先队列时,需要在堆的每个元素里存储对应对象的句柄。句柄(如一个指针或一个整型数):的准确含义依赖于具体的应用程序。同样,在应用程序的对象中,也需要存储一个堆中对应元素的句柄。通常,这个句柄是数组的下标。由于在堆的操作过程中,元素会改变其在数组中的位置,因此,在具体的实现中,在重新确定堆元素位置时,我们也需要更新相应应用程序对象中的数组下标
相应代码
int heapMaximum(int A[], int heapSize)
{
buildMaxHeap(A, heapSize);
return A[1];
}
int heapExtractMax(int A[], int *heapSize)
{
if (*heapSize < 1)
{
cout << "heap underflow";
return INT_MIN;
}
int temp = A[1];
A[1] = A[*heapSize];
A[*heapSize] = temp;
*heapSize -= 1;
maxHeapify(A, *heapSize, 1);
return temp;
}
void heapIncreaseKey(int A[], int i, int key)
{
if (key < A[i])
{
cout << "new key is smaller than current key";
return;
}
A[i] = key;
int temp;
while (i>1 && A[parent(i)] < A[i])
{
temp = A[i];
A[i] = A[parent(i)];
A[parent(i)] = temp;
i = parent(i);
}
}
void heapInsert(int A[], int* heapSize, int key)
{
*heapSize += 1;
A[*heapSize] = INT_MIN;
heapIncreaseKey(A, *heapSize, key);
}