满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点(也可以这样理解,除叶子节点外的所有结点均有两个子结点。节点数达到最大值。所有叶子结点必须在同一层上)
1:堆排序的时间复杂度为O(nlgn)。具有空间原址性的特点,也就是任意时刻只需常数个额外元素空间存储临时数据。
2:堆是一颗近似完全二叉树:除了最底层外,该树是完全充满的,最底层是从左向右填充的。
3:堆可以用数组存储,对于元素下标i,可以很快得出它的父节点,左孩子,右孩子的下标(i从0开始):
PARENT(i) = (i-1)/2
LEFT(i) = 2i + 1
RIGHT(i) = 2i + 2
一般而言,上述计算父母,左孩子,右孩子索引的函数都是通过宏或者内联函数实现。
4:堆有两种:最大堆和最小堆。
最大堆是:除了根节点之外,所有节点i都要满足:A[PARENT(i)] >= A[i]。即某个节点的值最多与父节点一样大。因此,最大堆中的最大元素放在根节点中,任意一个子树中所包含的所有节点的值都小于等于子树根节点的值。如果是从小到大排序,一般用最大堆。
最小堆是:除了根节点之外,所有节点i都要满足:A[PARENT(i)] <= A[i]。最小堆的最小值放在根节点中。如果是从大到小排序,用最小堆。
5:一个包含n个元素的堆,高度为lgn 。堆结构上的一些基本操作的运行时间与堆高度成正比,所以时间复杂度为O(lg n)。
6:当用数组表示n个元素的堆时,叶节点的下标分别是:((n-2)/2)+1, ((n-2)/2)+2,……,n。(默认元素下标从0开始,所以,最后一个结点的父节点一定是最后一个内部结点。)
7:最大堆上的操作有:
MAX-HEAPIFY :维护最大堆的性质,时间复杂度为O(lg n)。
BUILD-MAX-HEAP:从无序的数组构造一个最大堆,时间复杂度为O(n)。
HEAPSORT: 对数组进行原址排序,时间复杂度为O(nlg n)
MAX-HEAP-INSERT, HEAP-EXTRACT-MAX, HEAP-INCREASE-KEY, HEAP-MAXIMUM
8:MAX-HEAPIFY:维护最大堆性质,输入为数组A和下标i。调用MAX-HEAPIFY时,始终假定以LEFT(i)和RIGHT(i)为根的左右子树都已经是最大堆了。MAX-HEAPIFY通过让A[i]的值在最大堆中“逐级下降”,从而使得以A[i]为根节点的子树成为最大堆。
该算法中,每一步都是从A[i],RIGHT[i],LEFT[i]这三者中选出最大值。如果A[i]是最大值,则算法结束,否则,将A[i]与最大值A[j](j= RIGHT[i] or LEFT[i])交换,从而使i及其孩子满足最大堆的性质。然后针对j再次调用MAX-HEAPIFY过程,直到堆的末尾。代码如下:
//from up to down to keep the property of max heap (recursion)
void maxheapify(int *set, int index, int size)
{
int left = LEFT(index);
int right = RIGHT(index);
int largest;
if(left < size && set[index] < set[left])
{
largest = left;
}
else
{
largest = index;
}
if(right < size && set[largest] < set[right])
{
largest = right;
}
if(largest != index)
{
exchange(set+index, set+largest);
maxheapify(set, largest, size);
}
}
//from up to down to keep the property of max heap (loop)
void maxheapify2(int *set, int index, int size)
{
int left;
int right;
int largest;
while(index < size)
{
left = LEFT(index);
right = RIGHT(index);
if(left < size && set[index] < set[left])
{
largest = left;
}
else
{
largest = index;
}
if(right < size && set[largest] < set[right])
{
largest = right;
}
if(largest != index)
{
exchange(set + index, set + largest);
index = largest;
}
else
{
return;
}
}
}
若以i为根节点的子树元素个数为n,则其左孩子结点的个数最多为2/3 * n。证明如下:假设左子树节点数为x,右子树节点数为y,所以n = x + y + 1。因堆是从左向右填充的,所以x >= y。如果要使x达到最大值,那么这种情况就是堆的最底层元素恰好半满。
假设堆的高度为h,那么左子树高度为h-1,右子树高度为h-2。所以,n <= 2^(h+1) - 1,
x = 2^h - 1, y = 2^(h-1) - 1。所以 x/y ≈ 2/1 。所以,x ≈ 2n/3 。
所以,递归的算法:T(n) = T(2n/3) + Θ(1)。从而T(n) = O(lg n)。
9:因为MAX-HEAPIFY(A,i)包含一个假设,就是i的左右子树都已经是最大堆了,所以一般情况下,都是自底向上的调用MAX-HEAPIFY,比如建堆函数BUILD-MAX-HEAP,该算法把一个大小为n的数组转换为最大堆。因为叶子节点可以认为已经是最大堆了,所以建堆的过程从最后一个内部结点开始,也就是下标为(size-2)/2的元素。从该节点开始,依次向前调用MAX-HEAPIFY,建立最大堆。
在BUILD-MAX-HEAP中,需要调用n次MAX-HEAPIFY过程,所以错略计算该算法的时间复杂度为O(nlg n)。这不是一个紧缺的上界,因为MAX-HEAPIFY的时间跟高度h有关,h的范围是[0,lg n],而且高度为h的元素个数最多为n/2^(h+1) 。所以,实际上BUILD-MAX-HEAP的时间复杂度为O(n)。代码如下:
//build the max heap from set with call maxheapify
void buildmaxheap(int *set, int size)
{
int index = size-1;
int parent = PARENT(index);
int i;
for(i = parent; i >= 0; i--)
{
maxheapify2(set, i, size);
}
}
10:利用最大堆进行排序的算法HEAPSORT的基本思想是:
首相利用BUILD-MAX-HEAP算法将一个n元素数组转换为最大堆,此时该数组的最大元素就是A[0],所以,交换A[0]和A[n-1]。
然后,将数组A[0…n-2]看成新的数组,该数组中,除了根节点之外,其他左右子树依然满足最大堆的性质,只是根节点因为发生了交换所以有可能不满足最大堆性质,所以,对根节点调用MAX-HEAPIFY即可。
重复以上这个过程直到堆的大小变为1。
该算法的时间复杂度为O(n lg n)。代码如下:
//sort use max heap
void maxheapsort(int *heap, int size)
{
int heapsize = size;
int i;
buildmaxheap(heap, size);
for(i = size - 1; i > 0; i--)
{
exchange(heap, heap+i);
heapsize--;
maxheapify2(heap, 0, heapsize);
}
}
11:优先队列
堆排序是一个优秀的算法,但是在实际应用中,快速排序一般会快于堆排序。堆还可以作为优先队列的实现。最大堆和最小堆分别对应于最大优先队列和最小优先队列。优先队列是一个集合,集合中的元素都有一个关键字(key),此关键字标识该元素的优先级。最大优先队列的例子是进程调度,每次调度进程时,都是从队列中选择优先级最高的进程进行运行。最小优先队列的例子是事件驱动的模拟器,每个事件以发生时间为关键字,每次选择到时的事件进行模拟。
优先队列支持的操作有(优先队列可用堆来实现,下面的优先队列的操作实际上是针对最大堆的):
INSERT:向优先队列中插入元素。
MAXIMUM:返回优先队列中优先级最大的元素。
EXTRACT-MAX:返回优先级最大的元素,并且将钙元素删除。
INCREASE-KEY:将某个元素的关键字增加为k,重新构造优先队列。
DELETE:从优先队列中删除元素。
12:EXTRACT-MAX:返回优先级最大的元素,并且将该元素删除。
该算法将A[1]记录,然后将最后的元素A[heapsize]存储到A[1]中,然后减少heapsize,然后调用MAXHEAPIFY重新调整最大堆。最后返回记录的A[1]的值。该算法的时间复杂度与MAXHEAPIFY相同,为O(lg n)。代码如下:
//return the max elements and del it
int heap_extractmax(int *set, int size)
{
if(size < 1)
{
printf("heap size error\n");
return -1;
}
int max = set[0];
int heapsize = size;
set[0] = set[size-1];
set[size-1] = NEINFINITE;
heapsize = size - 1;
maxheapify(set,0,heapsize);
return max;
}
14:INCREASE-KEY:将某个元素的关键字增加为key,重新构造优先队列。
该算法将A[i]的关键字置为key,然后从索引i开始向上比较,如果A[i] > A[PARENT(i)],则交换A[i] 和 A[PARENT(i)],然后i= PARENT(i)。重复这个过程直到I = 1。
该算法与MAXHEAPIFY类似,只不过该算法是从下往上维护最大堆的性质。该算法的时间复杂度为O(lg n)。代码如下:
//from down to up to keep the property of max heap
void heap_increasekey(int *set, int index, int key, int size)
{
int i, p;
if(set[index] > key)
{
printf("current is %d, key is %d, error\n", set[index], key);
return ;
}
set[index] = key;
i = index;
while(i > 0)
{
p = PARENT(i);
if(key > set[p])
{
set[i] = set[p];
i = p;
}
else
{
set[i] = key;
break;
}
}
}
15:INSERT:向优先队列中插入元素。
该算法首先将heapsiize增加,然后置A[heapsize]为 。然后调用INCREASE-KEY(set, heapsize, key)即可,该算法的时间复杂度为O(lg n)。代码如下:
//insert elements to max heap
void maxheapinsert(int *set, int key, int heapsize, int size)
{
if(heapsize >= size)
{
printf("heapsize larger than size, error\n");
return;
}
heapsize ++;
set[heapsize - 1] = NEINFINITE;
heap_increasekey(set, heapsize-1, key, heapsize);
}
16:DELETE:从优先队列中删除元素。
该算法将最后的元素A[heapsize]存储到A[index]中,然后减少heapsize。然后比较当前index元素的值与父母的值大小,如果A[index] < A[PARENT(index)],则从上往下维护堆的性质(MAXHEAPIFY),否则,从下往上维护堆的性质(INCREASE-KEY)。代码如下:
//del elements from max heap,note: it should be up to down or down to up
void maxheapdel(int *set, int index, int size)
{
int heapsize = size-1;
int p = PARENT(index);
int key = set[size-1];
if(key < set[p])
{//down
set[index] = set[size-1];
maxheapify(set, index, heapsize);
}
else
{//up
heap_increasekey(set,index,key,heapsize);
}
}
17:设计一个复杂度为O(nlg k)的算法,将k个有序链表合并为一个有序链表,其中n为所有链表元素的总和。
首先将所有k个链表的第一个元素组成一个最小堆,然后EXTRACT-MIN得到综合链表的第一个元素,然后将MIN所在链表的第二个元素INSERT到最小堆中,然后再次EXTRACT-MIN。如此重复下去,知道最小堆为空。该算法时间复杂度为O(nlg k)。
18:一个m * n的YOUNG氏矩阵中,每一行的数据都是从左向右进行排序的,每一列的元素都是从上到下排序的,如果矩阵中某个元素不存在,则记该元素为 比如包含元素{9,6,3,2,4,8,5,14,12}的4 x 4的Young氏矩阵如下:
2 3 4 5
6 8 9 12
14 ∞ ∞ ∞
∞ ∞ ∞ ∞
a:给出一个O(m + n)的EXTRACT-MIN算法。
类似于堆的EXTRACT-MIN算法,首先将A[1][1]记录下来以备返回,然后将矩阵的最后一个元素A[m][n]存储到A[1][1]中,然后比较A[1][1]。A[1][2]和A[2][1]的值,将其中的最小值与A[1][1]进行对换,然后再次调用该过程。每次调用都是比较A[i][j]、A[i][j+1]和A[i+1][j]的值,将最小值记录到A[i][j]中,然后针对(I,j+1)或者(j+1, i)再次调用该过程(类似于MINHEAPIFY)。可见该过程的时间复杂度为O(m+n)。
b:设计一个O(m + n)的算法,确定某个数是否在该矩阵中。
每次通过将X与矩阵中最右上角的元素进行比较,如果X大,则说明X可能在剩下的(m-1) * n的矩阵中,如果X小,说明X有可能在剩下的m * (n-1)矩阵中。该算法的时间复杂度为O(m + n)。