rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • 堆排序

  • 算法定义
  • 算法思路
  • 算法复杂度
  • 优点与缺点
  • 应用场景
  • 各版本优化说明
  • 代码
  • 公式
  • heapify(堆化)
  • 构建堆
  • 堆排序
  • 完整堆排序
  • 代码
  • 资料

堆排序

算法定义

堆排序(Heap Sort)是一种基于堆数据结构的高效排序算法,它利用堆的特性(父节点的值大于等于或小于等于子节点的值)来实现排序。堆排序属于选择排序的一种改进,通过反复提取堆顶元素(最大值或最小值)并调整堆结构,最终得到有序数组。

堆是一种完全二叉树,分为两种类型:

  • 大顶堆:每个父节点的值大于等于其子节点的值
  • 小顶堆:每个父节点的值小于等于其子节点的值

堆排序通常使用大顶堆实现升序排序。

算法思路

堆排序的核心步骤如下:

  1. 构建堆:将无序数组转换为大顶堆(或小顶堆)。
  2. 提取堆顶元素:将堆顶元素(最大值)与数组末尾元素交换,此时最大值已放到正确位置。
  3. 调整堆:将剩余元素重新调整为大顶堆。
  4. 重复操作:重复步骤 2-3,直到所有元素排序完成。

例如,排序[4, 10, 3, 5, 1]:

  • 构建大顶堆:[10, 5, 3, 4, 1]
  • 交换堆顶与末尾元素:[1, 5, 3, 4, 10],调整堆为[5, 4, 3, 1, 10]
  • 重复操作,最终得到[1, 3, 4, 5, 10]

算法复杂度

  • 最好时间复杂度:O (n log n) - 无论输入数据分布如何,复杂度保持稳定
  • 最坏时间复杂度:O (n log n) - 与最好情况相同
  • 平均时间复杂度:O(n log n)
  • 空间复杂度:O (1) - 原地排序,只需常数级额外空间

优点与缺点

  • 优点:
    • 时间复杂度稳定为 O (n log n),不受输入数据影响
    • 原地排序,空间复杂度低(O (1))
    • 适合处理大规模数据
    • 可高效解决 Top K 问题
  • 缺点:
    • 不稳定的排序算法(相等元素的相对位置可能改变)
    • 缓存友好性较差(堆操作涉及非连续内存访问)
    • 实现相对复杂,需要理解堆的结构和调整逻辑

应用场景

  • 需要 O (n log n) 时间复杂度且空间有限的场景
  • Top K 问题(如获取数组中最大的 K 个元素)
  • 优先级队列的实现
  • 操作系统的进程调度
  • 大数据处理中的部分排序需求

各版本优化说明

  1. 基本版(大顶堆):
    • 经典实现,使用递归方式进行堆调整
    • 清晰展示堆排序的核心流程:建堆→提取堆顶→调整堆
    • 适合理解堆排序的基本原理
  2. 迭代版堆调整:
    • 将递归的堆调整改为迭代实现,减少递归调用开销
    • 避免了递归可能带来的栈溢出风险
    • 实际运行效率通常高于递归版本
  3. 小顶堆实现降序:
    • 使用小顶堆实现降序排序,扩展了堆排序的应用场景
    • 展示了堆排序的灵活性,通过改变堆的类型可实现不同排序方向
  4. 混合插入排序优化:
    • 当剩余元素数量小于阈值(如 15)时,改用插入排序
    • 利用插入排序在小规模数据上的高效性,减少堆操作的开销
    • 综合性能优于纯堆排序,尤其对中等规模数据

堆排序是一种高效稳定的排序算法,时间复杂度始终为 O (n log n),且空间开销小,适合处理大规模数据。虽然实现稍复杂,但在对排序稳定性要求不高且需要控制空间开销的场景中表现出色。Java 的PriorityQueue底层就使用了堆结构,可用于实现高效的优先级排序。

代码

import java.util.Arrays;

public class HeapSort {
    
    // 1. 基本版堆排序(大顶堆实现升序)
    public static void heapSortBasic(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        
        // 构建大顶堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapifyBasic(arr, n, i);
        }
        
