一、简介
字符串或者说串(String)是由数字、字母。下划线组成的一串字符。一般可以记为s="a0a1a2a3...an" (n>=0并且n是有限非负整数)。
从数据结构上来看,用c++来说,字符串是一种特殊的线性表,也就是里面的每个元素都是字符的一种线性表。可以是用数组实现,或者链表实现。具体的优缺点可以参照数组和链表的优缺点。
二、c++中的字符串string
而在c++中的string(头文件为string),其中保存的变量的char *,也就是一个不定长的字符数组,因为它重载了[]运算符,可以像数组一样去用下标访问元素;也可以说是一个链表,因为本质就是指针操作。但是其实内部实现是根据大小去调整string的大小的。我先贴出关于常用的一些string函数的声明:(没有考虑空间申请失败的情况)
#include <iostream>
using namespace std;
class String {
private :
char *s_data;
int size;
public :
//构造函数和析构函数
String();
String(const String &);
String(const size_t, const char);
~String();
//一些属性函数
size_t length(); //返回长度
bool empty(); //是否为空
const char *c_str(); //返回指向开头的char指针
//运算符重载
friend String operator+ (const String &, const String &);
friend String operator+ (const String &);
friend String operator== (const String &, const String &);
friend String operator!= (const String &, const String &);
friend String operator< (const String &, const String &);
friend String operator<= (const String &, const String &);
friend String operator>= (const String &, const String &);
friend String operator> (const String &, const String &);
char &operator[] (const size_t);
String &operator= (const String &);
//一些串操作
String substr(size_t, size_t); //返回两个size_t间的子串
String& append(const String&); //添加
String& insert(size_t, const String&); // 插入
String& assign(const String&, size_t, size_t); //替换
String& erase(size_t, size_t); //删除
};
其实实现也不是很难的,我觉得比较核心的是关于长度的变换这一部分,所以我只贴出构造函数和+号重载的实现:
String::String() {
Length = 0;
s_data = NULL;
}
String::String(const size_t length, const char c) {
this -> Length = length;
s_data = new char[length + 1]; //加一的目的是为了在最后添加一个结束标志\0
s_data[length] = '\0';
strset(s_data, c);
}
String::String(const String& str) {
Length = str.Length;
s_data = new char[Length];
strcpy(s_data, str.s_data);
}
String operator+(const String& s1, const String& s2) {
String s;
s.Length = s1.Length + s2.Length;
s.s_data = new char[s.Length + 1];
strcpy(s.s_data, s1.s_data);
strcat(s.s_data, s2.s_data);
return s;
}
其中上面涉及的str开头的函数,是C标准库里面的<string.h>或者<cstring>里面的函数,是对char数组进行的一系列操作,所以string实际上是为了把char数组变成一种更加方便使用的一种对象,通过重载操作符,能做到像int,float这种数据类型的一些操作,同时又保留着char数组下标访问的特性,能直接用s[i]的形式去访问某一元素,而且也是在常数时间内就完成的。所以用完string,会有一种不再想用char*的感觉。
不过话说回来,如果对<string.h>里面的一些函数不了解的话,我建议先回去把这部分的学一学,如果需要的话,我可以把<string.h>的自己实现的一个头文件给你参考一下= =(你不嫌弃的话)。
三、字符串的模式匹配
模式匹配(Pattern matching)
-一个目标对象T(字符串)
-模式P(字符串)
在目标T中寻找一个给定的模式P的过程
例如:文本编辑时的特定词、句的查找;DNA信息的提取等等
简单来讲,就是给定你一大段字符串,然后查看里面是否存在某个子串,例如"abc"。
解决匹配问题的算法:朴素算法(Brute Force)和KMP算法(Knuth-Morrit-pratt)等等
*朴素算法
例如给定一个字符串 T="abcdabcdabcdef"
然后寻找在T中是否存在一个模式P=“abcdef"
所以朴素算法是指在T中一位一位得开始寻求匹配,例如第一步是:
abcdabcdabcdef
abcdef
这时候发现不匹配,然后就继续从T的第二位开始寻求匹配:
abcdabcdabcdef
abcdef
发现不匹配,就这样一直下去,一直到:
abcdabcdabcdef
abcdef
发现这时候匹配了模式P,所以存在,然后返回T中匹配的开头的下标。
这个就是朴素算法,像个大傻一样一个一个的挪,挪到合适的位置。分析一下,上面的例子肯定得嵌套两层的循环才能做到这样的遍历,所以时间复杂度肯定是在O(T * P)
假如这个字符串T很长很长,例如1W+个字符,而且模式P也有还几千个字符,那不就很麻烦?所以这就是朴素的局限性。下面会讲到KMP算法,这个就是一个比较简单的匹配的算法。
*KMP算法
-KMP是一种不回溯的匹配算法,也就是当T子串t不匹配模式P的是否,但是却存在模式P的子串p和t的子串t'匹配的话,假如能消除这个冗余的操作的话,就能大大加快速度。
-KMP算法就是确定这样的一个情况,确定应该右移多少位。
-而且KMP右移k位的k值仅仅依赖于模式P的本身,和T无关。
如下直接推荐一个写KMP算法的博客,写得很好!
传送门:KMP算法
以下是KMP算法代码,主要分为构造next数组,已经主体的循环框架:(没测试过的)
#include <iostream>
#include <string>
using namespace std;
int KMPStrMatching(string T, string P, int *N, int start) {
int j = 0;
int i = start;
int tLen = T.length();
int pLen = P.length();
if (tLen - start < pLen) {
return -1;
}
while (j < pLen && i < tLen) {
if (j == -1 || T[i] == P[j]) {
i++;
j++;
} else {
j = N[j];
}
}
if (j >= pLen) {
return (i - pLen);
} else {
return -1;
}
}
int findNext(string P) {
int j, k;
int m = P.length();
int *next = new int[m];
next[0] = -1;
j = 0;
k = -1;
while (j < m - 1) {
while (k >= 0 && P[k] != P[j]) {
k = next[k];
}
j++;
k++;
next[j] = k;
}
return next;
}