这一篇讲一讲堆排序。
一、复习二叉堆
二叉堆是完全二叉树或者是近似完全二叉树。
完全二叉树:只有最下面的两层结点度能够小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
(如果完全没有二叉树数据结构概念的童鞋建议先去学习下再来看)
二叉堆满足两个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
二、堆的存储
二叉堆有一个经典的存储方式就是直接使用数组。先看下图:
通过数学分析我们可以证明,对于数组中的任意下标为i的元素,它的左叶子节点的下标为2 * i + 1,右叶子节点的下标为2 * i + 2,它的父亲节点的下标为(i - 1) / 2。
三、堆的操作(这里都以最大堆为例)
1. 插入数据
从数组尾部插入数据时,会导致当前的堆不再满足二叉堆的性质,这时候需要一步叫做shiftUp的经典操作,用语言来表达,就是让插入的元素每次都和自己的父节点做比较,如果大于父节点的值,就进行交换操作,直到把这个插入的节点放到它应该所在的位置为止,后面会上代码。
2.取出数据
从数组头部取出的数据一定是当前数组中的最大值,取完后当前数组不再满足最大堆的性质,这时候需要一步ShiftDown的经典操作,用语言来表达就是,首先将数组尾部的数据移动到数组头部,然后对比它的左右叶子,如果左右叶子都小于等于该元素,则循环结束,否则则对比左右叶子,将较大的元素与该元素进行交换,交换后再对比当前位置的该元素的左右叶子,直到循环结束。
三、堆排序
首先可以看到堆建好之后堆中第0个数据是堆中最大的数据。取出这个数据再执行shiftDown操作。这样堆中第0个数据又是堆中最大的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。
所以按以上步骤操作完成后,取出的数据组成的数组就是一个排序好的数组了。上代码:
//
// maxheap.h
//
// Created by Amuro on 2017/5/5.
// Copyright © 2017年 Amuro. All rights reserved.
//
//
// Created by Amuro on 17/4/17.
//
#ifndef ALGORITHMCOMPACT_MAXHEAP_H
#define ALGORITHMCOMPACT_MAXHEAP_H
#include <iostream>
using namespace std;
template <typename Item>
class MaxHeap
{
private:
Item* data;
int count;
int capacity;
void shiftUp(int position)
{
while(position > 0 && data[position] > data[(position - 1) / 2])
{
int temp = data[position];
data[position] = data[(position - 1) / 2];
data[(position - 1) / 2] = temp;
position = (position - 1) / 2;
}
}
void shiftDown(int position)
{
//left child exists
while((2 * position + 1) < count)
{
int left = 2 * position + 1;
int right = left + 1;
int fPosition = left;
//right child exists
if(right < count && data[right] > data[left])
{
fPosition = right;
}
if(data[position] >= data[fPosition])
{
break;
}
Item temp = data[position];
data[position] = data[fPosition];
data[fPosition] = temp;
position = fPosition;
}
}
public:
MaxHeap(int capacity)
{
data = new Item[capacity];
count = 0;
this->capacity = capacity;
}
MaxHeap(Item arr[], int length)
{
data = new Item[length];
capacity = length;
for(int i = 0; i < length; i++)
{
data[i] = arr[i];
}
count = length;
for(int i = (count - 1) / 2; i >= 0; i--)
{
shiftDown(i);
}
}
int getLength()
{
return count;
}
bool isEmpty()
{
return count == 0;
}
void insert(Item item)
{
if(count == capacity)
{
return;
}
data[count] = item;
count++;
shiftUp(count - 1);
}
Item extractMax()
{
if(count > 0)
{
Item result = data[0];
data[0] = data[count - 1];
count--;
shiftDown(0);
return result;
}
return NULL;
}
~MaxHeap()
{
delete[] data;
}
void printData()
{
for(int i = 0; i < count; i++)
{
cout << data[i] << ", ";
}
}
};
#endif //ALGORITHMCOMPACT_MAXHEAP_H
注意看代码中其实有两种排序的方法,一种是初始化MaxHeap后,把需要排序的数组中的元素一个个插入到data中,这时候data就是一个最大二叉堆了,然后再调用extractMax方法来取出数据构建一个排序完成的数组。另一种方法简称Heapify,也就是在MaxHeap构造的时候就完成最大二叉堆的构建,它的算法就是从最后一个元素的父节点开始,从后向前执行shiftDown操作,直到根节点。感兴趣的童鞋可以测试下,第二种方法的效率会优于第一种方法。
四、原地堆排序
上文所述的方法有一个较大的问题,就是需要O(n)级别的空间的消耗,实际上我们是可以直接对数组本身进行操作而无需额外的空间消耗的。看代码:
#ifndef ALGORITHMCOMPACT_HEAPSORT_H
#define ALGORITHMCOMPACT_HEAPSORT_H
#include "maxheap.h"
void _shiftDown(int arr[], int length, int position)
{
//left child exists
while((2 * position + 1) < length)
{
int left = 2 * position + 1;
int right = left + 1;
int fPosition = left;
//right child exists
if(right < length && arr[right] > arr[left])
{
fPosition = right;
}
if(arr[position] >= arr[fPosition])
{
break;
}
int temp = arr[position];
arr[position] = arr[fPosition];
arr[fPosition] = temp;
position = fPosition;
}
}
void heapSort(int arr[], int length)
{
//heapify in place
for(int i = (length - 1) / 2; i >= 0; i--)
{
_shiftDown(arr, length, i);
}
for(int i = length - 1; i > 0; i--)
{
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
_shiftDown(arr, i, 0);
}
}
#endif //ALGORITHMCOMPACT_HEAPSORT_H
同样也是先Heapify使原数组变成最大二叉堆,因为数组头元素一定是当前数组的最大元素,所以每次把数组头元素和数组尾部元素进行交换,再对前面n - 1的元素进行shiftDown操作就可以了。
测试可以看到原地堆排序的效率要比前面两种方法都要高一些,原因就是没有了数据复制粘贴等操作带来的时间消耗。
五、对比下常见的排序算法
最常用的还是快速排序。