字符串(字符数组)在java中是内置类型,不可更改,要更改的话考虑转StringBuffer,StringBuilder,char[]之类。在C++中,std::string可更改,也可考虑用char[] (char *)。值得注意的是,C++中“+”运算符(字符串连接符)在官方文档中复杂度未定义,但一般认为是线性的O(n),在实际使用中要防止退化为O(n2)。C++中std:substr()和java中subString()都是截取字符串,但是两者的参数不同
c++中的字符范围是[-128, +127],一般转化为无符号整型[0,255]。java中字符范围[0, 65535],提及字符范围的目的是为了将字符和整型数字挂钩,起到简易的hash函数功能,如对一个字符串中的每种字符进行统计,则可定义一个长度256的数组,下标代表字符,数组元素的值是字符出现的次数,这种思想很重要,下面会涉及到具体的demo。
面试题的总体分析
字符串数组涉及的题目非常广泛
- 概念理解:字典序(请自行百度其含义)
- 简单操作:插入、删除字符、旋转
- 规则判断:罗马数字转换,判断是否为合法的整数或浮点数
- 数字运算:大数运算,二进制加法
- 排序、交换:partition的过程
- 数字计数(hash):变位词
- 匹配:正则表达式、全串匹配、KMP,周期检查
- 动态匹配:LCS、编辑距离、最长回文子串
- 搜索:单词变换,排列组合
下面会逐一对上面九种常见面试题做分析
- 字典序
按照两个字符串字符的ASCII码值,先比较首字符,如果相同再比较第二个,依此类推。如果两个字符串相同,字符串长的大
int strCmp(const char *sl, const char *s2)
{
/*compare unsigned char sl[],s2[]*/
for (;*sl == *s2;++sl, ++s2)
if (*sl == '\0')
return(0);
if (*sl == '\0') return(-1);
if (*s2 == '\0') return(1);
return ((*(unsigned*)sl < *(unsigned*)s2) ? -1 : +1);
}
- 字符串操作
插入的话,为了防止后面的字符被覆盖,要“倒着”复制。删除的操作,“向前”覆盖即可。这两个操作很简单,就不写demo,不会的同学请自行百度。下面通过例子讲一下旋转。
翻转句子中的全部单词,但是单词内容不变,如 I’m a student,翻转为student a I’m。
思路:in-place翻转,将i位和j位互换,则对全句进行翻转得到:tneduts a m’I。再对单词进行翻转,则得到student a I’m。通过两次in-place翻转就可以满足题目要求,唯一的难点是如何区别不同单词,通过空格即可。
#include <stdio.h>
#include <string.h>
int invert_string(char* str, int i, int j) {
int length = strlen(str);
char tmp;
if (i > j || j >= length) {
printf("length error i: %d j: %d \n",i, j);
return -1;
}
for (; i < j; i++, j--) {
tmp = str[j];
str[j] = str[i];
str[i] = tmp;
}
return 0;
}
int main() {
int i, j;
char str[128] = "kyle is a student";
int length = strlen(str); // strlen()返回长度是不带'\0'
printf("length: %d\n", length);
invert_string(str, 0, length - 1);
for (i = 0, j = 0; j < length && i < length; j++) {
if (str[j] == ' ') {
invert_string(str, i, j - 1);
i = j + 1;
}
else if (j == length - 1) {
invert_string(str, i, j);
}
}
printf("%s\n", str);
return 0;
}
- partition例题
0-1串的交换,把一个0-1串(只包含0和1的串)进行排序,你可以交换任意两个位置,问最少交换的次数?
这个问题涉及到了快排partition的思想,非常重要,可以背下来。0-1字符串只是表象,我们可以把任意数组按照我们的标准,划分成两部分,这才是本质。
#include <stdio.h>
#include <string.h>
int main() {
char buf[128] = "10010101001010101010";
int length = strlen(buf);
int i, j;
char tmp;
j = 0;
for (i = 0; i < length; i++) {
if (buf[i] == '0') {
tmp = buf[j];
buf[j++] = buf[i];
buf[i] = tmp;
}
}
printf("%s", buf);
return 0;
}
字符的删除与插入,删除就是"正着"覆盖,插入就是”倒着“复制。请看下面一题。
删除一个字符串所有的a,并且复制所有的b。注:字符数组足够大。这一题将这两种思想都用到了
分析:
1)首先利用”正着“覆盖,删除a,同时计算b的个数,查看需要多少的空间来复制额外的b。
2)先计算有几个b,得到复制后的长度,再使用”倒着“复制的技巧。
#include <stdio.h>
#include <string.h>
int main() {
char buf[256] = { "kyle is a smart boy" };
int length = strlen(buf);
int n, i, j, num;
n = i = num = 0;
for (; i < length; i++) {
if (buf[i] != 'a') buf[n++] = buf[i];
if (buf[i] == 'b') num++;
}
buf[n] = 0;
int newlength = n + num;
buf[newlength] = 0;
for (i = newlength - 1, j = n - 1; j >= 0; j--) {
buf[i--] = buf[j];
if (buf[j] == 'b') buf[i--] = buf[j];
}
printf("%s", buf);
return 0;
}
交换星号,一个字符串只包含*和数字,请把它的*号都放开头。
也是一个二分类的问题,使用快排partition的思想。也可以使用”倒着“复制的方法。
快排的方法会把数字的顺序打乱,还有一种倒着复制的方法,可以不打乱顺序
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main() {
char buf[128] = "37*6*1";
int length = strlen(buf);
int i, j;
for (i = j = length - 1; i >= 0; i--) {
if (isdigit(buf[i])) buf[j--] = buf[i];
}
for (i = j; i >= 0; i--) {
buf[i] = '*';
}
printf("%s", buf);
return 0;
}
- 字符计数(hash)
给定两个串a和b,问b是否是a的子串的变位词。例如输入a = hello, b = lel, lle, ello都是true,但是b = elo是false。
分析:
1)滑动窗口的思想,动态维护一个滑动窗口,比如b的长度是3,我们考察a[0…2], [1…3],[2…4]是否和b是变位词。
2)滑动窗口如何与b进行比较呢,使用hash进行字符计数,基于字符串的特殊性,我们可以用[0…255]的数组,我们暂且认为它们都是小写英文字母,用num[26]来表示b中每个单词出现多少次。下标是字母的acsii码,元素值是对相应字母的计数。
3)当num[]数组中元素全为0时,则符合条件,但如何判断数组为空呢,这里引入变量nonZero,来做判断。
4)窗口的动态维护,就是向右移动一位,旧窗口a[i – lenb… i – 1],新窗口a[i - lenb + 1…i]
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
bool main() {
char a[] = { "hello word" };
char b[] = { "d" };
int lena = strlen(a);
const int lenb = strlen(b);
int nonZero = 0;
int num[26] = { 0 };
for (int i = 0; i < lenb; i++) {
//对b中字符进行计数,并统计num数组非0的个数
if((++num[b[i] - 'a']) == 1) nonZero++;
}
//与第一个窗口进行比较
for (int i = 0; i < lenb; i++) {
--num[a[i] - 'a'];
if (num[a[i] - 'a'] == 0) nonZero--;
if (num[a[i] - 'a'] == -1) nonZero++;
}
if (nonZero == 0) {
return true;
}
//对窗口进行动态维护
for (int i = 0; i < lena - lenb; i++) {
//去掉旧的元素
unsigned c = a[i] - 'a';
++num[c]; //恢复现场
if (num[c] == 0) nonZero--;
if (num[c] == 1) nonZero++;
//增加新的元素
c = a[i + lenb] - 'a';
--num[c];
if (num[c] == 0) nonZero--;
if (num[c] == -1) nonZero++;
if (nonZero == 0) return true;
}
return false;
}
5 字符串的循环移位
如abcd,循环移动一位结果是dabc,移动两位是cdab,移动三位是bcda,移动四位又回到初始状态。
分析:长度为n, 移动m次,相当于移动m % n次。先整体反转一次,再前m % n位翻转, 后n – m % n位翻转。
#include <stdio.h>
#include <string.h>
int str_swap(char* s, int i, int j) {
int len = strlen(s);
if (j >= len || i > j) {
printf("formate error\n");
return -1;
}
char tmp;
int a, b;
for (a = i, b = j; a < b; a++, b--) {
tmp = s[a];
s[a] = s[b];
s[b] = tmp;
}
return 0;
}
int main() {
char s[] = { "abcd" };
int a = 2;
int len = strlen(s);
int mod = 2 % len; //移动mod位
str_swap(s, 0, len-1);
str_swap(s, 0, mod - 1);
str_swap(s, mod, len - 1);
printf("%s", s);
}
总结
我理解的in-place (原地)
1)本身O(1)空间
2)递归,堆栈空间可以不考虑
原地相关的问题
1)字符串循环左移、右移动
2)快排partition相关
滑动窗口
1)能达到O(n)的的时间复杂度
2)O(1)的空间复杂度
规则相关——细致
匹配 (暴力):KMP比较少见
Manacher——要求比较高的笔试