堆与堆排序
堆(二叉堆)
定义描述:
-
堆的逻辑结构为完全二叉树,在物理存储上一般表示为一种数组对象。
-
数组中的数据按照其逻辑结构树的广度优先算法(队列优先)来存储对应的值。
性质:
-
堆中某个结点的值总是不大于或不小于其父结点的值;
即:n个元素的序列{k1,k2,ki,…,kn}。 κ i \kappa_i κi ≤ \leq ≤ κ 2 i \kappa_{2i} κ2i && κ i \kappa_i κi ≤ \leq ≤ κ 2 i + 1 \kappa_{2i+1} κ2i+1 (小根堆)
κ i \kappa_i κi ≥ \geq ≥ κ 2 i \kappa_{2i} κ2i && κ i \kappa_i κi ≥ \geq ≥ κ 2 i + 1 \kappa_{2i+1} κ2i+1 (大根堆)
-
堆总是一棵完全二叉树。树的每一层都是填满的,最后一层可能除外(最后一个从左子树填写值)。
-
堆的数组A是一个具有两个属性的对象:length[A] 是数组元素个数,heap-size[A] 是存放在A中堆元素个数。
故:A[ 0, length[A]-1 ] 中都可以包含有效值, 但是A[ heap-size[A] - 1 ]之后的元素都不属于相应的堆。
heap-size[A] <= length[A]
-
树中的每个结点与数组中存放该结点值的那个元素对应。
因此 父节点和左右节点的关系如下:
- Parent(i) = floor((i-1)/2), i 的父节点下标 floor 向下取整
- Left(i) = 2i+1, i 的左子节点下标
- Right(i) = 2(i+1) , i 的右子节点下标
堆的高度:
堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目。
堆的高度为树根的高度。具有n个元素的堆是基于一棵完全二叉树的,因而其高度为O(lgn)。
思维拓展:
堆这种数据结构是处理海量数据比较常见的结构,海量数据的TOP K问题一般可以通过分治法+hash+堆这种数据结构解决。
- 比如:
在spark shuffle 中的 SortShuffleWriter 就是使用了 堆的特性, 进行堆排序,实现大数据量的排序的。
堆排序
定义:
- 利用堆这种数据结构所设计的一种排序算法。
堆排序流程:
-
构建最大堆:
-
使单个子堆成为最大堆(Max-Heapify)
-
自下而上的调用 Max-Heapify 来改造数组, 使整个数组改造成一个最大堆。
构建最大堆
-
-
堆排序:
-
使用上面构造的最大堆,将堆顶和堆底元素交换,
-
这个时候利用上面第3点性质 将heap-size[A] -1,但数组length[A]不变,堆中存在的最大元素被分离出堆。
-
这时候又不是最大堆了,故 :对 整个堆构造最大堆。
-
使用递归 重复上面 2,3 步骤
对最大堆进行堆排序
-
具体实现如下:
import java.util.Arrays;
/**
* 堆排序
* 1. 构建最大堆 build_max_heap
* 2. 对最大堆排序 heapSort
*/
public class heap_sort {
/**
* 交换值
* @param arr
* @param a 元素的下标
* @param b 元素的下标
*/
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
/**
* 对每一个 子堆进行 最大堆调整
* PARENT floor(i/2) LEFT 2*i + 1 RIGHT 2*i + 2
*/
public static void max_heapify(int[] arr, int len, int index){
int parent_node = (int) Math.floor(index/2);
int left_node = 2 * index + 1;
int right_node = 2 * index + 2;
int largest = index;
if(left_node < len && arr[left_node] > arr[index]){
largest = left_node;
}
if(right_node < len && arr[right_node] > arr[largest]){
largest = right_node;
}
if(largest != index){
swap(arr, largest, index);
//调整交换后的子堆
max_heapify(arr, len, largest);
}
}
/**
* 对整个数组进行 最大堆调整, 逻辑结构为二叉堆
* len/2 -1 找出最后一个有子节点的子堆
*/
public static void build_max_heap(int[] arr, int len){
if(len<=1){
System.out.println("");
}
// 从最后一个自下而上进行最大堆构建
for(int i = len/2 -1; i >= 0; i--){
max_heapify(arr, len, i);
}
System.out.println("最大堆:" + Arrays.toString(arr));
}
/**
* 根据最大堆 进行堆排序, 输出排好序的数组
* len -1 数组和逻辑二叉树 最后一个节点 最后一个节点和最大根节点交换
*/
public static int[] heapSort(int[] arr, int len){
if(len<=1){
return arr;
}
build_max_heap(arr, len);
// 从最后一个节点利用 堆的性质排序
for(int i = len -1; i >= 1; --i){
swap(arr, 0, i);
max_heapify(arr, --len, 0);
}
// 排序后数组
System.out.println("排序后:" + Arrays.toString(arr));
return arr;
}
public static void main(String[] args) {
int[] arr = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = arr.length;
heapSort(arr, len);
}
}
时间复杂度:
= 初始化最大堆阶段 + 调整堆的阶段
-
初始化最大堆阶段
O(n) 调用一次
-
调整堆的阶段 时间复杂度
调整堆的时间复杂度是lgn,调用了n-1次 O(nlgn)O(n)+O(nlgn)≈ O(nlgn)
故:整体的时间复杂度约为: O(nlgn)