堆排序的基本三步走:
1、将无序序列构造为堆结构,如果是升序排序则为 大顶堆,降序则为 小顶堆。(这里使用大顶堆)
2、将 堆顶元素 与 堆尾元素进行交换(即,把当前最大元素 “沉” 入到 堆尾)
3、重新构造堆结构,构造堆结构的目的就是将未排序序列的最大值 “抬” 到堆顶。(但是要注意,此时我们已经把一个最大值成功排序到堆尾了,因此我们应当排除掉已经排序好的元素。 我们可以用 len 来表示当前序列还未排序的序列长度,每次交换完 堆顶 和 堆尾 元素后 len–,然后重新构造堆结构时,如果当前元素下标大于 len 则排除掉该元素,即该元素不参与堆的重新构造)
终止条件:这样 每一次重新构造堆结构,我们都会把一个未排序序列中的最大值排列到堆尾,因此,对于一个长度为 n 的无序序列,只需要重构 n - 1 次堆结构即可(因为对于最后一个元素而言,当它前面的元素都排序完毕时,它也就自动排序完了)
private static void HeapSort(int[] nums) {
// 安全判断
if (nums == null || nums.length == 0) return;
// 构建大顶堆
BuildHeap(nums);
// 每一次重构顶堆,都会把当前的最大值给沉到堆尾,即排序完一个当前最大值
// 而最后一个当前最大值就不需要排序了(即最小值), 因为前面排完,最后一个就自动确定了
// 所以要排完整个长度为n的序列, 需要重构顶堆n - 1次
int len = nums.Length - 1;
for (int i = nums.Length - 1; i > 0; i--) {
// 交互堆顶与堆尾, 把当前最大值沉到最小面
Swap(nums, 0, i);
// 因为每重构一次,当前最大值都会被沉到当前的最后一层
// 所以每重构一次,要len--, 防止最大值又被重构到最顶层
Heapify(nums, 0, len--);
}
}
private static void BuildHeap(int[] nums) {
//第一个非叶子结点的下标是 nums.Length / 2 - 1
// // 从数组中最后一个 非叶子节点开始向前遍历
int start = nums.Length / 2 - 1;
for (int i = start; i >= 0; i--) {
Heapify(nums, i, nums.Length);
}
}
private static void Heapify(int[] nums, int i, int len) {
// 计算当前非叶子节点的左右节点索引
int left = 2 * i + 1;
int right = 2 * i + 2;
// 默认当前节点是最大值
int largestIndex = i;
// 如果有左节点,且左节点比当前节点大,则更新最大值的索引
if (left < len && nums[left] > nums[largestIndex]) largestIndex = left;
if (right < len && nums[right] > nums[largestIndex]) largestIndex = right;
if (largestIndex != i) {
// 如果当前节点比它的子节点小, 则交换位置
Swap(nums, largestIndex, i);
// 交换位置后子节点的值就变化了(子节点的值,变成了原来当前节点值)
// 所以要判断子节点(当前节点的子节点也是非叶子节点的情况)
Heapify(nums, largestIndex, len);
}
}
private static void swap(int[] nums, int i, int j) {
nums[i] ^= nums[j];
nums[j] ^= nums[i];
nums[i] ^= nums[j];
}