【数据结构与算法】第34篇:选择排序:简单选择排序与堆排序

一、简单选择排序

1.1 算法思想

每次从待排序序列中选出最小元素,放到已排序序列的末尾。

步骤

  1. [0, n-1]范围找最小值,与arr[0]交换

  2. [1, n-1]范围找最小值,与arr[1]交换

  3. 重复,直到所有元素有序

1.2 图解示例

text

复制代码
初始: [5, 2, 4, 6, 1, 3]

第1趟:找最小值1,与5交换 → [1, 2, 4, 6, 5, 3]
第2趟:找最小值2(已在位)→ [1, 2, 4, 6, 5, 3]
第3趟:找最小值3,与4交换 → [1, 2, 3, 6, 5, 4]
第4趟:找最小值4,与6交换 → [1, 2, 3, 4, 5, 6]
第5趟:找最小值5(已在位)→ [1, 2, 3, 4, 5, 6]

1.3 代码实现

c

复制代码
#include <stdio.h>

void selectionSort(int arr[], int n) {
    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) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {5, 2, 4, 6, 1, 3};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("原数组: ");
    printArray(arr, n);
    
    selectionSort(arr, n);
    
    printf("排序后: ");
    printArray(arr, n);
    
    return 0;
}

运行结果:

text

复制代码
原数组: 5 2 4 6 1 3 
排序后: 1 2 3 4 5 6 

1.4 复杂度分析

情况 比较次数 交换次数 时间复杂度
最好 n(n-1)/2 0 O(n²)
最坏 n(n-1)/2 n-1 O(n²)
平均 n(n-1)/2 n-1 O(n²)

空间复杂度 :O(1)
稳定性:不稳定(交换可能打乱相等元素的顺序)


二、堆排序

2.1 堆的概念

堆是一棵完全二叉树,分为:

  • 大根堆:每个节点的值 ≥ 左右孩子节点的值

  • 小根堆:每个节点的值 ≤ 左右孩子节点的值

数组存储:下标i的节点

  • 左孩子:2*i + 1

  • 右孩子:2*i + 2

  • 父节点:(i - 1) / 2

2.2 核心操作:向下调整(Heapify)

作用:当只有根节点不满足堆性质时,将其向下调整到合适位置。

步骤

  1. 找到当前节点和左右孩子中的最大值

  2. 如果最大值不是当前节点,交换,继续向下调整

c

复制代码
// 向下调整:将i位置的元素向下调整,使以i为根的子树满足大根堆
void heapify(int arr[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    
    if (largest != i) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        heapify(arr, n, largest);  // 递归调整
    }
}

2.3 建堆

从最后一个非叶子节点开始,从下往上执行向下调整。

c

复制代码
void buildHeap(int arr[], int n) {
    // 最后一个非叶子节点的下标 = (n/2) - 1
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
}

建堆过程图解 (数组 [4, 6, 3, 5, 2, 1]):

text

复制代码
初始完全二叉树:
        4
       / \
      6   3
     / \ /
    5  2 1

从下标2(值3)开始调整 → 不变
下标1(值6):左右孩子5、2,6最大 → 不变
下标0(值4):左右孩子6、3,6最大 → 交换4和6

        6
       / \
      4   3
     / \ /
    5  2 1

继续调整下标1(值4):左右孩子5、2,5最大 → 交换4和5

        6
       / \
      5   3
     / \ /
    4  2 1

建堆完成

2.4 堆排序流程

升序排序用大根堆

  1. 建大根堆

  2. 将堆顶(最大值)与最后一个元素交换

  3. 堆大小减1,对新的堆顶执行向下调整

  4. 重复2-3,直到堆大小为1

c

复制代码
void heapSort(int arr[], int n) {
    // 1. 建大根堆
    buildHeap(arr, n);
    
    // 2. 反复取出堆顶
    for (int i = n - 1; i > 0; i--) {
        // 交换堆顶和末尾
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        
        // 调整剩余部分
        heapify(arr, i, 0);
    }
}

为什么升序建大堆:每次把最大值放到末尾,最后得到升序序列。


三、完整代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 向下调整(大根堆)
void heapify(int arr[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    
    if (largest != i) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        heapify(arr, n, largest);
    }
}

// 建堆
void buildHeap(int arr[], int n) {
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
}

// 堆排序(升序)
void heapSort(int arr[], int n) {
    buildHeap(arr, n);
    
    for (int i = n - 1; i > 0; i--) {
        // 交换堆顶和末尾
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        
        // 调整剩余部分
        heapify(arr, i, 0);
    }
}

// 简单选择排序
void selectionSort(int arr[], int n) {
    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) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

// 打印数组
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 生成随机数组
void generateRandomArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        arr[i] = rand() % 10000;
    }
}

// 复制数组
void copyArray(int src[], int dst[], int n) {
    for (int i = 0; i < n; i++) {
        dst[i] = src[i];
    }
}

