数据结构
一、数据结构
1.连续结构,数组,拿出数组某个下标的值很快,一整块区域都是它。

2.跳转结构,不是连续的,内存中分配一小块区域给a,a上面记录着值,还有一小块是记录指向下一个节点的内存地址。

二叉树,a这个节点一小块内存,记录着值,还有两块内存记录着地址,可以想象为左指针和右指针,分别指向两个内存区域。指向下个b和c分别有各自有两块内存地址分别指向下节点...,二叉树结构就是两个跳转地址的跳转结构。

图,就是a点,往外七八条指针,a内存区域里有很多内存地址,a往外多少个分支,在内存区域里就有多少个指向地址。

所有数据结构都是连续结构、跳转结构,或者这两者拼的连续结构 + 跳转结构。
二、最基本的数据结构
1)数组
便于寻址,不便于增删数据。
比如数组里面是2,3,5,6,7,8,要把4插入到3和5之间,这对数据来说代价很高,因为它要维持自己的连续结构,只能是5,6,7,8往右边挪,腾出一个空,4插进去,很麻烦。
删除也一样。比如2,3,4,5,6,7,8,要把4删掉,5往左挪个位置把4盖掉,6,7,8都往左挪。很麻烦。
2)链表
便于增删数据,不便于寻址
比如a->b->c->d,要想在b和c中间加个e,直接e申请个内存,内存里面随便一块给我申请好,b原来的下个指针的地址不是记c的吗?改成记e就行了。e上面的内存指针下一个记c就行了。只需要改两个指针的走向,e就加进去了。其他数据根本不需要动,所以加数据很容易。
再比如a->b->e->c->d,现在要把e删掉,b原来指向e的,现在b你改成指向c就行了。e也别指向c了,内存里直接把我e释放掉就行了。
但是寻址不容易,它没有偏移量,这么一个乱七八糟的来回跳的联表,我想找到第100万个数,我怎么算偏移量啊,它在哪我都不知道,我只能一个个的数,数a是第一个,数b是第二个,数c是第三个....数到第100万个,把它拿出来,因为没有办法,他们不靠在一起,我也不知道在哪,只能这么干。
三、一个需求引发的数据结构的讨论
现在有个这样的需求:有一个数组结构,我想调用一个方法sum(arr, L, R)表示从数组中计算从下标L ~ 下标R之间的和,比如输入sum(arr, 1, 4)就是计算数组arr[1] ~ arr[4]之间的和,4 + 2 + 1 + 6,返回13,因为这个方法调用的非常频繁,几十亿次的数量级,所以如果每次调用sum方法去遍历累加计算的话,那效率不高。有没有更好的办法?
设计一个正方形的表格,长和宽为数组的长度n,为n*n的格子。每个横竖坐标对应的L,R,就是该数组对应下标的求和,把这个求和的值填在这个格子里。
如下图所示把这个表格填满。只要填满了一次,那么以后每次计算sum(arr, L, R)的值就方便了,只需要拿出对应格子的数值就可以了。
怎么做?

方案一,草图:




还有一种设计方案:构建help数组:

H数组表示什么含义呢?
H[i] 表示,arr[0] .. arr[i] 一路累加的整体结果

边界条件:

