经典排序算法的比较与实现

C++ 实现

以下是常见排序算法的原理介绍:


1. 冒泡排序 (Bubble Sort)

  • 原理
    冒泡排序通过重复地遍历数组,比较相邻的元素并交换它们的位置,将较大的元素逐渐“冒泡”到数组的末尾。每一轮遍历都会将当前未排序部分的最大元素放到正确的位置。
  • 关键点
    • 每次遍历都会减少未排序部分的长度。
    • 如果某一轮遍历中没有发生交换,说明数组已经有序,可以提前终止。
  • C++实现
copy
void bubbleSort(vector<int>& nums) {
    for(int i=0; i<nums.size(); ++i) {
        for(int j=0; j<nums.size()-i-1; ++j) {
            if(nums[j] > nums[j+1]) {
                swap(nums[j], nums[j+1]);
            }
        }
    }
}

2. 选择排序 (Selection Sort)

  • 原理
    选择排序每次从未排序部分中选择最小(或最大)的元素,将其与未排序部分的第一个元素交换位置。通过不断缩小未排序部分的范围,最终完成排序。
  • 关键点
    • 每次选择最小元素,确保每次交换后,已排序部分增加一个元素。
    • 无论输入数据是否有序,时间复杂度始终为 O(n²)。
  • C++实现
copy
void selectionSort(vector<int>& nums) {
    for(int i=0;i<nums.size();++i) {
        int minIndex = i;
        for(int j=i+1;j<nums.size();++j) {
            if(nums[j] < nums[minIndex]) {
                minIndex = j;
            }
        }
        swap(nums[i], nums[minIndex]);
    }
}

3. 插入排序 (Insertion Sort)

  • 原理
    插入排序将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,将其插入到已排序部分的正确位置。通过不断扩展已排序部分,最终完成排序。
  • 关键点
    • 对于几乎有序的数组,插入排序的效率非常高,时间复杂度接近 O(n)。
    • 适合小规模数据或部分有序的数据。

4. 快速排序 (Quick Sort)

  • 原理
    快速排序是一种分治算法。它选择一个“基准元素”(pivot),将数组分为两部分:一部分小于基准元素,另一部分大于基准元素。然后递归地对这两部分进行排序。
  • 关键点
    • 基准元素的选择对性能影响很大(通常选择第一个、最后一个或中间元素)。
    • 平均时间复杂度为 O(n log n),但最坏情况下(如数组已经有序)会退化为 O(n²)。
    • 是一种原地排序算法。

5. 归并排序 (Merge Sort)

  • 原理
    归并排序也是一种分治算法。它将数组分成两半,分别对每一半进行排序,然后将两个有序的子数组合并成一个有序的数组。
  • 关键点
    • 需要额外的空间来存储临时数组,空间复杂度为 O(n)。
    • 时间复杂度始终为 O(n log n),适合大规模数据。
    • 是一种稳定的排序算法。

6. 堆排序 (Heap Sort)

  • 原理
    堆排序利用堆(一种完全二叉树)的性质进行排序。首先将数组构建成一个最大堆(或最小堆),然后依次将堆顶元素(最大值或最小值)与堆的最后一个元素交换,并调整堆,直到堆为空。
  • 关键点
    • 时间复杂度为 O(n log n),适合大规模数据。
    • 是一种原地排序算法,但不稳定。
  • C++代码
