heap-sort
堆的基本操作
什么是堆我也不说了,大家都懂,这里讲讲大根堆的两个基本操作,小根堆类似
heapInsert(插入操作)
在index处插入新值,默认在[0...index-1]
已经是一个堆了,插入后需要重新调整在[0...index]
范围的堆
``` c++ "大根堆" void heapInsert(vector& target,int index){ while(target[index] > target[(index-1)/2]){ swap(target[index],target[(index-1)/2]); //交换后需要更新index index = (index-1)/2; } }
当当前节点的值大于其父节点的值时,才会交换二者的值,当index达到堆顶点时,`target[index]=target[(index-1)/2]`,会终止循环
**时间复杂度**
由于堆的高度为$log N$,所以一个数从堆底部到达堆顶只需要交换$log N$次,复杂度即$O(log N)$
> 当数组已经达到最大时,需要将数组扩容才能继续插入数据构建大根堆,假设当前数组长度为1,一次扩大一倍,总共需要扩容$log N$次至N的大小。
当扩容完成后,还需将原数组的数据拷贝至新扩容的数组,是$O(N)$的复杂度,总的扩容复杂度为$O(N*log N)$ ,平均到每个元素,也是$\frac{O(N*log N)}{N}=O(log N)$
### heapify(弹出操作)
{% note info %}
`heapify`仅仅完成调整操作,同样无需关心堆的大小
{% endnote %}
所要做的就是调整index处的值,使堆重新调整大根堆
``` c++
void heapify(vector<int>& target,int index,int heapSize){
auto left = index*2 + 1;
//当左子节点存在时
while(left< heapsize){
//在子节点中挑出大的
auto largest= (left+1)<heapsize && target[left]<target[left+1]? left+1 : left;
//在index和其较大子节点中挑出大的
largest = target[index]< target[largest]? largest:index;
//如果最大的就是index本身,无需继续调整
if(largest == index)
break;
swap(target[targest],target[index]);
//如果最大的值是index子节点,将二者交换,并将index更新为largest
index = largest;
left = index*2 +1;
}
}
时间复杂度 同样的,一个数从堆顶到达堆底,所需的操作次数也为堆的高度:$log N$次,所以弹出操作的时间复杂度同样为$O(log N)$
堆排序
大根堆的构建
常规方法 有了堆的两个基本操作,堆排序就很简单了。如果给定一个序列,将序列中的值依次插入堆中,就可构建一个大根堆。
for(auto index=0;index<target.size();++index){//O(N)
heapInsert(target,index);//O(log N)
}
时间复杂度为$O(NlogN)$,因为每插入一个节点其最差都可能走到堆顶点,复杂度为$log N$,所以插入N个节点的时间复杂度为$O(Nlog N)$
高效方法 上述常规方法是基于一次只给一个值的情况构建一个大根堆,如果一次给了所以所有值,可以假设现在已经有了一个堆,只需要使用heapify
操作快速构建大根堆,值的调整顺序从下往上,从右往左。这样保证了每一颗子树都是大根堆。
for(auto index=target.size()-1;index >= 0;--index){
//假设现在已经有个一个target.size()的堆,只需调整即可
heapify(target,index,target.size());
}
时间复杂度为$O(N)$,计算过程有点复杂,以后有时间再讲。至于为什么不是$O(N*log N)$,因为每个节点不一定都移动$log N$次。比如最下层节点,最坏情况也是移动0次,跟常规方法不同。
堆构建后的排序过程
现在已经拿到了一个大根堆,每次将堆顶点依次和堆的最后一个顶点交换结合,即最大值放到序列后侧,当大根堆的大小为0时,就完成了堆排序
while(heapSize){//O(N)
swap(target[0],target[heapSize-1]);
heapify(target,0,--heapSize);//O(log N)
}
时间复杂度为$O(Nlog N)$,因为每个值最差时都需要调整$log N$次,所以总的时间复杂度为$O(N logN)$
堆排序复杂度
时间:$O(N*log N)$
空间:$O(1)$
堆结构
系统实现vs自己实现 什么时候用系统的,什么时候需要自己写? 除非需要在数据插入到堆后,还要更改这个数据的大小,否则一般情况都可以用系统实现的堆结构
比较器 比较器就是比较规则,一般遵循一下规定:
返回正数,第二个数据排在前面
返回负数,第一个数据排在前面
0, 返回谁无所谓
相关问题
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序给定一个int数组A,同时给定A的大小n和题意中的k,请返回排序后的数组。
void almost_heapify(vector<int>& target, int index, int heapSize) {
auto left = index * 2 + 1;
while (left < heapSize) {
auto minst = left + 1 < heapSize && target[left + 1] < target[left] ? left + 1 : left;
minst = target[index] < target[minst] ? index : minst;
if (minst == index)
break;
swap(target[index], target[minst]);
index = minst;
left = index * 2 + 1;
}
}
void almost_heapInsert(vector<int>& target, int elem, int index) {
target[index] = elem;
while (target[index] < target[(index - 1) / 2]) {
swap(target[index], target[(index - 1) / 2]);
index = (index - 1) / 2;
}
}
void almost_sort(vector<int>& target, int k) {
//首先建立大小为(k+1)的小根堆
vector<int> temp(k+1);
int heapSize = 0;
for (auto index = 0; index < k+1; ++index) {
almost_heapInsert(temp, target[index], heapSize++);
}
//滑动窗口n次
auto right_point = k+1;
for (auto index = 0; index < (int)target.size(); ++index) {
//heapSize = (index + 3) > (target.size() - 1) ? heapSize - 1 : heapSize;
target[index] = temp[0];
swap(temp[0], temp[heapSize - 1]);
almost_heapify(temp, 0, --heapSize);
//当index+k == target.size()-1时,已经没有数可以入堆了,即最后一个长度为(K+1)的窗口已经处理完了
if((index + k) < (target.size() - 1))
almost_heapInsert(temp, target[right_point++], heapSize++);
//heapSize= k==n-1? heapSize-1:heapSize;
//k = k == (target.size() - 1) ? k : k + 1;
}
}
需要注意的地方有俩点:
移动不会超过k个距离,则需要准备大小为(k+1)的小根堆,因为要包括自身,如从0位置移动3位置,即总长度为4
最后一个窗口需要额外处理,当处理晚最后一个长度为(k+1)的窗口时,就没有新数据进行heapInsert了。
Last updated
Was this helpful?