从节省空间上来说,第二种设计方案很巧妙,它就和原数组大小一样大就够了,而第一种设计方案需要一个正方形矩阵,可能会浪费空间。从生成时间上来说,第二种方案也好,因为它只需要遍历一遍就生成好了,以后就拿两个值一减,或者L=0的时候不用减直接返回。而第一种正方形矩阵生成的时候还需要来两个for循环的遍历。
但是要提醒大家的是,如果查询巨频繁,已经频繁到不管哪种方案生成预处理数组都无所谓的地步了,比如数组长度1000,生成正方形矩阵1000 *1000 是百万规模,但是查询十几亿,百亿次,那哪种好?
第一种好,因为第一种是直接拿值给你返回,而第二种还要减一下。所以第二种方案看起来是好,但是一旦查询量巨大无比的时候,查询次数非常非常多的时候,第一种方案其实更好。
代码实现:
package com.cy.class02;
public class Code01_PreSum {
/**
* 如果原始这么算
*/
public static class RangeSum1 {
private int[] arr;
public RangeSum1(int[] array) {
arr = array;
}
public int rangeSum(int L, int R) {
int sum = 0;
for (int i = L; i <= R; i++) {
sum += arr[i];
}
return sum;
}
}
/**
* 方案一 没有..
*/
/**
* 方案二
*/
public static class RangeSum2 {
private int[] preSum;
public RangeSum2(int[] array) {
int N = array.length;
preSum = new int[N];
preSum[0] = array[0];
for (int i = 1; i < N; i++) {
preSum[i] = preSum[i - 1] + array[i];
}
}
public int rangeSum(int L, int R) {
return L == 0 ? preSum[R] : preSum[R] - preSum[L - 1];
}
}
}
四、介绍随机函数
java中的Math.random()函数
package com.cy.class02;
/**
*
*/
public class Code02_RandToRand {
public static void main(String[] args) {
System.out.println("测试开始");
/**
* Math.random() -> double -> [0, 1) 左闭右开 等概率返回一个0.几的数
* 等概率返回,看起来很神奇,因为数学上是做不到0~1等概率返回,而计算机里可以,为什么?
* 因为计算机所有的小数都是有精度的,意味着0~1之间的小数不是无穷多的,是有穷尽的
*/
int testTimes = 10000000;
int count = 0;
for (int i = 0; i < testTimes; i++) {
if (Math.random() < 0.3) {
count++;
}
}
//System.out.println("出现了"+count+"次");
System.out.println((double) count / (double) testTimes); //0.2998836
System.out.println("---------------------------------");
/**
* Math.random() * 8 之后变为:
* [0, 1) -> [0, 8)
* [0, 8)有:
* 0.x
* 1.x
* ..
* 7.x
* 所以小于4的为 0.x ~ 3.x,概率为50%
*
* 表明:Math.random() * 8之后,[0, 8)之间的数也是等概率返回
*/
count = 0;
for (int i = 0; i < testTimes; i++) {
if (Math.random() * 8 < 4) {
count++;
}
}
System.out.println((double) count / (double) testTimes); //0.4999238
/**
* Math.random() * K 转成int之后表示的数范围:
* [0, K) -> [0, K-1]
*
* counts[0]: 0出现的次数
* counts[1]:1出现的次数
* ..
* counts[8]:8出现的次数
*
* 表明:(int) (Math.random() * K)之后,[0, K-1]之间的数也是等概率返回
*/
int K = 9;
int[] counts = new int[9];
for (int i = 0; i < testTimes; i++) {
int ans = (int) (Math.random() * K); // [0, K-1]
counts[ans]++;
}
for (int i = 0; i < K; i++) {
System.out.println(i + "这个数出现了" + counts[i] + "次");
}
}
}
上面代码解释草图,如何理解Math.random() * 8 < 4 出现的概率为50%:

现在问题来了:我们现在知道Math.random()在[0, x)之间出现的概率为x,现在怎么让它出现x²次呢?

package com.cy.class02;
/**
*
*/
public class Code02_RandToRand {
public static void main(String[] args) {
System.out.println("---------------------------------");
count = 0;
double x = 0.7;
for (int i = 0; i < testTimes; i++) {
if (xToXPower2() < x) {
count++;
}
}
System.out.println((double) count / (double) testTimes);
System.out.println(Math.pow(x, 2));
System.out.println("---------------------------------");
count = 0;
x = 0.7;
for (int i = 0; i < testTimes; i++) {
if (xToXPower3() < x) {
count++;
}
}
System.out.println((double) count / (double) testTimes);
System.out.println(Math.pow(x, 3));
}
/**
* 返回[0,1)的一个小数
* 任意的x,x属于[0,1), [0,x)范围上的数出现概率由原来的x调整成x平方
*
* 怎么做到的?
* 是求最大值,max,分别调了两次随机行为,两次随机行为是独立的,怎么才能落到[0,x)范围上?
* 只有两次行为都落在[0,x)上面才能返回[0,x)上,两次都命中这个范围,最终才能返回[0,x)范围上的数,所以是x²
*/
public static double xToXPower2() {
return Math.max(Math.random(), Math.random());
}
public static double xToXPower3() {
return Math.max(Math.random(), Math.max(Math.random(), Math.random()));
}
}
如果上面中的写成了Math.min(Math.random(), Math.random())呢,它在[0,x)上面出现的概率是什么?
1-(1-x)²
演算草图:

五、介绍随机函数
从1-5随机到1-7随机
从a-b随机到c-d随机
01不等概率随机到01等概率随机
需求:
假设f()可以等概率返回1-5之间的随机数。而且不能借助外力随机函数Math.random什么的,只能利用唯一的随机函数f();
在只使用f()的情况下, 如何精巧的使用f()能让1-7之间的数等概率返回?
那么我们想:可不可以让返回值+2呢?
不行,原来是1,2,3,4,5等概率返回,+2了之后只能是3,4,5,6,7等概率返回,得不到1和2等概率返回。
这样做:思路:
可不可以根据f()等概率返回1-5之间的数,能不能改造得到一个0~1之间返回的等概率发生器?比如f()得到1、2的时候我就返回0,f()得到4,5的时候我就返回1,f()得到3的时候就让f()重新来一遍,我就让f()得到1,2,4,5,其他数不让得到。这样就改造成了0,1发生器,而且是等概率的。

有同学会有疑问,为什么等于3的时候重新调用f了,最后返回0,1还是等概率的?演算草图:

