数组(二)———数组的排序算法②

目录

快速排序

基本步骤:

复杂度分析

实现示例(Java):

堆排序

基本步骤:

复杂度分析

实现示例(Java):

计数排序

基本步骤:

复杂度分析

实现示例(Java):

桶排序

基本步骤:

复杂度分析

实现示例(Java):

基数排序

基本步骤(LSD):

复杂度分析

实现示例(Java):


快速排序

定义:

快速排序是一种非常高效的排序算法,采用了分治策略来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。

基本步骤:

  1. 选择基准值:从序列中挑选一个元素作为基准值(pivot)。通常选择序列的第一个元素或者最后一个元素。
  2. 分区操作:重新排列序列中的元素,所有比基准值小的元素都移到基准前面,所有比基准值大的元素都移到基准后面。这个操作称为分区操作。分区操作完成后,基准元素会处于最终排序后的位置。
  3. 递归排序子序列:递归地对基准左边的子序列和右边的子序列重复上述过程。递归的终止条件是子序列为空或只有一个元素。

复杂度分析

时间复杂度分析:

最好情况:每次分区操作都能均匀划分序列,时间复杂度为O(n log n)。
平均情况:也是O(n log n)。
最坏情况:当输入序列已经是正序或逆序,且总是选择第一个或最后一个元素作为基准时,每次分区只将序列分成一个较大部分和一个空的部分,时间复杂度退化为O(n^2)。

空间复杂度:
快速排序是原地排序算法,但因为递归调用,所以空间复杂度主要取决于递归栈的深度,最好情况下为O(log n),最坏情况下为O(n)


稳定性:在进行哨兵划分时,基准数可能会被交换至相等元素的右侧。因此,快速排序是一种 不稳定排序算法

实现示例(Java):

以下是一个使用Java实现的快速排序示例:

/* 元素交换 */
void swap(int[] nums, int i, int j) {
    int tmp = nums[i];
    nums[i] = nums[j];
    nums[j] = tmp;
}

/* 哨兵划分 */
int partition(int[] nums, int left, int right) {
    // 以 nums[left] 为基准数
    int i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= nums[left])
            j--;          // 从右向左找首个小于基准数的元素
        while (i < j && nums[i] <= nums[left])
            i++;          // 从左向右找首个大于基准数的元素
        swap(nums, i, j); // 交换这两个元素
    }
    swap(nums, i, left);  // 将基准数交换至两子数组的分界线
    return i;             // 返回基准数的索引
}

堆排序

 定义:

堆排序是一种基于比较的排序算法,它利用了堆数据结构的特性来进行排序。堆排序可以分为两种主要类型:大顶堆排序和小顶堆排序。大顶堆保证每个父节点的值都大于或等于其子节点的值,而小顶堆则相反,每个父节点的值都小于或等于其子节点的值

基本步骤:

  1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶。
  2. 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 1 ,已排序元素数量加 1 。
  3. 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
  4. 循环执行第 2. 步和第 3. 步。循环 n−1 轮后,即可完成数组排序。

复杂度分析

时间复杂度分析:

  • 时间复杂度为 O(nlog⁡n)、非自适应排序:建堆操作使用 O(n) 时间。从堆中提取最大元素的时间复杂度为 O(log⁡n) ,共循环 n−1 轮。

空间复杂度:

  • 空间复杂度为 O(1)、原地排序:几个指针变量使用 O(1) 空间。元素交换和堆化操作都是在原数组上进行的。

稳定性:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化,因此属于非稳定排序。

实现示例(Java):

以下是一个使用Java实现的堆排序示例:

/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
void siftDown(int[] nums, int n, int i) {
    while (true) {
        // 判断节点 i, l, r 中值最大的节点,记为 ma
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        int ma = i;
        if (l < n && nums[l] > nums[ma])
            ma = l;
        if (r < n && nums[r] > nums[ma])
            ma = r;
        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
        if (ma == i)
            break;
        // 交换两节点
        int temp = nums[i];
        nums[i] = nums[ma];
        nums[ma] = temp;
        // 循环向下堆化
        i = ma;
    }
}

