时间复杂度为 O(n) 的排序算法

这篇具有很好参考价值的文章主要介绍了时间复杂度为 O(n) 的排序算法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

大家好,我是 方圆。本文介绍线性排序,即时间复杂度为 O(n) 的排序算法,包括桶排序,计数排序和基数排序,它们都不是基于比较的排序算法,大家重点关注一下这些算法的适用场景。

桶排序

桶排序是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据依次取出合并,组成的序列就是有序的了,如下图所示:

时间复杂度为 O(n) 的排序算法,排序算法,算法,数据结构

桶排序的算法流程我将其分成三个步骤:

  • 初始化桶:以范围为 0 - 49 的数据为例,分为 5 个桶

  • 分桶:将要排序数组中的元素加入桶中

  • 出桶:该步骤需要在桶中完成排序后,依次出桶合并

    /**
     * 桶排序:指定数据范围为0 - 49,分桶为5个,每10个数为一个桶
     */
    public void sort(int[] nums) {
        // 声明5个桶
        List<ArrayList<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            buckets.add(new ArrayList<>());
        }

        // 数组元素分桶
        intoBucket(buckets, nums);

        // 出桶
        outOfBucket(buckets, nums);
    }

    /**
     * 分桶
     */
    private void intoBucket(List<ArrayList<Integer>> buckets, int[] nums) {
        for (int num : nums) {
            int bucketIndex = num / 10;
            buckets.get(bucketIndex).add(num);
        }
    }

    /**
     * 出桶
     */
    private void outOfBucket(List<ArrayList<Integer>> buckets, int[] nums) {
        // 出桶覆盖原数组值
        int numsIndex = 0;
        for (ArrayList<Integer> bucket : buckets) {
            // 先排序 再出桶
            bucket.sort(Comparator.comparingInt(x -> x));

            for (Integer num : bucket) {
                nums[numsIndex++] = num;
            }
        }
    }

算法特性:

  • 空间复杂度:O(n + k)

  • 自适应排序:与桶划分情况和桶内使用的排序算法有关

  • 稳定排序/非稳定排序:与桶内使用的排序算法有关

  • 非原地排序

桶排序比较适用于 外部排序,所谓的外部排序就是数据存储在外部磁盘中,数据量很大,但是内存又有限,无法将所有数据全部加载进来,比如有 1G 的数据需要排序,但是内存只有几百MB的情况。我们可以根据数据范围将其划分到 N 个桶中,划分完成后每个桶的大小不超过可用内存大小,对每个桶内的数据进行排序,排序完成后生成 N 个小文件,最后我们再将这 N 个小文件写入到一个大文件中即可。如果数据在某些范围内并不是均匀分布的话,有些范围内的数据特别多,那么这就需要我们再对其划分成更细粒度的桶,直到满足内存的使用要求,但是这样我们的桶就不是按照范围均匀划分的了。

计数排序

计数排序是桶排序的一种特殊情况,只是它定义的“桶”的粒度更细,每个桶中只包含一个单位范围的数字,那么每个“桶”内的数值都是相等的。它适合数据范围不大,但数据量很大的排序场景,比如高考考生成绩排名,86 万考生,满分 750 分,需要划分 751 个桶,将这些考生的成绩划分到各个桶中后,依次取出即可。

看到这里你可能会觉得这不就是桶排序吗?计数排序的计数体现在哪里呢?别急,我们看下下面这个排序的例子,简单起见,假设有 8 个考生,他们的分数为 [2, 5, 3, 0, 2, 3, 0, 3],分数范围为 0 ~ 5,那么我们需要创建 6 个桶,规定桶中保存的不是对应的元素,而是对应分数元素出现的数量,并根据分数将桶中的计数值累加,如下图所示:

时间复杂度为 O(n) 的排序算法,排序算法,算法,数据结构

我们先看分数 0 的桶,它是该数据范围内最小的分数,它的计数为 2,根据计数值我们可以确定分数为 0 的两个元素占用该数据范围的前两个索引位置,所以计数表示的是对应数值的索引位置。我们再看看其他的桶来验证一下:可以发现分数 2 的桶计数也为 2,但是前两个索引位置已经被分数 0 占用了呀,分数 2 的计数应该是 4 才对,所以,我们还需要一步操作:叠加前面分数出现的次数,这样分数 2 的计数值便为 4,可以发现计数值其实表示的是某数字占用的第 N 个索引,如果我们想知道其中分数 2 的索引位置,将计数值 4 进行减 1 即可,即它的索引值为 3,而且每取完某数字一次,需要将该计数值减 1。排序流程如下:

