堆:
二叉堆满足二个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。
一般用数组实现二叉堆
当下标是i,左孩2*i+1,右孩2*i+2,父节点(i-1)/2
最后一个非叶节点 n/2-1 (n是数组长度) 推导过程: 堆排序(完全二叉树)最后一个非叶子节点的序号是n/2-1的原因 - 为了得到而努力 - 博客园
堆实现优先队列有两种关键操作:
1)插入 O(logN)
将新元素加到数组尾,然后把这个新元素上浮(swim)到合适的位置
2)删除 O(logN)
删除一般就是删除最大元素,也就是根节点 A, 数组第一个
一般采取的方法是 将根节点与数组最后一个 交换,然后 下沉(sink)新根节点, 把数组最后一个给删掉,--N
最大堆的 sink和swim
// n代表构成大根堆的数组长度
void sink(int *a, int n, int i)
{
int left = 2*i + 1; // 左孩子的下标
while (left < n) // 下方还有孩子的时候
{
int largest = left + 1 < n && a[left] < a[left + 1]? left+1: left; // 取最大孩子的下标
if (a[i] >= a[largest])
break;
std::swap(a[i], a[largest]);
i = largest;
left = 2*i + 1;
}
}
void swim(int *a, int n, int i)
{
// 父节点的下标 0的父节点是0 (0-1)/2 向下取整等于0
int father = (i-1) / 2;
while (a[i] > a[father])
{
std::swap(a[i], a[father]);
i = father;
father = (i-1)/2 ;
}
}
堆排序
基本思想是,先把一个数组构造成一个最大堆,然后再反复删除最大元素。
1)构造堆有个小窍门,从所有叶子节点不管(也就是下标为[N/2, N-1]的不动),只sink所有非叶节点,并且从最后一个非叶节点开始下沉,也就是从N/2-1开始下沉,最后是0节点。
构造堆的方法:
方法1: 从左到右 挨个swim 时间复杂度 O(nlogn)
时间复杂度 O(nlogn)推导过程:
log(1)+log(2)+...log(n) = log(n!) = O(nlogn)
方法2: 从右到左 挨个sink 。时间复杂度 O(n)
另外还有一个优化,所有叶子节点不管(也就是下标为[N/2, N-1]的不动),只sink所有非叶节点,并且从最后一个非叶节点开始下沉,也就是从N/2-1开始下沉,最后是0节点。
时间复杂度 O(n)推导过程:
最后n/2的节点只需要1次操作(因为都是叶子节点), 前n/2的节点的后一半只需要2次操作。。。。所以T(n) = N/2+2N/4+3N/8...
而2T(n) = N+2N/2+3N/4...
相减可得 T(n) = N+N/2+N/4.... 也就是等比数列求和, 最后可得T(n) = 2n = O(n)
2)然后再删除最大元素,第一次将A[0]与A[n - 1]交换,再对A[0]下沉 重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最大的数据并入到后面的有序区间,故操作完成后整个数组就有序了
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面构造堆的复杂度是O(N)。二次操作时间相加还是O(N * logN),故堆排序的时间复杂度为O(N * logN), 空间复杂度为O(1)。
堆排序代码
void heap_sort(int *a, int n)
{
if (n < 2)
return;
// 构建最大堆 时间复杂度O(N)
// 进一步优化
// for(int i=n/2-1;i>=0;--i)
for (int i = n - 1; i >= 0; i--)
{
sink(a, n, i);
}
// 排序 时间复杂度O(NlogN)
for (int i = n - 1; i > 0; i--)
{
std::swap(a[0], a[i]); // 把堆顶(堆的最大值) 扔到数组尾
sink(a, i, 0); // 堆的大小变为了i ,所以形参n这边传了i
}
}