Java算法 -- 选择排序冒泡排序插入排序前缀和数组Java中的Math.random()函数01不等概率随机到01等概率随机从[1,5]随机到[1,7]随机对数器的使用

Posted CodeJiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java算法 -- 选择排序冒泡排序插入排序前缀和数组Java中的Math.random()函数01不等概率随机到01等概率随机从[1,5]随机到[1,7]随机对数器的使用相关的知识,希望对你有一定的参考价值。

文章目录

1. 选择排序

选择排序的Java代码如下:

    public static void selectSort(int[] nums) 
        if (nums == null || nums.length < 2) 
            return;
        

        for (int i = 0; i < nums.length; i++) 
            int minIndex = i;
            // 比较 0 ~ nums.length-1
            // 比较 1 ~ nums.length-1
            // ...
            // 比较 i ~ nums.length-1
            for (int j = i + 1; j < nums.length; j++) 
                minIndex = nums[minIndex] > nums[j] ? j : minIndex;
            
            swap(nums, i, minIndex);
        
    

    /*
    交换nums中, i下标和j下标位置的数
     */
    public static void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

测试:


2. 冒泡排序

冒泡排序的Java代码如下:

    public static void bubbleSort(int[] nums) 
        if (nums == null || nums.length < 2) 
            return;
        
        for (int i = 0; i < nums.length; i++) 
            // 第一次找到最大的数
            // 第二次找到次大的数
            // ...
            for (int j = 1; j < nums.length - i; j++) 
                // 比较 0 1, 1 2 , 2 3,...nums.length -2 nums.length -1 索引位置的数据
                if (nums[j - 1] > nums[j]) 
                    swap(nums, j - 1, j);
                
            
        
    

    /*
    交换nums中, i下标和j下标位置的数
     */
    public static void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

测试:


3. 插入排序

插入排序的Java代码如下:

    public static void insertSort(int[] nums) 
        if (nums == null || nums.length < 2) 
            return;
        
        // 0~0 位置已经有序, 所以从 0 ~ 1 位置开始排序
        for (int i = 1; i < nums.length; i++) 
            // 记录当前操作的索引是 i
            int operation = i;
            // 如果 operation 的左边还有数据 并且 nums[operation] 小于 nums[operation - 1] 就交换
            // nums[operation] 和 nums[operation - 1] 然后operation左移
            // 当退出while循环的时候 0 ~ i位置已经排好序了
            while ((operation - 1) >= 0 && nums[operation] < nums[operation - 1]) 
                swap(nums, operation, operation - 1);
                operation--;
            
        
    

    /*
    交换nums中, i下标和j下标位置的数
     */
    public static void swap(int[] nums, int i, int j) 
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    

测试:


4. 前缀和数组

假设现在有一个整型数组nums,要求返回这个数组索引l位置到r位置之间数组元素的累加和。

你可以第一时间想到下面这个做法:

    public static int getLandRSum(int[] nums, int l, int r) throws Exception 
        if (nums == null || nums.length == 0 || l > r) 
            throw new Exception("参数不合法!");
        
        int sum = 0;
        while (l <= r) 
            sum += nums[l++];
        
        return sum;
    

但是入门每次拿数组都要经过r - l + 1次算术运算显然有点浪费时间了,于是我们可能想到了下面这种二维数组结构去用空间换时间

它的一维索引是r的值,二维索引是l的值,该二维数组的[r][l]位置代表了数组索引l位置到r位置之间数组元素的累加和。

假设现在 nums = 3,2,1,则该二维数组可以表示为:

[[3,5,6],
[x,2,3],
[x,x,1]]

其中x代表不赋值,因为l大于了r。这种方式有2个非常显著的弊端

  • 构造二维数组需要2层循环,比较麻烦
  • 浪费空间(对角线下面的空间都浪费了,假设nums的长度为n,则几乎浪费了 n2/2 个元素的空间。)

接下来我们就来讲解前缀和数组:

前缀和数组是一个一维的数组,该数组的第i个位置存储着第i个位置元素及之前元素的累加和。

例如对于 nums = 3,2,1,他的前缀和数组preNums为:[3,5,6]

  • preNums[0] = nums[0]
  • preNums[1] = nums[0] + nums[1] = preNums[0] + nums[1]
  • preNums[2] = nums[0] + nums[1] + nums[2] = preNums[1] + nums[2]

累加和数组的优点如下:

  • 占用空间较小,占用和原数组一样的空间大小。
  • 构造方便,一层循环即可完成。