        // 提取堆顶元素并调整堆
        for (int i = n - 1; i > 0; i--) {
            // 交换堆顶(最大值)和当前末尾元素
            swap(arr, 0, i);
            // 调整剩余元素为大顶堆
            heapifyBasic(arr, i, 0);
        }
    }
    
    // 基本版堆调整(大顶堆)
    private static void heapifyBasic(int[] arr, int heapSize, int i) {
        int largest = i;          // 根节点索引
        int left = 2 * i + 1;     // 左子节点索引
        int right = 2 * i + 2;    // 右子节点索引
        
        // 找出根、左、右三个节点中的最大值
        if (left < heapSize && arr[left] > arr[largest]) {
            largest = left;
        }
        if (right < heapSize && arr[right] > arr[largest]) {
            largest = right;
        }
        
        // 如果最大值不是根节点,则交换并继续调整
        if (largest != i) {
            swap(arr, i, largest);
            heapifyBasic(arr, heapSize, largest);
        }
    }
    
    // 2. 优化版1:迭代实现堆调整(避免递归开销)
    public static void heapSortIterative(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        
        // 构建大顶堆(迭代方式)
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapifyIterative(arr, n, i);
        }
        
        // 提取堆顶元素并调整
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);
            heapifyIterative(arr, i, 0);
        }
    }
    
    // 迭代版堆调整
    private static void heapifyIterative(int[] arr, int heapSize, int i) {
        while (true) {
            int largest = i;
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            
            if (left < heapSize && arr[left] > arr[largest]) {
                largest = left;
            }
            if (right < heapSize && arr[right] > arr[largest]) {
                largest = right;
            }
            
            if (largest == i) {
                break;  // 不需要继续调整
            }
            
            swap(arr, i, largest);
            i = largest;  // 继续调整子树
        }
    }
    
    // 3. 优化版2:小顶堆实现降序排序
    public static void heapSortDescending(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        
        // 构建小顶堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapifyMin(arr, n, i);
        }
        
        // 提取堆顶元素(最小值)并调整
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);
            heapifyMin(arr, i, 0);
        }
    }
    
    // 小顶堆调整
    private static void heapifyMin(int[] arr, int heapSize, int i) {
        int smallest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        
        if (left < heapSize && arr[left] < arr[smallest]) {
            smallest = left;
        }
        if (right < heapSize && arr[right] < arr[smallest]) {
            smallest = right;
        }
        
        if (smallest != i) {
            swap(arr, i, smallest);
            heapifyMin(arr, heapSize, smallest);
        }
    }
    
    // 4. 优化版3:混合排序(小规模数据使用插入排序)
    public static void heapSortHybrid(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        final int INSERTION_THRESHOLD = 15;
        
        // 对于小规模数据,直接使用插入排序
        if (n <= INSERTION_THRESHOLD) {
            insertionSort(arr, 0, n - 1);
            return;
        }
        
        // 构建大顶堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapifyIterative(arr, n, i);
        }
        
        // 提取堆顶元素,当剩余元素小于阈值时使用插入排序
        for (int i = n - 1; i > INSERTION_THRESHOLD; i--) {
            swap(arr, 0, i);
            heapifyIterative(arr, i, 0);
        }
        
        // 对剩余的小规模元素使用插入排序
        insertionSort(arr, 0, INSERTION_THRESHOLD);
    }
    
    // 辅助插入排序
    private static void insertionSort(int[] arr, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int key = arr[i];
            int j = i - 1;
            while (j >= left && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
    }
    
    // 交换元素
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7, 9, 3, 2, 1};
        int[] arr1 = Arrays.copyOf(arr, arr.length);
        int[] arr2 = Arrays.copyOf(arr, arr.length);
        int[] arr3 = Arrays.copyOf(arr, arr.length);
        int[] arr4 = Arrays.copyOf(arr, arr.length);
        
        heapSortBasic(arr1);
        System.out.println("基本版(大顶堆升序): " + Arrays.toString(arr1));
        
        heapSortIterative(arr2);
        System.out.println("迭代版堆调整: " + Arrays.toString(arr2));
        
        heapSortDescending(arr3);
        System.out.println("小顶堆实现降序: " + Arrays.toString(arr3));
        
        heapSortHybrid(arr4);
        System.out.println("混合插入排序优化: " + Arrays.toString(arr4));
    }
}

