什么是优先队列和堆?
class MaxHeap<E> where E : IComparable<E>
{
private E[] heap;
private int N;
public MaxHeap(int capacity)
{
heap = new E[capacity + 1];
N = 0;
}
public MaxHeap() : this(10) { }
public int Count { get { return N; } }
public bool IsEmpty { get { return N == 0; } }
}
元素上游
在11的位置添加68。虽然是一个完全二叉树,但是不满足最大堆的性质,因为68比它的父节点还要大,所以需要执行元素的上游操作,让68往上游,和它的父节点完成位置的交换。
class MaxHeap<E> where E : IComparable<E>
{
private E[] heap;
private int N;
public MaxHeap(int capacity)
{
heap = new E[capacity + 1];
N = 0;
}
public MaxHeap() : this(10) { }
public int Count { get { return N; } }
public bool IsEmpty { get { return N == 0; } }
public void Insert(E e)
{
heap[N + 1] = e;
N++;
Swim(N);
}
private void Swim(int k)
{
while (k > 1 && heap[k].CompareTo(heap[k / 2]) > 0)
{
Swap(k, k / 2);
k = k / 2;
}
}
private void Swap(int i, int j)
{
E e = heap[i];
heap[i] = heap[j];
heap[j] = e;
}
}
元素下沉
目的:删除堆顶元素70。
将堆顶元素70和最后一个元素28交换位置。
交换完后将70删除,此时堆中只有元素28是不满足最大堆的性质,此时就要对28进行元素下沉操作。
选择大的元素进行位置的交换(将28和65进行位置进行交换)。
此时还是不满足最大堆的性质(28比40小),继续下沉,完成操作。
//删除堆顶元素
public E RemoveMax()
{
if (IsEmpty)
throw new ArgumentException("堆为空!");
Swap(1, N);
E max = heap[N];
heap[N] = default(E);
N--;
Sink(1);
return max;
}
//查看最大值
public E Max()
{
if (IsEmpty)
throw new ArgumentException("堆为空!");
return heap[1];
}
//元素下沉
private void Sink(int k)
{
//只有当k这个位置至少还有一个左孩子的时候,才有交换的可能,N是我们最后元素的索引
while (2 * k <= N)
{
int j = 2 * k; //j合适的交换位置,先看左孩子
//如果有右孩子,并且右孩子比左孩子大,将j更新为右孩子
if (j + 1 <= N && heap[j + 1].CompareTo(heap[j]) > 0)
j++;
//如果父亲比它们两个孩子都大,不需要交换,直接跳出while
if (heap[k].CompareTo(heap[j]) >= 0)
break;
//和较大的孩子进行交换
Swap(k, j);
//交换完毕后,更新k的位置。接着判断比较,是否需要接着下沉
k = j;
}
}
堆构造轨迹
扩容、缩容、调整数组容量的大小,输出的信息:
//扩容
public void Insert(E e)
{
if (N == (heap.Length - 1))
ResetCapacity(heap.Length * 2);
...
}
//缩容
public E RemoveMax()
{
...
if (N == (heap.Length - 1) / 4)
ResetCapacity(heap.Length / 2);
....
}
//调整数组容量的大小
private void ResetCapacity(int newCapacity)
{
E[] newHeap = new E[newCapacity];
for (int i = 1; i <= N; i++)
newHeap[i] = heap[i];
heap = newHeap;
}
//输出数组类的信息
public override string ToString()
{
StringBuilder res = new StringBuilder();
res.Append("[");
for (int i = 1; i <= N; i++)
{
res.Append(heap[i]);
if (i != N)
res.Append(", ");
}
res.Append("]");
return res.ToString();
}
测试:
class Program
{
static void Main(string[] args)
{
/
// 3 3 3 5 5 4 //
// / / \ / \ / \ / \ //
// 2 2 1 3 1 4 1 3 1 //
// / / \ / //
// 2 2 3 2 //
/
MaxHeap<int> maxHeap = new MaxHeap<int>();
int[] a = { 3, 2, 1, 5, 4 };
for (int i = 0; i < a.Length; i++)
{
maxHeap.Insert(a[i]);
Console.WriteLine(maxHeap);
}
maxHeap.RemoveMax();
Console.WriteLine(maxHeap);
Console.Read();
}
}
堆排序及优化
堆排序(HeapSort): 移除位在第一个数据的根节点,并做最大堆调整的递归运算。
class HeapSort1
{
//时间复杂度:建堆O(NlogN)+ 出堆(NlogN)=O(2NlogN)
//空间复杂度:O(N)
public static void Sort(int[] arr)
{
int n = arr.Length;
MaxHeap<int> maxHeap = new MaxHeap<int>(n);
for (int i = 0; i < arr.Length; i++)
maxHeap.Insert(arr[i]);
for (int i = n-1; i >= 0; i--)
arr[i] = maxHeap.RemoveMax();
}
}
原地堆排序:
从最后一个非叶子节点开始考察,(11-1-1)/ 2 = 4,也就是从35位置开始,一直考察到节点80。
35应该和它的右孩子65交换,交换完毕就满足了堆的性质(父节点比两个子节点都大)。
同理29和32交换,20和30交换,40和65交换。最后80已经比两个子节点都大了,所以80不用下沉。就这样将数组整理成了最大堆。
排序这个数组:首先排序最大值,把堆顶元素80放到最后一个位置,交换80和35。交换完毕后,80已经排好序,就不再需要考虑80这个位置了。
现在堆顶元素是35,比65小,不满足最大堆的性质,需要整理成最大堆。
让35下沉,和65交换,再和40交换。这样就又是最大堆了。
现在排序65,让65放着数组倒数第二个位置,和15交换位置,完成65的排序。
同样需要将元素15下沉,将它整理成最大堆。
然后再把40放到数组倒数第三个位置,就排序好了3个元素。按照这个思路,每次排序完,将剩余的元素整理成一个最大堆,再进行下一次的排序。如此往复,直到完成这个数组的原地堆排序。
class HeapSort2
{
public static void Sort(int[] arr)
{
int n = arr.Length;
//先将原数组整理成最大堆
for (int i = (n-1-1)/2; i >= 0; i--)
Sink(arr,i,n-1);
//原地的堆排序
for (int i = n-1; i >= 0; i--)
{
Swap(arr, 0, i);
Sink(arr, 0, i - 1);
}
}
//元素下沉
private static void Sink(int[] arr,int k,int N)
{
while (2 * k + 1<= N)
{
int j = 2 * k + 1;
if (j + 1 <= N && arr[j + 1].CompareTo(arr[j]) > 0) j++;
if (arr[k].CompareTo(arr[j]) >= 0) break;
Swap(arr,k, j);
k = j;
}
}
//交换数组中i和j对应位置的元素
private static void Swap(int[] arr,int i,int j)
{
int e = arr[i];
arr[i] = arr[j];
arr[j] = e;
}
}
测试:
class Program
{
static void Main(string[] args)
{
int N = 1000000;
Console.WriteLine("测试随机数组: ");
int[] a = TestHelper.RandomArray(N, N);
int[] b = TestHelper.CopyArray(a);
int[] c = TestHelper.CopyArray(a);
int[] d = TestHelper.CopyArray(a);
TestHelper.TestSort("QuickSort3", a);
TestHelper.TestSort("MergeSort2", b);
TestHelper.TestSort("HeapSort1", c);
TestHelper.TestSort("HeapSort2", d);
Console.WriteLine();
Console.WriteLine("测试近乎有序数组: ");
a = TestHelper.NearlyOrderedArray(N, 100);
b = TestHelper.CopyArray(a);
c = TestHelper.CopyArray(a);
d = TestHelper.CopyArray(a);
TestHelper.TestSort("QuickSort3", a);
TestHelper.TestSort("MergeSort2", b);
TestHelper.TestSort("HeapSort1", c);
TestHelper.TestSort("HeapSort2", d);
Console.WriteLine();
Console.WriteLine("测试大量重复元素数组: ");
a = TestHelper.RandomArray(N, 10);
b = TestHelper.CopyArray(a);
c = TestHelper.CopyArray(a);
d = TestHelper.CopyArray(a);
TestHelper.TestSort("QuickSort3", a);
TestHelper.TestSort("MergeSort2", b);
TestHelper.TestSort("HeapSort1", c);
TestHelper.TestSort("HeapSort2", d);
Console.Read();
}
}