下面介绍用Java构造前缀和数组的方法。

    public static int[] constructPrefixArrays(int[] nums) throws Exception 
        if (nums == null || nums.length == 0) 
            throw new Exception("参数不合法!");
        
        int[] prefixArr = new int[nums.length];
        prefixArr[0] = nums[0];
        for (int i = 1; i < nums.length; i++) 
            prefixArr[i] = prefixArr[i - 1] + nums[i];
        
        return prefixArr;
    

测试:

使用前缀和数组可以很方便的计算数组索引l位置到r位置之间数组元素的累加和。其计算公式如下(假设前缀和数组名称为prefixArr):

  1. L == 0,返回prefixArr[r]
  2. L != 0,返回prefixArr[r] - prefixArr[l-1]

前缀和数组在计算数组索引l位置到r位置之间数组元素的累加和比较好用。但是当访问量巨大的时候,我仍然建议使用第一种二维数组的方式去求和,因为它不需要计算,直接返回数组的元素就行了(数组的取值操作是O(1)复杂度),使用这些额外的空间去换取时间是非常值得的。


5. Java中的Math.random()函数,随机函数

Math.random方法将返回一个[0,1)之间(包含0、不包含1)的随机浮点数。如果用整数n乘以这个浮点数,并把结果转为整数,就可以得到从[0,n-1]的一个随机数整。

关于Math.random()函数最牛逼的地方在于,他返回每个随机数的概率是相等的。

测试:

    public static void main(String[] args) throws Exception 
        // 记录目标数字出现的次数
        int count1 = 0;
        int count2 = 0;
        int count3 = 0;
        for (int i = 0; i < 1_0000_0000; i++) 
            double num = Math.random();
            if (num < 0.3) 
                count1++;
             else if (num < 0.6) 
                count2++;
             else if (num < 0.8) 
                count3++;
            
        
        System.out.println("在一亿次随机数中, [0,0.3)的概率是:" + (double) count1 / 1_0000_0000);
        System.out.println("在一亿次随机数中, [0.3,0.6)的概率是:" + (double) count2 / 1_0000_0000);
        System.out.println("在一亿次随机数中, [0.6,0.8)的概率是:" + (double) count3 / 1_0000_0000);
    

运行结果:

接下来我们来做一些有事情的事情,因为random函数返回每个随机数的概率是相等的。所以对于ii ∈ [0,1)来说,落在[0,i)的概率是i。及是一个线性的函数,类似于这种:

如果我想让ii ∈ [0,1)来说,落在[0,i)的概率是 i2,类似于这种:

有没有什么办法呢?有的:

    /**
     * 这2次Math.random()是独立的
     * 每次落入[0,x)的概率是 x
     * 而Math.max的函数又会要求每次都落入[0,x), 所以该函数的返回值落入[0,x)的概率是 x * x
     */
    public static double getRandom() 
        return Math.max(Math.random(), Math.random());
    

再次测试:

下面的这些函数就不解释了,看下注释就懂了:

    /**
     * 这3次Math.random()是独立的
     * 每次落入[0,x)的概率是 x
     * 而Math.max的函数又会要求每次都落入[0,x), 所以该函数的返回值落入[0,x)的概率是 x * x * x
     */
    public static double getRandom3() 
        return Math.max(Math.random(), Math.max(Math.random(), Math.random()));
    
        /**
     * 这2次Math.random()是独立的
     * 每次落入[0,x)的概率是 x
     * 而Math.min的函数又会要求至少一次都落入[0,x), 所以该函数的返回值落入[0,x)的概率是 1 - (1 - x)^2
     */
    public static double getRandom4() 
        return Math.min(Math.random(), Math.random());
    
    /**
     * 这3次Math.random()是独立的
     * 每次落入[0,x)的概率是 x
     * 而Math.min的函数又会要求至少一次都落入[0,x), 所以该函数的返回值落入[0,x)的概率是 1 - (1 - x)^3
     */
    public static double getRandom5() 
        return Math.min(Math.random(), Math.max(Math.random(), Math.random()));
    

6. 从[1,5]随机到[1,7]随机

