代码地址:https://github.com/AlbinZhang/Mooc_DataAlgorithm/tree/master/04_heapSort
1. 二叉堆
要说堆排序,首先要说下数据结构中的二叉堆,有最大堆和最小堆。
二叉堆的定义:
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足二个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
二叉堆一般用数组来表示, 例如上图的例子,红字标识数组下标,空出下标为0的,
根节点的下标是1,l两个子节点就是2,3
第n个位置的子节点分别是2n,2n+1, 父节点是n/2取整
上图的数组形式:
-, 100. 34, 22, 25, 31, 19, 17, 20, 11
实现一个二叉堆(最大堆)
要求:
最大的元素在顶端(最大的元素在顶端)
每个元素都比它的父节点小,或者和父节点相等
主要逻辑:
插入:
先把带插入的点放到数组的最后,然后和他的父节点比较,谁大谁在上面
弹出:
弹出顶端元素,把最后一个元素放到顶端,然后与两个子节点中大的比较,谁大谁在上面
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <assert.h>
using namespace std;
template <typename T>
class MaxHeap
{
public:
MaxHeap(int capacity){
m_capacity = capacity;
m_data = new T[capacity + 1];
m_next = 1;
}
~MaxHeap(){ delete m_data; }
void insert(T node)
{
assert(m_next <= m_capacity);
int index = m_next;
m_data[index] = node;
shiftUp(index);
m_next++;
}
T pop(){
assert(m_next > 1);
int index = 1;
T ret = m_data[index];
m_data[index] = m_data[--m_next];
shiftDown(index);
return ret;
}
bool empty(){
return m_next == 1;
}
private:
void shiftUp(int index){
while (index > 1 && m_data[index] > m_data[index / 2]){
swap(m_data[index], m_data[index / 2]);
index = index / 2;
}
}
void shiftDown(int index)
{
while (index * 2 < m_next)
{
int maxIndex = index * 2;
int rightNode = index * 2 + 1;
if (rightNode < m_next && m_data[maxIndex] < m_data[rightNode]){
maxIndex = rightNode;
}
if (m_data[index] >= m_data[maxIndex])
break;
swap(m_data[index], m_data[maxIndex]);
index = maxIndex;
}
}
private:
int m_capacity;
int m_next; //只想下一个可以存储的数组下标
T *m_data;
};
int main(int argc, char *argv[])
{
int arr[100];
srand(time(NULL));
for (int i = 0; i < 10; i++){
//maxheap.push(rand() % 100);
arr[i] = rand() % 100;
}
MaxHeap<int> maxheap(arr, 10);
while (!maxheap.empty()){
cout << maxheap.pop() << endl;
}
return 0;
}
shiftUp:
是检查当前index元素是否比他的父节点大,如果大,就交换,然后继续和父节点比
shiftDown:
用来检查当前节点是否比他的两个子节点大,如果比子节点小,就和自己节点中最大的交换,然后继续跟子节点比较
2. 堆排序
因为二叉堆(最大堆)根节点是最大的,所以每次弹出根节点,都是当前堆中最大的,
那我们就使用二叉堆进行堆排序
template <typename T>
void heapSort1(T arr[], int n)
{
MaxHeap<T> maxheap = MaxHeap<T>(n);
int i = 0;
for (i = 0; i < n; i++){
maxheap.insert(i);
}
for (i = n - 1; i >= 0; i--)
{
arr[i] = maxheap.pop();
}
}
//
number = 1000000
MergeSort :0.279398s
QuitSort3Way :0.269952s
HeapSort1 :0.455481s
Heapify
如果直接使用一个数组生成二叉堆,可以使用另一个方法,看下图,
所有的叶节点(红色的)都可以看做是要给一个完整的二叉堆,
那节点4,就可以看作是这个新的二叉堆的根节点,使用shiftdown,重新使得这个二叉堆变成一个最大堆
然后再是节点3,2,1 之后 整个堆就是一个最大堆了
那么怎么找到最后一个非叶节点的呢?
在根节点是数组下标1的情况下,这个数组的长度为n,那么最后后一个非叶子节点是n/2
通过这个方法我们重新创建一个MaxHeap的构造函数
MaxHeap(T arr[], int n)
{
m_capacity = n;
m_data = new T[n + 1];
for (int i = 0; i < n; i++)
m_data[i + 1] = arr[i];
m_next = n+1;
for (int i = (m_next - 1) / 2; i >= 1; i--)
shiftDown(i);
}
//新的heapSort
void heapSort2(T arr[], int n)
{
MaxHeap<T> maxheap = MaxHeap<T>(arr, n);
int i = 0;
for (i = n - 1; i >= 0; i--)
{
arr[i] = maxheap.pop();
}
}
//////// 效果还是很明显的
number = 1000000
MergeSort :0.264709s
QuitSort3Way :0.257416s
HeapSort1 :0.447973s
HeapSort2 :0.364310s
3. 优化堆排序
在上面介绍的堆排序中,都是先把元素拷贝到MaxHeap中,然后进行排序,最后再pop出来,重新添加到数组中
这要就需要额外的开辟缓存,元素的拷贝,但是通过之前最MaxHeap的理解,完全可以不用这样
我们现在已有一个数组,我们可以直接把这个数组通过MaxHeap(T arr[], int n)这里的方法使其成为一个最大堆
然后堆顶的元素就是当前数组中最大的元素了, 把第一个和最后一个交换,这样最大值就已经放到末尾了
然后再把前n-1个元素重新变成一个最大堆,再放到倒数第二的位置 以此类推
之前我们都是以下标1开始的,当根结点是0的时候,各个点之间的关系如下
template <typename T>
void __shiftDown(T arr[], int n, int index)
{
while (index * 2+1 < n)
{ //如果还有子节点
int leftIndex = 2 * index + 1;
int rightIndex = 2 * index + 2;
if (rightIndex < n && arr[leftIndex] < arr[rightIndex]) {
leftIndex = rightIndex;
}
if (arr[index] >= arr[leftIndex]) {
break;
}
swap(arr[index], arr[leftIndex]);
index = leftIndex;
}
}
template <typename T>
void heapSort3(T arr[], int n)
{
for(int i = (n-1)/2; i >=0; i--){
__shiftDown(arr, n, i);
}
for(int i = n-1; i > 0; i--){
swap(arr[0], arr[i]);
__shiftDown(arr, i, 0);
}
}
//--------------------
number = 1000000
MergeSort :0.350852s
QuitSort3Way :0.249016s
HeapSort1 :0.428990s
HeapSort2 :0.323061s
HeapSort3 :0.295751s
根据排序的结果可以看到,在完全随机的情况下,原地堆排序是略优于其他堆排序的
4. 索引堆
在上面的最大堆实现中,我们把数组中的元素的位置都进行了交换,但是在实际工作中,原数组的顺序可能也是一项重要的信息,值得保存下来
所以为了保留元素组的顺序信息,我们不直接对原数组进行最大堆的计算,我们可以通过额外维护一个数组列表,
用来保存原数组经过最大堆计算后的下标的顺序,用来实现最大堆,这个就是索引堆
为了符合用户的使用习惯,索引数组是从下标0开始使用的
//从下标0开始的最大堆
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H
#include <cassert>
#include <string.h>
#include <stdio.h>
using namespace std;
template <typename T>
class IndexMaxHeap
{
public:
IndexMaxHeap(int capacity)
{
m_capacity = capacity;
m_data = new T[capacity];
m_next = 0;
}
~IndexMaxHeap()
{
delete m_data;
}
void insert(T node)
{
assert(m_next < m_capacity);
m_data[m_next] = node;
shiftUp(m_next);
m_next++;
}
T extractMaxItem()
{
assert(m_next > 0);
int index = 0;
T ret = m_data[index];
m_data[index] = m_data[m_next];
m_next--;
shiftDown(index);
return ret;
}
bool empty()
{
return m_next == 0;
}
private:
/*
跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
n 的父节点 。(n-1)/2
*/
void shiftUp(int index)
{
while(index > 0 && m_data[index] > m_data[(index-1)/2]) {
swap(m_data[index], m_data[(index-1)/2]);
index = (index-1)/2;
}
}
/*
跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
n left_node = 2*n+1
right_node = 2*n+2
*/
void shiftDown(int index)
{
while ((index*2 +1) < m_next) { //确保有子节点
int lhs = index * 2 + 1;
if((lhs+1) < m_next && m_data[lhs] < m_data[lhs+1]) {
lhs = lhs+1;
}
if(m_data[index] >= m_data[lhs]) {
break;
}
swap(m_data[index], m_data[lhs]);
index = lhs;
}
}
private:
int m_capacity;
int m_next; //指向下一个可以存储的数组下标
T *m_data;
};
#endif //IndexMAXHEAP_H
然后我们再次基础上,增加一个 m_indexes数组,用于存储原数组排序后的索引,
同时插入的时候,用户可以指定插入的位置,所以原来的insert要增加一个参数int i来指定插入的位置
因为现在我们通过m_indexes存储实际数组的下标,所以shiftUp和shiftDown进行比较数值大小时,也需要通过m_indexes获取下标
但是在我们进行shiftUp,shiftDown交换操作时,我们不能直接交换m_data中的值,而是m_indexes中的索引,不然我们要他何用
并且我们增加了3个函数
extractMaxIndex 获取最大值的索引
getItem 通过索引获取原数组的元素
change 指定更改一个数组中的元素,并且进行最大堆的维护,使m_indexes还是一个最大堆索引
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H
#include <cassert>
#include <string.h>
#include <stdio.h>
using namespace std;
template <typename T>
class IndexMaxHeap
{
public:
IndexMaxHeap(int capacity)
: m_capacity(capacity), m_next(0)
{
m_data = new T[capacity];
m_indexes = new int[capacity];
}
~IndexMaxHeap()
{
delete m_data;
delete m_indexes;
}
void insert(int i, T node)
{
assert(m_next < m_capacity);
assert(i < m_capacity);
m_data[i] = node;
m_indexes[m_next] = i;
shiftUp(m_next);
m_next++; //因为shiftUp是向上比较,所以m_next++,在前或在后都可以
}
T extractMaxItem()
{
assert(m_next > 0);
int index = 0;
T ret = m_data[m_indexes[index]];
m_indexes[index] = m_indexes[m_next];
m_next--;
shiftDown(index); //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
return ret;
}
int extractMaxIndex()
{
assert(m_next > 0);
int index = 0;
int ret = m_indexes[index];
m_indexes[index] = m_indexes[m_next];
m_next--;
shiftDown(index); //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
return ret;
}
T getItem(int i)
{
return m_data[i];
}
/*
修改一个已经存在的数组元素
*/
void change(int i, T newItem){
assert(i < m_next);
m_data[i] = newItem;
//此时我们要找到i,这个元素在m_indexes中的位置
//之后在shiftUp shifiDown,看当前元素能否上下移动
for(int j = 0; j < m_next; j++){
if(indexes[j] == i){
shiftUp(j);
shiftDown(j);
return ;
}
}
}
bool empty()
{
return m_next == 0;
}
private:
/*
跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
n 的父节点 。(n-1)/2
*/
void shiftUp(int index)
{
while(index > 0 && m_data[m_indexes[index] > m_data[m_indexes[(index-1)/2]]) {
swap(m_indexes[index], m_indexes[(index-1)/2]);
index = (index-1)/2;
}
}
/*
跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
n left_node = 2*n+1
right_node = 2*n+2
*/
void shiftDown(int index)
{
while ((index*2 +1) < m_next) { //确保有子节点
int lhs = index * 2 + 1;
if((lhs+1) < m_next && m_data[m_indexes[lhs]] < m_data[m_indexes[lhs+1]]) {
lhs = lhs+1;
}
if(m_data[m_indexes[index]] >= m_data[m_indexes[lhs]]) {
break;
}
swap(m_indexes[index], m_indexes[lhs]);
index = lhs;
}
}
private:
int m_capacity;
int m_next; //指向下一个可以存储的数组下标
T *m_data;
int *m_indexes;
};
#endif //IndexMAXHEAP_H
优化change方法
change说了,是指定更改一个数组中的元素,并且进行最大堆的维护,使m_indexes还是一个最大堆索引
为了维护最大堆,我们需要找到元素索引在m_indexes中的位置,之前我们使用的是循环遍历的方法,
因为循环是n,shiftUp,shiftDown的复杂度是log(n),这样时间复杂度就变成了n+log(n),也就是O(n)
但是我们的堆这种结构,之前的插入和删除复杂度都是log(n)级别的,现在change是O(n),整个把时间复杂度拉低了,所以我们需要进行优化
这里我们的优化方式是通过反向查找的方式,来提高change的效率
简单来说就是在创建一个数组,使用这个数组m_reverse来维护,m_indexes中,m_data的元素在什么位置
至此我们一共有了三个数组结构,分别是m_data,m_indexes,m_reverse
他们分别的作用是:
m_data: 按照用户指定下标的方式,存储用户的数据
m_indexes: 为了不破环用户指定的下标,额外建立一个数组用于存储,m_data最大堆运算之后的 各个元素的位置,使用下标的方式存储
m_reverse:为了方便通过m_data下标,直接找到m_indexes中当前下标的位置,所以使用此数组来存储当前数组下标在m_indexes中的位置
例:
m_data[j] 经过在最大堆中的位置是k,那么
m_indexes[k] = j;
m_reverse[m_indexes[j]] = k;
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H
#include <cassert>
#include <string.h>
#include <stdio.h>
using namespace std;
template <typename T>
class IndexMaxHeap
{
public:
IndexMaxHeap(int capacity)
: m_capacity(capacity), m_next(0) {
m_data = new T[capacity];
m_indexes = new int[capacity];
m_reverse = new int[capacity];
memset(m_reverse, 0, sizeof(int)*capacity);
}
~IndexMaxHeap() {
delete m_data;
delete m_indexes;
delete m_reverse;
}
void insert(int i, T node) {
assert(m_next < m_capacity);
assert(i < m_capacity);
m_data[i] = node;
m_indexes[m_next] = i;
m_reverse[i] = m_next;
shiftUp(m_next);
m_next++; //因为shiftUp是向上比较,所以m_next++,在前或在后都可以
}
T extractMaxItem() {
assert(m_next > 0);
int index = 0;
T ret = m_data[m_indexes[index]];
m_indexes[index] = m_indexes[m_next];
m_reverse[m_indexes[index]] = index;
m_reverse[m_indexes[m_next]] = 0;
m_next--;
shiftDown(index); //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
return ret;
}
int extractMaxIndex() {
assert(m_next > 0);
int index = 0;
int ret = m_indexes[index];
m_indexes[index] = m_indexes[m_next];
m_reverse[m_indexes[index]] = index;
m_reverse[m_indexes[m_next]] = 0;
m_next--;
shiftDown(index); //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
return ret;
}
bool contain(int i) {
assert (i >= 0 && i< m_capacity);
return m_reverse[i] == 0;
}
T getItem(int i) {
assert(contain(i));
return m_data[i];
}
/*
修改一个已经存在的数组元素
*/
void change(int i, T newItem) {
assert(contain(i));
m_data[i] = newItem;
//此时我们要找到i,这个元素在m_indexes中的位置
//之后在shiftUp shifiDown,看当前元素能否上下移动
/*
for(int j = 0; j < m_next; j++){
if(indexes[j] == i){
shiftUp(j);
shiftDown(j);
return ;
}
}
*/
int j = m_reverse[i];
shiftUp(j);
shiftDown(j);
}
bool empty() {
return m_next == 0;
}
private:
/*
跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
n 的父节点 。(n-1)/2
*/
void shiftUp(int index) {
while(index > 0 && m_data[m_indexes[index] > m_data[m_indexes[(index-1)/2]]) {
swap(m_indexes[index], m_indexes[(index-1)/2]);
m_reverse[m_indexes[index]] = index;
m_reverse[m_indexes[(index-1)/2]] = (index-1)/2;
index = (index-1)/2;
}
}
/*
跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
n left_node = 2*n+1
right_node = 2*n+2
*/
void shiftDown(int index) {
while ((index*2 +1) < m_next) { //确保有子节点
int lhs = index * 2 + 1;
if((lhs+1) < m_next && m_data[m_indexes[lhs]] < m_data[m_indexes[lhs+1]]) {
lhs = lhs+1;
}
if(m_data[m_indexes[index]] >= m_data[m_indexes[lhs]]) {
break;
}
swap(m_indexes[index], m_indexes[lhs]);
m_reverse[m_indexes[index]] = index;
m_reverse[m_indexes[lhs]] = lhs;
index = lhs;
}
}
private:
int m_capacity;
int m_next; //指向下一个可以存储的数组下标
T *m_data;
int *m_indexes;
int *m_reverse;
};
#endif //IndexMAXHEAP_H