目录
字符串的基本概念
字符串的三要素
字符串的基本概念
串的编码
串的实现及基本运算
顺序串的实现
串的静态数组实现
串的动态数组的实现
顺序存储的四种方案
链式串的实现
基本运算
方案三
方案一
字符串的基本概念
数据结构千千万,但距离我们最近的还是字符串。在这里我们定义:有限个字符组成的序列为字符串。在大多数人看来,字符串仅仅是一个char类型的数组,但在作者看来,数组也是一种特殊的线性表。回想顺序表、顺序栈、顺序队列这些都和数组有着千丝万缕的联系。但在我们这里要讲的字符串,它的格式不仅仅是数组格式,数组格式只是串格式的众多种类中的一种。
我们回顾数据结构的三要素:逻辑结构、物理结构和基本运算。线性表是指元素之间的逻辑结构为线性的一系列的数据结构,其特点是一对一。类似的,字符串各个元素之间的逻辑结构也可以视为线性,因此字符串是一种特殊的线性表。
字符串的三要素
- 逻辑结构:线性结构
- 物理结构:顺序存储/链式存储
- 基本运算
- StrCopy(&T,S)复制串,将串S复制到串T;
- StrEmpty(S)串判空,判断串S是否为空串;
- StrLength(S)求串长,求串S的长度;
- ClearStr(S)清空串,清空串S,使其成为空串;
- DestroyStr(&S)销毁串,清空串并释放其内存空间;
- ConcatStr(&T,S1,S2)串连两个串,在S1串后面链接上S2成为一个串T;
- SubString(&Sub,S,pos,len)求子串,求串S以pos为起点,长度为len的子串;
- StrCompare(S,T)比较串,比较串S与T,S>T则返回值大于0;S=T则返回值等于0;S<T则返回值小于0;
- Index(S,T)定位子串,如果主串S中存在与T相同的子串,则返回主串第一次出现子串的第一个位置;
上述基本操作涉及到一些串的相关名词,在这里作一下定义说明,没有疑问的可跳过。
字符串的基本概念
字符串:有限个字符组成的序列
- 串长:有限字符的个数n
- 空串:有限字符个数为0
- 空格串:串内所有字符均为空格;例如:“ ”
- 主串和子串:这是一个相对概念,一般来说,把一个完整的字符串当成主串,而子串是从主串中任意提出来的连续字符组成的序列。
例如:
a="qwer1314";
b="qwer"
c="1314",
d="wer1",
在这里b c d均是a的子串
注意空串和空格串,空串里面没有一个字符,而空格串是包含了字符,只不过这个字符是空格。
串的编码
- 英文字符:ASCII码等。
- 中英文字符:UTF8、UTF16、GB2312等。
我们知道,计算机的存储是二进制的,也就是说无论什么字符,其存储的数据都是二进制,那么如何将二进制数与符号相对应,这就是我们的编码。编码是一种规则,一个二进制数通过这个规则对应表示一个字符。抽象理解成一个y=f(x)的函数,自变量x为二进制数,编码为映射规则f,因变量为字符集y。满足函数一一对应的性质。编码有多种多样的,例如BCD码,格雷码等。
ASCII码是我们最常用的一种编码,其存储空间一般为1B。它包含了我们常见的大部分英文字符,例如26个大小写英文字母,数字,空格等。其具体的ASCII表在网上众多,我就不一一赘述了。
UTF8、UTF16、GB2312都是包含了中英文字符的编码,在需要使用中文的体系中常用,但需要注意的是使用这几种的人数较多较杂,不像英文字符大部分人都用ASCII,这几种也很多人使用,因此我们在打开其他人的文件时很容易出现乱码,其主要原因就是你打开打编码方式与他写的编码方式不一致。因此当遇到乱码,应当及时更正打开文件的编码方式。
串的实现及基本运算
顺序串的实现
顺序串即存储结构为连续顺序的字符串,和顺序表类似,其构建方法一般可以分为静态数组和动态数组两种。
串的静态数组实现
#define MaxSize 100;//定义最大分配空间为100个
typedef struct{char str[MaxSize];//定义静态字符数组int length;//定义串的实际长度
}SString;
静态数组在分配得到时候固定了大小空间
串的动态数组的实现
typedef struct{char *str;int length;
}HString;
动态数组只分配了数组的基址,大小可自己拓展。在应用过程中拓展需要用到malloc函数,同时销毁元素时也要手动free空间。
顺序存储的四种方案
方案1是最常见的格式,也是一般字符串的默认格式,不包含表长元素,以'\0'作为结尾符号;
方案2是我们默认使用的格式,前面是串的各个字符元素,最后存放表长元素;
方案3是舍弃数组的第一个元素,使数组下标与字符的位序对其,最后存放表长元素;
方案4不单独设置表长元素,将串的字符数组的第一个元素存放表长数据, 同时也让数组下标与字符位序对齐;但表长元素的数据类型是int,字符数组的数据类型是char,使用中有很多需要注意,因此实用性不是很好。
四个方案相较而言方案3更具有优势,但方案1的普遍性更强。
链式串的实现
链式串的物理结构类似于单链表,每个结点存放一组数据,并指向下一个结点,结构示意图:
其中S是头结点,从1开始才是串的实际字符数据;当然采用无头结点的串也是可以的,只需要注意第一个结点特殊处理就行。
typedef struct StringNode{char str;//每个结点存储一个字符struct StringNode *next;//指向下一个结点
}StringNode,*String;
一般而言我们会采用一个结点储存一个字符,但有些特殊情况下需要一个结点储存固定长度的字符,同时也节省空间。
typedef struct StringNode{char str[4];//每个结点存储4个字符struct StringNode *next;//指向下一个结点
}StringNode,*String;
基本运算
在讲基本操作之前,我们先确定一下其串的格式,物理存储格式决定这基本运算的操作思路、步骤与顺序。从编程的逻辑性和简易性而言,方案三是最合适的。
但又考虑到普遍的字符串存储格式是方案一:
因此我们选择在方案三的基础上完成思路和源码的讲解,同时展示完整的方案一源码,在源码中简单讲解。
方案三
方案三字符串结构体创建
#define MaxSize 100;//定义最大分配空间为100个
typedef struct{char str[MaxSize];//定义静态字符数组int length;//定义串的实际长度
}SString;
/*初始化串*/
void InitString(SString &S){for(int i=0;i<MaxSize;i++)//将所有值设定为字符串终止符,也可以是别的没歧义的内容S.str[i]='\0';S.length=0;//初始化字符串长度为0
}
复制串,将串S复制得到串T
void StrCopy(SString &T,SString S){T.length=S.length;for(int i=1;i<=T.length;i++){T.str[i]=S.str[i];}
}
串判空,判断串是否为空串,是返回true,不是返回false
bool StrEmpty(SString S){if(S.length==0)//字符串长度为0则为空字符串return true;else if(S.length>0)return false;
}
求串长,在这个格式下有点多此一举,但也还是说明一下吧
int StrLength(SString S){return S.length;
}
清空串,使其成为空串,但其实在这里没办法真的成为空,毕竟空间就在那,一般操作方式是给每个字符空间赋值为特殊值,例如0或者'\0',然后修改表长。对于char数据类型建议赋值为'\0';对于其他int或者float建议赋值为0
void ClearString(SString &S){for(int i=0;i<=S.length;i++)S.str[i]='\0';S.length=0;
}
销毁串,因为这个结构采用的是静态数组,静态数组不需要手动释放空间,内存会在超出变量作用域时自动释放空间。
连接串,串连两个串,在S1串后面连接上S2成为一个串T;
bool ConcatString(SString &T,SString S1,SSrting S2){T.length=S1.length+S2.length;//拼接后的长度if(T.length+1>MaxSize)//判断长度是否超限return false;//长度超限,拼接失败for(int i=1;i<=S1.length;i++)//将S1的数据复制到TT.str[i]=S1.str[i];for(int i=1;i<=S2.length;i++)//将S2的数据复制到S1后面T.str[S1.length+i]=S2.str[i];return true;//拼接成功
}
求子串,求主串S第pos位开始的长len的子串
bool SubString(SString &Sub,SString S,int pos,int len){if(pos+len-1>S.length)//判断子串是否越界return false;//子串越界,获取子串失败Sub.length=len;//子串长度为lenfor(int i=1;i<len+1;i++)//读取子串并复制给SubSub.str[i]=S.str[i+pos-1];Sub.str[i]='\0';//给子串添加终止符return true;//获取子串成功
}
StrCompare(S,T)比较串,比较串S与T,S>T则返回值大于0;S=T则返回值等于0;S<T则返回值小于0。
有人就说了字符串怎么能比较呢?这也是作者刚开始疑惑的问题,字符串比什么,除了能比一不一样还能干嘛?所以在比较字符串之前一定要有比较的标准,制定比较标准时需要注意以下三个要点
- 要不要考虑字符的大小写
- 字符串的长度
- 对于特殊字符和不同编码如何处理
下面我们先制定字符串比较的规则:
- 比较对字符大小写敏感,直接按顺序比较字符在ASCLL的码值,先出现更大字符的串更大
- 考虑字符串长度,长串前缀与短串字符一致时,长串更大
- 不考虑特殊字符,默认均为ASCII码
int StrCompare(SString S,SString T){//遍历查找两个字符串每个位置的字符比较情况for(int i=1;i<=S.length && i<=T.length;i++){if(S.str[i]!=T.str[i])//字符不相同则相减并返回return S.str[i]-T.str.[i];}return S.length-T.length;//
}
定位子串, 如果主串S中存在与T相同的子串,则返回主串第一次出现子串的第一个位置。
int IndexStr(SString S,SString T){int m=S.length;int n=t.length;int flag;SString Sub;for(int i=1;i<=n-m+1;i++){/*取出字符串S中长度为n的子串*/if(i+n-1>S.length)//判断子串是否越界return 0;//子串越界,获取子串失败Sub.length=n;//子串长度为nfor(int j=1;j<=n;j++)//读取子串并复制给SubSub.str[j]=S.str[j+i-1];/*比较T与S的子串Sub*/for(int j=1;j<=S.length && j<=T.length;j++){if(S.str[j]!=Sub.str[j])flag=T.str[j]-Sub.str[j];}flag=S.length-T.length;/*判断两串是否相等*/if(flag==0)return i;}return 0;//没有到与T相等的子串
}
上述代码中有求子串和比较子串的操作,我们采用源码表示,但也可以直接用函数:
int IndexStr(SString S,SString T){int m=S.length;int n=t.length;bool flag;SString Sub;for(int i=1;i<=n-m+1;i++){/*取出字符串S中长度为n的子串*/flag=SubString(Sub,S,i,n);if(flag==false)return 0;//子串违法/*判断两串是否相等*/if(StrCompare(T,Sub)==0)return i;}return 0;//没有到与T相等的子串
}
方案一
方案一是最常见的字符串类型例如:
char str[]="qwer1314";
这种方式不需要结构体,同时也是我们在大多数场合输入字符串的默认格式,因此在这里我们再次进行代码讲解;
/*将字符串S复制给T*/
void StrCopy(char *S,char *T){int i=0;while(S[i]!='\0'){T[i]=S[i];i++;}T[i]='\0';
}
/*判定字符串S是否为空*/
bool StrEmpty(char *S){if(S[0]=='\0')return true;elsereturn false;
}
/*求字符串S的长度*/
int StrLength(char *S){int len=0;while(S[len]!='\0'){len++;}return len;
}
/*清空字符串*/
void ClearString(char *S){int i=0;while(S[i]!='\0'){S[i]='\0';i++;}
}
/*串联两个字符串*/
char *ConcatStr(const char *str1, const char *str2) {// 检查输入是否为NULLif (str1 == NULL || str2 == NULL) {return NULL;}// 计算新字符串的长度int len1 = StrLength(str1);int len2 = StrLength(str2);int newLen = len1 + len2 + 1; // 分配内存char *result = (char *)malloc(newLen * sizeof(char));if (result == NULL) {// 内存分配失败return NULL;}// 复制第一个字符串到结果中int i = 0;while (i < len1) {result[i] = str1[i];i++;}// 连接第二个字符串到结果中int j = 0;while (j < len2) {result[i + j] = str2[j];j++;}// 添加终止符result[i + j] = '\0';return result;
}
/*求子串*/
char *SubString(char *str, int pos, int len) {int length = StrLength(str);if (pos + len > length || pos < 0 || len < 0) {return NULL; // 检查边界条件}// 分配内存char *result = (char *)malloc((len + 1) * sizeof(char));if (result == NULL) {return NULL; // 检查内存分配是否成功}for (int i = 0; i < len; i++) {result[i] = str[pos + i];}result[len] = '\0'; // 添加字符串结束符return result;
}
/*比较字符串*/
int Strcompare(char *str1,char *str2){int len1=StrLength(str1),len2=StrLebgth(str2);for(int i=0;i<len1 && i<len2;i++){if(str1[i]!=str2[i])return str1[i]-str2[i];}return len1-len2;
}
/*定位字符串*/
int Index(char *str1,char *str2){int m=StrLength(str1);int n=StrLength(str2);bool flag;char *Sub;for(int i=1;i<=n-m+1;i++){/*取出字符串S中长度为n的子串*/Sub=SubString(S,i,n);if(flag==NULL)return 0;//子串违法/*判断两串是否相等*/if(StrCompare(S,Sub)==0)return i;}return 0;//没有到与T相等的子串
}
常见字符串操作:
将字符串转换成int
//字符串转化为int
int string_int(char *str) {if (str == NULL || *str == '\0') {// 输入为空或空字符串return 0;}int num = 0;int i = 0;int flag = 1;// 检查负号if (str[i] == '-') {flag = -1;i++;}// 转换数字部分while (str[i] >= '0' && str[i] <= '9') {int digit = str[i] - '0';// 检查是否会发生溢出if (num > (INT_MAX - digit) / 10) {// 发生溢出,返回0或其他错误值return 0;}num = num * 10 + digit;i++;}return flag * num;
}
将int转化为字符串
char *int_string(int num) {// 假设整数最多有11位(包括负号)char *str = (char *)malloc(12 * sizeof(char)); // 分配内存用于存储转换后的字符串if (str == NULL) {return NULL; // 如果内存分配失败,返回NULL}int i = 0; // 用于记录当前字符的位置int isNegative = 0; // 标记是否为负数if (num < 0) {isNegative = 1; // 如果是负数,设置标记并取绝对值num = -num;}// 将数字转换为字符串do {str[i++] = (num % 10) + '0'; // 将最低位的数字转换为字符并存储num /= 10; // 去掉已经处理的最低位} while (num > 0);if (isNegative) {str[i++] = '-'; // 如果是负数,添加负号}// 反转字符串for (int j = 0; j < i / 2; j++) { // 注意这里需要声明j的类型为intchar temp = str[j];str[j] = str[i - j - 1];str[i - j - 1] = temp;}// 添加字符串终止符str[i] = '\0';return str; // 返回转换后的字符串
}
将字符串转换成float
// 将字符串转换为浮点数的函数
float string_float(char *str) {// 如果输入为空或空字符串,返回0.0fif (str == NULL || *str == '\0') {return 0.0f;}// 初始化变量float num = 0.0f; // 最终结果int i = 0; // 当前字符索引int flag = 1; // 符号标志,1表示正数,-1表示负数int fractional_part = 0; // 是否处理小数部分的标志float fractional_divisor = 1.0f; // 小数部分的除数// 检查负号if (str[i] == '-') {flag = -1;i++;}// 转换数字部分和小数部分while (str[i] != '\0') {if (str[i] >= '0' && str[i] <= '9') {int digit = str[i] - '0'; // 将字符转换为数字if (fractional_part) {// 处理小数部分fractional_divisor *= 10.0f;num += digit / fractional_divisor;} else {// 处理整数部分if (num > (FLT_MAX - digit) / 10.0f) {// 发生溢出,返回0或其他错误值return 0.0f;}num = num * 10.0f + digit;}} else if (str[i] == '.' && !fractional_part) {// 遇到小数点,开始处理小数部分fractional_part = 1;} else {// 非法字符,直接返回当前结果break;}i++;}// 返回最终结果,考虑符号return flag * num;
}
将float转换为字符串
// 将浮点数转换为字符串的函数
char *float_string(float num) {// 假设浮点数最多有38位(包括负号、小数点和指数)char *str = (char *)malloc(40 * sizeof(char)); // 分配内存用于存储转换后的字符串if (str == NULL) {return NULL; // 如果内存分配失败,返回NULL}int i = 0; // 用于记录当前字符的位置int isNegative = 0; // 标记是否为负数if (num < 0) {isNegative = 1; // 如果是负数,设置标记并取绝对值num = -num;}// 处理整数部分int integerPart = (int)num; // 获取整数部分do {str[i++] = (integerPart % 10) + '0'; // 将整数部分逐位转换为字符并存储integerPart /= 10; // 去掉已经处理的最低位} while (integerPart > 0);if (isNegative) {str[i++] = '-'; // 如果是负数,添加负号}// 反转整数部分,使其按正确顺序排列for (int j = 0; j < i / 2; j++) {char temp = str[j];str[j] = str[i - j - 1];str[i - j - 1] = temp;}// 添加小数点str[i++] = '.';// 处理小数部分float fractionalPart = num - (int)num; // 获取小数部分int precision = 6; // 设置精度为6位小数for (int k = 0; k < precision; k++) {fractionalPart *= 10; // 将小数部分放大10倍int digit = (int)fractionalPart; // 获取当前最高位的小数str[i++] = digit + '0'; // 将小数转换为字符并存储fractionalPart -= digit; // 去掉已经处理的最高位小数}// 添加字符串终止符str[i] = '\0';return str; // 返回转换后的字符串
}