数据结构之字符串

字符串算法简介

字符串排序

键索引计数法

应用领域:
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、遍历文本找单词并删掉出现频率最少的单词

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有时需要偏执狂

请我喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值