字符串算法简介
字符串排序
键索引计数法
应用领域:
1、通过字符串一个字符排序。
2、将全班同学按组分类。
3、通过地区码排序电话号码。
这是一种用于小整数键的排序方法。只需要理解下面的实际问题即可理解键索引计数法排序。
对于ASCII字符,我们可以直接用ASCII对应的数值作为键,初始化count数组,然后在数组里面记录,每一个ASCII字符出现的次数,进而转化成字符在结果数组里面的初始化位置。然后通过获得初始化位置进行排序。思路很简单,直接看代码就可以清楚。
//ASCII表的值作为字符对应的键
//通过索引键出现的次数,找到键在结果排序中的起始位置。
//然后遍历字符串进行排序
void KeyIndexCounting(char *source)
{
int R = 128,i;//标准ascii对应最大数值
int N = strlen(source);
int count[R+1];//
char *aux = (char *)malloc(N);//分配暂存数组
memset(count , 0 , sizeof(count));//将栈上数据清0
for(i = 0 ; i < N ; i++)
count[source[i]+1]++;//计算键出现的频率,存储在键+1的位置,方便后面计算起始位置。
for(i = 0 ; i < N ; i++){
printf("%c %d\n" , source[i] , count[source[i]+1]);
}
for(i = 0 ; i < R ; i++)
count[i+1] += count[i];//计算键在结果数组中出现的初始化位置
for(i = 0 ;i < N ; i++)
aux[ count[source[i]]++ ] = source[i];//遍历一次字符串,将字符放到aux合适位置
for(int i = 0 ; i < N ; i++)
source[i] = aux[i];//将排好序的字符串拷贝回原数组
free(aux);
}
void main(void)
{
char p[] = "lkjhgfdsawqwrtyuioknvbc";//字符串常量不可更改
printf("%s\n" , p);
KeyIndexCounting(p);
printf("%s\n" , p);
}
键索引计数法不需要比较字符,仅仅需要遍历几次字符串即可,是一个线性级别的排序方法。
低位优先字符串排序
低位优先排序从右向左检查每个字符,每检查一次则排序一次,低位优先字符串排序要求待排序的字符串长度至少都为w位。
低位优先字符串排序和基数排序非常的相似。假设字符串的长度为W,首先以最低位W-1位为键进行排序,再以W-2位为键进行排序,……,直到以0位为键进行排序,此时排序的结果就是最终的结果。显而易见,低位优先字符串排序是稳定排序。无论字符串数组N多大,都只需要遍历w次数据即可排序成功。下面直接上代码
/*
通过LSD排序字符串数组。从w到1位开始排序。
只需要通过w次键索引计数法即可排序成功。
指针数组,传入p则是指向指针的指针,p指针数组的维度,通过低位的w个字符排序。
*/
void LSDSort(char **p , int p_length , int w)
{
int R = 128,i,d;
int N = p_length;//p中元素个数
int count[R+1];
char **aux = (char **)malloc(sizeof(char *) * N);//分配暂存指针数组
for(d = w-1 ; d >= 0 ; d--){
memset(count , 0 , sizeof(count));//将数据清0,为下次键索引计数准备
for(i = 0 ; i < N ; i++)
count[p[i][d]+1]++;//计算键出现的频率,存储在键+1的位置,方便后面计算起始位置。
for(i = 0 ; i < R ; i++)
count[i+1] += count[i];//计算键在结果数组中出现的初始化位置
for(i = 0 ;i < N ; i++)
aux[count[p[i][d]]++] = p[i];//遍历一次字符串,将字符放到aux合适位置
for(int i = 0 ; i < N ; i++)
p[i] = aux[i];//将排好序的字符串拷贝回原数组
}
free(aux);
}
int main(void)
{
char *s[] = {
"4PGC938",
"2IYE230",
"3CIO720",
"1ICK750",
"1OHV845",
"4JZY524",
"1ICK750",
"3CIO720",
"1OHV845",
"1OHV845",
"2RLA629",
"2RLA629",
"3ATW723",
};
int s_length = sizeof(s)/sizeof(s[0]);
for(int i = 0 ; i < sizeof(s)/sizeof(s[0]) ; i++)
printf("%s\n" , s[i]);
LSDSort(s , s_length , 7);
printf("after sort:\n");
for(int i = 0 ; i < sizeof(s)/sizeof(s[0]) ; i++)
printf("%s\n" , s[i]);
return 0;
}
排序过程:
性能:
输出结果:
高位优先字符串排序
单词查找树
子字符串查找
字符串题目
1、下面运行结果
int main(void)
{
char str1[] = "hello world";
char str2[] = "hello world";
char *str3 = "hello world";
char *str4 = "hello world";
}
要点:常量字符串在内存空间中只有一份副本。所以str3 = str4
指向同一个副本。str1和str2
是数组,局部变量,里面存放12个字符,所以str1 != str2
。
2、替换空格
题目:请实现一个函数,把字符串中的每个空格替换成”%20”,例如输入”We are happy.”,则输出 “We%20are%20happy.”。
方法:在原字符串替换,则需要保证字符串后面空间足够;否则需要新键字符串。
注意:计算新字符串所需要空间的时候,必须考虑结束符。指针可以通过两个索引来保证。这样事件复杂度为O(n)。也要注意防御性编程,这是真的值得注意的。
//确保string对应的内存空间足够
void ReplaceBlank(char *string , int length)
{
//防御性编程
if(string == NULL || length <= 0)
return ;
int stringlength = 0;//记录字符串长度
int numofblank = 0;//记录空格长度
char *s = string;
while( *s != '\0'){//统计字符个数包括结束符 和 空格个数
stringlength++;
if(*s == ' ')
numofblank++;
s++;//指向下一个字符
}
stringlength++;//加上结束符才是字符串长度
int newlength = stringlength + numofblank*2;//新字符长度,考虑结束字符
//防御性编程,内存空间不够
if(newlength > length)
return;
int indexOfold = stringlength -1;//指向旧的最后一个字符
int indexOfnew = newlength - 1;//指向新的最后一个字符
while(indexOfold != indexOfnew){
if(string[indexOfold] == ' '){//替换
string[indexOfnew--] = '0';
string[indexOfnew--] = '2';
string[indexOfnew--] = '%';
indexOfold--;
}else
string[indexOfnew--] = string[indexOfold--];
}
}
3、实现strstr
const char* mystrstr(const char *str1, const char *str2)
{
if(NULL == str1 || NULL == str2)
return NULL;
while(*str1 != '\0')
{
const char *p = str1;
const char *q = str2;
const char *res = NULL;
if(*p == *q) //比较后序
{
res = p;
while(*p && *q && *p++ == *q++);
if(*q == '\0')//如果比较结束
return res;//返回
}
str1++;
}
return NULL;
}
3、实现strcpy memcpy(考虑重叠情况) strcat strcmp
当实现memcpy的时候,应该考虑是否重叠的问题。重叠主要分为两种情况。
这是第一种情况,des+n比src大,这是第一种重叠情况,但是最终可以将src正确的成功复制到des起始的位置,这种情况memcpy暂时不考虑。
这是第二中情况,src+n比des大,这种情况如果按照第一种方式复制,会导致最后des起始开始的位置数据发送错误。如上图最后des开始的就是12312。这种情况就需要倒过来复制。下面的代码就是基于这种思路实现出来的。
char* strcpy(char* dst, const char* src)
{
assert(dst);
assert(src);
char* ret = dst;
while((*dst++ = *src++) != '\0');//碰到src的字符串尾停止
return ret;
}//该函数是没有考虑重叠的
//字符串考虑重叠拷贝
char* strcpy(char* dst, const char* src)
{
assert( (dst != NULL) && (src != NULL) );//非空检测,错误则报错处理
if(dst == src)//地址相等,则直接返回
return src;
char* ret = dst;
int size = strlen(src) + 1;//考虑字符串结束符'\0'
if(dst > src && dst < src + len)//dest位于src+len中间,则当做重叠处理
{
dst = dst + size - 1;
src = src + size - 1;
while(size--)
{
*dst-- = *src--;//倒过来拷贝
}
}
else
{
while(size--)
{
*dst++ = *src++;
}
}
return ret;
}
//内存考虑重叠拷贝
//此函数可以防止重叠
//memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
void* memcpy(void* dst, const void* src, size_t size)//void决定可以输入任意类型
{
if(dst == NULL || src == NULL)
{
return NULL;
}
void* res = dst;
char* pdst = (char*)dst;//强转为char*
char* psrc = (char*)src;//强转为char*
if(pdst > psrc && pdst < psrc + size) //重叠
{
pdst = pdst + size - 1;//地址增加size个
psrc = pdst + size - 1;//
while(size--)
{
*pdst-- = *psrc--;//倒过来拷贝
}
}
else //无重叠
{
while(size--)
{
*dst++ = *src++;//优先级问题注意了
}
}
return ret;
}
char* strcat(char* dst, const char* src)
{
char* ret = dst;
while(*dst != '\0')//找到dst字符串尾
++dst;
while((*dst++ = *src) != '\0');//然后拷贝,找到src尾部
return ret;
}
//手写strcmp函数
int strcmp(const char* str1, const char* str2)
{
while(*str1 == *str2 && *str1 != '\0')
{
++str1;
++str2;
}
return *str1 - *str2;//相等则是0,返回正数或者负数则不等。
}
4、字符串移位包含的问题
const char* mystrstr(const char *str1, const char *str2)
{
if(NULL == str1 || NULL == str2)
return NULL;
while(*str1 != '\0')
{
const char *p = str1;
const char *q = str2;
const char *res = NULL;
if(*p == *q) //比较后序
{
res = p;
while(*p && *q && *p++ == *q++);
if(*q == '\0')//如果比较结束
return res;//返回
}
str1++;
}
return NULL;
}
bool FindStr(char *str1 , char *str2)
{
if(!str1 || !str2)
return false;
int len1 = strlen(str1);
for(int i = 0 ; i < len1 ; i++){//进行len次循环移位,每次进行字符串包含判断
char temp = str1[0];
for(int j = 0 ; j < len1 - 1 ; j++)
str1[j] = str1[j+1];
str1[len1 - 1] = temp;
if(mystrstr(str1 , str2) != 0){
return true;
}
}
return false;
}
int main()
{
char src[] = "AABCD";
char des[] = "CDAA";
printf("%d\n" , FindStr(src , des)? 1 :0);
}
5、翻转单词顺序
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
class Solution {
public:
string ReverseSentence(string str) {
//直接使用编译器帮我我们自动创建的栈变量str,注意必须通过值返回,否则析构出错
if(str.empty())
return str;
Reverse(str , 0 , str.size() - 1);//先整体反转str,这个str在栈上面,涉及一次拷贝构造哦,搞完右析构,比较麻烦的
int start = 0;
int end = 0;
int i = 0;
while(i < str.size()){
while(i < str.size() && str[i] == ' ')//跳过空格
i++;//
start = end = i;//记录单词第一次出现位置
while(i < str.size() && str[i] != ' '){//找到单词最后一个位置
i++;//j记录找到的位置。
end++;
}
//找到了一个单词,开始反转
Reverse(str , start , end-1);//局部单词反转
}
return str;
}
private:
void Reverse(string &s , int start , int end){
if(s.empty())
return;
while(start < end)//反转string指定的开始和结束位置,通过传引用获取的效果
swap(s[start++] , s[end--]);
}
};
6、左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
个人觉得做题目还是用C语言比较好,基本不用C++自带的vector和string等等,不行吗?
class Solution {
public:
/*解法一:
借助n个大小的额外空间,直接利用string从字符串中截取
这种做法当字符串很长的时候,不就是在浪费空间么,并且sting的扩容就对应着malloc。
string LeftRotateString(string str, int n) {
int len = str.length();//长度
if(len == 0) return "";//空,直接返回
n = n % len;//对n取余
str += str;//直接从n指定的地方取,不用反转很多次
return str.substr(n, len);
}*/
/*解法二:
直接在原字符串中,通过n分割字符串,然后进行3次翻转即可。
注意需要的防御性编程,这是手写代码必须注意的问题。明白不?
*/
string LeftRotateString(string str, int n) {
if(str.empty())//字符串为空,则返回空
return "";//空则返回空,防御性编程。
if(n == 0 || n == str.length() )//n为0或者长度,则直接返回
return str;
if(n > 0 && n < str.length() ){//0 < n < length。则使用三次翻转
Reverse(str , 0 , n-1);//
Reverse(str , n , str.length()-1);
Reverse(str , 0 , str.length()-1);
return str;
}else
return "";
}
private:
void Reverse(string &s , int start , int end){
if(s.empty())
return;
while(start < end)//反转string指定的开始和结束位置,通过传引用获取的效果
swap(s[start++] , s[end--]);
}
};
5、实现atoi
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
一定要注意边界条件,你知道不???明白就好。
class Solution {
public:
/*
策略则是遍历一遍字符串即可。
1、地址有效NULL或空字符串
2、数据是否上下溢出
3、有无正负号
4、错误标志输出
*/
enum Status{kValid = 0 , kInvalid};
int g_nStatus = kValid;//全局标志
int StrToInt(string str) {
g_nStatus = kInvalid;
long long num = 0;//long long 8字节,但是int 4字节,所以千万不能越界,占用4字节 最大为7f-ff-ff-ff 最小为 80-00-00-00
const char* cstr = str.c_str();//将str转换成c字符串形式,也就是指针指向内存区域。
if( (cstr != NULL) && (*cstr != '\0') )//1、防御性编程 字符存在且第一个非空,则继续处理
{
int minus = 1;//标记字符串符号
if(*cstr == '-')//第一个为减号
{
minus = -1;
cstr++;//指向下一个字符
}
else if(*cstr == '+')//第一个为加号
cstr++;
while(*cstr != '\0')//循环处理全部字符
{
if(*cstr > '0' && *cstr < '9')//为数字
{
g_nStatus = kValid;//因为是数字,则有效。
//2345 = ( ( (0*10+2) * 10 + 3)*10+4 )*10 + 5 这就是霍顿算法。
//这种迭代很聪明
num = num * 10 + (*cstr -'0');//字符串转换数字迭代计算
cstr++;//指向下一个
if( ( (minus > 0) && (num > 0x7FFFFFFF) ) ||
( (minus < 0) && (num < (int)0x80000000) ) )//是否越界?
{
g_nStatus = kInvalid;//无效,返回0
return 0;//无效,则直接返回0
}
}
else
{
g_nStatus = kInvalid;//中间含有非数字字符串,则标记此无效
return 0;
}
}
return (num*minus);
}
g_nStatus = kInvalid;//无效,则直接返回0,并且标记无效。
return 0;
}
};
5、电话号码包含问题
1、写一个函数找出一个整数数组中,第二大的数
2、写一个在一个字符串(n)中寻找字串(m)第一个位置的函数
3、printf的实现
4、strstr的实现
5、实现一个printf类似功能的函数
6、遍历文本找单词并删掉出现频率最少的单词