1. 堆
- (二叉)堆是一个数组,它可以看成是近似的完全二叉树,树上的每一个结点对应数组中的一个元素。
- 结点A[i]的左右孩子结点对应为A[2i] & A[2i+1]。则A[i]的父结点为A[i/2](此处以及后文统一默认向下取整)。乘2与除2做操作在计算机中可以用移位操作实现,因此会比较快。
- 由此可知这棵树除最后一层外,完全充满的,且有左向右填充。
- 要构成堆还需要满足满足以下性质:
构成最大堆:任意父结点的值不小于其左右子结点的值,即,对于任意的i: A[i] >= A[2i] && A[i] >= A[2i+1]。
构成最小堆: 任意父结点的值不大于其左右子结点的值,即,对于任意的i: A[i] <= A[2i] && A[i] >= A[2i+1]。
1.1 维护堆的性质
堆的性质的维护,以下都以最大堆为例。
堆的维护的主要思想是“逐层下降”。举例:某个结点i, 假设其左右子结点left(i),right(i)都已经是最大堆,那么需要调节(或者说是调换)i, left(i), right(i)的值,并保证调换后的子树继续调换下去直到子树继续满足堆的性质。首先,如果结点i的值大于左右子结点的值(即i的值是三个值中的最大值),则不需要调换,因为左右子结点均满足堆的性质,同时结点i也满足了结点值大于等于子结点的值,所以整棵树就是最大堆(也有叫大顶堆)。其次,如果结点i的值不是三个中的最大值,不妨设左结点的值最大(即左结点的值比右结点以及i的值都大),则把i的值跟左结点的值调换,此时i, left, right满足了父结点值大于等于左右子结点值的情况,那么被调换后的左结点的值(变小了),以该结点为参考对象,它因为变小了,那么它还满足堆的要去吗,所以需要继续进行检测和调换,即逐层的进行下去,直到原来的i值滑落到稳定的位置。时间复杂度是O(lgn)。以下是算法代码的具体验证:
/*
最大堆的维持输入参数是一个数组,和一个小标i,
表示维持i到heap-size使该范围的二叉树为一个最大堆。
注意一个前提:结点i的左右子树都已经是最大堆。
此处假设a的数组长度size与heap-size一致。
*/
void max_heapify(vector<int>& a, int i){
int largest = i;
int left = 2 * i+ 1;//因为数组的下标是从0开始的,与算法表述里的数组下标都是从1开始的,故应该进行相应转化,以使其对应
int right = 2 * i + 2;
if (left < a.size() && a[left] > a[i])
largest = left;
if (right < a.size() && a[right] > a[largest])
largest = right;
if (largest != i){
int tmp = a[i];
a[i] = a[largest];
a[largest] = tmp;
max_heapify(a, largest);
}
}
int main()
{
vector<int> a(10, 0);
for (int i= 0; i < 10; i++)
a[i] = 10-i;
for (auto val : a)
cout << val << " ";
cout << endl;
a[0] = 1;
max_heapify(a, 0);
for (auto val : a)
cout << val << " ";
return 0;
}
1.2 建堆
知道堆的性质的维护方法以后,建堆的思路也就有了,就是从已经堆化的部分逐步往上调用最大堆化函数,使整个数组逐渐全部满足堆的要求。底层的每一个单独的叶结点是满足堆的要求的,叶结点有A.length - A.length/2 个。所以从叶结点的父结点那一层开始建堆,这些点的下标范围是:1– A.length/2 建堆的过程是从后往前每个点调用最大堆化函数,所以可能会有人认为复杂度是O(nlgn),这个结果虽然正确但不是渐近紧确的,经过证明可以得到建堆过程的一个更紧确的界:O(n)。证明方法请参考算法导论。一下是建堆过程的代码示例:
void build_max_heap(vector<int>& a){
for (int i = a.size() / 2-1; i >= 0; i--)
max_heapify(a, i);
}
int main()
{
srand(time(0));
vector<int> a(10, 0);
for (int i= 0; i < 10; i++)
a[i] = rand()%20;
for (auto val : a)
cout << val << " ";
cout << endl;
cout << "buid max-heap: " << endl;
build_max_heap(a);
for (auto val : a)
cout << val << " ";
cout << endl;
return 0;
}
2. 堆排序算法
给定数组A,如要对数组A进行排序输出,则第一步是根据A数组建堆,此步时间复杂度为O(n), 建好堆的数组A的最大元素一定在A[1](算法描述里数组的首个位置下标为1,代码里数组的首个位置下标为0,此处再做一次区分)。那么我把A[1]与A[n]调换,则此时数组里的最大元素已经跑到数组的最后位置,那么数组的前n-1个元素是什么样的情况,由于A[1]和A[n]互换,所以新的A[1]结点以及其左右子结点组成的triangle不一定满足最大堆的性质了(父结点的值大于等于子结点的值),但是其左右子结点对应的树,依然是满足堆的性质的,所以对于A的前n-1个元素,只需要对A[1]结点调用维持堆化过程,整个n-1个元素就又变成了新的最大堆,其第一个元素A[1]就是n-1个元素中的最大值,也即是数组A中的次大值, 此时把A[1]与A[n-1]交换,则次大值放入了正确的位置,依次类推,直到位置2-n都放入了相应的值,则排序结束,时间复杂度O(nlgn)。以下是代码示例:
void max_heapify(vector<int>& a, int i, int heap_size){
int largest = i;
int left = 2 * i + 1;//因为数组的下标是从0开始的,与算法表述里的下标略微不对应,故应该用此表述
int right = 2 * i + 2;
if (left < heap_size && a[left] > a[i])
largest = left;
if (right < heap_size && a[right] > a[largest])
largest = right;
if (largest != i){
int tmp = a[i];
a[i] = a[largest];
a[largest] = tmp;
max_heapify(a, largest, heap_size);
}
}
void build_max_heap(vector<int>& a, int heap_size){
for (int i = heap_size / 2-1; i >= 0; i--)
max_heapify(a, i, heap_size);
}
void heapsort(vector<int>& a){
int n = a.size();
build_max_heap(a, a.size());
for (int i = n - 1; i >= 1; i--){
int tmp = a[0];
a[0] = a[i];
a[i] = tmp;
max_heapify(a, 0, i);
}
}
int main()
{
srand(time(0));
vector<int> a(10, 0);
for (int i= 0; i < 10; i++)
a[i] = rand()%20;
for (auto val : a)
cout << val << " ";
cout << endl;
cout << "heap sort: " << endl;
heapsort(a);
for (auto val : a)
cout << val << " ";
cout << endl;
return 0;
}
3. 优先队列
略
参考:算法导论