Skip to content

Latest commit

 

History

History
534 lines (479 loc) · 25.1 KB

编程面试_字符串.md

File metadata and controls

534 lines (479 loc) · 25.1 KB

数据结构

字符串

1 旋转字符串

  给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,
  如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原字符串变成字符串“cdefab”。
  请写一个函数完成此功能,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。

解法1.1 暴力移位法         将字符一个一个的移动到尾部 或头部

  // 左边的第一个字符移动到最右边
  void LeftShiftOne(char* s, int n)
  {
    char tep = s[0];    // 保存左边的第一个字符 tep
    for (int i = 1; i < n; ++i)
     {
        s[i - 1] = s[i];// 从前向后 靠右的字符 依次向左移动1位
     }
    s[n - 1] = tep;     // tep 放入最右边的位置
  }
  // 右边的第一个字符移动到 最左边
  void RightShiftOne(char* s, int n)
  {
     char tep = s[n - 1];// 保存右边的最后一个字符 tep
     for(int  i = n-1; i > 0; --i)
     {
        s[i] = s[i - 1]; // 从后向前 靠左的字符 依次向右 移动1位
     }
     s[0] = tep;         // tep 放入最左边的位置
  }

因此,若要把字符串开头的m个字符移动到字符串的尾部,则可以如下操作:

void LeftRotateSring(char *s, int n, int m)
{
    while(m--)
    {
         LeftShiftOne(s, n);// 每次移动一个字符 一共 移动 m次
    }
}

而,若要把字符串尾部的m个字符移动到字符串的头部,则可以如下操作:

void ReftRotateSring(char *s, int n, int m)
{
    while(m--)
    {
         ReftShiftOne(s, n);// 每次移动一个字符 一共 移动 m次
    }

   }

针对长度为n的字符串来说,假设需要移动m个字符到字符串的尾部,对每个字符需要进行移动n次,那么总共需要 mn 次操作,同时设立一个变量保存第一个字符,如此,时间复杂度为O(m n),空间复杂度为O(1),空间复杂度符合题目要求,但时间复杂度不符合,所以,我们得需要寻找其他更好的办法来降低时间复杂度。

解法1.2:三步反转法

  对于这个问题,换一个角度思考一下。
  将一个字符串XY分成X(m个字符)和Y(n-m个字符)两个部分,在每部分字符串上定义反转操作,
  如X^T,即把X的所有字符反转(如,X="abc",那么X^T="cba"),那么就得到下面的结论:
  (X^TY^T)^T=YX,显然就解决了字符串的反转问题。
  例如,字符串 abcdef ,若要让def翻转到abc的前头,只要按照下述3个步骤操作即可:
  1.首先将原字符串分为两个部分,即X:abc,Y:def;
  2.将X反转,X->X^T,即得:abc->cba;将Y反转,Y->Y^T,即得:def->fed。
  3.反转上述步骤得到的结果字符串X^TY^T,即反转字符串cbafed的两部分(cba和fed)给予反转,cbafed得到defabc,形式化表示为(X^TY^T)^T=YX,这就实现了整个反转。

三个步骤中后两个步骤都用到了 反转一个字符串

  void ReverseString(char *s, int form, int to)
  {
        while(from < to)// 从首尾开始交换首位位置的值,直到相遇
        {
              char tep  = s[from];// 保存左边
              s[from++] = s[to];  //右边 交换给左边 同时左边向右移动一个位置
              s[to--]   = tep;    // 前右边保存的值 交换给左边 同时 右边向左移动一个位置
        }
  }

  // 前 m 后 放在尾部
  // 反转[0..m - 1] 反转[m..n - 1] 反转[0..n - 1]
  void LeftRotateString(char* s, int n, int m)
  {
  m  %= n;// 避免左移数量大于n的情况
  ReverseString(s, 0, m - 1);// 反转[0..m - 1],套用到上面举的例子中,就是X->X^T,即 abc->cba
  ReverseString(s, m, n - 1);// 反转[m..n - 1],例如Y->Y^T,即 def->fed
  ReverseString(s, 0, n - 1);// 反转[0..n - 1],即如整个反转,(X^TY^T)^T=YX,即 cbafed->defabc。
  }
  // 后m个 放在 头部 
  // 反转[0..n - m - 1] 反转[n - m..n - 1] 反转[0..n - 1]
  void LeftRotateString(char* s, int n, int m)
  {
  m  %= n;// 避免左移数量大于n的情况
  ReverseString(s, 0, n - m - 1);// 反转[0..m - 1],套用到上面举的例子中,就是X->X^T,即 abc->cba
  ReverseString(s, n - m, n - 1);// 反转[n - m..n - 1],例如Y->Y^T,即 def->fed
  ReverseString(s, 0, n - 1);// 反转[0..n - 1],即如整个反转,(X^TY^T)^T=YX,即 cbafed->defabc。
  }

