数据结构四大简单排序算法详解:直接插入排序、选择排序、基数排序和冒泡排序

一、引言

排序算法是计算机科学中最基础也是最重要的内容之一。在实际开发和面试中,四大简单排序算法(直接插入排序、选择排序、基数排序、冒泡排序)是必须掌握的基础知识。本文将全面详细地讲解这四种排序算法的原理、实现和适用场景。

二、直接插入排序

2.1 排序思路

直接插入排序的基本思想是:将待排序的元素插入到已经排好序的有序序列中,从而得到一个新的、记录数增加1的有序序列。

2.2 排序过程

  1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

  5. 将新元素插入到该位置后

  6. 重复步骤2~5

2.3 复杂度分析

  • 时间复杂度:最好情况:O(n) - 数组已经有序 最坏情况:O(n²) - 数组逆序 平均情况:O(n²)

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

  • 稳定性:稳定排序

2.4 排序过程图解

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

2.5 代码实现

复制代码
#include <stdio.h>
void insertSort(int arr[], int n) {
    int i, j, key;
    for (i = 1; i < n; i++) {
        key = arr[i];
        j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
}
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]);
    insertSort(arr, n);
    printArray(arr, n);
    return 0;
}

三、选择排序

3.1 排序思路

选择排序的基本思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

3.2 排序过程

  1. 初始状态:无序区为R[0..n-1],有序区为空

  2. 第i趟排序(i=0,1,2,...,n-2)开始时,当前有序区和无序区分别为R[0..i-1]和R[i..n-1]

  3. 该趟排序从当前无序区中选出关键字最小的记录R[k]

  4. 将R[k]与无序区的第1个记录R[i]交换

  5. 重复n-1趟,排序完成

3.3 复杂度分析

  • 时间复杂度:最好情况:O(n²) 最坏情况:O(n²) 平均情况:O(n²)

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

  • 稳定性:不稳定排序

3.4 排序过程图解

复制代码
初始序列:[64, 25, 12, 22, 11]
第1趟: [11, 25, 12, 22, 64]
第2趟: [11, 12, 25, 22, 64]
第3趟: [11, 12, 22, 25, 64]
第4趟: [11, 12, 22, 25, 64]

3.5 代码实现

复制代码
#include <stdio.h>
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
void selectSort(int arr[], int n) {
    int i, j, minIndex;
    for (i = 0; i < n - 1; i++) {
        minIndex = i;
        for (j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        swap(&arr[minIndex], &arr[i]);
    }
}
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    int arr[] = {64, 25, 12, 22, 11};
    int n = sizeof(arr) / sizeof(arr[0]);
    selectSort(arr, n);
    printArray(arr, n);
    return 0;
}

四、基数排序

4.1 排序思路

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。

4.2 排序过程

  1. 取得数组中的最大数,并取得位数

  2. 从最低位开始,依次进行一次稳定的排序(如计数排序)

  3. 从最低位排序一直到最高位排序完成以后,数组就变成一个有序序列

4.3 复杂度分析

  • 时间复杂度:O(d*(n+k)) - d为位数,k为基数范围

  • 空间复杂度:O(n+k) - 需要额外的计数数组和输出数组

  • 稳定性:稳定排序

4.4 排序过程图解

复制代码
初始序列:[170, 45, 75, 90, 2, 802, 24, 66]
按个位排序:[170, 90, 2, 802, 24, 45, 75, 66]
按十位排序:[2, 802, 24, 45, 66, 170, 75, 90]
按百位排序:[2, 24, 45, 66, 75, 90, 170, 802]

4.5 代码实现

复制代码
#include <stdio.h>
#include <stdlib.h>
int getMax(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}
void countSort(int arr[], int n, int exp) {
    int output[n];
    int count[10] = {0};
    for (int i = 0; i < n; i++) {
        count[(arr[i] / exp) % 10]++;
    }
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }
    for (int i = n - 1; i >= 0; i--) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}
