4.选择序—堆排序(Heap Sort)
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
基本思想:
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆(Java数组从0开始,i为0到n-1),则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96, 83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)
1.堆
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:
- Key[i] <= key[2i+1] && Key[i] <= key[2i+2]
- 或者
- Key[i] >= Key[2i+1] && key >= key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
堆分为大顶堆和小顶堆,
满足 Key[i] >= Key[2i+1] && key >= key[2i+2] 称为大顶堆,满足 Key[i] <= key[2i+1] && Key[i] <= key[2i+2] 称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
2.堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
操作过程如下:
1)初始化堆:将R[1..n]构造为堆;
2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。
因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。
下面举例说明:
给定一个整形数组a[]={16,7,3,20,17,8}(小顶堆),对其进行堆排序。
首先根据该数组元素构建一个完全二叉树,得到
然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:
20和16交换后导致16不满足堆的性质,因此需重新调整
这样就得到了初始堆。
即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了。
这样整个区间便已经有序了。
从上述过程可知,堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1...n]中选择最大记录,需比较n-1次,然后从R[1...n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。
- import java.util.Random;
-
- public class HeapSort {
-
- public static int[] heapSort(int[] arr) {
-
- System.out.println("----------------建堆----------------");
- int lastIndex = 0;
- for (int i = 0; i < arr.length; i++) {
- lastIndex = arr.length - 1 - i;
-
- createHeap(arr, lastIndex);
- isHeap(arr, lastIndex);
- swap(arr, 0, arr.length - 1 - i);
-
- System.out.println("堆顶与第"+(lastIndex+1)+"个数交换后 :");
- print(arr);
- System.out.println("----------------建堆----------------");
- }
-
- return arr;
- }
-
-
- private static void createHeap(int[] arr, int lastIndex) {
-
- int j = lastIndex;
- int i = 0;
- for (i = (lastIndex - 1) / 2; i >= 0; i--) {
-
- int k = i;
-
-
- if ((2 * k) + 1 <= j) {
-
- int biggerIndex = 2 * k + 1;
-
- if (biggerIndex < j) {
-
- if (arr[biggerIndex] < arr[j]) {
- biggerIndex++;
- }
- }
-
- if (arr[k] < arr[biggerIndex]) {
-
- swap(arr, k, biggerIndex);
- j = (--k) * 2 + 2;
-
- }else{
- j = (--k) * 2 + 2;
- }
-
- }
- print(arr);
- }
- }
-
-
- private static void isHeap(int[] arr, int lastIndex) {
-
- int leftIndex = 0;
- int rightIndex = 0;
- int parent = 0;
-
- for (int i = 0; i < (arr.length - 2) / 2; i++) {
-
- leftIndex = 2 * i + 1;
- rightIndex = 2 * i + 2;
- parent = i;
-
- if (arr[leftIndex] > arr[parent] || arr[rightIndex] > arr[parent]) {
- createHeap(arr, lastIndex);
- }
- }
-
- }
-
-
- private static void swap(int[] arr, int i, int j) {
-
- if (i == j) {
- return;
- }
- arr[i] = arr[i] + arr[j];
- arr[j] = arr[i] - arr[j];
- arr[i] = arr[i] - arr[j];
- }
-
-
- public static void print(int[] arr) {
- for (int i = 0; i < arr.length; i++) {
- System.out.print(arr[i] + " ");
- }
- System.out.println();
- }
-
- public static void main(String[] args) {
-
- System.out.println("任意数组测试:");
- Random r = new Random();
- int[] testArr = new int[20];
- for (int i = 0; i < 20; i++) {
- testArr[i] = r.nextInt(100);
- }
-
- int a[] = { 16, 7, 3, 20, 17, 8 };
- System.out.println("排序前 : ");
- print(a);
-
- System.out.println("排序 : ");
- print(heapSort(a));
-
-
-
-
-
-
-
- }
-
- }
打印结果:
- 任意数组测试:
- 排序前 :
- 16 7 3 20 17 8
- 排序 :
- ----------------建堆----------------
- 16 7 8 20 17 3
- 16 20 8 7 17 3
- 20 16 8 7 17 3
- 20 16 8 7 17 3
- 20 17 8 7 16 3
- 20 17 8 7 16 3
- 堆顶与第6个数交换后 :
- 3 17 8 7 16 20
- ----------------建堆----------------
- 3 17 8 7 16 20
- 17 3 8 7 16 20
- 17 16 8 7 3 20
- 17 16 8 7 3 20
- 堆顶与第5个数交换后 :
- 3 16 8 7 17 20
- ----------------建堆----------------
- 3 16 8 7 17 20
- 16 3 8 7 17 20
- 16 7 8 3 17 20
- 16 7 8 3 17 20
- 堆顶与第4个数交换后 :
- 3 7 8 16 17 20
- ----------------建堆----------------
- 8 7 3 16 17 20
- 8 7 3 16 17 20
- 堆顶与第3个数交换后 :
- 3 7 8 16 17 20
- ----------------建堆----------------
- 7 3 8 16 17 20
- 7 3 8 16 17 20
- 7 3 8 16 17 20
- 堆顶与第2个数交换后 :
- 3 7 8 16 17 20
- ----------------建堆----------------
- 3 7 8 16 17 20
- 3 7 8 16 17 20
- 3 7 8 16 17 20
- 堆顶与第1个数交换后 :
- 3 7 8 16 17 20
- ----------------建堆----------------
- 3 7 8 16 17 20
(本人写的代码输出结果意在看排列的过程,请去除重复后,再与下面结论进行比较验证)
分析:
设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式:
而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。