1.3 序列反转 举一反三

  1、链表翻转。给出一个链表和一个数k,比如,链表为1→2→3→4→5→6,
  k=2,则翻转后2→1→6→5→4→3,
  若k=3,翻转后3→2→1→6→5→4,
  若k=4,翻转后4→3→2→1→6→5,用程序实现。
  注解:原序列 XY 则反转后的序列为 X^T Y^T

  2、单词翻转。输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变,句子中单词以空格符隔开。
  为简单起见,标点符号和普通字母一样处理。例如,输入“I am a student.”,则输出“student. a am I”。
  注解:先将整个字符串反转,在将其中每个单词反转回来

  void Reverse_word(char *str)      
  {      
      if(str == NULL)// 空指针 返回 
          return;      
      int len = strlen(str);// 字符串长度
      // 反转整个字符串
      ReverseString(str, 0, len - 1);    
      int s = 0;//每个单词的 起始 index
      int e = 0;// 每个单词的 后 index
      for(int i = 0; i < len; i++) // 遍历字符串 查找每一个单词
      {       
          e = i;      
          if(str[e] == ' ')// 此处 遇到空格 
          {      
              ReverseString(str, s, e-1); // 反转这个单词 
              s = e+1;// 更新 单词的起始指针 
          }      
      }    
      ReverseString(str, s, len - 1);  // 反转最后一个单词 因为最后一个单词后面无空格
  }  

2 字符串包含

  给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
  为了简单起见,我们规定输入的字符串只包含大写英文字母,请实现函数bool StringContains(string &A, string &B)
  比如,如果是下面两个字符串:
  String 1:ABCD
  String 2:BAD

     答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集,当 String 2:BAE 时答案为false

解法2.1 暴力匹配法

  判断string2中的字符是否在string1中?最直观也是最简单的思路是,

     针对string2中每一个字符,逐个与string1中每个字符比较,看它是否在String1中。

代码可如下编写:    

  bool StringContain(string &a, string &b)
  {
        for(int i = 0; i < b.size(); ++i)
        {
              int j;
              //b中的每一个字符,从a的开头一直向后找,直到出现相同字符/已经到达末尾
              for(j = 0; (j <  a.size()) && (a[j] != b[i]); ++j) ;
              if(j >= a.size()) return false;//如果到达末尾 b中的这个字符 都还没有 在a中找到一样的字符 ,则返回错误
        }
        return true;//b中每个字符都在 a中找到了 返回 true

     }

假设n是字符串String1的长度,m是字符串String2的长度,那么此算法,需要O(n*m)次操作。显然,时间开销太大,应该找到一种更好的办法。

解法2.2 排序后匹配法 如果允许排序的话,我们可以考虑下排序

  比如可先对这两个字符串的字母进行排序,然后再同时对两个字串依次轮询。
  两个字串的排序需要(常规情况)O(m log m) + O(n log n)次操作,之后的线性扫描需要O(m+n)次操作。

