排序算法学习08_计数排序(Java)

计数排序


前言:在博客写这些文章的目的用于记录所学,怕以后忘了,如果哪里写的不对欢迎指正,谢谢!!

学习目标:掌握计数排序算法的原理和思想

一、前提知识

  排序算法概念、时间复杂度。可前往此网址 排序算法学习01_算法基础介绍阅读

二、计数排序介绍

  计数排序(Counting sort)是一种稳定线性时间排序算法。计数排序不是比较排序, 它利用数组下标来排序元素

三、计数排序工作原理

  计数排序利用数组下标来排序元素。通过把要排序序列中的依次放入一个新的数组中。如何放置?根据序列中值的大小放入数组对应的索引上,但此时并不真正实现放入元素,而是让对应索引位置上元素+1,进行计数

  排序时,则依次查看每个索引位置上元素值是否大于0(有个数的话),则把对应索引放入原序列。。即可得到一个排序好的序列

  画个图理解吧(图片下面文字是大于0,我写错了~~~)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FK8pyUeZ-1611150716941)(E:\哈哈\md\数据结构和算法\计数排序放置过程.png)]

四、计数排序设计思路

主要思考以下几点

  1. 新数组长度如何选择
  2. 序列中的值如何依次“放入”新数组,并实现新数组对应索引位置上的元素+1
  3. 放置完毕,如何依次取出新数组的内容,放置到原序列,达到排序效果
  • 关于第一点
    • 从计数排序的工作原理可得知:序列的值是根据新数组(以下称为计数数组)的索引来放置的
      • 那么计数数组长度的选择,则要大于序列中的最大值,才可以“存储”所有值
  • 关于第二点
    • 我们知道原序列的值对应着计数数组的索引,那么我们即可通过这样一种巧妙的组合达成这样的效果
      • countArr[ arr[ 1 ] ],假设原序列arr[1],元素为5,那么就刚好可以对计数数组下标为5的地方进行改动
      • 那要做什么改动呢?就是对该位置上的元素+1,进行计数,即conutArr[ arr[ 1 ] ]++,表明该位置此时有一个元素5
  • 关于第三点
    • 这就简单了,依次遍历计数数组,看看每个计数数组索引上元素有没有大于0,那么把索引赋值到原序列,记得循环哦,有可能数量不只一个

五、代码实现

package com.migu.sortingAlgorithm;

import java.util.Arrays;
/**
 * 计数排序算法
 */
public class CountSort {
    public static void main(String[] args) {
        int arr[] = {3,2,10,5,1,7,9,4,4,3,12,0};
        countSort(arr);
        System.out.println(Arrays.toString(arr)); // 调用工具类
    }
    public static void countSort(int[] arr) {
        // 找出数组中的最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];  // 找最大值,用于建立数组长度
            }
        }
        // 初始化计数数组,长度记得再+1
        int[] countArr = new int[max + 1];

        // 计数,遍历原序列,并计数到新数组
        for (int i = 0; i < arr.length; i++) {
            countArr[arr[i]]++;
        }

        // 排序
        int index = 0;  // 作为指针辅助原序列排序
        //依次遍历计数数组,如果索引上有元素大于0,则代表有数据
        for (int i = 0; i < countArr.length; i++) {
            while (countArr[i] > 0) {
                arr[index++] = i;
                countArr[i]--;
            }
        }
    }
}

六、计数排序优化

认真的童孩可能发现,以上计数排序具有一定局限性

  1. 如果当取值为负数时,那么将没有索引与之对应,则无法排序
  2. 如果一个序列中的最小值很大,比如这样 { 101,108,104,103 },那么如果依然采用上面方法,将会导致牺牲掉计数数组前面100个空间

基于以上两点,可以采用一种叫偏移量的东西解决

  • 取偏移量,需取序列中元素最小的值作为偏移量,也就是元素101

  • 此时数组长度length就为 max- offset +1 = 8,再加1,也就是数组长度为9,索引为0~8,序列里值减去偏移量正好可以放进该计数数组里

  • 创建计数数组之后,如何“放置”元素呢?

    • 每次放置之前减去偏移量,取出到原序列再加上即可

    • // 计数,遍历原序列,并计数到新数组
      for (int i = 0; i < arr.length; i++) {
          countArr[arr[i]-offset]++;
      }
      //-------取出-----------
      while (countArr[i] > 0) {
          arr[index++] = i+offset;
          countArr[i]--;
      }
      
    • 自始至终都是对索引来操作,计数数组的元素只用来计数用的

  • 对于存在负数的序列,反之则加上偏移量即可,但还会有一些bug,就不细讲了

  细心的童孩还可发现,我取的数列都是相近的,没有偏离很多,这也是计数排序的一些局限性,如果取的值相差太大,那么必然导致一些空间会牺牲掉

  最后,还存在一个问题,就是排序算法很重要的一点:稳定性

  采用以上方法并无法得知此算法是稳定的,那么我们需要进行二次改造

