finish kmp.md.

This commit is contained in:
Shine wOng
2019-10-02 16:45:50 +08:00
parent f1d3094034
commit 81edc0cf86
3 changed files with 106 additions and 6 deletions

View File

@@ -27,7 +27,7 @@ kmp conclusion
应该注意到,采用`kmp`策略时,每次移动的距离只与模式串`P`有关,而与文本串`T`无关。这是因为在第`k`个位置失配后,文本串中的这`k - 1`个字符和模式串长度为`k - 1`的前缀完全相同。因此所谓“值得”对齐的位置,其实就是这`k - 1`字符构成的串,前缀和后缀自相匹配的位置。
需要指出的是,这样的位置可能有多个,所有的这些位置都是“值得”的对齐位置,因此,为了不错过其中的任意一个对齐位置,移动距离应该取所有这些自匹配位置中最小的,也就是自匹配长度最长的。为了在`kmp`算法运行过程中,迅速更新串的对齐位置,可以对模式串`P`做预处理,将在第`k`个字符处匹配失败的最长自匹配长度,保存在`next[k]`中,以便于查询。这样,就可以实现`kmp`算法了:
需要指出的是,这样的位置可能有多个,所有的这些位置都是“值得”的对齐位置,因此,为了不错过其中的任意一个对齐位置,移动距离应该取所有这些自匹配位置移动距离中最小的,也就是自匹配长度最长的。为了在`kmp`算法运行过程中,迅速更新串的对齐位置,可以对模式串`P`做预处理,将在第`k`个字符处匹配失败的最长自匹配长度,保存在`next[k]`中,以便于查询。这样,就可以实现`kmp`算法了:
```cpp
int match(char* text, char* pattern){
@@ -47,3 +47,103 @@ int match(char* text, char* pattern){
可以看到,如果匹配成功,则同时移动文本串和模式串的指针;一旦匹配失败,就将模式串的指针移动到`next[j]`,即实现上面所说的快速移动。需要注意的是对`j < 0`情况的处理,此时相当于在模式串的左边具有一通配符,即`pattern[-1] = *;`,它可以匹配任何的字符,这样就可以将该情况与匹配成功做相同的处理。
这样,现在的主要问题就是如何构造这样一个`next`数组,即实现上面的`makeNext`函数。
## next的构造
在前面已经指出了`next`数组的语意,即对于其中的第`i`项,`next[i]`表示模式串在第`i`个位置失配时,模式串的下一个对齐位置。设子串`K = P[0, i-1]`,该位置应该满足:
```c
K[0, next[i] - 1] = K[i - next[i] ,i - 1]
```
所以`next[i]`的值也等于字符串`K`的最长自匹配长度。因此,构造任意一个`next[i]`,其实就是找到`P`的一个子串`P[0, i-1]`的最长自匹配长度
为了高效地构造出`next[i]`,不难注意到`next[i]`的值其实在一定程度上依赖于`next[i-1]`的,这是因为如果`P[i-1] = P[next[i-1]]`,即在子串`P[0, i-2]`的最长自匹配,可以直接延伸到子串`P[0, i-1]`,所以有
```c
next[i] = next[i - 1] + 1;
```
可是如果不满足`P[i-1] = P[next[i-1]]`,则上一个位置的最长自匹配长度将在新的位置断裂,此时必有`next[i] <= next[i-1]`,所以只能到子串`P[0, next[i-1]]`中去寻找新的最长自匹配长度。由于`P[0, next[i-1]] = P[i-2-next[i-1] ,i-2]`,因此`next[next[i-1]] + 1``next[i]`的下一个候选值,如果有
```c
P[i-1] == P[next[next[i-1]]];
```
则有
```c
next[i] = next[next[i - 1]] + 1;
```
否则应该重复上述过程,直到条件的确满足,或者不存在这样的一个自匹配。此时应该令`next[i] = -1`,表示与模式串左边假想的通配符`P[-1] = *`是自匹配的。因此,构造`next`表的代码如下:
```c
int* makeNext(char* str){
int* next;
int len, i = 0, j = -1;
for (len = 0; str[len] != '\0'; ++len);
next = new int[len];
next[0] = -1;
while(i < len - 1){
if(j < 0 || str[i] == str[j]) next[++i] = ++j;
else j = next[j];
}
return next;
}
```
应该看到,`next`表的构造的代码非常类似于`kmp`算法,这是因为`next`的构造本质上就是模式串的自我匹配,这个过程就是利用`kmp`的思想实现的。
可以证明,`kmp`算法的时间复杂度是`O(m + n)`。证明如下:令`k = 2i - j`,在每一次迭代中,要么执行`i++, j++`,要么执行`j = next[j]`,其中`next[j] < j`,因此每次迭代中`k`都至少增加1`k`的最大值不过为`2n`,即循环至多进行`O(n)`次,每一次的时间复杂度都是`O(1)`,故总的时间复杂度为`O(n)`
## kmp的改进
上面的`kmp`算法还存在一些缺陷,例如下面的这种情况:
![kmp_improvement](kmp_improvement.png)
可以注意到,在文本串的`1`这个位置,统共需要进行四次比对,然而后面的三次比对都是无意义的,因为模式串的四个字符都是`0`,而在第一次失败的比对中我们就可以知道,文本串的该字符并不是`0`,因此聪明的做法应该是直接跳过这三个不可能会成功的比对。
对上述的问题进行总结,目前的`kmp`算法的确充分利用了先前成功比对的`经验`,来快速移动到下一个值得的对齐位置;而它的问题在于忽略了比对失败的`教训`,而这就是它在这里低效的原因。
为了对`kmp`进行改进,可以在构造`next`数组时,就将这种失败的负面信息纳入考虑。一般地,对于此前的`next[i]`,必然有
```c
P[0, next[i] - 1] == P[i-next[i] ,i-1]
```
但是如果
```c
P[next[i]] == P[i]
```
那么这里的`next[i]`就是没有意义的,因为将模式串的对齐位置到`next[i]`后,下一次比对一定是失败的。因此,为了解决这个问题,只需在构造`next`数组时,对这种情况加以判断,具体的代码如下:
```c
int* makeNext(char* str){
int* next;
int len, i = 0, j = -1;
for (len = 0; str[len] != '\0'; ++len);
next = new int[len];
next[0] = -1;
while(i < len - 1){
if(j < 0 || str[i] == str[j]){
++i, ++j;
next[i] = (str[i] == str[j]? next[j]: j);
}
else
j = next[j];
}
return next;
}
```
需要注意的是,之所以在`P[next[i]] == P[i]`时,可以直接将`next[i]`更新为`next[next[i]]`,而不需要做额外的比对以确定是否满足
```
P[i] == P[next[next[i]]
```
是因为在此前的循环中,已经判断过`P[next[i]] != P[next[next[i]]]`了。

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -26,7 +26,7 @@
## 死锁的解决
可以从多个方面来解决死锁问题。比如说,我可以防患于未然,一开始就从原理上完全杜绝死锁的出现,即`死锁预防`;或者可以退一步,在临了要分配资源的时候,对整个系统的状态进行检查,看如果分配了这个资源是否会出现死锁,如果的确会,就拒绝分配资源,这就是`死锁避免`;与预防措施相对,我也可以提前做好预案,即允许死锁的发生,在死锁发生后再去对系统进行调控,从而恢复正常的状态,即`死锁检测与恢复`。下面就分讨论各种措施。
可以从多个方面来解决死锁问题。比如说,我可以防患于未然,一开始就从原理上完全杜绝死锁的出现,即`死锁预防`;或者可以退一步,在临了要分配资源的时候,对整个系统的状态进行检查,看如果分配了这个资源是否会出现死锁,如果的确会,就拒绝分配资源,这就是`死锁避免`;与预防措施相对,我也可以提前做好预案,即允许死锁的发生,在死锁发生后再去对系统进行调控,从而恢复正常的状态,即`死锁检测与恢复`。下面就分讨论各种措施。
### 死锁预防
@@ -58,12 +58,12 @@
为了找到这样的一个安全序列,需要各个进程告知操作系统其最大的资源需求量。在一个进程请求资源时,假想地将资源分配给该进程,随后对所有占有资源的进程做一次遍历,找到第一个这样的进程,它的资源需求量小于操作系统的资源剩余量,这样该进程一定可以成功执行完毕,然后将它当前占有的资源全部归还给操作系统,此时操作系统的资源剩余量就可以加上该进程的资源占有量。重复上面的过程,直到所有进程都遍历完毕,则的确存在这样一个`安全序列`,或者某一次遍历中找不到一个这样的进程,表示系统当前处于`不安全状态`
上述算法类似于银行家在向多个客户放贷的时候,采取的借贷分配策略,因此称之为`银行家算法(banker's algorithm`。下面给出`银行家算法`的算法描述:
上述算法类似于银行家在向多个客户放贷的时候,采取的借贷分配策略,因此称之为`银行家算法(banker's algorithm)`。下面给出`银行家算法`的算法描述:
设系统中存在`n`个进程,`m`类资源,设置一个`m`维列向量`available`,表示操作系统中各类资源的剩余数量;为了表示各个进程对每个资源的占用情况,设置一个`n x m`的矩阵`allocation`,其中第`i`行表示第`i`个进程对`m`个资源的占用情况;此外还设置两个`n x m`的矩阵`need``max`,分别表示每个进程对各个资源的需求量和最大需求量,容易看出,
```c
need[i, j] = max[i, j] - allocation[i, j]
need[i, j] == max[i, j] - allocation[i, j]
```
当一个进程请求资源时,设置`m`维列向量`request`,表示对各个资源的请求量,算法流程如下:
@@ -73,7 +73,7 @@ need[i, j] = max[i, j] - allocation[i, j]
+ 更新`availble -= request``allocation[i] += request, need[i] -= request`,即假想将资源分配给该进程。
+ 遍历所有的进程,直到发现第一个进程,满足`need[i] < available`,将该进程标记为`finish`状态,表示加入`安全序列`中,更新`available += allocation[i]`。重复该过程,直到全部进程都加入了`安全序列`中,返回`安全状态`;否则,如果没有找到满足条件的进程,则返回`不安全状态`
`银行家算法`可以看出,`不安全状态`并非就一定会发生死锁,实际上,死锁只是不安全状态的一个真子集。这是因为`银行家算法`在判断`不安全状态`的时候,总是从最坏的打算出发,即每个进程只有在得到它所要求的全部资源时,才会执行完毕并归还资源给操作系统。实际上,进程未必会申请它告知系统的最大需求量,而在进程执行的过程中,也会因为某些资源使用完毕就归还给操作系统了。
`银行家算法`可以看出,`不安全状态`并非就一定会发生死锁,实际上,死锁只是不安全状态的一个真子集。这是因为`银行家算法`在判断`不安全状态`的时候,总是从最坏的情况出发,即每个进程只有在得到它所要求的全部资源时,才会执行完毕并归还资源给操作系统。实际上,进程未必会申请它告知系统的最大需求量,而在进程执行的过程中,也会因为某些资源使用完毕就归还给操作系统了。
### 死锁检测与恢复
@@ -87,7 +87,7 @@ need[i, j] = max[i, j] - allocation[i, j]
即直接将发生死锁的进程终止。最简单暴力的是把所有死锁进程给终止了,得劲是挺得劲的,这个开销未免太大。
柔和一点的就是一次终止一个进程,直到死锁被解除。所以这里就涉及到应该选择进程被终止的问题,一般说来,总是选择优先级较低的,已经运行时间较短的,以及后台的批处理进程(交互进程留下),此外,还要考虑进程占有的资源数量,进程完成需要的资源,以及终止的进程数量越少越好。进程终止策略其实就是类似于我被清华的面试老师直接赶出了面试教室。
柔和一点的就是一次终止一个进程,直到死锁被解除。所以这里就涉及到应该选择进程被终止的问题,一般说来,总是选择优先级较低的,已经运行时间较短的,以及后台的批处理进程(交互进程留下),此外,还要考虑进程占有的资源数量,进程完成需要的资源,以及终止的进程数量越少越好。进程终止策略其实就是类似于我被清华的面试老师直接赶出了面试教室这种情形
> 资源抢占策略