串和字符串
串是由零个或多个单独的元素组成的有限长序列.
在计算机中,串的最广泛的用处是字符串,因此一般情况下,串和字符串是等价的,字符串也简称为串,串就是字符串
串的结构
串实际上是一个特殊的数组,它的元素一定是字符类型的,因此他也具有数组所拥有的特性
读取字符串中的一个字符的时间复杂度是O(1),因为可以直接使用地址准确定位,修改字符串当中的一个字符也非常快,但是字符串无法动态地延长或减短,因为数组的长度是固定的
实际上在C语言中,字符串是一个char[]类型的变量,并且以“\0”为结尾,你可以通过修改“\0”的位置来增长或减短字符串,但是这只是一个停止标志,它所占用的空间仍然是不变的,如果你把“\0”移动到数组外面,那么系统会把本不属于它的内存读进去,造成显示异常
在更多的语言中,字符串并不是一个单纯的数组,而是一个构造复杂的类,其中包括了一个数组.并且字符串一旦被创建就再也无法修改,你只能在它的基础上构造新的字符串
子串
由串中任意个连续字符所组成的新字符串,称为原字符串的子串,例如“345”是“123456”的子串,同时任意字符串总是自己的子串
串的储存
堆存储
这种存储方法的特点是,字符串以一维数组的方式存放在堆中,但是数组长度并不固定,而是视字符串长度改变
1 | class HString{ |
块链存储
如果字符串很长,以至于在逻辑地址空间上找不到一个足够长的连续空间来存放字符串,就会导致程序异常.块链存储的思想是把字符串切割为多个更小的子串分开存放,这样就可以充分利用内存中的碎片,只要内存足够,就不会出现无法分配的问题
在下面的代码中,我们以4个字符为一组切割字符串
1 | //一个存储块存放4个字符 |
模式匹配算法
算法思想
模式匹配是一个查找子串的过程
查找子串的思路是,将原字符串的第一个字符与子串的第一个字符相比较,如果相同,则比较原字符串和子串的第二个字符,否则将子串位置后移一位,比较原字符串的第二个字符与子串的第一个字符
这种方法有一个缺点,假设原字符串和子串如下
1 | string ori = "1231234"; //原字符串 |
当比较到第4个位置时,发现两者不同,于是子串后移一位
1 | 第一次 |
子串共移动了四次,并且每次都会从头开始比较,而实际上这是不必要的,因为我们知道子串的前三项互不相同,所以第二次和第三次移动是多余的
算法改进
假设子串为“ABABC”,当匹配到第4个字符“B”时发现不一致,这就说明前面3个字符一定是一致的,即原字符串的前4位可能是“ABAC”,所以我们知道原字符串的第3位一定是“A”,而子串的第1位也是“A”,那么就可以跳过这个“A”
跳过“A”的方法是将子串的指针直接向后移动,我们可以设置一个 next 数组,用来存放当前字符不匹配时,指针应该指向子串的第几个字符
i 表示原字符串内的指针,j 表示子串内的指针,i 和 j 同时从0开始递增,其中 i 会永远加下去,而 j 一旦遇到不同就会回退
以“ABABC”为例
- next[0]=-1,因为子串的第一位就不匹配时,下次肯定是也是从子串的第一位开始匹配,并且原字符串的指针 i 也要跟着后移一位,-1用于标记这种情况,没有其它实际含义.下面的四种情况里,都是 j 在移动,而 i 不动.i 只在匹配到相同字符时才会后移一位
- next[1]=0,因为子串的第二位不匹配时,说明原字符串是“A?”,要从第一位开始匹配,而原字符串的指针 i 不动
- next[2]=0,因为子串的第三位不匹配时,说明原字符串是“AB?”,要从第一位开始匹配,同理 i 也是不动
- next[3]=1,因为子串的第四位不匹配时,说明原字符串是“ABA?”,问号前面的字符“A”恰好是子串的第一个字符“A”,所以我们不需要再次比较,只需要比较子串的第二个字符
- next[4]=2,因为子串的第五位不匹配时,说明原字符串是“ABAB?”,问号前面的两个字符“AB”恰好等于子串的开头两个字符“AB”,那么我们就不需要比较这两个字符,直接从子串的第三个开始
于是我们得到next数组: {-1,0,0,1,2}
下面编写查找该子串的代码
1 | char ori[] = "ABABDBFABABABCCA"; |
如果代码正确,那么应该会打印“9”
next数组
这个算法的关键在于next数组
同样以“ABABC”为例
- next[0]=-1,理由与上面的一致
- 从字串的第二个开始,需要判断子串中是否存在相同子串,例如“ABABC”中就出现了两次完全一致的“AB”,那么下次“AB”出现时我们就知道要如何跳过了,假设子串的第5个字符“C”出现了不匹配,那么我们只需要把它指向“AB”第一次出现的位置的后一位,也就是 next[4]=2,这样下次就不用重复匹配“AB”字符了
由此我们发现计算next数组的关键在于寻找重复子串,而这实际上又是一个模式匹配过程,只不过并没有现成的子串给我们查找,而是需要我们自己发现子串,这个结论将会在下面用到
以“ABABC”为例,原字符串和子串都是“ABABC”,i 和 j 同时从 0 开始,如果ori[i] == sub[j],说明找到了某个相同子串
1 | i |
那么我们就得到下面结论
1 | //如果下一个字符不匹配,那么把它指向第一个重复子串的后一位 |
同时我们还要把 i 和 j 后移一位,以继续匹配下一个字符
现在 i 和 j 都已经后移一位,我们遇到了下面的情况: ori[i] != sub[j]
1 | i |
这时需要把 j 前移,重新开始比较前面的字符,但是要前移到哪个位置?
实际上,通过上述步骤,我们可以得到下面两个结论
1.模式匹配用到的的next数组仅和子串有关,与原字符串无关
2.计算next数组的过程也是一次模式匹配
得到第一个结论很方便,因为我们在分析“ABABC”的next数组时根本就没有用到原字符串,第二个结论上面已经做过解释
于是我们就得到另一个结论
当 ori[i] != sub[j] 时:
1 | j = next[j] |
我们在 ori[i-1] == sub[j-1] ,也就是上一步时,已经得到了 next[j] 的值,而next数组就是子串遇到不匹配时,j 应该指向的位置,所以我们可以直接使用
现在我们已经有了完整的计算next数组的函数
1 | int* GetNext(char* str, int length){ |
完整代码
如果程序正确,下面的代码将打印结果:30
1 | int* GetNext(char* str, int length){ |