您的位置:首页 > 编程语言 > C语言/C++

C++ 堆排序算法的实现与改进(含笔试面试题)

2017-03-31 09:21 239 查看
堆排序(Heap sort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以用到上一次的排序结果,所以不像其他一般的排序方法一样,每次都要进行n-1次的比较,复杂度为O(nlogn)。

这里先说明以下几个基本概念:

完全二叉树:假设一个二叉树有n层,那么如果第1到n-1层的每个节点都达到最大的个数:2,且第n层的排列是从左往右依次排开的,那么就称其为完全二叉树



堆:本身就是一个完全二叉树,但是需要满足一定条件,当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆,当二叉树的每个节点都小于它的子节点的时候,称为小顶堆,上图即为小顶堆。

关键的性质:

将堆的内容填入一个一位数组,这样通过下标就能计算出每个结点的父子节点,编号顺序从0开始,从左往右,从上至下层次遍历。a[10]
= {2,8,5,10,9,12,7,14,15,13}

若一个结点的下标为k,那么它的父结点为(k-1)/2,其子节点为2k+1和2k+2

例:数字为10的节点的下标为3,父结点为1号:8,子节点为7号和8号:14,15

算法步骤:

1)利用给定数组创建一个堆H[0..n-1](我们这里使用最小堆),输出堆顶元素

2)以最后一个元素代替堆顶,调整成堆,输出堆顶元素

3)把堆的尺寸缩小1

4) 重复步骤2,直到堆的尺寸为1

实现代码:

/***************************************************************************
*  @file       main.cpp
*  @author     MISAYAONE
*  @date       25  March 2017
*  @remark     25  March 2017
*  @theme      Heap Sort
***************************************************************************/

#include <iostream>
#include <vector>
#include <time.h>
#include <Windows.h>
using namespace std;

//辅助交换函数
void Swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}

//堆排序的核心是建堆,传入参数为数组,根节点位置,数组长度
void Heap_build(int a[],int root,int length)
{
int lchild = root*2+1;//根节点的左子结点下标
if (lchild < length)//左子结点下标不能超出数组的长度
{
int flag = lchild;//flag保存左右节点中最大值的下标
int rchild = lchild+1;//根节点的右子结点下标
if (rchild < length)//右子结点下标不能超出数组的长度(如果有的话)
{
if (a[rchild] > a[flag])//找出左右子结点中的最大值
{
flag = rchild;
}
}
if (a[root] < a[flag])
{
//交换父结点和比父结点大的最大子节点
Swap(a[root],a[flag]);
//从此次最大子节点的那个位置开始递归建堆
Heap_build(a,flag,length);
}
}
}

void Heap_sort(int a[],int len)
{
for (int i = len/2; i >= 0; --i)//从最后一个非叶子节点的父结点开始建堆
{
Heap_build(a,i,len);
}

for (int j = len-1; j > 0; --j)//j表示数组此时的长度,因为len长度已经建过了,从len-1开始
{
Swap(a[0],a[j]);//交换首尾元素,将最大值交换到数组的最后位置保存
Heap_build(a,0,j);//去除最后位置的元素重新建堆,此处j表示数组的长度,最后一个位置下标变为len-2
}

}
int main(int argc, char **argv)
{
clock_t Start_time = clock();
int a[10] = {12,45,748,12,56,3,89,4,48,2};
Heap_sort(a,10);
for (size_t i = 0; i != 10; ++i)
{
cout<<a[i]<<" ";
}
clock_t End_time = clock();
cout<<endl;
cout<<"Total running time is: "<<static_cast<double>(End_time-Start_time)/CLOCKS_PER_SEC*1000<<" ms"<<endl;
cin.get();
return 0;
}

建堆的过程,堆调整的过程,这些过程的时间复杂度,空间复杂度,以及如何应用在海量数据Top K问题中等等,都是需要重点掌握的。

复杂度分析:

最差时间复杂度O(n log n)

最优时间复杂度O(n log n)

平均时间复杂度O(n log n)

最差空间复杂度O(n)

注意:此排序方法不适用于个数少的序列,因为初始构建堆需要时间;

特点分析:不稳定算法(unstable sort)、In-place sort。

例1:编写算法,从10亿个浮点数当中,选出其中最大的10000个。

典型的Top K问题,用堆是最典型的思路。建10000个数的小顶堆,然后将10亿个数依次读取,大于堆顶,则替换堆顶,做一次堆调整。结束之后,小顶堆中存放的数即为所求。为了方便,直接使用STL容器

理论上有两种方法,

一是数据全部排序,时间复杂度一般为O(n * log(n) ),但是考虑到10亿这种数量级不适合全部载入内存,所以实际的算法估计只能采用外排序。这是个算法效率太低,不太实用。

另外一种是用一个有序容器保存10000个数,然后其他的数字依次和容器中的最小数字比较,如果大于容器已有的,就插入容器并删除原先最小的那个,而容器仍旧保持有序。

这个算法的时间复杂度是O(n),理论上可能已经没有更好的算法了。

#include "stdafx.h"
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional> // for greater<>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
vector<float> bigs(10000,0);
vector<float>::iterator it;
// Init vector data
for (it = bigs.begin(); it != bigs.end(); it++)
{
*it = (float)rand()/7; // random values;
}
cout << bigs.size() << endl;
make_heap(bigs.begin(),bigs.end(), greater<float>()); // The first one is the smallest one!
float ff;
for (int i = 0; i < 1000000000; i++)
{
ff = (float) rand() / 7;//十亿个数都以随机数代替
if (ff > bigs.front()) // replace the first one ?
{
// set the smallest one to the end!
pop_heap(bigs.begin(), bigs.end(), greater<float>());
// remove the last/smallest one
bigs.pop_back();
// add to the last one
bigs.push_back(ff);
// mask heap again, the first one is still the smallest one
push_heap(bigs.begin(),bigs.end(),greater<float>());
}
}
// sort by ascent
sort_heap(bigs.begin(), bigs.end(), greater<float>());
return 0;
}


例题2:设计一个数据结构,其中包含两个函数,1.插入一个数字,2.获得中数。并估计时间复杂度。

使用大顶堆和小顶堆存储。

  使用大顶堆存储较小的一半数字,使用小顶堆存储较大的一半数字。

  插入数字时,在O(logn)时间内将该数字插入到对应的堆当中,并适当移动根节点以保持两个堆数字相等(或相差1)。

  获取中数时,在O(1)时间内找到中数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息