/**
* 【堆】
* 堆的概念:堆是一棵顺序存储的完全二叉树。
*
* 堆分为大根堆和小根堆:
* 大根堆:每个节点的值不小于等于其左、右孩子的值
* 小根堆:每个节点的值不大于等于其左、右孩子的值
*
* 【堆排序】
*
* 1)概念:指利用堆的特性将待排序的序列进行排序。
*
* 2)过程:
* 1)将一个待排序的序列构造成一个堆:从最后一个非叶子结点向上遍历,直到所有的非叶子节点都遍历完毕(筛选法)。
* 2)移走堆顶元素后,将剩余的元素再次构造成一个新的堆。
*
* 3)大根堆的排序:
* 1)将待排序的n个元素构造成一个大根堆。
* 2)移走堆顶元素(即:将堆顶元素与堆数组的末尾元素进行交换,此时末尾元素为最大值)。
* 3)将剩余的n-1个元素重新构造成一个大根堆,重复上面的步骤,直到剩余的元素只有一个时,排序完成。
*
* 4)说明:将数组构造成初始堆时,若想升序排列构造大根堆,若想降序排列则构造小根堆。
*
* 将一个完全二叉树按层序排号依次存入数组:
* 8
* / \
* 3 9 ---按层序排号存入数组---> [8,3,9,5,7,4,2]
* / \ / \
* 5 7 4 2
*
* 5)在数组中,很容易得到下面的结论:
* 1)下标为i的节点,其父节点的下标为(i-1)/2
* 2)下标为i的节点,其左子节点的下标为2*i+1,右子节点的下标为2*i+2
*
* 6)使用场景:建立堆和调整堆的过程中会产生比较大的开销,故堆排序适用于排序元素较多的时候。eg:
* 问题:top K
* 方案:
* 第1步)分治:先将所有的数据按照hash方法分解成多个较小数据集。
* 第2步)使用堆排序分别找出这几个较小数据集中的topK。
* 第3步)将第2步中各数据集的topk放在一个集合里,然后求出最终的topK。
*
* 7)复杂度:
* 1)时间复杂度:最好、最坏、平均 的复杂度都是 O(nlogn),故堆排序堆输入的数据不敏感。
* 建堆:O(n)
* 调整:O(logn)
* 总的时间复杂度 = 建堆(initHeap) + 排序(sortHeap) = O(n) + O(nlogn) = O(nlogn + n) = O(nlogn)
* 2)空间复杂度:堆排序是就地排序,故其空间复杂度为O(1)。
*
* 8)稳定性:排序前后相同元素间的相对位置可能会发生改变,故堆排序是一种不稳定的排序算法。
*
*/
public class HeapSort {
public static int[] array;
public static int adjustTime = 0;
public static int whileTime = 0;
public HeapSort(int[] array) {
this.array = array;
}
/**
* 根据子节点的索引来获取父节点的索引
*
* @param child
* @return
*/
public static int parentIndex(int child) {
return (child - 1) / 2;
}
/**
* 根据父节点的索引来获取左子节点的索引
*
* @param parent
* @return
*/
public static int leftChildIndex(int parent) {
return parent * 2 + 1;
}
/**
* 第一步:将待排序的n个元素构造成一个大根堆。
*
* 时间复杂度:O(n) 注:建堆的时间复杂度的推导过程比较复杂,记住建堆的时间复杂度为O(n)即可。
*
* 将数组初始化为大根堆:从下(最后一个非叶子节点)往上(堆的根节点)循环遍历。
*
* 要点:遍历存在左子节点的父节点,从最后一个非叶子结点开始遍历。
* 说明:
* 1)完全二叉树是按层序排号存入数组的,故二叉树的最后一个节点(即:数组中索引值最大的元素)一定是叶子节点,故最后一个节点一定有父节点,且最后一个节点的父节点就是 堆最后一个非叶子节点。
* 2)二叉树的最后一个节点的索引为array.length-1,则其父节点(即:最后一个非叶子节点)的索引为(array.length-1-1)/2,故我们从array.length/2-1开始遍历!
*/
public static void initHeap() {
// 从下往上的循环
for (int parentIndex = parentIndex(array.length-1); parentIndex >= 0; parentIndex--) {
adjustHeap(array, parentIndex, array.length - 1);
}
}
/**
* 对堆进行排序
*
* 时间复杂度:O(nlogn)
*/
public static void sortHeap() {
// array.length-1次 调整完成排序
for (int i = array.length - 1; i > 0; i--) {
// 第二步:将堆顶元素(数组中第一个元素)和当前未排序子序列中的最后一个元素交换
swap(array, 0, i);
// 第三步:交换后,将剩余的n-1个元素重新构造成一个大根堆
adjustHeap(array, 0, i-1);
}
}
/**
* 调整堆:从上往下循环遍历,即 沿父节点的较大子节点向下调整
*
* 时间复杂度:O(logn)
*
* @param array
* @param parentIndex
* @param maxIndex
*/
public static void adjustHeap(int[] array, int parentIndex, int maxIndex) { adjustTime++;
int temp = array[parentIndex]; // 父节点的值
int child = leftChildIndex(parentIndex); // 左子节点的索引
// 从上往下循环:沿父节点的较大子节点向下调整
while (child <= maxIndex) { // 左子节点必须在未排序子序列中
whileTime++; // 记录循环的次数,测试用。
// 若当前节点(即:父节点)存在右子节点(且右子节点在未排序的子序列中),并且右子节点的值大于左子节点时,将右子节点的索引赋值给child
if (child + 1 <= maxIndex && array[child + 1] > array[child]) {
child++; // 将左子节点转换为右子节点
}
// 此时,child表示 子节点中值最大的那个节点 的索引
// 若当前节点(即:父节点)的值大于子节点的值时,直接退出。
if (temp > array[child]) {
break;
} else {
array[parentIndex] = array[child]; // 将子节点的值赋值给父节点
// 选取子节点的左子节点继续向下调整(执行wile循环)
parentIndex = child;
child = leftChildIndex(parentIndex);
}
}
// 若发生了交换,则parentIndex表示子节点的索引;若没有发生交换,则parentIndex仍旧表示父节点的索引。
array[parentIndex] = temp;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int[] array = {2, 13, 5, 7, 14, 6, 10, 8, 11, 4, 3, 9, 1, 12, 0};
HeapSort heapSort = new HeapSort(array);
heapSort.initHeap();
heapSort.sortHeap();
System.out.println("排序后数组" + Arrays.toString(heapSort.array) + " 调整次数" + adjustTime + " while循环次数" + whileTime);
}
}