假设现在有一个f1()函数,它可以随机等概率返回[1,5]的整数。我们写的程序只可以用f1()函数获取随机整数,问我们怎么样才可以等概率的返回[1,7]之间的随机整数。

    // 等概率返回[1,5]之间的整数
    // lib里面的, 不可以更改
    public int f1() 
        return (int) (Math.random() * 5) + 1;
    

    // 等概率返回0或者1
    public int f2() 
        int number;
        do 
            number = f1();
         while (number == 3);
        return number < 3 ? 0 : 1;
    

    // 等概率返回[0,7]之间的整数 ( 000 ~ 111 )
    // f2() << 2决定第3位, f2() << 1决定第2位, f2()决定第1位
    public int f3() 
        return f2() << 2 + f2() << 1 + f2();
    

上面的f3()函数就可以实现等概率的返回[0,7]之间的随机整数。

如果我们想等概率的返回[0,6]之间的随机整数可以这样做:

    public int f4() 
        int number;
        do 
            number = f3();
         while (number == 7);
        return number;
    

我们如何实现等概率的返回[1,7]之间的随机整数?如下:

    public int f5() 
        return f4() + 1;
    

7. 01不等概率随机到01等概率随机

假设现在有一个函数f(),它可以以固定概率返回0和1,但不是0和1。现在请你设计一个g()函数,使得它可以在函数f()实现等概率返回0和1。

    /**
     * 0.25 的概率返回1, 0.75的概率返回0
     */
    public int f() 
        return Math.random() < 0.75 ? 0 : 1;
    

    /**
     * f有0.25 的概率返回1, 0.75的概率返回0
     * 如果调用2次f()
     * 有 0.25^2 的概率都返回1
     * 有 0.75^2 的概率都返回0
     * 有 0.25 * 0.75 的概率各返回一次 0 和 1
     * 所以各返回一次 0 和 1 的概率是相同的, 我们可以从这里入手等概率返回0和1
     */
    public int g() 
        int num;
        do 
            num = f();
         while (num == f());
        return num;
    

8. 对数器的使用

对数器是通过用大量测试数据来验证算法是否正确的一种方式。对数器可以生成大量的随机样本并自动做比对,并返回比对结果的程序。

比如我们可以为前面的选择排序写一个对数器:

    // 返回一个数组arr,arr的长度是[0,maxLen - 1],arr中的每个值是[0,maxValue - 1]
    // 这个方法是为了生成一个随机数
    public static int[] lvRandomArr(int maxLen, int maxValue) 
        int resLen = (int) (Math.random() * maxLen);
        int[] resArr = new int[resLen];
        for (int i = 0; i < resLen; i++) 
            resArr[i] = (int) (Math.random() * maxValue);
        
        return resArr;
    

    // 深拷贝数组nums 这个方法是为了找出错误样本
    public static int[] deepCopyArr(int[] nums) 
        int[] resArr = new int[nums.length];
        for (int i = 0; i < nums.length; i++) 
            resArr[i] = nums[i];
        
        return resArr;
    

    // 这个方法是为了测试数组是否排好序
    public static boolean isSorted(int[] arr) 
        if (arr.length < 2) 
            return true;
        
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) 
            if (max > arr[i]) 
                return false;
            
            max = Math.max(max, arr[i]);
        
        return true;
    

    // 冒泡排序的对数器
    public static void main(String[] args) 
        int maxLen = 100;// 数组最大长度
        int maxValue = 50;// 数组最大值
        int testCount = 10_0000; // 测试10万次
        for (int i = 0; i < testCount; i++) 
            int[] arr1 = lvRandomArr(maxLen, maxValue);
            int[] temp = deepCopyArr(arr1);
            // 调用冒泡排序
            bubbleSort(arr1);
            // 如果排序失败, 则打印错误信息
            if (!isSorted(arr1)) 
                System.out.println("排序有问题, 错误样本: " + Arrays.toString(temp));
                break;
            
        
    


以上是关于Java算法 -- 选择排序冒泡排序插入排序前缀和数组Java中的Math.random()函数01不等概率随机到01等概率随机从[1,5]随机到[1,7]随机对数器的使用的主要内容,如果未能解决你的问题,请参考以下文章

Java算法 -- 选择排序冒泡排序插入排序前缀和数组Java中的Math.random()函数01不等概率随机到01等概率随机从[1,5]随机到[1,7]随机对数器的使用

Java算法 -- 选择排序冒泡排序插入排序前缀和数组Java中的Math.random()函数01不等概率随机到01等概率随机从[1,5]随机到[1,7]随机对数器的使用

排序算法之冒泡选择插入排序(Java)

排序算法之冒泡选择插入排序(Java)

Java数据结构和算法——冒泡选择插入排序算法

Java数据结构和算法总结-冒泡排序选择排序插入排序算法分析