/* 堆排序 */
void heapSort(int[] nums) {
    // 建堆操作:堆化除叶节点以外的其他所有节点
    for (int i = nums.length / 2 - 1; i >= 0; i--) {
        siftDown(nums, nums.length, i);
    }
    // 从堆中提取最大元素,循环 n-1 轮
    for (int i = nums.length - 1; i > 0; i--) {
        // 交换根节点与最右叶节点(交换首元素与尾元素)
        int tmp = nums[0];
        nums[0] = nums[i];
        nums[i] = tmp;
        // 以根节点为起点,从顶至底进行堆化
        siftDown(nums, i, 0);
    }
}

计数排序

 定义:

计数排序(Counting Sort)是一种非比较型的整数排序算法,适用于一定范围内的整数排序。它的基本思想是统计数组中每个数值出现的次数,然后根据这些计数来构造排序后的数组。计数排序的优势在于它的时间复杂度为 O(n + k),其中 n 是数组的长度,k 是数组中数值的范围。这意味着在某些情况下,它可以比基于比较的排序算法(如快速排序或归并排序)更高效,尤其是当 k 不是特别大时。

基本步骤:

  1. 确定数值范围:首先确定待排序数组中的最大值和最小值,从而知道数值的范围。
  2. 初始化计数数组:创建一个长度为 k 的计数数组,用来统计每个数值出现的次数。将所有元素初始化为0。
  3. 计数:遍历待排序的数组,对于数组中的每个元素,将其值作为索引,在计数数组的相应位置加1,以记录该数值出现的次数。
  4. 累计计数:修改计数数组,使得每个元素的值变成小于或等于该索引值的所有元素的个数。这一步是为了确定每个数值在最终排序数组中的位置。
  5. 输出排序后的数组:创建一个新的数组,用于存放排序后的结果。遍历原数组,根据每个元素的值和计数数组中的累计计数,将元素放置在正确的位置上。每放置一个元素,就将计数数组中相应位置的值减1。
  6. 返回排序后的数组:完成上述步骤后,新的数组就是排序后的结果。

复杂度分析

时间复杂度分析:

  • 时间复杂度为 O(n+m)、非自适应排序:涉及遍历 nums 和遍历 counter ,都使用线性时间。一般情况下 n≫m ,时间复杂度趋于 O(n) 。

空间复杂度:

  • 空间复杂度为 O(n+m)、原地排序:借助了长度分别为 n 和 m 的数组 res 和 counter 。

稳定性:由于向 res 中填充元素的顺序是“从右向左”的,因此倒序遍历 nums 可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 nums 也可以得到正确的排序结果,但结果是非稳定的。

实现示例(Java):

以下是一个使用Java实现的计数排序示例:

/* 计数排序 */
// 完整实现,可排序对象,并且是稳定排序
void countingSort(int[] nums) {
    // 1. 统计数组最大元素 m
    int m = 0;
    for (int num : nums) {
        m = Math.max(m, num);
    }
    // 2. 统计各数字的出现次数
    // counter[num] 代表 num 的出现次数
    int[] counter = new int[m + 1];
    for (int num : nums) {
        counter[num]++;
    }
    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”
    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引
    for (int i = 0; i < m; i++) {
        counter[i + 1] += counter[i];
    }
    // 4. 倒序遍历 nums ,将各元素填入结果数组 res
    // 初始化数组 res 用于记录结果
    int n = nums.length;
    int[] res = new int[n];
    for (int i = n - 1; i >= 0; i--) {
        int num = nums[i];
        res[counter[num] - 1] = num; // 将 num 放置到对应索引处
        counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引
    }
    // 使用结果数组 res 覆盖原数组 nums
    for (int i = 0; i < n; i++) {
        nums[i] = res[i];
    }
}

桶排序

 定义:

桶排序(Bucket Sort)是一种分布式排序算法,它将数组分到有限数量的“桶”中,每个桶再分别排序(可以使用任何其他的排序算法),最后将各个桶中的元素合并成一个有序的结果。桶排序适用于数据分布较为均匀的情况,通常假设输入数据服从某种统计分布,比如均匀分布

基本步骤:

  1. 确定桶的数量和范围:首先,需要确定桶的数量以及每个桶应该覆盖的值的范围。桶的数量和范围取决于输入数据的特性和预期的排序效率。
  2. 分配元素到桶中:遍历输入数组,将每个元素放入相应的桶中。元素被放置在哪个桶中,取决于其值与桶的范围的关系。
  3. 对每个桶进行排序:对每个非空的桶内部的元素进行排序。这里可以使用任何排序算法,例如插入排序、快速排序或其他适合小数据量的排序算法。
  4. 合并桶中的元素:将所有桶中的元素按顺序合并成一个单一的有序数组。

