堆是一种特殊的树,只要知足如下两点,这个树就是一个堆。算法
①彻底二叉树,彻底二叉树要求除了最后一层,其余层的节点个数都是满的,最后一层的节点都靠左排列。api
②树中每个结点的值都必须大于等于(或小于等于)其子树中每一个节点的值。大于等于的状况称为大顶堆,小于等于的状况称为小顶堆。数组
彻底二叉树适合用数组来存储,由于数组中对于下标从1开始的状况,下标为i的节点的左子节点就是下标为i*2的节点,右子节点就是i下标为i*2+1的节点,其父节点时下标为i/2的节点缓存
往堆中插入一个元素数据结构
把新插入的元素放到堆的最后就不符合第二个特性了,因此咱们须要进行调整,让其从新知足堆的特性,这个过程咱们起了一个名字,就叫做堆化(heapify)。性能
堆化就是顺着节点所在的路径,向上或者向下,对比,而后交换。咱们先使用从下往上的堆化方法。测试
让新插入的节点与父节点对比大小。若是不知足子节点小于等于父节点的大小关系,咱们就互换两个节点。一直重复这个过程,直到父子节点之间知足刚说的那种大小关系。ui
public class Heap{ private int[] data;//数组,从下标1开始存储 private int maxNum;//数组容量 private int count;//当前数组成员数量 //构造器初始化数组,大小和数量 public Heap(int size){ data = new int[size + 1]; maxNum = size; count = 0; } public void Insert(int item){ //堆满返回 if (count >= maxNum) return; //先将节点插入堆尾 data[count++] = item; int i = count; //再自下向上堆化,直到堆顶或者父节点比子节点大为止 while (i / 2 > 0 && data[i] > data[i / 2]){ //交换位置 int temp = data[i]; data[i] = data[i / 2]; data[i / 2] = temp; //更新下标 i = i / 2; } } }
删除堆顶元素spa
根据对的第二条定义,堆顶元素存储的就是堆中的最大值或最小值。code
这里咱们使用从上往下的堆化方法。将最后一个节点放到堆顶,而后利用一样的父子节点对比法,进行互换节点直到父子节点之间知足大小关系为止。
这样移除的就是数组中的最后一个元素,不会破环彻底二叉树的定义。
public void RemoveMax(){ //堆空返回 if (count == 0) return; //将最后一个节点提到堆顶 data[1] = data[count--]; //进行堆化 Heapify(data,count,1); } public static void Heapify(int[] data,int n,int i){ while (true){ //记录更大节点的位置,初始化为当前节点的位置 int maxPos = i; //若是其左右子节点存在,且比当前节点大,就将左右节点下标设为更大的节点 if (i * 2 <= n && data[i] < data[i * 2]) maxPos = i * 2; if (i * 2 + 1 <= n && data[maxPos] < data[i * 2 + 1]) maxPos = i * 2 + 1; //不然就结束循环,堆化结束 if (maxPos == i) break; //节点交换位置 int temp = data[i]; data[i] = data[maxPos]; data[maxPos] = temp; //更新当前节点的下标,循环继续与下一个左右子节点比较 i = maxPos; } }
咱们借助于堆这种数据结构实现的排序算法,就叫做堆排序。
咱们能够把堆排序的过程大体分解成两个大的步骤,建堆和排序。
首先将数组原地建成一个堆。借助另外一个数组,就在原数组上操做。咱们要实现从后往前处理数组,而且每一个数据都是从上往下堆化的建堆方法。
public static void BuildHeap(int[] data, int n){ //从下标n/2到1开始进行堆化,n/2就是最后一个叶子节点的父节点。 for (int i = n / 2; i >= 1; --i) Heapify(data,n,i); }
咱们对下标从n/2开始到 111 的数据进行堆化,下标是n/2+1到n的节点是叶子节点,咱们不须要堆化。
建堆操做的时间复杂度
排序的建堆过程的时间复杂度是 O(n)。
建堆结束以后,数组中的数据已是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。咱们把它跟最后一个元素交换,那最大元素就放到了下标为n的位置。
这个过程有点相似删除堆顶元素的操做,当堆顶元素移除以后,咱们把下标为n的元素放到堆顶,而后再经过堆化的方法,将剩下的n-1个元素从新构建成堆。
堆化完成以后,咱们再取堆顶的元素,放到下标是的位置,一直重复这个过程,直到最后堆中只剩下标为1的一个元素,排序工做就完成了。
public static void Sort(int[] data,int n){ //将数组建造为堆 BuildHeap(data, n); //获取堆尾的下标 int k = n; //循环直到k为1 while (k > 1){ //交换堆顶和堆尾的元素 int temp = data[k]; data[k] = data[1]; data[1] = temp; //将堆尾的下标递减并对1到k的下标的数组成员进行堆化 Heapify(data,--k,1); } }
堆排序的时间复杂度、空间复杂度以及稳定性
堆排序是原地排序算法。堆排序包括建堆和排序两个操做,建堆过程的时间复杂度是O(n),排序过程的时间复杂度是O(nlogn)因此,堆排序总体的时间复杂度是O(nlogn)。
堆排序不是稳定的排序算法,由于在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操做,因此就有可能改变值相同数据的原始相对顺序。
//Main方法 int[] data = new int[] {0,3,5,2,9,4,7 }; Heap.Sort(data,data.Length-1); for (int i=0;i<data.Length;i++) Console.Write(data[i]+","); //测试结果 0,2,3,4,5,7,9,
数组的第1个成员,即下标0的数据是不做为数据的一部分的,这是为了算法上的方便,若是下标是从0开始,那么左右子节点的下标公式就是i*2+1和i*2+2。
对于快速排序来讲,数据是顺序访问的而对于堆排序来讲,数据是跳着访问的。这样对 CPU 缓存是不友好的。
对于一样的数据,在排序过程当中,堆排序算法的数据交换次数要多于快速排序。堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对前后顺序,致使原数据的有序度下降。好比,对于一组已经有序的数据来讲,通过建堆以后,数据反而变得更无序了。