公式

// 父节点的值等于 (子结点i-1)/2
parent = (i-1)/2

// 左子节点等于(父结点i * 2) + 1
c1 = i*2 + 1

// 左子节点等于(父结点i * 2) + 2
c2 = i*2 + 2

heapify(堆化)

/**
 * @param tree 要做heapify的数组
 * @param n    这个数组的长度
 * @param i    要对哪个结点做heapify
 */
public static void heapify(int[] tree, int n, int i) {
    if (i >= n) return;  // 结点的长度超过总长度就退出

    int c1 = 2 * i + 1; // i结点的左孩子结点
    int c2 = 2 * i + 2; // i结点的右孩子结点
    int max = i; // 先假设i结点的值最大

    // 判断左孩子结点小于数组长度并且左孩子的值大于父亲结点,那么更换最大结点索引位置为左节点
    if (c1 < n && tree[c1] > tree[max]) {
        max = c1;
    }

    // 判断右孩子结点小于数组长度并且右孩子的值大于父亲结点,那么更换最大结点索引位置为右节点
    if (c2 < n && tree[c2] > tree[max]) {
        max = c2;
    }

    // 如果max值和父亲结点不一致,说明孩子结点比父节点大,那么更换两个结点的值,并且重新根据孩子结点的位置进行堆化
    if (max != i) {
        SortUtils.swap(tree, max, i);
        heapify(tree, n, max);
    }
}

构建堆


// nums.length - 1 是完全二叉树中最后一个结点
int cLast = nums.length - 1;

// 最底层的开始heapify结点(从第一个非叶子节点开始)公式 ((nums.length-1) - 1)/2
int heapifyBeginIndex = cLast - 1 >> 1;

for (int i = heapifyBeginIndex; i >= 0; i--) {
    heapify(nums, nums.length, i); // 从第一个非叶子结点开始堆化,直到第0个位置
}

堆排序

每次都是移出最顶层的根节点A[0],与最尾部节点位置调换
然后重新整理被换到根节点的末尾元素,使其符合堆的特性(堆化 heapify)
直至未排序的堆长度为 0。
for (int i = cLast; i >= 0; i--) {
    SortUtils.swap(nums, i, 0); // 交换0位置和最后位置,使最大值排在最后
    heapify(nums, i, 0); // 然后对减一的数组长度进行堆化
}

完整堆排序

public static void main(String[] args) {

    int[] nums = new int[]{4, 1, 3, 5, 10, 2};
    System.out.println(Arrays.toString(nums));
    heapSort(nums);
    System.out.println(Arrays.toString(nums));
}

public static void heapSort(int[] nums) {

	int cLast = nums.length - 1;
	int heapifyBeginIndex = cLast - 1 >> 1;

	/*
	 *  第一步:将数组堆化
	 *  beginIndex = 第一个非叶子节点(最后一个叶子结点的父节点) 求父结点公式(i-1)/2  ((n-1)-1)/2
	 *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
	 *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
	 */
	for (int i = heapifyBeginIndex; i >= 0; i--) {
	    heapify(nums, nums.length, i);
	}

	/*
	 * 第二步:对堆化数据排序
	 * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,遍历n-1次,从后面遍历i=n-1大于0,交换两个元素循环次数就是n-1,像冒冒泡排序i=0,<n-1
	 * 然后重新整理被换到根节点的末尾元素,使其符合堆的特性(堆化 heapify)
	 * 直至未排序的堆长度为 0。
	 */
	for (int i = cLast; i >= 0; i--) {
	    SortUtils.swap(nums, i, 0);
	    heapify(nums, i, 0);
	}
}