代码可如下编写:

  //注意A B中可能包含重复字符,所以注意A下标不要轻易移动。这种方法改变了字符串。如不想改变请自己复制
  bool StringContain(string &a,string &b)
  {
      sort(a.begin(),a.end());// 采用最常用的快速排序
      sort(b.begin(),b.end());
      for (int pa = 0, pb = 0; pb < b.length();)// b中的每一个字符
      {
          while ((pa < a.length()) && (a[pa] < b[pb]))// 在a中找 
          {
              ++pa;
          }
          if ((pa >= a.length()) || (a[pa] > b[pb]))//找到最后 或 a中的字符已经大于b对应的字符了 就已经无相等的了
          {
              return false;
          }
          //到这里就是 有相同的字符 a[pa] == b[pb]
          ++pb;//遍历 b中 下一个字符
      }
      return true;
  }

解法2.3 素数乘积取余法

  按照从小到大的顺序,用26个素数分别与字符'A'到'Z'一一对应。

     遍历长字符串,求得每个字符对应素数的乘积f。      遍历短字符串,判断乘积f 能否被短字符串中的字符对应的素数整除,如果整除则包含该字符,如果不能整除则这个字符不包含。 输出结果。

代码可如下编写:

  //此方法只有理论意义,因为整数乘积很大,有溢出风险
  // 此种素数相乘的方法看似完美,但缺点是素数相乘的结果容易导致整数溢出。
  // 算法的时间复杂度为O(m+n)的最好的情况为O(n)
  //(遍历短的字符串的第一个数,与长字符串素数的乘积相除,即出现余数,便退出,返回false)n为长字串的长度,空间复杂度为O(1)
  bool StringContain(string &a,string &b)
  {
      const int p[26] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,61, 67, 71, 73, 79, 83, 89, 97, 101};
      int f = 1;
      for (int i = 0; i < a.length(); ++i)
      {
          int x = p[a[i] - 'A'];// 字符对应的素数
          if (f % x)// 对于长字符串中出现相同的字符 可不用再次 相乘
          {
              f *= x;// a中所有不相同字符 对应的素数 的乘积
         }
      }
      for (int i = 0; i < b.length(); ++i)
      {
          int x = p[b[i] - 'A'];// 字符对应的素数
          if (f % x)// 能够被整除 则说明出现相同的字符
          {
              return false;// 否者出现 了一个不相同的字符 则退出
          }
      }
      return true;
  }

解法2.4 Hash 签名查找

  事实上,可以先把长字符串a中的所有字符都放入一个Hashtable里,
  然后轮询短字符串b,看短字符串b的每个字符是否都在Hashtable里,
  如果都存在,说明长字符串a包含短字符串b,否则,说明不包含。
  再进一步,我们可以对字符串A,用位运算(26bit整数表示)计算出一个“签名”,再用B中的字符到A里面进行查找。

代码可如下编写:

  // “最好的方法”,时间复杂度O(n + m),空间复杂度O(1)
  // 这个方法的实质是用一个整数代替了hashtable,空间复杂度为O(1),时间复杂度还是O(n + m)。
  bool StringContain(string &a,string &b)
  {
      int hash = 0;
      for (int i = 0; i < a.length(); ++i)
      {
          hash |= (1 << (a[i] - 'A'));// (a[i] - 'A')为0~25 右移动一位 位或上签名hash 得到 长串a的最后签名
          // 相当于把 a的每一个不同的 字符 放入 每一个而进行位上
      }
      for (int i = 0; i < b.length(); ++i)
      {
          if ((hash & (1 << (b[i] - 'A'))) == 0)// 位与 上(1 << (b[i] - 'A')) 的0的话就说明对应的位上(字符)与a中午匹配
          {
              return false;// 对应位上 a中未出现相应的字符  则查找失败
          }
      }
      return true;
  }

