0%

一个人能走得多远并不取决于他顺境时能走的多块,而在于他逆境时能多快找回曾经的自己。——KMP

上面这句话还是蛮能概括KMP算法的精髓,不过貌似动态规划都能这么解释…

KMP

KMP 是由 Knuth、Morris 和 Pratt 所发明的字符串匹配算法,其核心就是当遇到字符不匹配时由于已经知道此前匹配过的字符,因此没有必要重头开始匹配!只要从某个已知字符起匹配即可。

具体做法就是先生成一个前后缀最长相等长度的数组,至于有什么用往下看就行了。

前后缀最长相等长度

这个一长串的名词是什么意思呢,举个例子,str = “123123”,那么它的前后缀最长相等长度就是 3(123 = 123),注意前后缀不能相等。

我们需要生成一个数组,这个数组存放着以 0 开头,以 i-1 结尾的子串的前后缀最长相等长度。

image

arr[0] 因为不存在在 0 之前的子串,因此没有意义,一律填 -1,而 arr[1] 也是固定填0(前后缀不能相等)。

后面需要经由状态转移变化而来,即该位前一位字符和之前的前后缀最长长度上的字符是否相等。

举个特殊的例子,abcabcbc 前一位是 b,而 b 上的前后缀长度为 0,则需要判断 0 位置上的字符和 c 前一位的字符是否相等,a != c,所以填 0。

image

一直直到下标 5,也就是字符 b 所在位置,此时 b 的前一位 a 和该 a 所对前后缀长度为 0 的下标上的 a 相等,这里填 0 + 1 = 1。

image

这里再详细讲一下最后一位是如何填的,下标为 6 的 b,前一位是下标为 5 的 a,而前后缀数组下标 5 上对应的是 2。

这个 2 则代表了蓝色区域等于绿色区域,因此我们只需要判断蓝色区域的下一个和绿色区域的下一个是否相同即可,这就是前后缀数组的强大之处了,这个2不仅代表了最长的前后缀,还代表了蓝色区域的下一位的下标,因为数组下标是从0开始计算的。

image

很自然地,我们判断了蓝色和绿色的下一位相等,那么该位上的前后缀长度就为上一位前后缀长度+1。

image

很明显,9 上的 b 不等于 4 上的 x

image

image

而下标 4 上的前后缀长度为 1,表明了下标 0 和 3 上字符相等,也就可以得到 0,3,5,8上的字符都相等,因此如果我们得到了下标 1 和下标 9 字符相等的话,不就仍然可以得到该位置之前的前后缀长度了吗,为前后缀数组下标为1的数值+1。

image

而如果我们一直迭代,到了0位置上的 -1也仍然没有发现匹配的字符,那么就填入-1 + 1 = 0,表示前后缀长度为 0。

1
2
3
4
5
6
7
8
9
10
11
12
public int[] getPrefix(char[] pattern) {
int[] prefix = new int[pattern.length];
prefix[0] = -1;
for (int i = 2; i < pattern.length; i++) {
int t = i - 1;
while (t != 0 && pattern[i - 1] != pattern[prefix[t]]) {
t = prefix[t];
}
prefix[i] = prefix[t] + 1;
}
return prefix;
}

==以上前后缀数组是求的 pattern 匹配子串的==

子串匹配