copy
void heapify(vector<int>& nums, int start, int end) {
    int dad = start;
    int son = 2 * dad + 1;
    while(son <= end) {
        if(son + 1 <= end && nums[son+1] > nums[son]) {
            son++;
        }
        if(nums[son] < nums[dad]) {
            return;
        }else {
            swap(nums[son], nums[dad]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}
void heapSort(vector<int>& nums) {
    // 从最后一个非叶子节点开始, 从右向左, 从下向上进行建堆
    for(int i = nums.size() / 2 - 1; i >= 0; --i) {
        heapify(nums, i, nums.size()-1);
    }
    // 依次将堆顶元素与最后一个元素交换,并重新建堆
    for(int i=nums.size()-1; i >= 0; ++i) {
        swap(nums[0], nums[i]);
        heapify(nums, 0, i-1);
    }
}

7. 希尔排序 (Shell Sort)

  • 原理
    希尔排序是插入排序的改进版本。它通过将数组分成多个子序列(间隔为 gap),对每个子序列进行插入排序,然后逐渐缩小 gap,直到 gap 为 1,最终对整个数组进行一次插入排序。
  • 关键点
    • 时间复杂度取决于 gap 序列的选择,通常为 O(n log n) 到 O(n²)。
    • 是一种原地排序算法,但不稳定。

8. 计数排序 (Counting Sort)

  • 原理
    计数排序是一种非比较排序算法。它通过统计每个元素的出现次数,然后根据统计结果将元素放回正确的位置。
  • 关键点
    • 适用于元素范围较小的整数数组。
    • 时间复杂度为 O(n + k),其中 k 是元素的范围。
    • 需要额外的空间来存储计数数组和输出数组。

9. 基数排序 (Radix Sort)

  • 原理
    基数排序是一种非比较排序算法。它将整数按位数切割成不同的数字,然后从最低位到最高位依次进行排序(通常使用计数排序作为子排序算法)。
  • 关键点
    • 适用于整数或字符串的排序。
    • 时间复杂度为 O(nk),其中 k 是最大数字的位数。
    • 需要额外的空间来存储中间结果。

10. 桶排序 (Bucket Sort)

  • 原理
    桶排序将数组分到有限数量的桶中,每个桶内的元素单独排序(通常使用插入排序),然后将所有桶中的元素按顺序合并。
  • 关键点
    • 适用于元素分布均匀的数组。
    • 时间复杂度为 O(n + k),其中 k 是桶的数量。
    • 需要额外的空间来存储桶。

11. 猴子排序 (Bogo Sort)

  • 原理
    猴子排序是一种基于随机化的排序算法。它随机打乱数组,然后检查是否有序。如果未排序,则重复打乱和检查的过程,直到数组有序。
  • 关键点
    • 时间复杂度为 O(n × n!),最坏情况下可能永远无法完成排序。
    • 仅用于教学或娱乐目的,无实际应用价值。

总结

  • 基于比较的排序(如冒泡、选择、插入、快速、归并、堆排序)适用于通用场景。
  • 非比较排序(如计数、基数、桶排序)适用于特定类型的数据(如整数或浮点数)。
  • 猴子排序是一种极端低效的算法,仅用于教学或娱乐。

根据数据规模、数据类型和性能需求,可以选择合适的排序算法。

性能比较

在比较排序算法之前,我们需要先了解一些关键的判断因素,这些因素可以帮助我们评估和选择适合特定场景的排序算法。以下是常见的判断因素及其含义:

1. 时间复杂度 (Time Complexity)

时间复杂度描述了算法在最坏情况、平均情况和最好情况下执行所需的时间与输入规模(通常是数组的长度)之间的关系。常见的时间复杂度表示法有:

  • O(1): 常数时间,算法的执行时间不随输入规模变化。
  • O(log n): 对数时间,算法的执行时间随输入规模的对数增长。
  • O(n): 线性时间,算法的执行时间与输入规模成正比。
  • O(n log n): 线性对数时间,算法的执行时间随输入规模的对数线性增长。
  • O(n²): 平方时间,算法的执行时间随输入规模的平方增长。
  • O(n!): 阶乘时间,算法的执行时间随输入规模的阶乘增长。

2. 空间复杂度 (Space Complexity)

空间复杂度描述了算法在执行过程中所需的额外空间与输入规模之间的关系。常见的空间复杂度表示法有:

  • O(1): 常数空间,算法所需的额外空间不随输入规模变化。
  • O(n): 线性空间,算法所需的额外空间与输入规模成正比。
  • O(log n): 对数空间,算法所需的额外空间随输入规模的对数增长。

3. 稳定性 (Stability)

稳定性描述了排序算法在排序过程中是否保持相等元素的相对顺序。如果一个排序算法是稳定的,那么相等的元素在排序后的顺序与它们在排序前的顺序相同。稳定性在某些应用场景中非常重要,例如在对多个字段进行排序时。

4. 适应性 (Adaptability)

适应性描述了排序算法在输入数据已经部分有序时是否能够利用这一点来提高效率。一些排序算法在输入数据已经部分有序时表现更好。

5. 原地排序 (In-place Sorting)

原地排序算法是指在排序过程中不需要额外的存储空间(或只需要常数级别的额外空间)的排序算法。原地排序算法的空间复杂度通常是O(1)。

6. 比较排序与非比较排序

  • 比较排序: 通过比较元素的大小来决定它们的顺序。大多数常见的排序算法(如快速排序、归并排序、堆排序等)都是比较排序。
  • 非比较排序: 不通过比较元素的大小来决定它们的顺序,而是利用其他方法(如计数、哈希等)来排序。常见的非比较排序算法有计数排序、基数排序和桶排序。

排序算法比较

以下是常见排序算法的比较:

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 原地排序 适应性
冒泡排序 O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(1) 不稳定
插入排序 O(n²) O(n²) O(1) 稳定
快速排序 O(n log n) O(n²) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n) 稳定
堆排序 O(n log n) O(n log n) O(1) 不稳定
希尔排序 O(n log n) O(n²) O(1) 不稳定
计数排序 O(n + k) O(n + k) O(k) 稳定
基数排序 O(nk) O(nk) O(n + k) 稳定
桶排序 O(n + k) O(n²) O(n + k) 稳定
猴子排序 O(n × n!) O(∞) O(1) 不稳定

总结

  • 冒泡排序选择排序插入排序适用于小规模数据,但时间复杂度较高。
  • 快速排序归并排序适用于大规模数据,平均时间复杂度较低。
  • 堆排序适用于需要原地排序且时间复杂度较低的场景。
  • 计数排序基数排序桶排序适用于特定类型的数据(如整数或浮点数),且时间复杂度较低。
  • 猴子排序由于其极低效率,仅用于教学或娱乐目的。

根据具体的应用场景和需求,可以选择合适的排序算法。

posted @   RunTimeErrors  阅读(12)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
🚀