一 算法描述
堆排序(Heap Sort)是利用一种被称作二叉堆的数据结构进行排序的排序算法。
二叉堆在内部维护一个数组,可被看成一棵近似的完全二叉树,树上每个节点对应数组中的一个元素。除最底层外,该树是满的。
二叉堆中,有两个与所维护数组相关的属性。Length表示数组的元素个数,而HeapSize则表示二叉堆中所维护的数组中的元素的个数(并不是数组中的所有元素都一定是二叉堆的有效元素)。因此,根据上述定义有: 0 <= HeapSize <= Length。
二叉堆可分为最大堆和最小堆两种类型。在最大堆中,二叉树上所有的节点都不大于其父节点,即 A[Parent(i)] >= A[i]。最小堆正好相反:A[Parent(i)] <= A[i]。
为维护一个二叉堆是最大(小)堆,我们调用一个叫做MaxHeapify(MinHeapify)的过程。以MaxHeapify,在调用MaxHeapify时,先假定根节点为Left(i)和Right(i)的二叉树都是最大堆,如果A[i]小于其子节点中元素,则交换A[i]和其子节点中的较大的元素。但这样一来,以被交换的子节点为根元素的二叉堆有可能又不满足最大堆性质,此时则递归调用MaxHeapify方法,直到所有的子级二叉堆都满足最大堆性质。如下图所示:
因为在调用MaxHeapify(MinHeapify)方法使根节点为A[i]的二叉堆满足最大(小)堆性质时我们有其左右子堆均已满足最大(小)堆性质这个假设,所以如果我们在将一个待排序的数组构造成最大(小)堆时,需要自底向上地调用 MaxHeapify(MinHeapify)方法。
在利用最大堆进行排序时,我们先将待排序数组构造成一个最大堆,此时A[0](根节点)则为数组中的最大元素,将A[0]与A[n - 1]交换,则把A[0]放到了最终正确的排序位置。然后通过将HeapSize减去1,将(交换后的)最后一个元素从堆中去掉。然后通过MaxHeapify方法将余下的堆改造成最大堆,然后重复以上的交换。重复这一动作,直到堆中元素只有2个。则最终得到的数组为按照升序排列的数组。
二 算法实现
1 注意到在C#中数组的起始下标为0,因此,计算一个给定下标的节点的父节点和左右子节点时应该特别小心。
private static int Parrent(int i)
{
return (i - 1) / 2;
}
private static int Left(int i)
{
return 2 * i + 1;
}
private static int Right(int i)
{
return 2 * i + 2;
}
private static void MaxHeapify(int[] array, int i, int heapSize)
{
int left = Left(i);
int right = Right(i);
int largest = i;
if (left < heapSize && array[left] > array[i])
{
largest = left;
}
if (right < heapSize && array[right] > array[largest])
{
largest = right;
}
if (largest != i)
{
Exchange(ref array[i], ref array[largest]);
MaxHeapify(array, largest, heapSize);
}
}
private static void MinHeapify(int[] array, int i, int heapSize)
{
int left = Left(i);
int right = Right(i);
int smallest = i;
if (left < heapSize && array[left] < array[i])
{
smallest = left;
}
if (right < heapSize && array[right] < array[smallest])
{
smallest = right;
}
if (smallest != i)
{
Exchange(ref array[i], ref array[smallest]);
MinHeapify(array, smallest, heapSize);
}
}
private static void MHeapify(T[] array, int i, int heapSize, Comparison comparison)
{
int left = Left(i);
int right = Right(i);
int extremumIndex = i;
if (left < heapSize && comparison(array[left], array[i]) > 0)
{
extremumIndex = left;
}
if (right < heapSize && comparison(array[right], array[extremumIndex]) > 0)
{
extremumIndex = right;
}
if (extremumIndex != i)
{
Exchange(ref array[extremumIndex], ref array[i]);
MHeapify(array, extremumIndex, heapSize, comparison);
}
}
private static void BuildMaxHeap(int[] array)
{
for (int i = array.Length / 2 - 1; i >= 0; i--)
{
MaxHeapify(array, i, array.Length);
}
}
private static void BuildMinHeap(int[] array)
{
for (int i = array.Length / 2 - 1; i >= 0; i--)
{
MinHeapify(array, i, array.Length);
}
}
private static void BuildMHeap(T[] array, Comparison comparison)
{
for (int i = array.Length / 2 - 1; i >= 0; i--)
{
MHeapify(array, i, array.Length, comparison);
}
}
public static void HeapSort(int[] array)
{
BuildMaxHeap(array);
for (int i = array.Length - 1; i > 0; i--)
{
Exchange(ref array[i], ref array[0]);
MaxHeapify(array, 0, i);
}
}
public static void HeapDesSort(int[] array)
{
BuildMinHeap(array);
for (int i = array.Length - 1; i > 0; i--)
{
Exchange(ref array[i], ref array[0]);
MinHeapify(array, 0, i);
}
}
public static void HeapSort(T[] array, Comparison comparison)
{
BuildMHeap(array, comparison);
for (int i = array.Length - 1; i > 0; i--)
{
Exchange(ref array[i], ref array[0]);
MHeapify(array, 0, i, comparison);
}
}
上述的代码是一种常规的堆排序的实现方式。但既然是用C#来实现堆排序,应当尽可能的考虑面向对象的方式去实现算法。考虑到上述代码中,无论是求节点的子节点、父节点、维护最大(小)堆、建立最大(小)堆等方法,本身是属于对堆这种数据结构本身的操作。因此,可以考虑将其封装成一个数据结构类,在类中进行相关的排序操作。如下所示:
public class Heap
{
#region Fields
private int _heapSize = 0;
private T[] _array = null;
#endregion
#region Properties
public int HeapSize
{
get { return _heapSize; }
set { _heapSize = value; }
}
#endregion
#region Constructors
public Heap(T[] array, int heapSize)
{
_array = array;
if(heapSize > array.Length)
{
Exception ex = new Exception("The heap size is larger than the array length");
throw (ex);
}
_heapSize = heapSize;
}
public Heap(T[] array)
{
_array = array;
_heapSize = array.Length;
}
#endregion
#region Methods
private int Parrent(int index)
{
return (index - 1) / 2;
}
private int Left(int index)
{
return 2 * index + 1;
}
private int Right(int index)
{
return 2 * index + 2;
}
private void MHeapify(int rootIndex, Comparison comparison)
{
int leftChildIndex = Left(rootIndex);
int rightChildIndex = Right(rootIndex);
int extremumIndex = rootIndex;
if (leftChildIndex < _heapSize && comparison(_array[leftChildIndex], _array[rootIndex]) > 0)
{
extremumIndex = leftChildIndex;
}
if (rightChildIndex < _heapSize && comparison(_array[rightChildIndex], _array[extremumIndex]) > 0)
{
extremumIndex = rightChildIndex;
}
if (extremumIndex != rootIndex)
{
Helper.Exchange(ref _array[extremumIndex], ref _array[rootIndex]);
MHeapify(extremumIndex, comparison);
}
}
private void BuildMHeap(Comparison comparison)
{
for (int i = _array.Length / 2 - 1; i >= 0; i--)
{
MHeapify(i, comparison);
}
}
public void Sort(Comparison comparison)
{
BuildMHeap(comparison);
for (int i = _array.Length - 1; i > 0; i--)
{
Helper.Exchange(ref _array[i], ref _array[0]);
_heapSize--;
MHeapify(0, comparison);
}
}
#endregion
}
public class Helper
{
public static void Exchange(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
}
1 在整个堆排序的过程中,只是在原有的数组里对元素进行操作,只需要常数的额外空间。因此,堆排序是一种原址排序算法,其空间复杂度为O(1)。
2 在每一轮的最大(小)化堆后,为了将堆中最大的元素放到指定的最终位置,会直接交换堆中的根元素和最后一个元素,这有可能导致堆中和最后一个元素值相同的元素与最后一个元素的相对位置的改编。因而,堆排序是不稳定的。
3 在堆排序的整个过程中,最耗时的操作在于维护最大(小)堆性质的MaxHeapify(MinHeapify)过程。对于MaxHeapify(MinHeapify)而言,其运行时间的递归式为:
T(n) <= T(2n / 3) + θ(1)
根据主定理,可知T(n) = O(lgn).
因此,堆排序的时间复杂度为T(n) = nlgn.
五 运行结果
同样可以根据计数元素的比较次数来估测算法的执行规模。
在算法的泛型实现版本中,通过在委托传入的比较方法里加入计数语句,则能很容易的得到比较语句执行的次数。
private static int AscComparison(int x, int y)
{
count++;
if (x > y)
{
return 1;
}
else if (x == y)
{
return 0;
}
else
{
return -1;
}
}
为了测试该算法的平均运行情况,通过对10000个随机数组进行排序取平均:
static void Main(string[] args)
{
for (int i = 0; i < 10000; i++)
{
//在1-100内产生10个随机数
int[] randomIntArray = DataCreator.CreateRandomIntArray(1, 100, 10);
Heap heap = new Heap(randomIntArray);
heap.Sort(AscComparison);
PrintAarry(randomIntArray);
}
int averageCount = count / 10000;
Console.WriteLine(averageCount);
}
n = 10, averageCount = 38= 1.144* 10 * lg10;
n = 100, averageCount = 1026= 1.544 * 100 * lg100;
n = 1000, averageCount = 16851 = 1.691 * 1000 * lg1000;
n = 10000, averageCount = 235371 = 1.771 * 10000 * lg10000;
由测试结果可知,堆排序算法的平均运算时间复杂度也是θ(nlgn)。