昨天刚学了串这一章,今天趁热在自己的学习记录里记下来好了,数据结构第三篇:串的操作与应用。和以前一样,参考严蔚敏版《数据结构》。同时,将串的匹配(重点是KMP匹配算法)单独拿出来作为一篇博文,毕竟这个比较复杂。其实主要是为了充数。
目录:
一、串的概述
二、定长顺序串的基本操作
三、堆分配串的基本操作
一、串的概述与基本操作
串说白了就是我们在很多高级语言中用到的字符串类型,这并不是基本数据类型,但这些高级语言已经将对串的操作封装的很全、很好了。所以我们用起来也比较方便。其实完全可以自己去试着简单实现一下那些方法。这是一个非常好的练习。
对串来说,我们一般就三种存储方式:定长顺序存储、堆分配存储、块链存储。
定长顺序存储:串的顺序存储方式,利用一块连续的内存空间来存储字符序列,这段内存空间的长度固定,可用宏来表示,整个串采用顺序数组存储,同时规定数组的第一个元素(0号下标)为串的长度,便于以后的操作。
常量与数据结构定义:
#define MAX_STRING_LENGTH 255 //串的最大串长
typedef unsigned char SString[MAX_STRING_LENGTH + 1]; //0号单元存放串的长度
仅仅使用一个数组就可以代表一个串,如前文所述,将数组第一个单元(0号位置)设为串的长度,不用再定义新的变量。
堆分配存储:串的动态存储方法。设置一个字符指针代表串的内容,增设一个变量存储串的长度方便以后的操作。堆分配存储最大的特点就是灵活、动态。这里不会出现字符串的“截断”现象,当空间不够时重做分配,记录新空间基址即可。
/**
* 串的堆分配存储表示
*/
typedef struct {
char *ch; //若是非空串,则按串长分配存储区,否则为NULL
int length; //串长
}HString;
对堆分配存储的串进行操作时,由于串的存储空间是在堆上的,需要我们自己管理,例如在串复制时需要首先释放原串的空间,再重新申请一块空间进行操作。
块链存储:串的链式存储。由于串结构的数据域仅为一个字符(1字节),如果为每个结点加一个指针域(4字节),则显得有些浪费空间。因此在采用链式存储时往往采用”块链“——每个结点存放多个字符。这样当串长不是结点大小的整数倍时,最后一个结点由特殊字符(例如‘#’)补满。由于连式存储较前两者而言并无太大优势,加之操作复杂,所以不做要求。
顺带一提,对于每个结点存放字符的数目,有一个概念叫做“存储密度”。存储密度 = 串值所占的存储位 / 实际分配的存储位。该值越小块链处理起来就越方便。
块链的数据结构定义:
#define CHUNK_SIZE 80
typedef struct Chunk{
char ch[CHUNK_SIZE]; //数据域
struct Chunk *next; //指针域
}Chunk;
typedef struct {
Chunk *head; //串的头指针
Chunk *tail; //串的尾指针
int curlen; //串的当前长度
}LString;
二、定长顺序串的基本操作
串的基本操作我们都比较熟悉,例如StrCopy和StrCompare等。在这里提一下,关于串面试时的考点还是挺多的。例如:while (*s++ = *t++); 的作用?(赋值字符串),自己编写strcmp函数?等等。掌握这些基本操作对于应试非常有帮助。
先看一下定长顺序串的所有基本操作:
/**
* 根据字符串常量生成串T
*
* @param T 生成的串
* @param constChars 字符串常量
*/
void StrAssign(SString *T, char constChars[]);
/**
* 由串S复制得串T
*/
void StrCopy(SString *T, SString S);
/**
* 若S为空串,返回TRUE,否则返回FALSE
*/
BOOL StrEmpty(SString S);
/**
* 返回串S的长度
*/
int StrLen(SString S);
/**
* 将S清为空串
*/
void ClearString(SString *S);
/**
* 用T返回将S1和S2连接成的新串
*/
void Concat(SString *T, SString S1, SString S2);
/**
* 用Sub返回串S中第pos个字符起长度为len的字串
*/
void SubString(SString *Sub, SString S, int pos, int len);
/**
* 返回在主串S中第pos个字符之后第一次出现字串T的位置,若不存在字串T,则返回0。(朴素匹配算法)
*/
int Index(SString S, SString T, int pos);
/**
* 用V替换主串S中得所有与T相等的不重叠的字串
*/
void Replace(SString *S, SString T, SString V);
/**
* 在串S的第pos个位置之前插入串T
*/
void StrInsert(SString *S, int pos, SString T);
/**
* 从串S中删除第pos个字符起长度为len的字串
*/
void StrDelete(SString *S, int pos, int len);
/**
* 销毁串S
*/
void DestroyString(SString *S);
/**
* 比较亮字符串
*
* @param S 第一个字符串
* @param T 第二个字符串
*
* @return 结果为1说明S>T,结果为-1说明S<T,结果为0说明S=T
*/
int StrCompare(SString S, SString T);
/**
* 打印串S
*
* @param S 要打印的串
*/
void PrintStr(SString S);
其中子串的定位函数Index即为串的模式匹配算法之一,关于串的模式匹配,单独总结。这里将其他的操作给出实现
void StrAssign(SString *T, char constChars[])
{
int i = 1, j = 0;
while (constChars[j] != '\0') {
(*T)[i++] = constChars[j++];
}
(*T)[0] = j;
}
void StrCopy(SString *T, SString S)
{
int i = 1, j = 1;
while (S[j] != '\0') {
(*T)[i++] = S[j++];
}
(*T)[0] = S[0];
}
BOOL StrEmpty(SString S)
{
return (S[0] > 0) ? FALSE : TRUE;
}
int StrLen(SString S)
{
return S[0];
}
void ClearString(SString *S)
{
(*S)[1] = '\0';
(*S)[0] = 0;
}
void Concat(SString *T, SString S1, SString S2)
{
int s1_len = S1[0];
int s2_len = S2[0];
int k = 1;
if (s1_len + s2_len <= MAX_STRING_LENGTH) { //未被截断
for (int i = 1; i <= s1_len; i++) {
(*T)[k++] = S1[i];
}
for (int j = 1; j <= s2_len; j++) {
(*T)[k++] = S2[j];
}
(*T)[0] = s1_len + s2_len;
} else if (s1_len < MAX_STRING_LENGTH) { //S2被截断
for (int i = 0; i < s1_len; i++) {
(*T)[k++] = S1[i];
}
for (int j = 0; j < MAX_STRING_LENGTH - s1_len; j++) {
(*T)[k++] = S2[j];
}
(*T)[0] = MAX_STRING_LENGTH;
} else { //仅取S1
for (int i = 0; i < MAX_STRING_LENGTH; i++) {
(*T)[k++] = S1[i];
}
(*T)[0] = MAX_STRING_LENGTH;
}
}
void SubString(SString *Sub, SString S, int pos, int len)
{
if (pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1) {
(*Sub)[0] = '\0';
return;
}
int k = 1;
for (int i = pos; i < pos + len; i++) {
(*Sub)[k++] = S[i];
}
(*Sub)[0] = len;
}
void Replace(SString *S, SString T, SString V)
{
if (T[0] != V[0]) {
return;
}
int pos = 0;
while ((pos = Index(*S, T, 1)) > 0) {
int k = 1;
for (int i = pos; i < pos + T[0]; i++) {
(*S)[i] = V[k++];
}
}
}
void StrInsert(SString *S, int pos, SString T)
{
if (pos < 1 || pos > (*S)[0] || (*S)[0] + T[0] > MAX_STRING_LENGTH) {
return;
}
int t_len = T[0];
for (int i = (*S)[0]; i >= pos; i--) {
(*S)[i + t_len] = (*S)[i];
}
int k = 1;
for (int i = pos; i < pos + t_len; i++) {
(*S)[i] = T[k++];
}
(*S)[0] += T[0];
}
void StrDelete(SString *S, int pos, int len)
{
if (pos < 1 || pos + len > (*S)[0]) {
return;
}
for (int i = pos; i < len + pos; i++) {
(*S)[i] = (*S)[i + len];
}
(*S)[0] -= len;
}
void DestroyString(SString *S)
{
free(S);
}
int StrCompare(SString S, SString T)
{
for (int i = 1; i <= S[0] && i <= T[0]; i++) {
if (S[i] == T[i]) {
continue;
} else if (S[i] < T[i]) {
return 1;
} else {
return -1;
}
}
if (S[0] == T[0]) {
return 0;
} else if (S[0] < T[0]) {
return 1;
} else {
return -1;
}
}
void PrintStr(SString S)
{
printf("SString = ");
for (int i = 1; i <= S[0]; i++) {
printf("%c", S[i]);
}
printf(" length = %d", S[0]);
printf("\n");
}
由于定长顺序串的数组0元素位置为串长,所以在循环处理时应该注意下标值的范围:1 ≤ i ≤ S[0]。
三、堆分配串的基本操作
对比定长顺序串,可以看到堆分配串的灵活性以及方便操作性。
堆分配串的基本操作:
/**
* 在串S的第pos个字符之前插入串T
*
* @return 若正确插入,则返回1,否则返回0
*/
Status HStrInsert(HString *S, int pos, HString T);
/**
* 删除串S的第pos个位置的字符开始len长度的字串
*
* @param S 待删除的主串
* @param pos 待删除字串在主串的位置
* @param len 待删除字串的长度
*
* @return 若成功删除,则返回1,否则返回0
*/
Status HStrDelete(HString *S, int pos, int len);
/**
* 生成一个其值等于串常量chars的串T
*
* @return 若成功生成串T,则返回1,否则返回0
*/
Status HStrAssign(HString *T, char *chars);
/**
* 返回S的元素个数,即S串的长度
*
* @return 串S的长度
*/
int HStrLength(HString S);
/**
* 按字典顺序比较两个串
*
* @param S 第一个串
* @param T 第二个串
*
* @return 若S>T,则返回值大于零,若S=T,则返回值等于零,否则返回值小于零。
*/
int HStrCompare(HString S, HString T);
/**
* 清空串S
*/
Status ClearHString(HString *S);
/**
* 用T返回由S1和S2联接而成的新串
*
* @param T 联接成的新串
* @param S1 待联接的第一个串
* @param S2 待联接的第二个串
*
* @return 联接成功时返回1,否则返回0
*/
Status ConcatHString(HString *T, HString S1, HString S2);
/**
* 返回串S的第pos个字符起长度为len的子串
*
* @param Sub 生成的字串
* @param S 传入的串
* @param pos 子串在主串中的起始位置
* @param len 字串的长度
*
* @return 若操作成功则返回1,否则返回0
*/
Status SubHString(HString *Sub, HString S, int pos, int len);
/**
* 打印串S
*/
void PrintHString(HString S);
以及实现:
Status HStrInsert(HString *S, int pos, HString T)
{
if (pos < 1 || pos > (*S).length + 1) {
return ERROR;
}
//如果T非空,则重新为S分配空间
if (T.length) {
if (!((*S).ch = (char *)realloc((*S).ch, ((*S).length + T.length) * sizeof(char)))) {
exit(OVERFLOW);
}
//插入位置后面的所有元素后移
for (int i = (*S).length - 1; i >= pos - 1; i--) {
(*S).ch[i + T.length] = (*S).ch[i];
}
//插入T
for (int i = 0; i < T.length; i++) {
(*S).ch[i + pos - 1] = T.ch[i];
}
(*S).length += T.length;
}
return OK;
}
Status HStrDelete(HString *S, int pos, int len)
{
if (pos < 1 || pos > (*S).length || len < 0 || len > (*S).length - pos + 1) {
return ERROR;
}
if (len) {
//元素前移
for (int i = pos - 1; i < (*S).length - len + 1; i++) {
(*S).ch[i] = (*S).ch[i + len];
}
//长度减少
(*S).length -= len;
}
return OK;
}
Status HStrAssign(HString *T, char *chars)
{
//如果T本来不为空,则释放内存空间
if ((*T).ch) {
free((*T).ch);
}
//求chars的长度
int count = 0;
for (char *c = chars; *c; c++, count++);
if (count == 0) {
(*T).ch = NULL;
(*T).length = 0;
} else {
if (!((*T).ch = (char *)malloc(sizeof(char) * count))) {
exit(OVERFLOW);
}
for (int i = 0; i < count; i++) {
(*T).ch[i] = chars[i];
}
(*T).length = count;
}
return OK;
}
int HStrLength(HString S)
{
return S.length;
}
int HStrCompare(HString S, HString T)
{
for (int i = 0; i < S.length && i < T.length; i++) {
if (S.ch[i] != T.ch[i]) {
return S.ch[i] - T.ch[i];
}
}
return S.length - T.length;
}
Status ClearHString(HString *S)
{
if ((*S).ch) {
free((*S).ch);
(*S).ch = NULL;
}
(*S).length = 0;
return OK;
}
Status ConcatHString(HString *T, HString S1, HString S2)
{
//释放旧空间
if ((*T).ch) {
free((*T).ch);
}
if (!((*T).ch = (char *)malloc((S1.length + S2.length) * sizeof(char)))) {
exit(OVERFLOW);
}
for (int i = 0; i < S1.length; i++) {
(*T).ch[i] = S1.ch[i];
}
for (int i = 0; i < S2.length; i++) {
(*T).ch[i + S1.length] = S2.ch[i];
}
(*T).length = S1.length + S2.length;
return OK;
}
Status SubHString(HString *Sub, HString S, int pos, int len)
{
if (pos < 1 || pos > S.length || len < 0 || len > S.length - pos + 1) {
return ERROR;
}
if ((*Sub).ch) {
free((*Sub).ch);
}
if (len == 0) {
(*Sub).ch = NULL;
(*Sub).length = 0;
} else {
(*Sub).ch = (char *)malloc(len * sizeof(char));
for (int i = 0; i < len; i++) {
(*Sub).ch[i] = S.ch[i + pos - 1];
}
(*Sub).length = len;
}
return OK;
}
void PrintHString(HString S)
{
printf("HString = ");
for (int i = 0; i < S.length; i++) {
printf("%c", S.ch[i]);
}
printf("\tlength = %d\n", S.length);
}
对于堆分配串的注意事项:不要忘记释放原来的空间,对于malloc的空间,如果不手动释放就会造成内存泄露。另外,与定长顺序串不同,堆分配串是从数组下标为0开始就存储数据的,所以在操作循环的下标时应该注意一下。
之前也说过,串的块链表示并不常用,所以在此就不作实现了。