/**
 * @param tree 要做heapify的数组
 * @param n    这个数组的长度
 * @param i    要对哪个结点做heapify
 */
public static void heapify(int[] tree, int n, int i) {
    if (i >= n) return;  // 结点的长度超过总长度就退出

    int c1 = 2 * i + 1; // i结点的左孩子结点
    int c2 = 2 * i + 2; // i结点的右孩子结点
    int max = i; // 先假设i结点的值最大

    // 判断左孩子结点小于数组长度并且左孩子的值大于父亲结点,那么更换最大结点索引位置为左节点
    if (c1 < n && tree[c1] > tree[max]) {
        max = c1;
    }

    // 判断右孩子结点小于数组长度并且右孩子的值大于父亲结点,那么更换最大结点索引位置为右节点
    if (c2 < n && tree[c2] > tree[max]) {
        max = c2;
    }

    // 如果max值和父亲结点不一致,说明孩子结点比父节点大,那么更换两个结点的值,并且重新根据孩子结点的位置进行堆化
    if (max != i) {
        SortUtils.swap(tree, max, i);
        heapify(tree, n, max);
    }
}

代码

import java.util.Arrays;

/**
 * 堆排序(Heap Sort)
 */
public class Heap {

    public static void main(String[] args) {

        int[] nums = SortUtils.arrays();
        heapSortExam(nums);
        System.out.println("堆排序:" + Arrays.toString(nums));
    }

    public static void heapSort(int[] nums) {

        int cLast = nums.length - 1;
        int heapifyBeginIndex = cLast - 1 >> 1;

        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点(最后一个叶子结点的父节点) 求父结点公式(i-1)/2  ((n-1)-1)/2
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        for (int i = heapifyBeginIndex; i >= 0; i--) {
            heapify(nums, nums.length, i);
        }

        /*
         * 第二步:对堆化数据排序
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,遍历n-1次,从后面遍历i=n-1大于0,交换两个元素循环次数就是n-1,像冒冒泡排序i=0,<n-1
         * 然后重新整理被换到根节点的末尾元素,使其符合堆的特性(堆化 heapify)
         * 直至未排序的堆长度为 0。
         */
        for (int i = cLast; i >= 0; i--) {
            SortUtils.swap(nums, i, 0);
            heapify(nums, i, 0);
        }
    }

    /**
     * @param tree 要做heapify的数组
     * @param n    这个数组的长度
     * @param i    要对哪个结点做heapify
     */
    public static void heapify(int[] tree, int n, int i) {
        if (i >= n) return;  // 结点的长度超过总长度就退出

        int c1 = 2 * i + 1; // i结点的左孩子结点
        int c2 = 2 * i + 2; // i结点的右孩子结点
        int max = i; // 先假设i结点的值最大

        // 判断左孩子结点小于数组长度并且左孩子的值大于父亲结点,那么更换最大结点索引位置为左节点
        if (c1 < n && tree[c1] > tree[max]) {
            max = c1;
        }

        // 判断右孩子结点小于数组长度并且右孩子的值大于父亲结点,那么更换最大结点索引位置为右节点
        if (c2 < n && tree[c2] > tree[max]) {
            max = c2;
        }

        // 如果max值和父亲结点不一致,说明孩子结点比父节点大,那么更换两个结点的值,并且重新根据孩子结点的位置进行堆化
        if (max != i) {
            SortUtils.swap(tree, max, i);
            heapify(tree, n, max);
        }
    }

    // ========第二种========

    /**
     * 堆排序的主要入口方法,共两步。
     */
    public static void heapSort2(int[] nums) {
        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点(最后一个叶子结点的父节点) 求父结点公式(i-1)/2  ((n-1)-1)/2
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        int lastNode = nums.length - 1;
        int beginHeapifyNode = lastNode - 1 >> 1;
        for (int i = beginHeapifyNode; i >= 0; i--)
            maxHeapify(nums, i, lastNode);
        /*
         * 第二步:对堆化数据排序
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,遍历n-1次,从后面遍历i=n-1大于0,交换两个元素循环次数就是n-1,像冒冒泡排序i=0,<n-1
         * 然后重新整理被换到根节点的末尾元素,使其符合堆的特性(堆化 heapify)
         * 直至未排序的堆长度为 0。
         */
        for (int i = lastNode; i >= 0; i--) {
            SortUtils.swap(nums, 0, i);
            maxHeapify(nums, 0, i);
        }
    }