int main() {
    srand(time(NULL));
    
    // 基本测试
    printf("=== 基本测试 ===\n");
    int arr1[] = {4, 6, 3, 5, 2, 1};
    int n1 = sizeof(arr1) / sizeof(arr1[0]);
    printf("原数组: ");
    printArray(arr1, n1);
    
    heapSort(arr1, n1);
    printf("堆排序后: ");
    printArray(arr1, n1);
    
    // 简单选择排序测试
    int arr2[] = {5, 2, 4, 6, 1, 3};
    int n2 = sizeof(arr2) / sizeof(arr2[0]);
    selectionSort(arr2, n2);
    printf("\n简单选择排序后: ");
    printArray(arr2, n2);
    
    // 性能对比
    printf("\n=== 性能对比 ===\n");
    int sizes[] = {1000, 5000, 10000};
    int nTests = sizeof(sizes) / sizeof(sizes[0]);
    
    for (int t = 0; t < nTests; t++) {
        int n = sizes[t];
        int *arr = (int*)malloc(n * sizeof(int));
        int *arr2 = (int*)malloc(n * sizeof(int));
        
        generateRandomArray(arr, n);
        copyArray(arr, arr2, n);
        
        clock_t start, end;
        double time1, time2;
        
        start = clock();
        selectionSort(arr, n);
        end = clock();
        time1 = (double)(end - start) / CLOCKS_PER_SEC * 1000;
        
        start = clock();
        heapSort(arr2, n);
        end = clock();
        time2 = (double)(end - start) / CLOCKS_PER_SEC * 1000;
        
        printf("n=%d:\n", n);
        printf("  简单选择排序: %.2f ms\n", time1);
        printf("  堆排序:       %.2f ms\n", time2);
        printf("  堆排序是简单选择的 %.2f 倍\n\n", time1 / time2);
        
        free(arr);
        free(arr2);
    }
    
    return 0;
}

运行结果:

text

复制代码
=== 基本测试 ===
原数组: 4 6 3 5 2 1 
堆排序后: 1 2 3 4 5 6 

简单选择排序后: 1 2 3 4 5 6 

=== 性能对比 ===
n=1000:
  简单选择排序: 2.35 ms
  堆排序:       0.18 ms
  堆排序是简单选择的 13.06 倍

n=5000:
  简单选择排序: 57.82 ms
  堆排序:       1.05 ms
  堆排序是简单选择的 55.07 倍

n=10000:
  简单选择排序: 230.45 ms
  堆排序:       2.31 ms
  堆排序是简单选择的 99.76 倍

四、堆排序的详细过程演示

c

复制代码
// 打印堆排序每一步
void heapSortWithSteps(int arr[], int n) {
    printf("原数组: ");
    printArray(arr, n);
    
    buildHeap(arr, n);
    printf("建堆后: ");
    printArray(arr, n);
    
    for (int i = n - 1; i > 0; i--) {
        printf("交换 arr[0]=%d 和 arr[%d]=%d\n", arr[0], i, arr[i]);
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        
        heapify(arr, i, 0);
        printf("调整后: ");
        printArray(arr, n);
    }
}

int main() {
    int arr[] = {4, 6, 3, 5, 2, 1};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    heapSortWithSteps(arr, n);
    
    return 0;
}

运行结果:

text

复制代码
原数组: 4 6 3 5 2 1 
建堆后: 6 5 3 4 2 1 
交换 arr[0]=6 和 arr[5]=1
调整后: 5 4 3 1 2 6 
交换 arr[0]=5 和 arr[4]=2
调整后: 4 2 3 1 5 6 
交换 arr[0]=4 和 arr[3]=1
调整后: 3 2 1 4 5 6 
交换 arr[0]=3 和 arr[2]=1
调整后: 2 1 3 4 5 6 
交换 arr[0]=2 和 arr[1]=1
调整后: 1 2 3 4 5 6 

五、两种选择排序对比

对比项 简单选择排序 堆排序
时间复杂度 O(n²) O(n log n)
空间复杂度 O(1) O(1)
稳定性 不稳定 不稳定
是否原地
实现难度 简单 较难
适用场景 小规模数据 大规模数据

六、堆排序的特点

优点 缺点
时间复杂度稳定 O(n log n) 不稳定排序
空间复杂度 O(1) 常数因子较大
适合大数据量 对缓存不友好
能处理数据流(TopK问题) 实现较复杂

七、小结

这一篇我们学习了两种选择排序:

算法 核心思想 时间复杂度 特点
简单选择排序 每次选最小值 O(n²) 简单但慢
堆排序 用堆结构选最大值 O(n log n) 高效稳定

堆排序的关键

  • 大根堆:每个节点 ≥ 孩子节点

  • 建堆:从最后一个非叶子节点开始向下调整

  • 升序用大根堆:每次把堆顶(最大值)放到末尾

向下调整的递归/非递归

c

复制代码
// 非递归版本(避免递归深度)
void heapifyIter(int arr[], int n, int i) {
    while (1) {
        int largest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        
        if (left < n && arr[left] > arr[largest]) largest = left;
        if (right < n && arr[right] > arr[largest]) largest = right;
        
        if (largest == i) break;
        
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        i = largest;
    }
}

下一篇我们讲归并排序与基数排序。


八、思考题

  1. 简单选择排序和冒泡排序相比,哪个交换次数更少?为什么?

  2. 为什么升序排序要用大根堆而不是小根堆?

  3. 堆排序中,建堆的时间复杂度是多少?为什么?

  4. 如何在O(n log k)的时间内找到数组中最小的k个元素?

欢迎在评论区讨论你的答案。

相关推荐
.柒宇.1 分钟前
FastAPI进阶教程
开发语言·python·fastapi
迷途之人不知返3 分钟前
List的模拟实现
数据结构·c++·学习·list
JQLvopkk6 分钟前
C# 工业级上位机:交互实战
开发语言·c#·交互
jimy116 分钟前
C语言中的 “size_t ”类型
c语言·开发语言
techdashen18 分钟前
Cloudflare 如何用 Rust 构建一个高性能解释器
开发语言·后端·rust
无敌秋26 分钟前
C++ 抽象工厂模式实战指南
开发语言·c++·抽象工厂模式
Chat_zhanggong34533 分钟前
主推NT98336BG作用有哪些?
嵌入式硬件·算法
小书房34 分钟前
Kotlin使用体验及理解1
android·开发语言·kotlin
CoderMeijun40 分钟前
C++ 智能指针:auto_ptr
c++·内存管理·智能指针·raii·auto_ptr
勤劳的进取家43 分钟前
传输层基础
运维·开发语言·学习·php