【算法】排序

在这里插入图片描述

上期回顾: 【算法】查找
个人主页:GUIQU.
归属专栏:算法

在这里插入图片描述

正文

1. 冒泡排序

1.1 基本原理

冒泡排序是一种简单的排序算法,它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来,直到没有元素再需要交换,此时数列就已经排序完成。这个过程就像气泡从水底逐渐上浮,较大的元素会像气泡一样慢慢“浮”到数列的末尾。

例如,对于数组 [5, 3, 8, 2, 9],第一轮比较会依次比较相邻的元素 53,因为 5 > 3,所以交换它们,得到 [3, 5, 8, 2, 9];接着比较 58,无需交换;再比较 82,交换得到 [3, 5, 2, 8, 9],依次类推,经过第一轮后,最大的元素 9 就“浮”到了末尾。然后进行第二轮比较,此时只需比较前 n - 1 个元素(因为最后一个元素已经是最大的了),重复这个过程,直到整个数组有序。

1.2 代码实现(以升序排序为例)

#include <iostream>
using namespace std;

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 2, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, n);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

1.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:当数组是逆序排列时,需要进行 n - 1 轮比较,每轮比较 n - i - 1 次(i 为轮数),总共的比较次数接近 n(n - 1)/2,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    • 最好情况:当数组本身已经有序时,只需进行一轮比较,且没有元素交换,时间复杂度为 O ( n ) O(n) O(n),但这种情况相对较少出现。
    • 平均情况:平均时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为在大多数随机排列的情况下,都需要进行较多轮次的比较交换操作。
  • 空间复杂度:只需要常数级别的额外空间来交换元素等,空间复杂度为 O ( 1 ) O(1) O(1)

1.4 应用场景

适用于数据量较小且对排序效率要求不是特别高的场景,由于其实现简单易懂,常用于初学者学习排序算法或者在一些简单的数据处理场景中,比如对一个班级学生的少量成绩数据进行初步排序查看。

2. 选择排序

2.1 基本原理

选择排序的基本思想是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕。

例如,对于数组 [5, 3, 8, 2, 9],第一轮会遍历整个数组,找到最小的元素 2,将它与数组的第一个元素 5 交换,得到 [2, 3, 8, 5, 9];接着从剩下的元素 [3, 8, 5, 9] 中再找最小元素 3,由于它已经在第二个位置,无需交换;然后继续从剩余元素中找最小元素,重复这个过程,直到整个数组有序。

2.2 代码实现(以升序排序为例)

