#Z1ZMOJ0003. 排序算法

排序算法

当前没有测试数据。

排序算法

验收要求:点击下面的题目完成程序,15分钟内完成一个排序的实现,需要验收排序的思路和时间复杂度

计数排序(countSort)

1、 需要额外的空间来存储计数器
2、 根据元素的值直接定位其在排序后数组中的位置
3、 稳定的排序
4、 适用于数据范围不大的整数排序

实现思想:统计每个元素出现的次数,存储在计数器数组中。根据计数器数组重新排列所有元素,将所有元素放在其正确的位置上。

// 计数排序
void count_sort(vector<int>& arr,int n){
    //求数组中的最大值:max_element
    int max_v = *max_element(arr.begin(),arr.end());
    vector<int> count(max_v+1,0); //计数数组
    vector<int> output(n); //输出数组
    // 统计每一个元素输出的数量
    for(int i=0;i<n;i++){
        count[arr[i]]++;
    }
    // 累积计数确定每个元素的位置
    for(int i=1;i<=max_v;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];
    }

}

实现过程: 假设我们有一个待排序的数组 A

A = [4, 2, 2, 8, 3, 3, 1]

1. 找到数组中的最大值

我们需要找到数组 A 中的最大值,以确定计数数组的长度。在这个例子中,最大值是 8

2. 创建计数数组

创建一个长度为 9(最大值 8 + 1)的计数数组 count,并初始化为全零:

count = [0, 0, 0, 0, 0, 0, 0, 0, 0]

3. 统计元素出现次数

遍历输入数组 A,统计每个元素出现的次数并存储在 count 数组中:

A = [4, 2, 2, 8, 3, 3, 1]

count[4] += 1  -> count = [0, 0, 0, 0, 1, 0, 0, 0, 0]
count[2] += 1  -> count = [0, 0, 1, 0, 1, 0, 0, 0, 0]
count[2] += 1  -> count = [0, 0, 2, 0, 1, 0, 0, 0, 0]
count[8] += 1  -> count = [0, 0, 2, 0, 1, 0, 0, 0, 1]
count[3] += 1  -> count = [0, 0, 2, 1, 1, 0, 0, 0, 1]
count[3] += 1  -> count = [0, 0, 2, 2, 1, 0, 0, 0, 1]
count[1] += 1  -> count = [0, 1, 2, 2, 1, 0, 0, 0, 1]

4. 计算累计计数

count 数组转换为累计计数数组。每个位置 i 的值表示小于或等于 i 的元素个数:

count = [0, 1, 3, 5, 6, 6, 6, 6, 7]

5. 生成排序后的数组

创建一个输出数组 B,长度与输入数组 A 相同,初始化为全零。然后反向遍历输入数组 A,根据累计计数数组 count 确定每个元素在输出数组 B 中的位置,并在 count 中递减相应位置的计数:

B = [0, 0, 0, 0, 0, 0, 0]

遍历 A = [4, 2, 2, 8, 3, 3, 1]:

A[6] = 1  ->  count[1] -= 1  ->  count = [0, 0, 3, 5, 6, 6, 6, 6, 7]
              B[0] = 1       ->  B = [1, 0, 0, 0, 0, 0, 0]

A[5] = 3  ->  count[3] -= 1  ->  count = [0, 0, 3, 4, 6, 6, 6, 6, 7]
              B[4] = 3       ->  B = [1, 0, 0, 0, 3, 0, 0]

A[4] = 3  ->  count[3] -= 1  ->  count = [0, 0, 3, 3, 6, 6, 6, 6, 7]
              B[3] = 3       ->  B = [1, 0, 0, 3, 3, 0, 0]

A[3] = 8  ->  count[8] -= 1  ->  count = [0, 0, 3, 3, 6, 6, 6, 6, 6]
              B[6] = 8       ->  B = [1, 0, 0, 3, 3, 0, 8]