2.5 字符串包含 举一反三 变位词

  如果两个字符串的字符一样,但是顺序不一样,被认为是兄弟字符串。
  比如bad和adb即为兄弟字符串,现提供一个字符串,如何在字典中迅速找到它的兄弟字符串,请描述数据结构和查询过程。

     解答: a包含b b也包含a      数据结构为: 设置一个64bit的变量long long hash1,将字符串A中的字符通过移位在hash1中表示;设置另外一个64bit的变量long long hash2, 将字符串B中的字符通过移位在hash2中表示。 查询过程: 比较hash1与hash2的大小,若相等,则为兄弟字符串;若不等,则不为兄弟字符串。

代码可如下编写:

  bool anagramJudge(string &a, string &b){
      long long hash1=0;
      long long hash2=0;
      for(int i=0; i<a.length(); i++){
          hash1 |= (1<<(a[i] - 'A'));// a字符串的 hash 签名
      }
      for(int j=0; j<b.length(); j++){
          hash2 |= (1<<(b[j] - 'A'));// b字符串的 hash 签名
      }
      if(hash1 == hash2)// 若相等,则为兄弟字符串
          return true;
      else
          return false;// 若不等,则不为兄弟字符串
  }

3字符串转换成整数

  输入一个由数字组成的字符串,把它转换成整数并输出。
  例如:输入字符串"123",输出整数123。
  给定函数原型int StrToInt(const char *str) ,
  实现字符串转换成整数的功能,不能使用库函数atoi。

3.1不考虑溢出 正负号

  以"123"作为例子:
     当我们扫描到字符串的第一个字符'1'时,由于我们知道这是第一位,所以得到数字1。
     当扫描到第二个数字'2'时,而之前我们知道前面有一个1,
  所以便在后面加上一个数字2,那前面的1相当于10,因此得到数字:1*10+2=12。
     继续扫描到字符'3','3'的前面已经有了12,由于前面的12相当于120,
  加上后面扫描到的3,最终得到的数是:12*10+3=123。
  因此,此题的基本思路便是:
  从左至右扫描字符串,把之前得到的数字乘以10,再加上当前字符表示的数字。

代码可如下编写:

  int StrToInt(const char *str)
  {
      if(str == NULL)   // 空指针返回
         return -1;
      int n = 0;
      int isNeg = 0;    // 负数标志
      if(str[0] == '-') isNeg = 1;
      while (*str != 0) // 遍历到字符串末尾
      {
          if(*str > '9' || *str < '0') {continue; ++str;}// 跳过非数字字符 或者 直接 退出 return -1;
          int c = *str - '0';
          n = n * 10 + c;// 累加和
          ++str;         // 移动指针
      }
      if(isNeg) return -n;
      else      return n;
  }

3.2 完整的考虑溢出等情况

  int StrToInt(const char* str)
  {
      static const int MAX_INT = (int)((unsigned)~0 >> 1);
      static const int MIN_INT = -(int)((unsigned)~0 >> 1) - 1;
      unsigned int n = 0;

      //判断是否输入为空
      if (str == 0)
      {
          return 0;
      }

      //处理空格 跳过第一个空格
      while (isspace(*str))
          ++str;

      //处理正负
      int sign = 1;
      if (*str == '+' || *str == '-')
      {
          if (*str == '-')
              sign = -1;
          ++str;// 如果有正负号 确定标志后 从符号后一位开始处理
      }

      //确定是数字后才执行循环
      while ( *str > '0' && *str < '9' )
      {
          //处理溢出
          int c = *str - '0';// 字符转换成 数字
          if (sign > 0 && (n > MAX_INT / 10 || (n == MAX_INT / 10 && c > MAX_INT % 10)))
          {
              n = MAX_INT;
              break;
          }
          else if (sign < 0 && (n >(unsigned)MIN_INT / 10 || (n == (unsigned)MIN_INT / 10 && c > (unsigned)MIN_INT % 10)))
          {
              n = MIN_INT;
              break;
          }

          //把之前得到的数字乘以10,再加上当前字符表示的数字。
          n = n * 10 + c;
          ++str;
      }
      return sign > 0 ? n : -n;
  }

3.3 举一反三

  实现string到double的转换