既然得到了f2()可以等概率返回0,1发生器。怎么等概率返回1~7之间的数呢?
往前推:怎么得到0~6之间等概率返回呢? 要返回1~7那就0~6再+1就行了。再想:
如果1个二进制位,可以返回0~1
如果2个二进制位,可以返回0~3
如果3个二进制位,可以返回0~7
所以返回0~6需要3个二进制位。三个二进制位上分别调用f2(),都是独立的,那么返回:
000
001
010
...
111
这些数是等概率返回的,也就是0~7一共8个数等概率返回,每个数的概率是1/8
然后遇到7就重新生成,只让他产生0~6的,那么最后就返回0~6之间的数等概率返回喽!

代码实现:
package com.cy.class02;
public class Code02_RandToRand {
public static void main(String[] args) {
// f2():0,1等概率发生器测试
int count = 0;
for (int i = 0; i < testTimes; i++) {
if (f2() == 0) {
count ++;
}
}
System.out.println((double) count / (double) testTimes);
System.out.println("---------------------------------");
counts = new int[8];
for (int i = 0; i < testTimes; i++) {
int num = f3();
counts[num] ++;
}
for (int i = 0; i < 8; i++) {
System.out.println(i + "这个数出现了" + counts[i] + "次");
}
System.out.println("---------------------------------");
counts = new int[8];
for (int i = 0; i < testTimes; i++) {
int num = g();
counts[num] ++;
}
for (int i = 0; i < 8; i++) {
System.out.println(i + "这个数出现了" + counts[i] + "次");
}
}
/**
* f1()为题目中要求的等概率随机返回1~5之间的数。
* lib中的,黑盒,不能改。只能调用。
*/
public static int f1() {
return (int) (Math.random() * 5) + 1;
}
/**
* 随机机制,只能用f1()
* 等概率返回0和1
*/
public static int f2() {
int ans = 0;
do {
ans = f1();
} while (ans == 3);
/**
* 1 2 返回0
* 4 5 返回1
* 根本没有返回3的时候
*/
return ans < 3 ? 0 : 1;
}
/**
* 得到000 ~ 111做到等概率返回
* 0 ~ 7等概率返回
*/
public static int f3() {
return (f2() << 2) + (f2() << 1) + (f2() << 0); //相当于 2² + 2¹ + 2º
}
/**
* 0~6等概率返回
* 调用f3,只要得到7就重做,保证只返回0~6
*/
public static int f4() {
int ans = 0;
do {
ans = f3();
} while (ans == 7);
return ans;
}
/**
* 1 ~ 7 等概率返回
*/
public static int g() {
return f4() + 1;
}
}
推广一下,如果给定f()是3~19之间等概率返回的, 要你实现一个g()做到20~56之间等概率返回,怎么做?
1.先做f()对应的0~1之间发生器f1(),3~10返回0, 12~19返回1,11的时候重做,得到0,1发生器。(如果a~b之间的个数正好是偶数,那就不需要重做了,奇数次需要重做)
2.要20~56之间等概率返回,减去20,得到0~36之间等概率返回,结果+20就行了。
3.0~36需要6个二进制位,6个二进制位表示0~63。让f1()发生器生成6次,得到能返回0~63之间等概率返回。
4.然后当返回的数>36的时候重做。得到0~36等概率返回了。
那就上述中的 从a-b随机到c-d随机 也实现了。
01不等概率随机到01等概率随机,如何实现?
f()函数返回0的概率为P,返回1的概率为1-P;是固定概率,但是不是等概率。这个意思。怎么写出g(),等概率返回0和1?
思路:
将f做两次:
如果得到0 0 不要了,重做
如果得到1 1 不要了,重做
如果得到0 1,返回0 概率是 p * (1-p)
如果得到1 0,返回1 概率是 p * (1-p)

代码实现:
package com.cy.class02;
/**
*
*/
public class Code02_RandToRand {
public static void main(String[] args) {
/**
* x函数以0.84的概率返回0,1-0.84的概率返回1.
* 你只能知道,x会以固定概率返回0和1,但是x的内容,你看不到。
* 也就是你不知道x的内部返回0和1具体的固定概率是多少
*/
public static int x() {
return Math.random() < 0.84 ? 0 : 1;
}
/**
* 等概率返回0和1
* ans = x(); x做第一次,0或1
* ans == x() x做第二次, 0或1
* 如果第二次等于第一次的值,那就重做。
*/
public static int y() {
int ans = 0;
do {
ans = x();
} while (ans == x());
// ans 0 1
// ans 1 0
return ans;
}
}
左神名言:
生活的意义不是一开始赋予的,是你付出了之后赋予的。你吃过苦你就觉得它有意义。就这么回事,虽然有点悲观主义。我是谁是我经历决定的。
--
浙公网安备 33010602011771号