跳到主要内容

string

在计算机上的非数值处理的对象基本上是字符串数据。字符串是我们最常用的对象。但是,它在计算机底层又是怎么实现的呢?

字符串(string):由零到多个字符组成的有限序列。

$$s = 'a_1 a_2 .... a_n' [a≥0]$$

字符串的数据结构(5个基本操作)

package linear;

/**
* 字符串接口
* @author will
*/
public interface IString {
/**
* 生成一个由chars组成的字符串
* @param chars
* @return
*/
IString assign[char[] chars];

/**
* 对比两个字符串,返回 this - st
* @param st
* @return
*/
int compare[IString st];

/**
* 字符串的长度
* @return
*/
int length[];

/**
* 字符串拼接
* @param st
* @return
*/
IString concat[IString st];

/**
* 子字符串
* @param start
* @param length
* @return
*/
IString subString[int start_index, int length];

/******字符串操作最小集:以上五个5个******/


IString copy[IString str];

boolean isEmpty[IString str];

/**
* 清空字符串
* @return
*/
boolean clear[];

/**
* 返回st在this从start_index开始第一次出现的位置
* @param st
* @param start_index
* @return
*/
int index[IString st, int start_index];

/**
* 用new_str替换所有在this出现的old_str
* @param old_str
* @param new_str
* @return
*/
IString replace[IString old_str, IString new_str];

IString insert[IString str, int index];

IString delete[int start_index, int length];

boolean destroy[];
}

字符串的表示和实现

高级语言里面随便使用的字符的底层实现其实是很复杂的,有多种方式。我们应该去了解。

定长顺序存储表示

用一组地址连续长度固定的存储单元存储字符序列。

它会预先分配一定长度的空间,用特殊字符标记结尾。要是长度大于预分配的空间长度则会被截取。

堆分配存储表示

用一组连续的存储单元存放字符序列,但是空间长度动态分配。

C语言中使用malloc[] 和 free[] 来管理堆存储区,故得名。 在字符串操作时,当发现原始的存储空间不够用,先释放原始的存储空间,再申请更大的存储空间。

块链存储表示

一到多个字符为一个节点,多个节点链接表示字符序列。

这种存储方式需要的是:当字符串特别长时,我们需要考虑存储密度。

$$存储密度 = \frac{字符串所占存储位} {实际分配的存储位}$$

当每个节点只有一个字符时,运算最简单,但是存储占用过大。

字符串的模式匹配

子串的定位操作通常称为字符串的模式匹配,其中,被比较的字符串叫做模式串。

java代码:

public int index(IString pattern, int start_index) {
int i = start_index;
int j = 0;
while(i < this.length() && j < pattern.length()){
if(this.subString(i, 1) == pattern.subString(j, 1)){
j++;
i++;
}else{
i = i - j + 1;
j = 0;
}
}

if(j == pattern.length() - 1){
return i - pattern.length();
}

return -1;
}

代码的逻辑很简单,主串和模式串进行比较,从主串[start_index]全匹配模式串,要是有不匹配的 start_index 累加,继续全匹配。 对于"ABCDEFG" 匹配 "CDEF"这样的效率还可以,但是对于 "0000000001" 匹配 "00001",最坏情况时间复杂度为O[n*n]。我们可以进行改进。

KMP子字符串匹配算法

KMP算法:每当一趟匹配出现字符不匹配的情况,不直接回溯start_index到 start_index + 1, 而是根据已经得到的部分匹配,尽可能的滑行到 start_index + n。

看定义和含糊,我们看一个例子: 假设主串s是:

s1s2s3s4s5s6s7s8s9s10s11s12s13s14s15s16s17
acabaabaabcacaabc

模式串p是:

p1p2p3p4p5p6p7p8
abaabcac

其中一次比较时:

|-|s1|s2|s3|s4|s5|s6|s7|s8|s9|s10|s11|s12|s13|s14|s15|s16|s17| |-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-| |s_i|a|c|a|b|a|a|b|a|a|b|c|a|c|a|a|b|c| |-|-|-|p1|p2|p3|p4|p5|p6|-|-|-|-|-|-|-|-|-| |p_j|-|-|a|b|a|a|b|c|-|-|-|-|-|-|-|-|-| |滑|-|-|-|-|-|a|b|a|a|b|c|-|-|-|-|-|-|

可见 i = 8, j = 6 时失配了。要是使用前面讲的方法进行暴力求解,i = 4, j = 1 继续进行。但是,我们经过观察发现:

k表示当模式中第j个字符与主串相应字符失配时,模式串中需要重新和主串中该字符进行比较的字符的位置。

我们尝试将p1...p6向右移动n位使得p[1]...p[6-n]与s[3+n]...s8相等,匹配就可以重新开始了。p1...p6向右滑动3位(如图)s6...s8就和p1...p3相等,所有的比较就可以从i = 8,j = (6 - 3) = 3开始比较。 有:k = p.next(s_sub, j)。又因为s3...s8 == p1...p6 恒成立,既s_sub = p,所以我们用模式本身就可以求出k,有 k = p.next(j)。

KMP算法中i一定不回溯,它就为8,需要移动的是模式串,将它向右滑动到适当的位置使得比较从i = 8,k = j = p.next(j)开始。

我们的所有的注意力都在k = p.next(j)的逻辑上:

  • 当 j = 1,k = 0 :当模式中第一个字符直接不匹配时为0;
  • 当存在p1...p[k-1] == p[j-k+1]...p[j-1]时,k为其最大值:当p1...p[k-1] == s[i-k+1]...s[i-1]存在时,我们只需要主串从i,模式串从k开始比较。而s[i-k+1]...s[i-1] == p1...p[k-1],故得出;
  • 其他情况,k = 1:没有字符串对齐情况,从第一位开始匹配。

比较时,当出现失配的情况,i不变,k = p.next(j):

  • k = 0 时:i++,j++。即主串的第i个字符和模式串的第1个字符不匹配,主串从第i+1个字符串开始匹配;
  • 其他:j = k 继续匹配。