时间复杂度为 O(n) 的排序算法,排序算法,算法,数据结构

这样一步步操作完成之后,最终数组是有序的。计数排序的代码如下:

    /**
     * 计数排序的计数体现在小于等于某个数出现的次数 - 1 即为该数在原数组排序后的位置
     */
    public void sort(int[] nums) {
        if (nums.length <= 1) {
            return;
        }

        // 寻找数组中的最大值来以此定义max + 1个桶
        int max = Arrays.stream(nums).max().getAsInt();

        // 定义桶,索引范围即数组值的最大范围,每个桶中保存的是该数字出现的次数,计数排序的计数概念出现
        int[] bucket = new int[max + 1];

        // 计算每个数的个数在桶中累加
        Arrays.stream(bucket).forEach(x -> bucket[x]++);
        // 依次累加桶中的数,该数表示小于等于该索引值的数量
        for (int i = 1; i < bucket.length; i++) {
            bucket[i] += bucket[i - 1];
        }

        // 创建临时数组来保存排序结果值
        int[] res = new int[nums.length];
        // 倒序遍历原数组,不改变相同元素的相对顺序
        for (int i = nums.length - 1; i >= 0; i--) {
            // 根据桶中的 计数 找出该数的索引
            int index = bucket[nums[i]] - 1;
            // 根据索引在结果数组中赋值
            res[index] = nums[i];
            // 该数分配完成后,需要将桶中的计数-1
            bucket[nums[i]]--;
        }

        // 结果数组覆盖原数组
        System.arraycopy(res, 0, nums, 0, res.length);
    }

基数排序

基数排序对待排序数据是有特殊要求的,需要数据可以分割出独立的“位”,并且位与位之间要有递进关系,根据递进关系对每一位进行排序,获得最终排序结果。

我们先来看一下使用基数排序处理 [3, 4, 100, 11, 33] 数组的过程:

时间复杂度为 O(n) 的排序算法,排序算法,算法,数据结构

因为整数每位取数范围为 0 ~ 9,所以创建了 10 个桶,这 10 个桶已经有了从大到小的顺序。排序时从整数的个位开始,到最高位终止,排序的轮次为最大整数的位数,在每轮排序中,根据当前位数值大小入桶,完成后再按顺序出桶,最终结果即为排序结果。代码如下:

    private void sort(int[] nums) {
        if (nums.length <= 1) {
            return;
        }

        // 1. 整数的每位取值范围为 0-9,因此需要创建10个桶
        Queue<Integer>[] buckets = createBuckets();

        // 2. 获取基数排序的执行轮次
        int radixRounds = getRadixRounds(nums);

        // 3. 根据执行轮次处理各个"位",eg: 第一轮处理个位...
        for (int round = 1; round <= radixRounds; round++) {
            for (int num : nums) {
                // 获取所在桶的索引
                int bucketIndex = getBucketIndex(num, round);
                // 进桶
                buckets[bucketIndex].offer(num);
            }

            // 出桶赋值,当前结果为根据当前位排序的结果
            int numsIndex = 0;
            for (Queue<Integer> bucket : buckets) {
                while (!bucket.isEmpty()) {
                    nums[numsIndex++] = bucket.poll();
                }
            }
        }
    }

    /**
     * 创建大小为10的数组作为桶,每个桶都是一个队列
     */
    @SuppressWarnings("unchecked")
    private Queue<Integer>[] createBuckets() {
        Queue<Integer>[] buckets = new Queue[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new LinkedList<>();
        }

        return buckets;
    }

    /**
     * 获取基数排序的执行轮次
     */
    private int getRadixRounds(int[] nums) {
        return String.valueOf(Arrays.stream(nums).max().getAsInt()).length();
    }

    /**
     * 获取该数所在桶的索引
     */
    private int getBucketIndex(int num, int round) {
        int bucketIndex = 0;

        while (round != 0) {
            bucketIndex = num % 10;
            num /= 10;

            round--;
        }

        return bucketIndex;
    }

