堆排序是利用堆这种数据结构而设计的一种排序算法,它的最坏,最好,平均时间复杂度均为O(nlogn),它是一种不稳定排序。这里有必要给大家解释一下排序算法的稳定性,它并不是指排序时间复杂度的不稳定,而是指排序结果的不稳定,比如A=B,排序前A在B的前面,排序后可能A在B的前面,也可能B在A的前面。这个结果是我们无法预知的,所以这个排序算法是不稳定的。
首先简单了解下堆结构
堆是一种完全二叉树,分为大顶堆和小顶堆。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
我们将根节点作为数组中下标为1的元素,然后堆的每一层从左到右依次排列在数组中。如上述大顶堆在数组中为:null,10,8,7,6,4,3(这里的null指的是数组下标为0的元素,我们不存放数据)
那么该数组从逻辑上讲就是一个堆结构,以i来表示数组的下标,那么具有堆性质的数组满足以下性质:
大顶堆:arr[i] >= arr[2i] && arr[i] >= arr[2i+1]
小顶堆:arr[i] <= arr[2i] && arr[i] <= arr[2i+1]
了解了堆这种数据结构,那么下面来说一下堆排序的思路(这里以大顶堆为例)
我们会先将待排序的数组构造成一个大顶堆,那么此时整个数组的最大值就是堆的根节点,将其与尾部的元素交换,然后取出尾部元素,最后将剩下的元素重新构造成一个堆,循环执行,取出的元素就会构成一个有序序列。
在每次将数据插入堆中时,我们首先都会将被插入数据放到数组的末尾,也就是堆的底部,然后会将它与父节点比较,如果比父节点大,那么就进行交换,然后继续与新的父节点比较,直到比父节点小或者到达根节点。
同样,在取最大值时,我们会将根节点与尾部元素交换,此时根节点的元素就不是最大了,我们会将它与它的最大的子节点进行比较,如果比最大的子节点小,那么就进行交换,继续与新的最大的子节点比较,直到比最大的子节点还要大或者到达叶节点。
实现代码
首先写一个大顶堆类
Heap.h
文件
#pragma once
#ifndef Heap_h
#define Heap_h
#include <vector>
//堆
template <typename T>
class Heap
{
private:
std::vector<T> values; //数据值
int count; //计算当前值的大小
void moveUp(int k) //如果父亲节点要比当前节点小,节点向上移动,
{
//是否父亲节点比当前节点要小
while (k>1 && values[k / 2]<values[k])
{
//当前节点和父亲节点进行交换
std::swap(values[k], values[k / 2]); //交换两个数据
k = k / 2;
}
}
//数据往下移动
void moveDown()
{
unsigned int k = 1;
while (k * 2 < values.size())
{
unsigned int j = 2 * k; //左子树
if (j + 1<values.size() && values[j + 1]>values[j])//左子树和右子树进行比较
{
j++; //j下标指向右子树
}
if (values[k]>=values[j])
{
break;
}
std::swap(values[k], values[j]);
k = j;
}
}
public:
//构造初始化
Heap()
{
values.resize(1); //完全二叉数下标从1开始
count = 0;
}
//返回二叉堆的大小
int size()
{
return count;
}
//判断是否为空
bool empty()
{
return count <= 0;
}
//插入元素
void insert(T val)
{
values.push_back(val); //完全二叉树的特性,从数组尾部插入数据
count++;
moveUp(count);
}
//获取堆顶的元素
T top()
{
if (empty())
{
throw nullptr;
}
return values[1];
}
//取出堆顶元素
T poptop()
{
T val = top(); //获取准备返回的值
std::swap(values[1], values.back()); //交换完了数据
values.pop_back(); //取出数据
moveDown();
count--;
return val;
}
};
#endif
堆排序的优点在于,当我们不需要全部的排序结果,比如某排行榜只需要显示分数最高的前一百位数据时,堆排序的效率是很高的,我们不需要把数据全部取出来,只需要从堆中取一百条数据即可。
下面的代码比较了快速排序和堆排序在基数为十万的情况下获取值最大的一百个数的时间。
main.cpp
文件
#include <iostream>
#include <ctime>
#include "Heap.h"
using namespace std;
void Random(vector<int> &v)
{
for (auto &e : v)
e = (rand() + rand()) % v.size()+(rand()+ rand()) % v.size();
}
void fast_sort(vector<int> &v, int left, int right, bool Ascending = true)
{
//当左边的下标大于等于右边即视为不可分割,递归结束
if (left >= right)
{
return;
}
int pt = left + 1;
//基准值
int val = v[left];
//默认为升序排序
if (Ascending)
{
//一次快速排序
for (int i = left + 1; i <= right; i++)
{
if (val > v[i])
{
swap(v[i], v[pt++]);
}
}
}
//降序排序
else
{
for (int i = left + 1; i <= right; i++)
{
if (val < v[i])
{
swap(v[i], v[pt++]);
}
}
}
swap(v[left], v[--pt]);
//将左边进行快速排序
fast_sort(v, left, pt - 1, Ascending);
//将右边进行快速排序
fast_sort(v, pt + 1, right, Ascending);
}
void fast_time(vector<int> vec)
{
clock_t start = clock()
fast_sort(vec, 0, vec.size() - 1, false);
clock_t end = clock();
cout << "前一百位数据为:";
for (int i = 0; i < 100; i++)
{
cout << vec[i] << " ";
}
cout << "\n快速排序取出前100名的数据 所需时间:" << (float)(end - start) / 1000 << "s" << endl;
}
void heap_time(vector<int> vec)
{
Heap<int> heap; //
vector<int> t(100);
clock_t start = clock();
for (auto e : vec)
{
heap.insert(e);
}
for (int i = 0; i < 100; i++)
{
t[i] = heap.poptop();
}
clock_t end = clock();
cout << "前一百位数据为:";
for (int i = 0; i < 100; i++)
{
cout << t[i] << " ";
}
cout << "\n堆排序取出前100名的数据 所需时间:" << (float)(end - start) / 1000 << "s" << endl;
}
int main()
{
vector<int> data(100000);
Random(data);
heap_time(data);
fast_time(data);
data.clear();
return 0;
}
运行结果如下:
可以看到堆排序所用的时间比快速排序少了很多,但是如果将这十万个数全部进行排序的话,快速排序是更加高效的,大家在选择排序算法的时候需要根据实际情况来选择。
今天的分享就到这里了,希望大家能够有所收获。