母串:abcabcabcb(指针i

子串:abcabcb(指针j

image

前面都没有问题,直到 ij 指向下标 6,a != b,但由于我们是知道前面六个字符是 abcabc 的(==体现在该位置的前后缀长度为 3,具体是什么字符不重要==),没有必要再从头继续匹配。

image

只需要将 j 退回到 3 即可,蓝色为已匹配,红色为待匹配。

若 j 指向0,仍然匹配不成功,i++。

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int indexOf(String str1, String str2) {
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
if (chars2.length == 0) {
return 0;
}
int[] prefix = getPrefix(chars2);
int l1 = 0;
int l2 = 0;
while (l1 < chars1.length && l2 < chars2.length) {
if (chars1[l1] == chars2[l2]) {
l1++;
l2++;
} else {
if (l2 == 0) {
l1++;
} else {
l2 = prefix[l2];
}
}
}
return l2 == chars2.length ? l1 - l2 : -1;
}

时间复杂度:O(n)

空间复杂度:O(n)

设计流程

需求分析

产物:软件需求规格说明书,需要确定用户对软件的需求,要作到明确、无歧义。不涉及具体实现方法。用户能看得明白,开发人员也可据此进行下面的工作(概要设计)

概要设计

产物:软件概要设计说明书,说明系统模块划分、选择的技术路线等,整体说明软件的实现思路。并且需要指出关键技术难点等。

详细设计

产物:软件详细设计说明书,对概要设计的进一步细化,一般由各部分的担当人员依据概要设计分别完成,然后在集成,是具体的实现细节。理论上要求可以照此编码。

一般说来,需求分析属于软件定义方面;而概要设计、详细设计属于软件开发的阶段

软件测试级别

单元测试

单元测试是对程序中的某个接口或者模块进行测试,主要使用白盒测试,由编程人员(主导)和测试人员共同完成。

单元测试的目的是开发人员确定这段子程序做了它应该做的事。

集成测试

集成测试是单元测试的逻辑扩展。它的最简单的形式是:两个已经测试过的单元组合成一个组件,并且测试它们之间的接口。

集成测试的目的旨在测试各个组件间是否能互相配合,正常工作。

集成测试又分为一次性集成方式和渐增式集成方式。

  • 一次性集成方式

    首先对各个单元分别进行测试,然后将所有单元组装在一起进行测试,最终得到要求的软件系统。

  • 渐增式集成方式

    首先对某两三个单元进行测试,然后将这些单元逐步组装成较大的系统。在组装的过程中,一边连接一边测试,以发现连接连接过程中产生的问题,最后完成所有单元的继承,构造为一个完整的软件系统。

系统测试

系统功能测试需要在集成测试完成之后进行,采用黑盒测试,只检查程序功能是否按照需求规格说明书能够正常被使用。包含用户界面、各种操作、不同的数据输入输出和存储等的测试。

系统非功能测试是在实际运行环境或模拟实际运行环境,内容包括负载、性能、压力、灾难等测试。

验收测试

验收测试一般根据产品需求规格说明书严格检查产品,逐行逐字地对照说明书上对软件产品所做出的各方面要求,确保所开发的软件产品符合用户的各项要求。

验收测试方法有正式验收测试,alpha 测试和 beta 测试

  • alpha 测试

    软件开发公司内部人员在模拟实际操作环境下进行的测试。

  • beta 测试

    在 alpha 测试之后,由软件的最终用户们进行试用,公司会要求用户报告异常情况,再对 beta 版本进行修正和完成,最终得到正式发布的版本。

软件测试步骤

软件测试要经过的步骤是:单元测试→集成测试→确认测试→系统测试。

单元测试:对源程序中每一个模块单元进行测试,检查各个模块是否正确实现规定的功能,从而发现模块在编码中或算法中的错误。该阶段涉及==编码和详细设计文档==。

集成测试:是为了检查软件体系结构的有关问题,也就是检查==概要设计==是否合理有效。

确认测试:主要是检查已实现的软件是否满足==需求规格说明书==中的各种需求。

系统测试:是把已确认的软件与其他系统元素(如硬件、其他支持软件、数据、人工等)结合在一起进行测试。以确定软件是否可以支付使用。

逻辑覆盖

逻辑覆盖率:语句覆盖 < 条件覆盖 < 判定覆盖 < 条件判定覆盖 < 条件组合覆盖 < 路径覆盖

语句覆盖

使程序中每个==可执行语句==至少执行一次

image

覆盖条件:

条件1:T,条件3:T

判定覆盖

使得程序中每一个==判断==至少取真一次取假一次

image

覆盖条件:

条件1:T,条件3:F
条件1:F,条件3:T

条件覆盖

使每个的判断中==每个条件==的可能取值至少满足一次

image

1
2
3
4
5
6
7
8
9
10
11
判断表达式1:
设条件 a>0 为真 记T1
假 记F1
条件 b>0 为真 记T2
假 记F2

判断表达式3:
设条件 a>1 为真 记T3
假 记F3
条件 c>1 为真 记T4
假 记F4

覆盖条件:

T1,F2,T3,F4
F1,T2,F3,T4

条件判定覆盖

使每个的判断中==每个条件==的可能取值至少满足一次的同时,还需要让==每个判定==各真假各一次,即条件覆盖 + 判定覆盖。

image

覆盖条件用例:

T1,T2,T3,T4
F1,F2,F3,F4

条件组合覆盖

使所有可能的==条件取值==组合至少执行一次

image

重点:所有条件取值的组合

编号 覆盖条件取值
1 T1,T2
2 T1,F2
3 F1,T2
4 F1,F2
5 T3,T4
6 T3,F4
7 F3,T4
8 F3,F4
覆盖条件 覆盖组合
T1,T2 , T3 , T4 1,5
T1,F2 , T3 , F4 2,6
F1,T2 , F3 , T4 3,7
F1,F2 , F3 , F4 4,8

路径覆盖

是程序中==所有可能==执行的路径都执行一遍

image

继上面的的条件取值表格

覆盖路径 覆盖组合
1-2-4 1,5
1-2-5 1,8
1-3-4 4,7
1-3-5 4,8

性能测试

负载测试(Large amount of users)

负载测试是一种性能测试,指数据在超负荷环境中运行,程序是否能够承担。 关注点:how much

压力测试(Too many users, too much data, too little time and too little room)

压力测试(又叫强度测试)也是一种性能测试,它在系统资源特别低的情况下软件系统运行情况,目的是找到系统在哪里失效以及如何失效的地方。

容量测试(Large amounts of data)

确定系统可处理同时在线的最大用户数 关注点:how much(而不是how fast) 容量测试,通常和数据库有关,容量和负载的区别在于:容量关注的是大容量,而不需要关注使用中的实际表现。

其中,容量测试、负载测试、压力测试的英文解释为:
Volume Testing = Large amounts of data
Load Testing = Large amount of users
Stress Testing = Too many users, too much data, too little time and too little room

负载测试和性能测试的主要区别在于负载测试时,系统负载是逐渐增加的,而不是一步到位,负载测试需要观察系统在各种不同的负载情况下是否都能够正常工作。

边界值测试

基本边界值测试

测试用例数:4n + 1

image

健壮边界值测试

测试用例数:6n + 1

image

最坏情况边界值测试

测试用例数:$5^n$

image

健壮最坏情况边界值测试

测试用例数:$7^n$

image

因果图

image

image

位运算

位运算是程序设计中对位模式按位或二进制数的一元和二元操作。

一般情况下,位运算和加减运算的运算速度相当,且极大程度上快于乘除运算(10倍)。

很多编程语言都有位运算符,Java语言也不例外。在Java语言中,提供了7种位运算符,分别是 &(与)、|(或)、^(异或)、\~(非)、<<(左移)、>>(带符号右移)和>>>(无符号右移)。这些运算符当中,仅有 ~ 是单目运算符,其他运算符均为双目运算符。

位运算只能对 longintshortbytechar 这五个整型类型数据进行运算,不能用于 double、float、boolean 以及其他 引用类型。

一、按位与运算

按位与运算符的写法是一个 & 符号,规则如下。

image

运算规则总结成一句话就是:如果两个二进制位上的数都是1,那么运算结果为1,其他情况运算结果均为0。下面举例说明按位与运算符的运算过程,我们用数字5和6进行按位与运算。这个过程可以用下图表示:

image

可以使用与运算来判断一个数是否是奇数。

1
2
3
4
public static void main(String[] args) {
System.out.println((788 & 1) == 1); // false
System.out.println((789 & 1) == 1); // true
}

二、按位或运算

按位或运算符的写法是一个 | 符号,规则如下。

image

运算规则概括成比较好记的一句话就是:两个二进制位上的数字如果都为0,那么运算结果为0,否则运算结果是1。同按位与运算一样,符号位也要参与运算。下面我们还是用5和6为例来讲解一下按位或的运算过程,如下图所示:

image


三、按位异或运算【重点】

按位或运算符的写法是一个 ^ 符号,规则如下。

image

如上图,运算规则为:两个二进制位上的数字如果相同,则运算结果为0,如果两个二进制位上的数字不相同,则运算结果为1。下面我们还是用5和6为例来讲解一下异或的运算过程,如下图:

image

异或运算的性质

$I$、异或运算满足交换律。

也就是说,a ^ b 与 b ^ a是等价的,虽然 a 和 b 交换了位置,但还是会运算出相同的结果。这个规律还可以推广到 N 个操作数,也就是说,如果有N个变量都参与了异或运算,那么它们的位置无论如何交换,运算的结果都是相同的。

$II$、任何两个相同的数字进行异或运算,所得到的结果为 0。

因为两个相同的数字,换算成补码后,每个二进制位上的数也都相同,这样在进行异或运算时,按照运算规则,每个二进制位上得到的运算结果也都是 0,这 N 个 0 所组成的二进制串就是数字 0 的补码。

  • 可以判断两数是否相等:a ^ b = 0 -> a = b。
  • 快速清零:a ^= a。

$III$、对于任意一个二进制位来说,这个位上的数与 0 进行异或运算,运算结果不变。

0 ^ 1 = 1,0 ^ 0 = 0,结果不变。

$IV$、对于任意一个二进制位来说,这个位上的数与 1 进行异或运算,运算结果取反。

1 ^ 0 = 1,1 ^ 1 = 0,结果取反。

$V$、对于任意两个整数 a 和 b,a ^ b ^ a = b 。

a ^ b ^ a = a ^ a ^ b = 0 ^ b = b。

这个特性在加密运算方面有着很普遍的应用。我们可以把 a 当作要加密的数据,而把 b 当作密钥。a 异或 b 就是把 a 用密钥 b 进行了加密操作,当需要解密时,仍然以 b 作为密钥,再进行一次异或就实现了解密。


四、按位取反运算

按位或运算符的写法是一个 ~ 符号,规则如下。

image

1
2
3
4
5
6
7
8
public class MyTest {
public static void main(String[] args) {
int a = 5;
int b = ~a;
System.out.println(a); // 5
System.out.println(b); // -6
}
}

首先把数字5转换成补码形式,之后把每个二进制位上的数字进行取反,如果是0就变成1,如果1就变成0,经过取反后得到的二进制串就是运算结果,这个运算结果被还原为十进制数是-6。


五、左移运算符

按位或运算符的写法是一个 << 符号,规则如下。

image

左移运算有乘以2的N次方的效果。一个数向左移动1位,就相当于乘以2的1次方,移动两位就相当于乘以2的2次方,也就是乘以4。位移操作在实际运算时远远快于乘法操作,所以在某些对运算速度要求非常高的场合,可以考虑用左移代替乘以2的N次方的乘法操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 可实际测试下来两者的速度几乎一致
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(int i=0;i<1e9;i++){
int a = i * 1024;
}
System.out.println(System.currentTimeMillis() - start); // 1031

start = System.currentTimeMillis();
for(int i=0;i<1e9;i++){
int a = i << 10;
}
System.out.println(System.currentTimeMillis() - start); // 1022
}

但当位移的位数很多时,导致最左边的符号位发生变化,就不再具有乘以2的N次方的效果了。比如十进制的5转换为补码形式是:前面29个0最后3位是101,如果移动29位,那么最前面的符号位就变成了1,此时运算的结果就成为了一个负数,不再是5乘以2的29次方的乘法结果。

1
2
3
4
5
6
public static void main(String[] args) {
int a = 5;
System.out.println(a << 29); // -1610612736
a = -5;
System.out.println(a << 29); // 1610612736
}

对于 byte/short/int 三种类型的数据,Java 语言最多支持31位的位移运算(包括下面的右移运算)。如果位移数超过31,则虚拟机会对位移数按连续减去32,直到得到一个小于32并且大于等于0的数,然后以这个数作为最终的位移数。

例如对 int 型变量进行位移97位的操作,虚拟机会首先对97连续减去3个32,最终得到数字1,实际进行位移运算时只对变量位移1位。

1
2
3
4
public static void main(String[] args) {
int a = 2;
System.out.println(a << 97); // 4
}

而对于 long 类型的数据而言,最多支持63位的位移运算,如果位移数超过63,则连续减去64,以最终得到的小于64并且大于等于0的数作为位移数。小伙伴们可以试一下数字5左移32位是什么结果。


六、带符号右移运算符

右移运算分为两种,分别是带符号右移和无符号右移。首先我们来说说带符号右移运算符。带符号右移运算符的写法是 **>>**,与左移运算符的方向恰好相反。

image

带符号右移运算和左移运算类似,具有 /2 的功能,小数会被舍弃。

但是对于负数而言,带符号右移的效果分为两种情况,我们分别来讨论。

如果这个负数是“2的N次方”的整数倍,那么带符号右移N位的效果也等于除以2的N次方。举个例子:我们还是把N的值设为3,如果对于“-16”来说,它是“2的3次方”的整数倍,那么带符号右移3位的结果是-2,这个结果相当于“-16除以2的3次方”。

1
2
3
4
5
public static void main(String[] args) {
int a = -16;
System.out.println(a >> 3); // -2
System.out.println(a / 8); // -2
}

而如果这个负数不是“2的N次方”的整数倍,那么右移N位之后,是在除以2的N次方的结果之上还要减去1。比如,对于-15来说,它不是“2的3次方”的整数倍,那么带符号右移3位的结果是-2,这个运算结果其实就是“-15被2的3次方整除再减去1”(减1是因为右移的时候丢弃了位数是 1 的位)。

1
2
3
4
5
public static void main(String[] args) {
int a = -15;
System.out.println(a >> 3); // -2
System.out.println(a / 8); // -1
}

带符号右移的操作可以保证移动之前和移动之后数字的正负属性不变,原来是正数,不管移动多少位,移动之后还是正数,原来是负数,移动之后还是负数。

对于任何 一个byte、short 或者 int 类型的数据而言,带符号右移31位之后,得到的必然是 0 或者是 -1。对于 long 类型的数据而言,带符号右移 63 位之后,得到的也必然是 0 或者是 -1。


七、无符号右移运算符

无符号右移运算符的写法是 **>>>**,带符号右移的运算规则与无符号右移的运算规则差别就在于:无符号右移在二进制串移动之后,空位由0来补充,与符号位是0还是1毫无关系,如下图:

image

进行位运算的时候,最左边的符号位也是要参与运算的。

1. 准备工作

  1. 一张 TF 卡(class10)

  2. Raspberry Pi OS:https://www.raspberrypi.com/software/operating-systems

  3. 分区软件:DiskGenius

  4. 烧写系统软件:Win32DiskImager

2. 远程连接

2.1 烧写系统

如果原本 TF 卡中有分区,可以用 DiskGenius 格式化在创建一个新分区。

image

然后启动 Win32DiskImager 选择之前在官网上下载的镜像文件烧写到 TF 卡中。

image

注意,Raspberry 默认是不开启 SSH 服务的,需要在 TF 卡中创建一个名为 SSH 的文件,就能开启了!

image

之后将 TF 卡插到树莓派上就能正常开机了,但因为没有画面,我们先要给树莓派连上以太网,然后查询 IP,这样通过刚刚开启的 SSH 服务远程连接我们的树莓派了!

2.2 配置数据源

连接后先配置数据源,改为清华源(国外的源很慢)。

1
2
3
4
5
6
7
8
9
sudo nano /etc/apt/sources.list
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib rpi
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib rpi

sudo nano /etc/apt/sources.list.d/raspi.list
deb http://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui

# CTRL+O 保存 -> ENTER 确认 -> CTRL+X 退出

更新数据源。

1
2
sudo apt-get update
sudo apt-get upgrade

安装 xrdp 实现 Windows 远程访问。

1
sudo apt-get install xrdp

3. 网络配置

3.1 ping 通树莓派

在不知道树莓派 ip 的情况下,在 windows 的 cmd 窗口中输入

1
ping raspberrypi.local

即可得到树莓派的 ip 地址。

(实际上在 XShell 的 ip 地址上直接输入 raspberrypi.local 就能连接上树莓派)

3.2 配置网卡

修改 /etc/dhcpcd.conf,修改之前先备份!!!

1
2
3
4
5
6
7
8
9
10
# 其中eth0是有线网卡,wlan0是无线网卡。
interface eth0
static ip_address=192.168.1.88/24
static routers=192.168.1.1
static domain_name_servers=114.114.114.114

interface wlan0
static ip_address=192.168.1.89/24
static routers=192.168.1.1
static domain_name_dervers=114.114.114.114

通过这个文件将有线网卡和无线网卡都配置上了静态ip。

但这样还不够,还要修改/etc/network/interfaces文件,在这个文件后面添加如下几行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 自动开启loopback
auto lo
iface lo inet loopback

# 手动开启eth0
iface eth0 inet manual

# 自动开启wlan0
auto wlan0
allow-hotplug wlan0

iface wlan0 inet static
wpa-ssid "wifi_name"
wpa-psk "password"
address 192.168.1.89
netmask 255.255.255.0
gateway 192.168.1.1
network 192.168.1.1
iface default inet dhcp

然后重启树莓派即可。

1
sudo reboot

修改完静态 ip 后,就可以使用 XShell 通过新设置的静态 ip 连接树莓派了。