#include <iostream>
using namespace std;

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) {
            swap(arr[i], arr[minIndex]);
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 2, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    selectionSort(arr, n);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

2.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:无论数组初始顺序如何,都需要进行 n - 1 轮选择操作,每轮要遍历 n - i 个元素(i 为轮数)来找到最小元素,总共比较次数为 n(n - 1)/2,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    • 最好情况:即使数组已经有序,依然要进行完整的轮次比较,时间复杂度同样为 O ( n 2 ) O(n^2) O(n2),所以选择排序的时间复杂度不受初始数据顺序影响,相对稳定。
    • 平均情况:平均时间复杂度也是 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度:只需要常数级别的额外空间来记录最小元素的下标等信息,空间复杂度为 O ( 1 ) O(1) O(1)

2.4 应用场景

和冒泡排序类似,适用于数据量较小且对排序效率要求不是特别高的简单场景,尤其是在数据元素交换成本较高(比如每个元素是一个复杂的结构体,交换涉及较多数据复制等操作)时,选择排序相对较好,因为它交换元素的次数相对较少,主要是比较次数较多。

3. 插入排序

3.1 基本原理

插入排序的工作方式就像人们打牌时整理手中的牌一样。它将待排序的数据分为已排序和未排序两部分,初始时已排序部分只有一个元素(第一个元素),然后每次从未排序部分取出一个元素,将它插入到已排序部分的合适位置,使得已排序部分始终保持有序,直到所有元素都插入到已排序部分为止。

例如,对于数组 [5, 3, 8, 2, 9],初始已排序部分为 [5],然后从第二个元素 3 开始,将 3 与已排序部分的 5 比较,因为 3 < 5,所以将 3 插入到 5 的前面,得到 [3, 5];接着取第三个元素 8,与已排序部分的 35 依次比较,发现 8 大于 5,所以 8 放在 5 的后面,得到 [3, 5, 8],依此类推,直到整个数组有序。

3.2 代码实现(以升序排序为例)

#include <iostream>
using namespace std;

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

int main() {
    int arr[] = {5, 3, 8, 2, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    insertionSort(arr, n);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

3.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:当数组是逆序排列时,每次插入一个元素都要与已排序部分的所有元素比较并移动,总共需要比较和移动的次数接近 n(n - 1)/2,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    • 最好情况:当数组本身已经有序时,每次插入元素只需比较一次就能确定位置,总共只需 n - 1 次比较,时间复杂度为 O ( n ) O(n) O(n)
    • 平均情况:平均时间复杂度为 O ( n 2 ) O(n^2) O(n2),不过在部分有序的数据情况下,插入排序的效率会比其他简单排序算法(如冒泡排序、选择排序)高一些。
  • 空间复杂度:同样只需要常数级别的额外空间来临时存储待插入元素等,空间复杂度为 O ( 1 ) O(1) O(1)

3.4 应用场景

适用于数据量较小且基本有序的数据排序场景,在一些实时数据处理中,如果新插入的数据量相对较少且已有数据相对有序,插入排序能较快地完成排序任务,比如在一个已经基本排好序的日志文件中,偶尔有新的数据记录插入时,可以用插入排序来维护数据的顺序。

4. 希尔排序

4.1 基本原理

希尔排序是对插入排序的一种改进算法,它又称缩小增量排序。其基本思想是先将待排序的数组按照一定的增量分成若干个子序列,分别对这些子序列进行插入排序,随着增量逐渐减小(一般最后一趟增量为1,也就是对整个数组进行普通的插入排序),使得数组逐渐趋近于有序,最终完成排序。

例如,对于数组 [5, 3, 8, 2, 9],假设初始增量为 2,那么可以将数组分为两个子序列 [5, 8, 9][3, 2],分别对这两个子序列进行插入排序,得到 [5, 2, 8, 3, 9];然后减小增量,比如增量变为 1,再对整个数组进行插入排序,最终得到有序数组。不同的增量序列选择会影响希尔排序的性能,常见的增量序列有希尔本人提出的 n/2, n/4, n/8,..., 1n 为数组长度)等。

4.2 代码实现(以希尔本人提出的增量序列为例,升序排序)

#include <iostream>
using namespace std;

void shellSort(int arr[], int n) {
    for (int gap = n / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < n; i++) {
            int key = arr[i];
            int j = i;
            while (j >= gap && arr[j - gap] > key) {
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = key;
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 2, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    shellSort(arr, n);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

4.3 时间复杂度与空间复杂度

  • 时间复杂度:希尔排序的时间复杂度与所选择的增量序列有关,其平均时间复杂度介于 O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( n 2 ) O(n^2) O(n2) 之间,在较好的增量序列选择下,时间复杂度可以更接近 O ( n 1.3 ) O(n^{1.3}) O(n1.3),相较于普通的插入排序在效率上有一定提升,特别是对于中等规模的数据排序效果更明显。
  • 空间复杂度:和插入排序类似,只需要常数级别的额外空间来临时存储元素等,空间复杂度为 O ( 1 ) O(1) O(1)

4.4 应用场景

适用于中等规模的数据排序,当数据量不是特别大且希望在不使用复杂排序算法(如快速排序等)的情况下,获得比简单排序算法(冒泡、选择、插入)更好的排序效率时,可以选择希尔排序,比如在一些嵌入式系统或者对内存资源有限制的环境中,对一些规模适中的数据进行排序。

5. 快速排序

5.1 基本原理

快速排序采用了分治的思想,它首先选择一个基准元素(通常可以选择数组的第一个元素、最后一个元素或者中间元素等),然后通过一趟排序将待排序的数组分割成两部分,使得左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素,接着对这两部分分别递归地进行同样的快速排序操作,直到整个数组有序。

例如,对于数组 [5, 3, 8, 2, 9],选择第一个元素 5 作为基准元素,从数组的两端开始向中间扫描,先从右往左找第一个小于 5 的元素,找到 2,再从左往右找第一个大于 5 的元素,找到 8,然后交换 28,继续这个过程,直到左右指针相遇,此时将基准元素 5 与相遇位置的元素交换,得到 [2, 3, 5, 8, 9],这样就将数组分成了左边部分 [2, 3] 和右边部分 [8, 9],然后分别对这两部分递归排序。

5.2 代码实现(以选择第一个元素为基准元素,升序排序为例)

#include <iostream>
using namespace std;

int partition(int arr[], int low, int high) {
    int pivot = arr[low];
    int i = low + 1;
    int j = high;
    while (true) {
        while (i <= j && arr[i] <= pivot) {
            i++;
        }
        while (i <= j && arr[j] > pivot) {
            j--;
        }
        if (i > j) break;
        swap(arr[i], arr[j]);
    }
    swap(arr[low], arr[j]);
    return j;
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pivotIndex = partition(arr, low, high);
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

int main() {
    int arr[] = {5, 3, 8, 2, 9};
    int n = sizeof(arr) / sizeof(arr[0]);
    quickSort(arr, 0, n - 1);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

5.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:当每次选择的基准元素都是数组中最大或最小的元素时(比如数组本身已经有序或逆序),快速排序就会退化成类似冒泡排序的情况,需要进行 n - 1 轮划分,每轮划分都要遍历 n - i 个元素(i 为轮数),时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    • 最好情况:每次选择的基准元素都能将数组均匀地分成两部分,此时划分的层数为 O ( log ⁡ n ) O(\log n) O(logn),每一层划分需要遍历 n 个元素左右,时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)
    • 平均情况:平均时间复杂度也为 O ( n log ⁡ n ) O(n \log n) O(nlogn),在实际应用中,快速排序通常是一种高效的排序算法,特别是对于大规模的数据排序效果较好。
  • 空间复杂度:快速排序在递归调用过程中需要使用栈来存储函数调用信息等,在最坏情况下,递归深度为 n(也就是退化成 O ( n 2 ) O(n^2) O(n2) 时间复杂度的情况),空间复杂度为 O ( n ) O(n) O(n);在最好和平均情况下,递归深度为 O ( log ⁡ n ) O(\log n) O(logn),空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

5.4 应用场景

广泛应用于各种需要对大规模数据进行高效排序的场景,比如在数据库的查询结果排序、大数据分析中对数据的预处理排序等,由于其平均时间复杂度低,排序速度快,是很多排序场景中的首选算法之一,不过在一些对稳定性有要求的排序场景中(后面会介绍排序稳定性概念)不太适用,因为快速排序是不稳定的排序算法。

6. 归并排序

6.1 基本原理

归并排序同样基于分治思想,它将一个数组分成两个子数组,对每个子数组再分别进行分割,直到子数组的长度为1(也就是每个子数组只有一个元素,此时认为它已经有序),然后将这些有序的子数组两两合并,合并过程中保证合并后的数组依然有序,不断重复合并操作,最终得到整个有序的数组。

例如,对于数组 [5, 3, 8, 2, 9],首先将它分成 [5, 3][8, 2, 9] 两个子数组,再继续分割得到 [5][3][8][2][9]

这些长度为1的子数组都是有序的,接下来开始合并操作。先合并 [5][3],比较 53,得到有序的 [3, 5];再合并 [8][2],得到 [2, 8];然后合并 [2, 8][9],得到 [2, 8, 9];最后合并 [3, 5][2, 8, 9],通过依次比较元素大小,最终得到有序数组 [2, 3, 5, 8, 9]

6.2 代码实现(以升序排序为例)

#include <iostream>
#include <vector>

using namespace std;

// 合并两个已排序的子数组
void merge(vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    vector<int> L(n1), R(n2);

    // 拷贝数据到临时数组
    for (int i = 0; i < n1; i++) {
        L[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        R[j] = arr[mid + 1 + j];
    }

    int i = 0, j = 0, k = left;
    // 合并临时数组到原数组
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 拷贝剩余元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

// 归并排序主函数
void mergeSort(vector<int>& arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

int main() {
    vector<int> arr = {5, 3, 8, 2, 9};
    int n = arr.size();
    mergeSort(arr, 0, n - 1);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

6.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 归并排序不管输入数据是什么样的顺序,它每次都将数组分成两部分,然后不断合并,每次合并操作需要遍历所有待合并的元素,总共需要进行 O ( log ⁡ n ) O(\log n) O(logn) 层的合并(因为每次对半分,分的次数和以2为底 n n n 的对数相关),每层合并需要 O ( n ) O(n) O(n) 的时间复杂度,所以总的时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn),具有稳定的时间复杂度表现。
  • 空间复杂度
    • 在合并过程中需要借助额外的临时数组来存储子数组元素,临时数组的大小与原数组长度有关,所以空间复杂度为 O ( n ) O(n) O(n),相对来说空间消耗较大,但在一些对时间复杂度要求严格且内存允许的情况下,其稳定高效的排序特性依然使其应用广泛。

6.4 应用场景

常用于对稳定性有要求且数据规模较大的排序场景。例如在一些需要对文件内容进行排序,且排序后要保证相同元素的相对顺序不变的情况;或者在数据库中对数据进行排序整合,同时要求排序结果稳定可靠时,归并排序是一个很好的选择,因为它是一种稳定的排序算法(后面会详细解释排序稳定性概念),并且时间复杂度较为优秀。

7. 堆排序

7.1 基本原理

堆排序利用了堆这种数据结构的特性来进行排序。首先将待排序的数组构建成一个堆(可以是大顶堆或者小顶堆,以下以大顶堆为例,大顶堆满足每个节点的值都大于或等于它的子节点的值,根节点的值最大),然后将堆顶元素(也就是最大值)与堆的末尾元素交换,此时最大值就放到了数组的末尾,接着对剩下的 n - 1 个元素重新调整为大顶堆,重复这个交换和调整堆的过程,直到整个数组有序。

例如,对于数组 [5, 3, 8, 2, 9],先将其构建成大顶堆,得到 [9, 8, 5, 2, 3],然后交换堆顶元素 9 和末尾元素 3,数组变为 [3, 8, 5, 2, 9],接着对前 4 个元素 [3, 8, 5, 2] 重新调整为大顶堆,再交换堆顶和末尾元素,如此反复,最终实现数组有序。

7.2 代码实现(以升序排序为例,构建大顶堆及相关操作)

#include <iostream>
#include <vector>

using namespace std;

// 调整大顶堆,从节点i开始向下调整
void maxHeapify(vector<int>& arr, int i, int heapSize) {
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest = i;

    if (left < heapSize && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < heapSize && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest!= i) {
        swap(arr[i], arr[largest]);
        maxHeapify(arr, largest, heapSize);
    }
}

// 构建大顶堆
void buildMaxHeap(vector<int>& arr) {
    int n = arr.size();
    for (int i = n / 2 - 1; i >= 0; i--) {
        maxHeapify(arr, i, n);
    }
}

// 堆排序主函数
void heapSort(vector<int>& arr) {
    int n = arr.size();
    buildMaxHeap(arr);
    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        maxHeapify(arr, 0, i);
    }
}

int main() {
    vector<int> arr = {5, 3, 8, 2, 9};
    heapSort(arr);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

7.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 构建大顶堆的时间复杂度为 O ( n ) O(n) O(n),因为从最后一个非叶子节点开始依次向上调整堆,总共需要遍历的节点数和 n 呈线性关系。而每次调整堆(交换堆顶元素后重新调整)的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),一共需要进行 n - 1 次这样的调整,所以总的时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn),时间复杂度性能较为稳定,不受输入数据顺序的影响。
  • 空间复杂度
    • 堆排序只需要常数级别的额外空间来进行一些临时的交换和比较操作,空间复杂度为 O ( 1 ) O(1) O(1),在空间利用上比较高效,适合对空间要求较为严格的排序场景。

7.4 应用场景

在一些对空间复杂度要求低且需要稳定的 O ( n log ⁡ n ) O(n \log n) O(nlogn) 时间复杂度来排序的场景中应用广泛,比如在嵌入式系统等内存资源有限的环境中,要对一批数据进行高效排序时,堆排序是不错的选择;同时在一些需要对动态数据集合(不断有新数据加入或者旧数据删除)进行排序维护的场景中,利用堆排序基于堆结构的特性也能方便地进行操作。

8. 计数排序

8.1 基本原理

计数排序是一种非比较排序算法,它的基本思想是统计待排序数组中每个元素出现的次数,然后根据元素出现的次数将它们依次放回原数组,从而实现排序。这个算法要求待排序的元素必须是整数,并且其取值范围不能太大,最好是在一个已知的较小范围内,这样可以通过创建一个合适大小的计数数组来统计各元素出现次数。

例如,对于数组 [5, 3, 8, 2, 9, 3],假设元素的取值范围是 09,首先创建一个长度为 10 的计数数组 count,初始值都为 0,然后遍历原数组,每遇到一个元素,就在计数数组对应的位置上加 1,比如遇到 5,就将 count[5]1,遍历完原数组后,count 数组记录了每个元素出现的次数,接着按照计数数组中的次数信息,将元素依次放回原数组,就得到了有序数组。

8.2 代码实现(以升序排序为例,假设元素范围是 0kk 为已知整数)

#include <iostream>
#include <vector>

using namespace std;

void countingSort(vector<int>& arr, int k) {
    vector<int> count(k + 1, 0);
    int n = arr.size();
    vector<int> output(n);

    // 统计各元素出现次数
    for (int i = 0; i < n; i++) {
        count[arr[i]]++;
    }

    // 计算累计次数,用于确定元素在输出数组中的位置
    for (int i = 1; i <= k; i++) {
        count[i] += count[i - 1];
    }

    // 构建输出数组,实现排序
    for (int i = n - 1; i >= 0; i--) {
        output[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }

    // 拷贝回原数组
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}

int main() {
    vector<int> arr = {5, 3, 8, 2, 9, 3};
    int k = 9;
    countingSort(arr, k);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

8.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 计数排序需要遍历原数组一次来统计元素出现次数,时间复杂度为 O ( n ) O(n) O(n),然后遍历计数数组来确定元素在输出数组中的位置,时间复杂度为 O ( k ) O(k) O(k)k 为元素取值范围),最后将排序好的元素拷贝回原数组,时间复杂度为 O ( n ) O(n) O(n),所以总的时间复杂度为 O ( n + k ) O(n + k) O(n+k)。当 k 的值相对 n 来说是常数级别或者较小的时候,时间复杂度可以近似看作 O ( n ) O(n) O(n),效率很高。
  • 空间复杂度
    • 计数排序需要创建一个计数数组和一个输出数组,计数数组的大小取决于元素取值范围 k,输出数组的大小和原数组一样为 n,所以空间复杂度为 O ( n + k ) O(n + k) O(n+k),空间消耗相对较大,尤其是当 k 很大时。

8.4 应用场景

适用于元素取值范围较小且已知的整数排序场景,比如对学生成绩(通常成绩范围是 0100)进行排序,或者对一个月内每天的网站访问量(取值范围是有限的整数)进行排序等情况,能利用其线性时间复杂度的优势快速完成排序任务。

9. 桶排序

9.1 基本原理

桶排序的基本思想是将待排序的数据分到不同的桶里,每个桶可以看作是一个子区间,然后对每个桶内的数据分别进行排序(可以使用其他排序算法,比如插入排序等),最后将各个桶中的数据按照顺序依次取出,就得到了有序的数据集合。桶排序的关键在于合理地划分桶以及确定每个桶所对应的区间范围,理想情况下,每个桶内的数据分布比较均匀,这样后续的排序操作就会更高效。

例如,要对 01 之间均匀分布的一组浮点数进行排序,可以将这个区间平均分成 10 个桶,如 [0, 0.1)[0.1, 0.2) 等,然后将每个浮点数根据其值放入对应的桶中,接着对每个桶内的浮点数用插入排序等简单算法排序,最后依次取出各桶中的元素,就完成了排序。

9.2 代码实现(以下是一个简单的对 01 之间浮点数进行桶排序的示例,桶内使用插入排序)

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 桶内使用插入排序
void insertionSort(vector<double>& bucket) {
    int n = bucket.size();
    for (int i = 1; i < n; i++) {
        double key = bucket[i];
        int j = i - 1;
        while (j >= 0 && bucket[j] > key) {
            bucket[j + 1] = bucket[j];
            j--;
        }
        bucket[j + 1] = key;
    }
}

// 桶排序主函数
void bucketSort(vector<double>& arr) {
    int n = arr.size();
    vector<vector<double>> buckets(n);

    // 将元素分配到各个桶中
    for (double num : arr) {
        int index = num * n;
        buckets[index].push_back(num);
    }

    // 对每个桶内的数据进行排序
    for (vector<double>& bucket : buckets) {
        insertionSort(bucket);
    }

    // 依次取出桶中的元素放回原数组
    int k = 0;
    for (vector<double>& bucket : buckets) {
        for (double num : bucket) {
            arr[k++] = num;
        }
    }
}

int main() {
    vector<double> arr = {0.45, 0.23, 0.78, 0.12, 0.92};
    bucketSort(arr);
    for (double num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

9.3 时间复杂度与空间复杂度

  • 时间复杂度
    • 桶排序的时间复杂度取决于桶的数量 m 和每个桶内元素的平均数量 n/m(假设总共有 n 个元素)。将元素分配到桶中的时间复杂度为 O ( n ) O(n) O(n),对每个桶内排序的时间复杂度之和取决于所选用的桶内排序算法以及桶内元素数量情况,若桶内使用插入排序等平均时间复杂度为 O ( n 2 ) O(n^2) O(n2) 的算法,那么总的时间复杂度大致为 O ( n + m × ( n / m ) 2 ) = O ( n + n 2 / m ) O(n + m \times (n/m)^2) = O(n + n^2/m) O(n+m×(n/m)2)=O(n+n2/m)。当桶的数量 m 接近 n 且每个桶内元素数量比较均匀时,时间复杂度可以接近 O ( n ) O(n) O(n),效率很高,但如果桶划分不合理,比如所有元素都集中在少数几个桶内,时间复杂度可能会退化。
  • 空间复杂度
    • 桶排序需要创建若干个桶来存储数据,空间复杂度取决于桶的数量 m 和元素总数 n,大致为 O ( n + m ) O(n + m) O(n+m),同样如果桶数量过多或者每个桶内元素分布不均匀,空间消耗可能会较大。

9.4 应用场景

适用于数据分布比较均匀且可以划分成合适桶的场景,比如对一组年龄数据(大致均匀分布在某个区间内)进行排序,或者对一批考试成绩(假设成绩分布相对均匀)进行排序等情况,通过合理划分桶并结合简单的桶内排序算法,可以高效地完成排序任务。

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【Air】

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值