A[2] = 2  ->  count[2] -= 1  ->  count = [0, 0, 2, 3, 6, 6, 6, 6, 6]
              B[2] = 2       ->  B = [1, 0, 2, 3, 3, 0, 8]

A[1] = 2  ->  count[2] -= 1  ->  count = [0, 0, 1, 3, 6, 6, 6, 6, 6]
              B[1] = 2       ->  B = [1, 2, 2, 3, 3, 0, 8]

A[0] = 4  ->  count[4] -= 1  ->  count = [0, 0, 1, 3, 5, 6, 6, 6, 6]
              B[5] = 4       ->  B = [1, 2, 2, 3, 3, 4, 8]

最终得到排序后的数组 B

B = [1, 2, 2, 3, 3, 4, 8]

时间复杂度分析

  1. 统计元素出现次数:时间复杂度 O(n),遍历输入数组一次。
  2. 计算累计计数:时间复杂度 O(k),遍历计数数组一次。
  3. 生成排序后的数组:时间复杂度 O(n),反向遍历输入数组一次,并填充输出数组。

综合起来,计数排序的总时间复杂度为 O(n + k),其中 n 是输入数组的长度,k 是输入数组中最大元素的值。

基数排序(radixSort)

基数排序:是一种专门为处理数字序列而设计的排序算法。它的灵感来源于人类对数字的处理方式,类似于将数字按照位数逐位进行排序,实现了对整数的高效排序。

稳定性: 基数排序是一种稳定的排序算法,保持相等元素的相对顺序。

适应性: 对于位数相同的数字序列,基数排序表现出良好的适应性,排序效率较高。

非比较性: 基数排序不涉及元素之间的比较操作,减少了算法的复杂度,对于一些特定范围的数字排序更为高效。

基本原理: 基数排序的核心思想是将整数按照位数逐个进行排序,最终形成有序序列。具体步骤包括:

1 按位分配 从低位到高位,依次按照每一位的数值进行分配,形成桶。

2 按位收集 将桶中的元素按照顺序收集,形成新的序列。

3 迭代重复 重复以上两步,直到整个数字序列有序。

void radixSort(){
    // 1、求数组中最大值的位数
    int max_bit = to_string(*max_element(data+1,data+1+n)).length();

    // 2、创建临时数组
    int* temp = new int[n+1];
    // 3、创建计数器数组
    int* count = new int[10]; // 0 ~ 9

    // 进行最大位次排序
    int radix = 1; //表示个位
    for(int i=1;i<=max_bit;i++){
        for(int j=0;j<10;j++) count[j] = 0; // 每次分配前清空计数器
        for(int j=1;j<=n;j++){
            // 求当前在哪个计数器位置
            int k = (data[j] / radix) % 10;
            count[k]++;
        }
        for(int j=1;j<10;j++) count[j] += count[j-1];
        for(int j=n;j>=1;j--){
            // 将每一个数,按照顺序放入临时数组
            int k = (data[j] / radix) % 10;
            temp[count[k]]= data[j];
            count[k]--;
        }

        // 将临时数组取出
        for(int j=1;j<=n;j++){
            data[j] = temp[j];
        }

        // 将位数*10,开始下一位的排序
        radix *= 10;
    }

    delete[] temp;
    delete[] count;
}

假设要排序下列数字:2 13 8 3 28

1、先求出最大值的位数:28 2位

先按个位计数排序:

依次取出桶内的数, 得到新数组:2 3 8 13 28

归并排序

1、 将数组不断二分,直到每部分只剩一个元素
2、 将两个有序部分合并为一个有序部分
3、 稳定的排序
4、 平均时间复杂度为 O(n log⁡n)
5、 需要额外的存储空间

实现思想:将数组从中间分成两部分,分别对每部分进行归并排序。在对每部分排序后,将两个有序部分合并为一个有序数组。

核心代码:

// 归并排序函数,排序数组arr从索引l到r的部分
void mergeSort(int arr[], int l, int r) {
    // 如果左索引大于或等于右索引,说明数组已经不可分,返回
    if (l >= r) return;

    // 找到中间索引
    int mid = (l + r) / 2;

    // 递归地排序左半部分
    mergeSort(arr, l, mid);

    // 递归地排序右半部分
    mergeSort(arr, mid + 1, r);

    // 创建临时数组,用于存储合并后的结果
    int i = l, j = mid + 1, k = 0;
    int* temp = new int[r - l + 1];

    // 合并两个有序的子数组
    while (i <= mid && j <= r) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];  // 将左子数组的当前元素复制到temp
        } else {
            temp[k++] = arr[j++];  // 将右子数组的当前元素复制到temp
        }
    }

    // 如果左子数组还有剩余元素,继续复制到temp
    while (i <= mid) temp[k++] = arr[i++];

    // 如果右子数组还有剩余元素,继续复制到temp
    while (j <= r) temp[k++] = arr[j++];

    // 将合并后的结果复制回原数组的对应位置
    memcpy(arr + l, temp, sizeof(int) * (r - l + 1));

    // 释放临时数组的内存
    delete[] temp;
}

归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)的稳定排序算法。它通过将大问题分解为更小的子问题,递归地解决这些子问题,然后将它们合并成一个有序的解决方案。归并排序的时间复杂度稳定且较好,因此广泛应用于排序问题。

归并排序的步骤

  1. 分解(Divide)

    • 将数组分解为两个子数组,直到每个子数组只有一个元素(即,子数组的大小为 1)。
  2. 合并(Merge)

    • 递归地合并这些子数组,将两个已排序的子数组合并成一个有序的数组。

归并排序的时间复杂度

归并排序的时间复杂度主要由两个部分组成:分解和合并。

1. 分解(Divide)

在每一步,将数组分解为两个子数组。分解操作的时间复杂度为 (O(log n)),因为每次分解都会将数组的大小减少一半,递归深度为 (log n)。

2. 合并(Merge)

在每一层递归中,需要将所有的子数组合并。每次合并的操作时间复杂度为 (O(n)),因为每个元素都需要被比较和合并一次。对于每一层递归,需要进行 (O(n)) 次操作。

总时间复杂度

由于递归深度为 (log n),每一层的合并操作时间复杂度为 (O(n))。因此,总的时间复杂度为: [ O(n log n) ]

分析

假设有一个数组:[38, 27, 43, 3, 9, 82, 10],我们用归并排序进行排序。

  1. 分解阶段

    • 将数组分解为:[38, 27, 43, 3] 和 [9, 82, 10]
    • 继续分解:
      • [38, 27, 43, 3] 分解为:[38, 27] 和 [43, 3]
        • [38, 27] 分解为:[38] 和 [27]
        • [43, 3] 分解为:[43] 和 [3]
      • [9, 82, 10] 分解为:[9] 和 [82, 10]
        • [82, 10] 分解为:[82] 和 [10]
  2. 合并阶段

    • 合并子数组:
      • [38] 和 [27] 合并为:[27, 38]
      • [43] 和 [3] 合并为:[3, 43]
      • [82] 和 [10] 合并为:[10, 82]
    • 合并结果:
      • [27, 38] 和 [3, 43] 合并为:[3, 27, 38, 43]
      • [9] 和 [10, 82] 合并为:[9, 10, 82]
    • 最终合并:
      • [3, 27, 38, 43] 和 [9, 10, 82] 合并为:[3, 9, 10, 27, 38, 43, 82]

总结

归并排序的特点包括:

  • 时间复杂度:在最坏、最好和平均情况下均为 (O(n \log n)),表现稳定。
  • 空间复杂度:归并排序需要额外的空间来存储临时数组,因此空间复杂度为 (O(n))。
  • 稳定性:归并排序是一个稳定的排序算法,即相同值的元素在排序后保持其原有的相对位置。

计数排序:点击进入相关题目

基数排序:点击进入相关题目

归并排序:点击进入相关题目