void radixSort(int arr[], int n) {
    int max = getMax(arr, n);
    for (int exp = 1; max / exp > 0; exp *= 10) {
        countSort(arr, n, exp);
    }
}
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    int arr[] = {170, 45, 75, 90, 2, 802, 24, 66};
    int n = sizeof(arr) / sizeof(arr[0]);
    radixSort(arr, n);
    printArray(arr, n);
    return 0;
}

五、冒泡排序及其优化

5.1 基本冒泡排序思路

重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。

5.2 优化策略

  1. 设置标志位:如果某一趟排序中没有发生交换,说明已经有序

  2. 记录最后交换位置:下一趟排序只需比较到该位置

5.3 复杂度分析

  • 时间复杂度:最好情况:O(n) - 数组已经有序(优化后) 最坏情况:O(n²) - 数组逆序 平均情况:O(n²)

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

  • 稳定性:稳定排序

5.4 排序过程图解

复制代码
初始序列:[5, 3, 8, 6, 4]
第1趟: [3, 5, 6, 4, 8]
第2趟: [3, 5, 4, 6, 8]
第3趟: [3, 4, 5, 6, 8]
第4趟: [3, 4, 5, 6, 8](无交换,提前结束)

5.5 代码实现

复制代码
#include <stdio.h>
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
void bubbleSort(int arr[], int n) {
    int i, j;
    int swapped;
    for (i = 0; i < n - 1; i++) {
        swapped = 0;
        for (j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(&arr[j], &arr[j + 1]);
                swapped = 1;
            }
        }
        if (swapped == 0) {
            break;
        }
    }
}
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main() {
    int arr[] = {5, 3, 8, 6, 4};
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    printArray(arr, n);
    return 0;
}

六、四种排序算法的比较和选择

6.1 性能对比表

排序算法 平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
直接插入排序 O(n²) O(n) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
基数排序 O(d*(n+k)) O(d*(n+k)) O(d*(n+k)) O(n+k) 稳定
冒泡排序 O(n²) O(n) O(n²) O(1) 稳定

6.2 适用场景分析

直接插入排序适用场景:

数据量较小(n < 50)

数据基本有序

作为快速排序的补充(小区间优化)

选择排序适用场景:

数据量较小

对稳定性没有要求

交换次数最少的需求

基数排序适用场景:

数据为整数或字符串

数据范围不大

需要稳定排序且数据量较大

冒泡排序适用场景:

数据量很小

教学演示用途

对稳定性有要求的小数据量排序

七、实际应用代码示例

7.1 学生成绩排序(直接插入排序)

复制代码
#include <stdio.h>
typedef struct {
    int id;
    int score;
} Student;
void sortStudents(Student students[], int n) {
    int i, j;
    Student key;
    for (i = 1; i < n; i++) {
        key = students[i];
        j = i - 1;
        while (j >= 0 && students[j].score > key.score) {
            students[j + 1] = students[j];
            j = j - 1;
        }
        students[j + 1] = key;
    }
}
void printStudents(Student students[], int n) {
    for (int i = 0; i < n; i++) {
        printf("学号:%d 成绩:%d\n", students[i].id, students[i].score);
    }
}
int main() {
    Student students[] = {{1, 85}, {2, 92}, {3, 78}, {4, 90}, {5, 82}};
    int n = sizeof(students) / sizeof(students[0]);
    sortStudents(students, n);
    printStudents(students, n);
    return 0;
}