代码可如下编写:

  double StrToDou(const char *str)
  {
      if(str == NULL)   // 空指针返回
         return -1;
      double result = 0.0;// 最后的值
      double dec = 10.0;// 小数位比值
      int isNeg = 0;   // 负数标志
      int isDec = 0;
      if(str[0] == '-') isNeg = 1;
      while (*str != 0) // 遍历到字符串末尾
      {
          if(*str == '.') {isDec = 1; ++str;}//是小数
          else if(*str > '9' || *str < '0') {continue; ++str;}// 跳过非数字字符 或者 直接 退出 return -1;
          if(!isDec) // 整数部分
              result = result * 10 + *str - '0';// 整数部分 累加和
          else //识别小数点之后进入 这个分支
              { 
               result = result + (*str - '0')/dec;// 小数部分 算法
               dec *= 10;// 小数位比值递增
              }
          ++str;         // 移动指针
      }
      if(isNeg) return -result;
      else      return result;
  }

4 字符串 回文 判断

4.1 首尾并进判断法

  同时从字符串头尾开始向中间扫描字串,如果所有相对的字符都一样,那么这个字串就是一个回文。
  采用这种方法的话,我们只需要维护头部和尾部两个扫描指针即可。 Palindrome  英 ['pælɪndrəʊm]  pai lin drow mu

代码可如下编写:

  // 输入字符指针 s 和 字符串大小 num
  bool IsPalindrome(const char* s, int num)
  {
        if(s == NULL || num < 2)  return false;//指针为空 / 字符串长度小于2
        // 定义并初始化 首尾指针
        const char* low, *high;// 首尾指针
        low = s;// 首指针
        high = s + num - 1;// 尾指针
        while(low < high){
              if(*low ! *high) return false;// 出现一次相对应位置的字符不相同 就返回错误
              ++low;
              --high;
        }
        return true; // 到这里的话 就证明 相对应位置上的 字符相同 为 回文
  }

这是一个直白且效率不错的实现,时间复杂度:O(n),空间复杂度:O(1)。 上述解法一从两头向中间扫描,那么是否还有其它办法呢?我们可以先从中间开始、然后向两边扩展查看字符是否相等。

判断一条单向链表是不是“回文

  对于单链表结构,可以用两个指针从两端或者中间遍历并判断对应字符是否相等。
  但这里的关键就是如何朝两个方向遍历。由于单链表是单向的,所以要向两个方向遍历的话,可以采取经典的快慢指针的方法,
  即先位到链表的中间位置,再将链表的后半逆置,最后用两个指针同时从链表头部和中间开始同时遍历并比较即可。

判断一个栈是不是“回文”

  对于栈的话,
  只需要将字符串全部压入栈,然后依次将各字符出栈,
  这样得到的就是原字符串的逆置串,

     将逆置串和原字符串各个字符比较,就可以判断了。

5 最长回文子串

     如果一段字符串是回文,那么以某个字符为中心的前缀和后缀都是相同的, 例如以一段回文串“aba”为例,以b为中心,它的前缀和后缀都是相同的,都是a。      那么,我们是否可以可以枚举中心位置,然后再在该位置上用扩展法,记录并更新得到的最长的回文长度呢?

5.1 遍历子串中心位置,找最大回文子串(分为子串长度为 奇数和偶数的情况)

参考代码如下:

  int LongestPalindrom(const char* s, int num){
  if(s == NULL || num < 2)  return false;//指针为空 / 字符串长度小于2
  int i,j,max,cnt;// ij 为循环变量 cnt 记录每一个回文子串的长度
  max = 0;//最大字回文字符串长度 初始化为0

  // 遍历每一个子串 的 中心点
  for(i = 0; i < n; ++i){ 
     // 对于子串长度为奇数的情况
       for(j = 0; (i - j >= 0)&&(i + j < n); ++j){// j为以i字符点为中心 向两边扩展的间隔 区别首尾区间 [0, n-1]
              if(s[i - j] != s[i + j]) break; // 间隔相同的位置 字符不同 结束回文
              cnt = j*2 + 1;// 已此中心点i 的回文子串长度为 间隔j *2 + 1(中间一个字符)
        }
        if(cnt > max) max = cnt;// 保存最长的 回文子串的 长度

    // 对于子串 长度为 偶数的情况
        for(j = 0; (i - j >= 0)&&(i + j + 1 < n); ++j){
              if(s[i - j] != s[i + j +1]) break; // 间隔相同的位置 字符不同 结束回文
              cnt = j*2 + 2;
        }
        if(cnt > max) max = cnt;// 保存最长的 回文子串的 长度   
   }
  return max;
  }

6 字符串的全排列

  输入一个字符串,打印出该字符串中字符的所有排列。
  例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串
  abc、acb、bac、bca、cab 和 cba。

6.1 递归实现

  从集合中依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,
  如此递归处理,从而得到所有元素的全排列。     Permutation  英 [pɜːmjʊ'teɪʃ(ə)n]  Peer mu tai xing
  以对字符串abc进行全排列为例,我们可以这么做:以abc为例 

     abc a和a交换后 abc 对a后的 bc进行全排列      abc a和b交换后 bac 对b后的 ac进行全排列      abc a和c交换后 cba 对c后的 ba进行全排列

参考代码如下:

  void CalcAllPermutation(char* s, int from, int to)
  {
  if (s == NULL || to < 2)  return;

  // 打印排列后的字符串
  if (from == to)/
  {
    for (int i = 0; i <= to; i++)
        cout << perm[i];
    cout << endl;
  }
  else
  {
    for (int j = from; j <= to; j++)// 从当前位置from开始 依次向后和j位置交换元素后 对from后的元素进行全排列
    {
        swap(perm[j], perm[from]);// 从当前位置from开始 依次向后和j位置交换元素
        CalcAllPermutation(perm, from + 1, to);// 对from后的元素 [from + 1, to]进行全排列
        swap(perm[j], perm[from]);// 再还原字符串
    }
  }
  }

习题集

1 第一个只出现一次的字符

  在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。

遍历法

  //O(n^2)的时间复杂度
  char FirstNotRepeatingChar(char *pS, int num)
  {
      //如果是空指针,返回\0
      if(pS == NULL) return '\0';
      int i,j,flag=0;
      bool* bap;
      bap = (bool*)maoolc(sizeof(bool)*num);
      for(i =0; i < n; ++i) *bap[i] = false;
      for(i=0; i<num-1; ++i)// 遍历 前置 字符
      { 
          if(*bap[i]) continue;// 当前 字符串 之前出现过 跳过本次循环   有问题 后面再出现相同的呢???
          for(j=i+1; j<len; ++j)// 和其后面的字符 依次作比较
          {
              if(pS[i] == pS[j]) {*bap[j] = true; break;}// i位置上的字符 后面有出现 跳过 这个字符
          }
          return pS[i];// 打印这个出现一次的字符
      }
      return '\0';
  }

哈希表法 强烈推荐

  //O(n)的时间复杂度
  char FirstNotRepeatingChar(char *pS)
  {
      //如果是空指针,返回\0
      if(pS == NULL)  return '\0';
      //字符(char)是一个长度为8bit的数据类型,因此总共最多能够表示256种字符。 
      //定义hash表长度256,并创建哈希表
      const int len=256;
      int hashtable[len];
      for(int i=0;i<len;i++)  hashtable[i]=0;

      char *pHashkey=pS;
      // 第一遍遍历字符串,求出每个字符出现的次数
      // 字符根据其ASCII值作为数组的下标对应数组的一个数字,而数组中存储的是每个字符出现的次数
      while((*pHashkey)!='\0') hashtable[*(pHashkey++)]++;
      pHashkey=pS;
      //第二遍遍历字符串,求出第一个只出现一次的字符,每次都是按照字符串的顺序遍历
      while((*pHashkey)!='\0')
      {
          if(hashtable[*pHashkey]==1)// 记录只出现一次
              return *pHashkey;
          pHashkey++;
      }
      return '\0';
  }