复杂度分析

时间复杂度分析:

  • 桶排序的平均时间复杂度是O(n + k)其中n是输入数组的长度,k是桶的数量。但是,如果桶内排序使用的是O(n^2)的排序算法,那么桶排序的最坏情况时间复杂度可能退化到O(n^2)。如果使用O(n log n)的排序算法,则最坏情况时间复杂度为O(n log n)

空间复杂度:

  • 空间复杂度为 :桶排序的空间复杂度是O(n + k),因为它需要额外的空间来存储桶和桶中的元素。

稳定性:由于向 res 中填充元素的顺序是“从右向左”的,因此倒序遍历 nums 可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 nums 也可以得到正确的排序结果,但结果是非稳定的。

实现示例(Java):

以下是一个使用Java实现的桶排序示例:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class BucketSort {
    public static void bucketSort(float[] arr) {
        // 创建桶
        List<List<Float>> buckets = new ArrayList<>();
        int bucketSize = arr.length;
        for (int i = 0; i < bucketSize; i++) {
            buckets.add(new ArrayList<>());
        }

        // 分配元素到桶中
        for (float value : arr) {
            int index = (int) Math.floor(bucketSize * value);
            buckets.get(index).add(value);
        }

        // 对每个桶进行排序
        for (List<Float> bucket : buckets) {
            Collections.sort(bucket);
        }

        // 合并桶中的元素
        int index = 0;
        for (List<Float> bucket : buckets) {
            for (float value : bucket) {
                arr[index++] = value;
            }
        }
    }
}

基数排序

 定义:

计数排序(Counting Sort)是一种非比较型的整数排序算法,适用于一定范围内的整数排序。它的基本思想是统计数组中每个数值出现的次数,然后根据这些计数来构造排序后的数组。计数排序的优势在于它的时间复杂度为 O(n + k),其中 n 是数组的长度,k 是数组中数值的范围。这意味着在某些情况下,它可以比基于比较的排序算法(如快速排序或归并排序)更高效,尤其是当 k 不是特别大时。

基数排序有两种主要的方法:
最低位优先(Least Significant Digit, LSD):从最低有效位开始排序,然后逐步向高位移动。
最高位优先(Most Significant Digit, MSD):从最高有效位开始排序,然后逐步向低位移动。

基本步骤(LSD):

  1. 确定最大数的位数:找出待排序数组中最大的数,确定其位数,这将是排序的轮数。
  2. 按位数排序:从最低位开始,对每一位进行排序。通常使用计数排序作为子程序来完成这一任务,因为计数排序适合处理小范围的整数。
  3. 重复步骤2:对下一位进行排序,直到最高位为止。

复杂度分析

时间复杂度分析:

  • 基数排序的时间复杂度为O(d * (n + b)),其中d是最大数的位数,n是数组的长度,b是基数(通常为10,如果处理的是十进制数)。当d固定时,基数排序的时间复杂度接近线性,这使得它在处理大量数据时非常高效。

空间复杂度:

  • 基数排序的空间复杂度为O(n + k),其中k是基数。这是因为需要额外的空间来存储计数数组和输出数组。

稳定性:当计数排序稳定时,基数排序也稳定;当计数排序不稳定时,基数排序无法保证得到正确的排序结果。

实现示例(Java):

以下是一个使用Java实现的基数排序示例:

public class RadixSort {
    public static void radixsort(int[] arr) {
        int max = getMax(arr);
        for (int exp = 1; max/exp > 0; exp *= 10) {
            countingSort(arr, exp);
        }
    }

    private static void countingSort(int[] arr, int exp) {
        int[] output = new int[arr.length];
        int[] count = new int[10];
        Arrays.fill(count, 0);

        for (int i = 0; i < arr.length; i++) {
            count[(arr[i]/exp)%10]++;
        }

        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        for (int i = arr.length - 1; i >= 0; i--) {
            output[count[(arr[i]/exp)%10] - 1] = arr[i];
            count[(arr[i]/exp)%10]--;
        }

        System.arraycopy(output, 0, arr, 0, arr.length);
    }

    private static int getMax(int[] arr) {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    }
}

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值