7.2 手机号排序(基数排序)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void phoneRadixSort(char phones[][12], int n) {
    char output[n][12];
    for (int digit = 10; digit >= 0; digit--) {
        int count[11] = {0};
        for (int i = 0; i < n; i++) {
            int index = (phones[i][digit] == '\0') ? 0 : (phones[i][digit] - '0' + 1);
            count[index]++;
        }
        for (int i = 1; i < 11; i++) {
            count[i] += count[i - 1];
        }
        for (int i = n - 1; i >= 0; i--) {
            int index = (phones[i][digit] == '\0') ? 0 : (phones[i][digit] - '0' + 1);
            strcpy(output[count[index] - 1], phones[i]);
            count[index]--;
        }
        for (int i = 0; i < n; i++) {
            strcpy(phones[i], output[i]);
        }
    }
}
void printPhones(char phones[][12], int n) {
    for (int i = 0; i < n; i++) {
        printf("%s\n", phones[i]);
    }
}
int main() {
    char phones[][12] = {"13812345678", "13987654321", "13711112222", "13655556666", "13599998888"};
    int n = sizeof(phones) / sizeof(phones[0]);
    phoneRadixSort(phones, n);
    printPhones(phones, n);
    return 0;
}

八、常见面试题

8.1 基础概念题

  1. 直接插入排序和冒泡排序在最好情况下的时间复杂度各是多少?

    直接插入排序最好情况O(n),冒泡排序最好情况O(n)

  2. 选择排序为什么是不稳定的?举例说明

    因为交换可能改变相同元素的相对位置,如[5, 5, 2]排序后第一个5可能到第二个5后面

  3. 基数排序适用于什么类型的数据?

    适用于整数、字符串等可以按位分割的数据类型

  4. 冒泡排序的优化方法有哪些?

    设置交换标志位、记录最后交换位置

8.2 代码实现题

  1. 手写直接插入排序算法

  2. 实现一个稳定的选择排序版本

  3. 用基数排序对字符串数组进行排序

  4. 优化冒泡排序,使其在最好情况下达到O(n)

8.3 综合应用题

  1. 如果数据基本有序,应该选择哪种排序算法?为什么?

    选择直接插入排序,因为其在数据基本有序时接近O(n)时间复杂度

  2. 如果需要排序的数据量很小(n<10),推荐使用哪种算法?

    推荐使用直接插入排序或冒泡排序

  3. 如何选择合适的排序算法?考虑哪些因素?

    数据量大小、数据分布情况、稳定性要求、空间限制等

  4. 解释为什么快速排序在实际应用中比这些简单排序更常用

    快速排序平均时间复杂度O(nlogn),对于大数据量更高效,且缓存友好

九、总结

四大简单排序算法是学习更复杂算法的基础,并且4种简单排序都是以双重for循环为核心,****每种算法都有其独特的适用场景:

直接插入排序:小数据量、基本有序数据

选择排序:小数据量、交换次数少的场景

基数排序:整数、字符串排序,稳定且高效

冒泡排序:教学演示、小数据量稳定排序

掌握这些基础排序算法不仅有助于理解算法设计思想,也为大家学习更高级的排序算法打下坚实基础。在实际应用中,应根据具体需求选择合适的排序算法。

相关推荐
·白小白2 小时前
力扣(LeetCode) ——118.杨辉三角(C++)
c++·算法·leetcode
CoovallyAIHub3 小时前
超越“识别”:下一代机器视觉如何破解具身智能落地难题?
深度学习·算法·计算机视觉
小白.cpp3 小时前
list链表容器
数据结构·链表·list
仰泳的熊猫3 小时前
LeetCode:207. 课程表
数据结构·c++·算法·leetcode
liu****3 小时前
19.map和set的封装
开发语言·数据结构·c++·算法
水冗水孚3 小时前
双指针算法在实际开发中的具体应用之代码Review文章字符串的片段分割
算法·leetcode
DuHz3 小时前
用于汽车雷达应用的步进频率PMCW波形——论文阅读
论文阅读·算法·汽车·信息与通信·信号处理·毫米波雷达
张晓~183399481213 小时前
碰一碰发抖音源码技术搭建部署方案
线性代数·算法·microsoft·矩阵·html5
weixin_448119943 小时前
Datawhale人工智能的数学基础 202510第3次作业
人工智能·算法