其实字符串就是一种更加特殊的线性结构。他表现在:1.使用char类型的数组来存储。2.面向字符串的操作与一般的顺序表不同,比如元素的插入、删除、确定位置等,一般的顺序表定义的操作都是1次处理一个元素,而字符串的操作往往是处理若干个字符。
因为字符串操作的这些特殊性,所以C语言中专门在string.h头文件中定义了许多关于字符串的操作,其中比较重要的是:字符串输出函数 puts、字符串输入函数gets、字符串连接函数strcat、字符串拷贝函数strcpy、字符串比较函数strcmp、测字符串长度函数strlen等等。我们这里在不使用标准库的情况下,自己动手写一些与这些函数功能类似的函数。
首先考虑字符串的定义,按照我以前的风格,都不是通过定义一个很长的数组来实现,而是通过动态分配内存的方法来实现,这样的话,需要一个变量记录动态数组的长度,有了这个长度,我们就不需要像C语言那样通过在结尾加添'\0'来判断字符串是否结束了。
typedef struct String
{
char* data;
int size;
}String;
//分配字符串
bool assignStr(String *pT,char *chars)
{
//求字符串的长度
int i = 0;
while(chars[i] != '\0')
++i;
//如果是空串
if(0 == i)
{
pT->data = NULL;
pT->size = 0;
}
else
{
pT->size = i;
pT->data = (char*)malloc(sizeof(char) * pT->size);
if(NULL ==pT->data )
return false;
else
{
for(int j = 0; j < i;++j)
pT->data[j] = chars[j];
}
}
return true;
}
//销毁字符串
void clearStr(String *pS)
{
free(pS->data);
pS->data = NULL;
pS->size = 0;
}
//求字符串长度
int strLength(String *pS)
{
return pS->size;
}
//字符串拷贝
bool strCpy(String *pT,String *pS)
{
if(strLength(pT) < strLength(pS))
{
pT->data = (char*)realloc(pT->data,sizeof(char) * pS->size);
pT->size = pS->size;
}
if(NULL == pT->data)
return false;
else
{
for(int i = 0; i < strLength(pT);++i)
pT->data[i] = pS->data[i];
pT->size = pS->size;
return true;
}
}
//判断是否为空
bool is_empty(String *pS)
{
if(NULL == pS->data && 0 == pS->size)
{
printf("empty");
return true;
}
else
return false;
}
//字符串比较
int strCmp(String *pT,String *pS)
{
int i = strLength(pT);
int j = strLength(pS);
int k;
//找出长度较短的字符串
i < j ? k = i : k = j;
for(int i = 0; i< k; ++i)
{
if(pT->data[i] > pS->data[i])
return 1;
else if(pT->data[i] < pS->data[i])
return -1;
}
//如果程序能完整执行完for循环,说明两个字符串的前k个字符相等
//剩下只要比较它俩谁长就可以了
if(i < j)
return -1;
else if(i > j)
return 1;
else
return 0;
}
//将第二个字符串连接到第一个后面
void strCat(String *pT,String *pS)
{
int i = strLength(pT);
int j = strLength(pS);
pT->data = (char*)realloc(pT->data,sizeof(char) * (i+j));
for(int m = i,n = 0 ; m < (i+j); ++m,++n)
pT->data[m] = pS->data[n];
pT->size = i + j;
}
//从指定的位置返回指定长度的子串
bool getSubString(String *pS,int pos,int len,String *pSubString)
{
//判断输入的位置和长度是否合法
if(pos < 0 || len <= 0 || len+pos > strLength(pS))
return false;
for(int i = pos,j = 0; i<pos+len;++i,++j)
{
pSubString->data[j] = pS->data[i];
}
pSubString->size = len;
return true;
}
//在指定位置后插入字符串
bool insertStr(String *pT,String *pS,int pos)
{
int i = strLength(pT);
int j = strLength(pS);
if(pos < 0 || pos > j)
return false;
pS->data = (char*)realloc(pS->data,sizeof(char) * (i+j));
pS->size = i+j;
if(NULL == pS->data)
return false;
for(int m = i+j ; m > pos; --m)
pS->data[m] = pS->data[m-i];
for(int n = pos,l = 0; n < i+pos;++n,++l)
pS->data[n] = pT->data[l];
return true;
}
//删除指定位置后面的若干个字符串
bool deleteStr(String *pS,int pos,int len)
{
int j = strLength(pS);
if(pos < 0 || pos > j || len + pos > j)
return false;
for(int i = pos; i < j-len;++i)
pS->data[i] = pS->data[i+len];
pS->size -= len;
return false;
}
//检索:源字符串的某个位置以后是否有目标字符串有则返回位置,无则返回-1
int index(String *pT,String *pS,int pos)
{
int m = strLength(pS);
int n = strLength(pT);
//如果比较位置大于等于源字符串的长度,返回-1
if(pos >= m)
return -1;
int i = pos,j = 0;
//当两个字符都没有访问到最后一个元素时,继续
while(i + n < m && j < n)
{
//如果从某一个位置起二者相等,继续比较
if(pS->data[i] == pT->data[j])
{
++i;
++j;
}
//否则源字符串从第一个字符匹配的下一个位置开始继续,目标字符串从头开始继续
else
{
i = i - j+1 ;
j = 0;
}
//如果目标串已经遍历完,说明找到了
if(j == strLength(pT))
return i- strLength(pT);
}
//没有找到,返回-1
return -1;
}
//替换
void replace(String *pT,String *pS,String *pNew)
{
//使用已经定义的操作完成:查找、删除、插入
int m = strLength(pT);
int n = strLength(pNew);
int i = 0;
do{
i = index(pT,pS,i);
if(i != -1)
{
deleteStr(pS,i,m);
insertStr(pNew,pS,i);
i += n;
}
}
while(i != -1);
}
//打印字符串,调试时使用
void printStr(String *pT)
{
printf("word: ");
// 不能使用下面这句,因为pT->data之前可能曾经很长
// printf("%s",pT->data);
for(int i = 0 ; i < strLength(pT);++i)
printf("%c",pT->data[i]);
printf("\n");
}
需要注意的是,字符串的索引操作中,有一种更加有效率的办法,称为:KMP算法,这里并没有使用,而是使用的更加直观的索引算法。
下面通过一个复杂的例子来结束对字符串的讨论:求两个字符串的最长公共子串。
//查找最长的公共子串,返回这个子串的位置,子串从pNew中带出
//算法简述:
//1.将两个子串组成一个矩阵,若矩阵行、列对应的元素相同,这个元素为1,反之为0
//2.遍历对角线
//3.找出长度最大的非0对角线,这条对角线对应的元素就是最长的公共子串
//举例
/*
a b d c a b c d
d 0 0 1 0 0 0 0 1
c 0 0 0 1 0 0 1 0
a 1 0 0 0 1 0 0 0
b 0 1 0 0 0 1 0 0
c 0 0 0 1 0 0 1 0
c 0 0 0 1 0 0 1 0
a 1 0 0 0 1 0 0 0
则最长公共子串为cabc
*/
//函数声明
//对于矩阵的每一个元素,计算它的对角线为1的对角线的长度
//输入参数为:矩阵、当前点的行、列坐标,矩阵的长、宽
int getDiaLength(int **M,int m,int n,int row,int col);
int getLongestSubString(String *pS1,String *pS2,String *pR)
{
int row = strLength(pS1);
int col = strLength(pS2);
//动态分配2维数组m*n
//先分配第一维:每个是int*类型的指针,共m个
int **pM = (int**)malloc(sizeof(int*)*row);
//为每个int*类型的指针分配n个int的空间
for(int i = 0; i< row;++i)
pM[i] = (int*)malloc(sizeof(int)*col);
for(int i = 0; i < row ; ++i)
{
for(int j = 0; j < col ; ++j)
{
if(pS1->data[i] == pS2->data[j])
pM[i][j] = 1;
else
pM[i][j] = 0;
}
}
//打印矩阵结果
for(int i = 0; i < row;++i)
{
for(int j = 0; j < col; ++j)
printf("%d ",pM[i][j]);
printf("\n");
}
printf("\n");
//默认最长长度为0
int length = 0;
int k = 0;
for(int i = 0;i < row;++i)
{
for(int j = 0; j < col;++j)
{
k = getDiaLength(pM,i,j,row,col);
if(length < k)
{
length = k;
//标记这个元素
pM[i][j] = length;
}
}
}
//打印矩阵结果
for(int i = 0; i < row;++i)
{
for(int j = 0; j < col; ++j)
printf("%d ",pM[i][j]);
printf("\n");
}
char* pchar = (char*)malloc(sizeof(char)*length+1);
for(int i = 0 ; i < row; ++i)
{
for(int j = 0; j <col; ++j)
if(length == pM[i][j] )
{
for(int m = 0; m < length; ++m,++i)
{
pchar[m] = pS1->data[i];
}
//填充结尾标识符
pchar[length] = '\0';
//令循环条件不满足,这样就能跳出二重循环了
i = row;
j = col;
}
}
//打印一下字符串
printf("%s",pchar);
printf("\n");
//将这个字符串分配给pR指针
assignStr(pR,pchar);
//释放内存
free(pchar);
for(int i = 0;i < row;++i)
free(pM[i]);
free(pM);
pM = NULL;
return length;
}
int getDiaLength(int **M,int m,int n,int row,int col)
{
int len = 0;
while(m<row && n<col)
{
if(1 == M[m][n] )
{
++m;
++n;
++len;
}
else
break;
}
return len;
}
程序是我自己写的,略有点繁琐,但是基本要求还是达到了,中间的一些打印信息只是在调试的时候使用的,正常使用时可以关掉。
我不由得得吐一下槽,因为C语言中并没有C++类中的数据成员的概念,所以在getDiaLength函数中,我不得不传递了如此多的参数。如果有人有更好的办法,希望能告诉我。
再顺便说一句,在C++中,由于定义string这个类,所以很多字符串的操作可以通过这个类提供的函数更加安全的进行(使用类的程序员不必使用指针)。这种使用标准库而不是指针的风格,是现代C++程序设计所提倡的。