堆排序
堆的概念和特性
堆是一颗顺序存储的完全二叉树。
其中每个节点的关键字都不大于其孩子节点的关键字,称为小根堆。
其中每个节点的关键字都不小于其孩子节点的关键字,称为大根堆。
节点的两个子节点之间的大小关系没有约束。
堆一般采用数组实现,二叉树的根节点是下标0。 对于下标为i的节点,其左孩子下标为2i+1,右孩子下标为2i+2,其父节点下标为(i-1)/2。
堆排序解释
以升序为例,堆排序步骤如下:
- 首先将数组堆化(heapify),此时满足所有节点都大于等于其子节点,根元素为最大元素;
- 将根元素与堆中最后一个元素交换位置(swap),可以看作是把最大的元素移出堆,堆的大小减一,新的堆由于来了新的根节点而处于无序状态;
- 将根节点进行“下沉”(siftDown),也就是让根节点与其较大的子节点进行交换位置,然后迭代地处理子节点,直到当前节点比其左右子节点都大,此时堆有序。 重复2、3直到堆大小为1。
为什么swap之后,“下沉”可以使堆有序? 此时只有根节点不满足“每个节点不小于其子节点”,将根节点与较大的子节点交换后,现在的根节点即是二叉树中的最大元素了,迭代地处理交换后的子节点,类似冒泡排序,可将路径上的最小值下沉到叶子节点。
最终满足了“每个节点不小于其子节点”。
siftDown方法用来处理这种情况:有序堆中出现一个元素破坏了有序性,那么将该元素siftDown,可以使得堆有序。
堆化heapify的原理? 堆化是从最后一个非叶子节点开始,向前(根)逐个siftDown。 处理某个节点时,其左右子树已经是有序堆了,恰好满足siftDown的适用情况。
复杂度
堆排序属于选择排序类型,是一种不稳定的排序算法。 时间复杂度O(NlogN),空间复杂度O(1)。
代码
堆排序三部曲:堆化、交换、下沉。
public class HeapSort {
/**
* 堆化、交换、下沉
*/
public int[] sortArray(int[] nums) {
int len = nums.length;
heapify(nums);
for (int i = len - 1; i >= 1; ) {
swap(nums, 0, i);
i--;
siftDown(nums, 0, i);
}
return nums;
}
private void heapify(int[] nums) {
int len = nums.length;
// 从最后一个非叶子节点,到根逐个下沉
for (int i = (len - 2) / 2; i >= 0; i--) {
siftDown(nums, i, len - 1);
}
}
/**
* 下沉指定下标的元素
* @param k 要下沉的元素下标
* @param end 堆的最后一个元素下标
*/
private void siftDown(int[] nums, int k, int end) {
// 2*k+1 <= end 的含义是k存在孩子
while (2 * k + 1 <= end) {
// j是k的左孩子
int j = 2 * k + 1;
// 如果有右孩子且右孩子比左孩子大,那么取右孩子
if (j + 1 <= end && nums[j + 1] > nums[j]) {
j++;
}
// 现在j是k的孩子中较大的那个
// 如果k的孩子j比k大,那么交换j和k,接着迭代处理j
if (nums[j] > nums[k]) {
swap(nums, j, k);
k = j;
} else {
break;
}
}
}
private void swap(int[] nums, int a, int b) {
int tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}
}