    /**
     * 调整索引为 index 处的数据,使其符合堆的特性。
     *
     * @param index 需要堆化处理的数据的索引
     * @param len   未排序的堆(数组)的长度
     */
    private static void maxHeapify(int[] nums, int index, int len) {
//        int li = (index << 1) + 1; // 左子节点索引
//        int ri = li + 1;           // 右子节点索引
//        int max = li;              // 子节点值最大索引,默认左子节点。
//        if (li > len) return;      // 左子节点索引超出计算范围,直接返回。
//        if (ri <= len && nums[ri] > nums[li]) // 先判断左右子节点,哪个较大。
//            max = ri;
//        if (nums[max] > nums[index]) {
//            SortUtils.swap(nums, max, index); // 如果父节点被子节点调换,
//            maxHeapify(nums, max, len);  // 则需要继续判断换下后的父节点是否符合堆的特性。
//        }

        int left = (index << 1) + 1; // 左子节点
        int right = left + 1; // 右子节点
        int max = index; // 暂时定在Index的位置就是最大值
        // 如果左子节点的值,比当前最大的值大,就把最大值的位置换成左子节点的位置
        if (left <= len && nums[left] > nums[max]) {
            max = left;
        }
        // 如果右子节点的值,比当前最大的值大,就把最大值的位置换成右子节点的位置
        if (right <= len && nums[right] > nums[max]) {
            max = right;
        }
        // 如果不相等说明,这个子节点的值有比自己大的,位置发生了交换了位置
        if (max != index) {
            SortUtils.swap(nums, max, index); // 如果父节点被子节点调换,
            // 交换完位置后还需要判断子节点是否打破了最大堆的性质。最大堆性质:两个子节点都比父节点小。
            maxHeapify(nums, max, len);
        }
    }

    // ======第三种======

    public static void sort(int[] nums) {
        for (int i = nums.length / 2; i >= 0; i--)
            adjustHeap(nums, i, nums.length - 1);
        for (int i = nums.length - 1; i >= 0; i--) {
            SortUtils.swap(nums, 0, i);
            adjustHeap(nums, 0, i - 1);
        }
    }

    public static void adjustHeap(int[] input, int i, int n) {
        int child;
        for (; i <= n / 2; ) {
            child = i * 2;
            if (child + 1 <= n && input[child] < input[child + 1])
                child += 1;
            if (input[i] < input[child]) {
                int temp = input[i];
                input[i] = input[child];
                input[child] = temp;
                i = child;
            } else
                break;
        }
    }

    // ===练习===
    public static void heapSortExam(int[] nums) {

        // 1. 堆化数组
        int last = nums.length - 1;
        int beginHeapify = last - 1 >> 1;
        for (int i = beginHeapify; i >= 0; --i) {
            heapifyExam(nums, nums.length, i);
        }

        // 2. 把最大值移到最后
        for (int i = last; i >= 0; --i) {
            SortUtils.swap(nums, i, 0);
            heapifyExam(nums, i, 0);
        }
    }

    public static void heapifyExam(int[] nums, int n, int i) {
        if (i >= n) return;

        int c1 = (i << 1) + 1;
        int c2 = c1 + 1;
        int max = i;

        if (c1 < n && nums[c1] > nums[max]) {
            max = c1;
        }
        if (c2 < n && nums[c2] > nums[max]) {
            max = c2;
        }

        if (max != i) {
            SortUtils.swap(nums, max, i);
            heapify(nums, n, max);
        }
    }
}

资料

https://mp.weixin.qq.com/s/JIwmkT52G2A1PajLWijiuA

黃浩杰

最近更新:: 2025/9/8 23:27
Contributors: luokaiwen