七、稳定计数排序设计思路

  稳定的算法应该是什么样?比如存在这样一个序列{1,3,4,4,2,5,3}

  排序后:{1,2,3,3,4,4,5},稳定排序时原序列第一个4,依然要为排序后的第一个4

  思路转换到计数排序,首先我们依然先得到计数数组,我们的计数数组是如何接收同样数据的呢?在对应索引位置,再对元素+1进行计数。那么哪个计数值对应着原序列哪个元素,我们需要好好细究下,来看看看这张图

  计数数组索引为3这个位置,这个元素2是不是计数着原序列第二个32减去1之后,那个1才对应着原序列第一个3

  懂了之后,我们就要对这个计数数组进行二次改造

  改造目的:让计数数组索引上的计数值,可以一 一对应着原序列元素的位置

  • 第一步:老样子得到计数数组,然后进行变形,从第二位开始,每一位的元素等于与前面一位元素之和
    • 变形后的计数数组,元素不再是用来计数,而是表示一个位置

如图:

  • 第二步:(从上图可发现)
    • 变形后计数数组的索引5的位置,元素为7。对应到原序列为元素5不恰巧排序后处在会原序列的第七个位置嘛
    • 变形后计数数组的索引4的位置,元素为6。对应到原序列为元素4不恰巧排序后处在会原序列的第六个位置嘛
    • 接下来看索引3,元素为4。对应到原序列为元素3处在排序后原序列的第四个位置
    • 那第五个位置哪去了,这是因为原序列有两个元素5,前面那个5当然就要处在第五个位置
    • 所以呢?我们该如何让这个原序列中元素5不丢失,那就需要从后往前遍历变形后的这些内容
  • 第三步:如何遍历变形后的计数数组并排序
    • 知道了上面规律后,我们就来找联系关系
    • 首先我们需要再创建一个数组:用于排序的数组 sortedArr[ ],该数组用于接收排序好的内容,并依次赋值给原序列
    • sortedArr[ countArr[ arr[ i ] ] - 1 ] = arr[ i ],有大伙看出门道吗
      • 变形后的countArr[ ]数组此时的元素实际代表的是原序列元素排序所在位置,而该位置上的索引正好是原序列的元素。也就是我们只要构建这种关系,即可得到一个排序好的结果
      • 那为什么要减1的操作呢?这是因为变形后的计数数组,元素表示的位置是从1开始算的
      • 接着还要对 countArr[ arr[ i ] ]- -,对上一个位置的值进行处理绑定

八、稳定计数排序代码实现

public static void stableCountSort(int[] arr){
    // 找出数组中的最大值
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];  // 找最大值,用于建立数组长度
        }
    }
    // 初始化计数数组,长度记得再+1
    int[] countArr = new int[max + 1];

    // 计数,遍历原序列,并计数到新数组
    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i]]++;
    }

    // 变形
    for (int i = 1; i < countArr.length; ++i) {
        countArr[i] = countArr[i-1] + countArr[i];
    }

    // 用于接收排序结果,并赋值给原数组
    int[] sortedArr = new int[arr.length];

    // 根据规律进行赋值即可到排序效果
    for (int i = arr.length - 1; i >= 0; --i) {
        sortedArr[countArr[arr[i]]-1] = arr[i];
        countArr[arr[i]]--; // 操作上个位置
    }

    //将排序的结果赋值到原数组
    for (int i = 0; i < arr.length; ++i) {
        arr[i] = sortedArr[i];
    }
}

九、时间复杂度

  计数排序的时间复杂度为 O(n + k ),k 指的是原序列中最大最小元素差,说的简单点,计数排序算法的时间复杂度约等于 O(n),快于任何比较型的排序算法。

  空间复杂度只考虑创建的计数数组大小那就是O(k)了

十、总结

  计数排序虽然强大,但只适用于计算整数,且当差值过大时,并不适用

  关于稳定计数排序和偏移量结合使用问题,博主是个小菜鸡,想不明白。还有该篇哪里讲的有问题,欢迎指正!!然后很开心你能看到这里,加油打工人

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值