基数排序比较适用于数据范围比较大且位数相对均匀的数据排序,比如排序手机号或者学号,它的时间复杂度接近于 O(n)。


巨人的肩膀

  • 《数据结构与算法之美》:第 3.6 章 线性排序:如何根据年龄给 100 万个用户排序

  • 《Hello 算法》:第 11.8、11.9 和 11.10 章

  • 《算法导论》:第 8 章 线性时间排序文章来源地址https://www.toymoban.com/news/detail-832559.html

到了这里,关于时间复杂度为 O(n) 的排序算法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构与算法—时间复杂度和空间复杂度

    目录 1、什么是数据结构? 2、什么是算法? 3、算法的复杂度 4、时间复杂度 (1) 时间复杂度的概念:  (2) 大O的渐进表示法:  六个例题: (3) 时间复杂度对比:  三个例题:  OJ题分析时间复杂度 5、空间复杂度 (1)常见复杂度对比  (2)OJ题分析空间复杂度 小结 数据结构 (D

    2024年02月07日
    浏览(92)
  • 算法的时间复杂度和空间复杂度(数据结构)

    目录 1、算法效率 1如何衡量一个算法的好坏 2算法的复杂度 2、时间复杂度 1时间复杂度的概念 2大O的渐进表示法 2时间复杂度计算例题 1、计算Func2的时间复杂度 2、计算Func3的时间复杂度 3、计算Func4的时间复杂度 4、计算strchr的时间复杂度 5、计算BubbleSort的时间复杂度 6、计算

    2024年02月03日
    浏览(68)
  • 学习数据结构:算法的时间复杂度和空间复杂度

    衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。 算法的时间复杂度 算法中的基本操作的执行次数,为算法的时间复杂度。 算法的

    2024年04月11日
    浏览(46)
  • 【数据结构与算法】1.时间复杂度和空间复杂度

    📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏小杨水平有限,欢迎各位大佬指点,相互学习进步! 算法效率分为两种:第一种是时间效率;第二种是空间效率。时间效率又称为时间

    2024年01月20日
    浏览(54)
  • 数据结构 | 算法的时间复杂度和空间复杂度【详解】

    数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。 算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转

    2024年02月08日
    浏览(57)
  • 【数据结构与算法篇】时间复杂度与空间复杂度

       目录 一、数据结构和算法 1.什么是数据结构?  2.什么是算法? 3.数据结构和算法的重要性 二、算法的时间复杂度和空间复杂度 1.算法效率 2.算法的复杂度 3.复杂度在校招中的考察 4.时间复杂度 5.空间复杂度  6.常见复杂度对比 7.复杂度的OJ练习   👻内容专栏:《数据结

    2023年04月24日
    浏览(67)
  • 从头开始:数据结构和算法入门(时间复杂度、空间复杂度)

        目录 文章目录 前言 1.算法效率 1.1 如何衡量一个算法的好坏 1.2 算法的复杂度 2.时间复杂度  2.1 时间复杂度的概念 2.2 大O的渐进表示法 2.3常见时间复杂度计算 3.空间复杂度 4.常见复杂度对比 总结 前言         C语言的学习篇已经结束,今天开启新的篇章——数据结构

    2024年02月14日
    浏览(53)
  • 【数据结构与算法篇】之时间复杂度与空间复杂度

    ❤️博客主页: 小镇敲码人 🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏 🌞友友们暑假快乐,好久不见呀!!!💖💖💖 🍉 有人曾经问过我这样一个问题,“人终其一身,执着追求的东西究竟是什么?”我是这样回答的,”我们终其一生都在寻找着那个和我们灵魂极其契合

    2024年02月12日
    浏览(54)
  • 【数据结构初阶】算法的时间复杂度和空间复杂度

    1.1 如何衡量一个算法的好坏 如何衡量一个算法的好坏呢? 比如对于以下斐波那契数列: 斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢? 1.2 算法的复杂度 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此

    2024年02月08日
    浏览(75)
  • 【数据结构】算法的时间复杂度和空间复杂度(含代码分析)

    如何衡量一个算法的好坏呢?比如对于以下斐波那契数列: 这里的时间复杂度为: 2^N ,计算方法请看下文。 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复

    2024年02月05日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包