堆及堆排序
堆
堆是一种特殊的完全二叉树,这种二叉树的每个子树的根节点是该子树的最大值或者最小值,以此分为大根堆和小根堆。
存储方式:我们通常用数组的方式存储堆。
构造堆
这里默认构造大根堆
构造堆最重要的操作就是对每个子树的父亲节点和孩子节点的调整。这种调整要考虑以下几个方面:
- 父亲节点和左右孩子的下标的关系
- 父亲节点和左右孩子要做什么比较
- 这种操作何时进行?需要进行多少次?
下标关系
- 下标从0开始: 父:fa; 左:fa * 2 + 1; 右:fa * 2 + 2
- 下标从1开始: 父:fa; 左:fa * 2; 右:fa * 2 + 1
比较
- 大根锥:比较左右孩子和父亲节点,并将大者置为父节点
- 小根锥:比较左右孩子和父亲节点,并将小者置为父节点
自下而上还是自上而下
这样的比较通常有两个方向,即自上而下和自下而上(这里的初始下标为0)
-
自上而下:这是父节点不断向下走的过程
void adjustDown(int left, int right){ //待调整区间 int i = left, j = 2 * i + 1; // i为父节点,j为左孩子 while(j <= right){ //孩子节点没有超出待调整范围 if(j + 1 <= right && heap[j] < heap[j + 1]){//右孩子存在并且比左孩子大 j = j + 1; } if(heap[j] > heap[i]){ swap(heap[j], heap[i]); i = j; j = 2 * i + 1; //更新节点 父节点和左孩子 } else{ break; } } }
-
自下而上:这是孩子节点不断向上走的过程
void adjustUp(int left, int right){ //待调整区间 int i = right, j = (i - 1) / 2; //孩子节点和父节点 while(j >= left){ if(heap[i] > heap[j]){ swap(heap[i], heap[j]); i = j; j = (i - 1) / 2; } else{ break; } } }
何时需要调整节点之间的关系?
- 建堆:在初始化一个堆的时候,需要将所有节点进行调整
- 采用自上而下的方式调整,把更大的数拉上来
- 每个节点都要向下扫描,尝试把它下面的节点拉上来
- 这样的调整应该由叶子结点到根节点,也就是自下而上(注意这里指的是遍历节点的顺序,而不是调整节点的方式)
- 插入元素:插入在数组的最后,然后自下而上,尝试把自个新元素往上拉
- 删除堆顶元素:数组的最后一个元素覆盖堆顶元素,再将新的堆顶元素向下调整
总结:不难发现,只有建堆的过程需要对每个节点都进行一次向下调整,而删除和插入只对某个特定的节点进行向上或者向下调整,这是因为在删除和插入节点的时候,堆在进行该操作之前所有节点之间的关系是符合条件的。
堆类实现
弄清楚以上的内容,堆的实现方式就很清晰了,代码如下:
class Heap{
private:
const int MAXN = 10000000;
int* heap;
int n;
int end;
public:
Heap(int n){
this -> n = n;
this -> end = n - 1;
heap = new int[MAXN];
cout << "输入n个节点的权值" << endl;
for(int i = n - 1; i >= 0; i--){
cin >> heap[i];
adjustDown(i, n - 1);
}
}
void adjustDown(int left, int right){ //用于删除节点,新建堆
int i = left, j = 2 * i + 1; // j 为左孩子
while(j <= right){
if(j + 1 <= right && heap[j] < heap[j + 1]){
j = j + 1;
}
if(heap[j] > heap[i]){
swap(heap[j], heap[i]);
i = j;
j = 2 * i + 1; //更新节点 父节点和左孩子
}
else{
break;
}
}
}
void adjustUp(int left, int right){ //用于新增节点,将新节点和父亲比较
int i = right, j = (i - 1) / 2;
while(j >= left){
if(heap[i] > heap[j]){
swap(heap[i], heap[j]);
i = j;
j = (i - 1) / 2;
}
else{
break;
}
}
}
int pop(){
if(end < 0) return INT_MIN;
int tmp = heap[0];
heap[0] = heap[end--];
if(n > 0) n--;
adjustDown(0, end);
return tmp;
}
void push(int val){
if(end == MAXN - 1){
cout << "堆已满" << endl;
return;
}
heap[++end] = val;
n++;
adjustUp(0, end);
}
int size(){
return n;
}
void print(){
for(int i = 0; i < n; i++){
cout << heap[i] << " ";
}
}
};
堆排序
利用堆的特性,通过n次出堆操作,能够实现对无序数组的排序。每次推出堆顶元素,堆的维护需要 O ( l o g n ) O(log^n) O(logn)的时间复杂度,即堆排序的时间复杂度为 O ( n l o g n ) O(nlog^n) O(nlogn)
构造出了堆之后,所谓的堆排序不过是无脑的推出堆顶元素并放到数组最后而已
void heap_Sort(){
int flag = end;
while(flag > 0){
swap(heap[0], heap[flag--]);
adjustDown(0, flag);
}
}