排序(2)-选择排序专题——简单选择排序与堆排序的结构优化

📌 引言

在每一轮选拔中,最直观的策略就是"统览全局,挑出最优"。在排序算法中,选择排序(Selection Sort)正是这一策略的完美体现。今天我们将从最朴素的简单选择排序 出发,看它如何借助"二叉堆"这一高效的数据结构,华丽蜕变为工业级的高性能算法------堆排序

1. 简单选择排序(Simple Selection Sort)

💡 核心思想

将数组分为已排序区未排序区

  1. 初始时,已排序区为空。

  2. 每次遍历未排序区,记录下其中最小(或最大)元素的索引。

  3. 遍历结束后,将这个最小元素与未排序区的第一个元素进行交换(使其加入已排序区)。

  4. 重复上述步骤,直到所有元素都有序。

它的核心特点是:无论初始状态如何,元素比较的次数都是固定的,但数据交换(Swap)的次数极少。

💻 Java 代码实现
复制代码
public class SelectionSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        int n = arr.length;
        
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i; // 暂定未排序区的第一个元素为最小值
            
            // 在未排序区中寻找真正的最小值
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j; // 记录更小元素的索引
                }
            }
            
            // 如果最小值不是未排序区的第一个元素,则交换
            if (minIndex != i) {
                swap(arr, i, minIndex);
            }
        }
    }

    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
📊 性能分析
  • 时间复杂度: 最好、最坏、平均情况均为 O(n^2)。因为它无论如何都需要双重循环遍历。

  • 空间复杂度: O(1),原地排序。

  • 稳定性: 不稳定 。例如数组 [5, 8, 5, 2, 9],在第一轮选择中,第一位的 5 会与 2 交换,从而破坏了两个 5 的相对顺序。

2. 堆排序(Heap Sort)------ 借树蜕变

🚀 简单选择排序的优化痛点

简单选择排序之所以慢,是因为它在每一轮比较中,并没有把比较的结果保存下来。下一轮找最小值时,很多元素又要重复比较一遍。

如果我们能用一种数据结构把比较过的大小关系"记下来",不就能省去大量无用功吗?二叉堆(Binary Heap)就是这个秘密武器!

💡 核心思想

堆排序是利用大顶堆(Max Heap)小顶堆(Min Heap)进行选择排序的算法:

  1. 构造初始堆: 将无序数组构造成一个大顶堆(所有父节点的值都大于或等于其左右孩子)。此时,堆顶(根节点)必然是整个数组的最大值

  2. 交换与调整: 将堆顶元素(最大值)与数组末尾元素交换。此时最大值已归位。

  3. 重建堆: 将剩余的 n-1 个元素重新调整为大顶堆,再次将堆顶与倒数第二个元素交换。如此反复,直到整个数组有序。

复制代码
   public class HeapSort {
       public static void sort(int[] arr) {
           if (arr == null || arr.length < 2) return;
           int n = arr.length;

           // 1. 构建初始大顶堆(从最后一个非叶子节点开始往前调整)
           for (int i = n / 2 - 1; i >= 0; i--) {
               heapify(arr, n, i);
           }

           // 2. 逐个将堆顶元素移到数组末尾,并重新调整堆
           for (int i = n - 1; i > 0; i--) {
               swap(arr, 0, i); // 将当前最大的堆顶换到末尾
               heapify(arr, i, 0); // 重新调整剩余的 i 个元素,使堆顶保持最大
           }
       }

       /**
        * 维护大顶堆性质的"下沉(Sift Down)"操作
        * @param arr 数组
        * @param heapSize 当前参与建堆的元素个数
        * @param root 当前需要下沉的根节点索引
        */
       private static void heapify(int[] arr, int heapSize, int root) {
           int largest = root; // 初始化最大值为根节点
           int left = 2 * root + 1;  // 左孩子索引
           int right = 2 * root + 2; // 右孩子索引

           // 如果左孩子比根节点大
           if (left < heapSize && arr[left] > arr[largest]) {
               largest = left;
           }

           // 如果右孩子比当前最大值还大
           if (right < heapSize && arr[right] > arr[largest]) {
               largest = right;
           }

           // 如果最大值不是根节点,说明需要"下沉"
           if (largest != root) {
               swap(arr, root, largest);
               // 递归调整受影响的子树
               heapify(arr, heapSize, largest);
           }
       }

       private static void swap(int[] arr, int i, int j) {
           int temp = arr[i];
           arr[i] = arr[j];
           arr[j] = temp;
       }
   }
📊 性能分析
  1. 时间复杂度: 构建初始堆耗时 O(n),执行 n 次交换和下沉调整耗时 O(n \log n)。因此最好、最坏、平均时间复杂度稳稳保持在 O(n \log n)

  2. 空间复杂度: O(1)。不需要额外辅助数组,直接在原数组的二叉树映射上操作。

  3. 稳定性: 不稳定。在下沉和交换过程中,长距离的跨越会打乱相同元素的相对位置。

相关推荐
琥珀色糖2 分钟前
leetcode hot100题(持续更新)移动零(双指针)
算法·leetcode·职场和发展·双指针·移动零
AndrewHZ1 小时前
【LLM技术全景】RAG 从原理到实战——检索增强生成完整指南
人工智能·深度学习·算法·llm·检索增强·生成式模型·rag
LuminousCPP1 小时前
单链表专题(三)-刷题复盘篇:从快慢指针到环形链表 II 数学推导
数据结构·经验分享·笔记·学习·算法·链表
-dzk-1 小时前
【贪心算法】LC 121.买卖股票的最佳时机
算法·贪心算法
hanlin031 小时前
动态规划专练:力扣第718、1143题
算法·leetcode·动态规划
ChaoZiLL1 小时前
二叉树经典例题
数据结构·算法
不正经学生1 小时前
C语言字符串函数进阶:安全版函数 + 错误处理 + 子串查找
c语言·开发语言·c++·算法·面试
三克的油2 小时前
算法基础-1
数据结构·c++·算法
oort1232 小时前
OortCodex 是奥尔特云 OortCloudSmart 推出的国产化工程化 AI 编码 Agent(奥尔特云编码智能体)
大数据·人工智能·算法
TheBestRucy2 小时前
基于Dify的旅游攻略&王者荣耀攻略智能助手项目
服务器·开发语言·人工智能·python·算法·旅游