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. 选择排序
- 2. 冒泡排序
- 3. 插入排序
- 4. 前缀和数组
- 5. Java中的Math.random()函数,随机函数
- 6. 从[1,5]随机到[1,7]随机
- 7. 01不等概率随机到01等概率随机
- 8. 对数器的使用
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
):
L == 0
,返回prefixArr[r]
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
函数返回每个随机数的概率是相等的。所以对于i
,i ∈ [0,1)
来说,落在[0,i)
的概率是i
。及是一个线性的函数,类似于这种:
如果我想让i
,i ∈ [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]随机对数器的使用