字符串高频面试题一网打尽(基于C/C++实现)

字符串(字符数组)在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。

面试题的总体分析
字符串数组涉及的题目非常广泛

  1. 概念理解:字典序(请自行百度其含义)
  2. 简单操作:插入、删除字符、旋转
  3. 规则判断:罗马数字转换,判断是否为合法的整数或浮点数
  4. 数字运算:大数运算,二进制加法
  5. 排序、交换:partition的过程
  6. 数字计数(hash):变位词
  7. 匹配:正则表达式、全串匹配、KMP,周期检查
  8. 动态匹配:LCS、编辑距离、最长回文子串
  9. 搜索:单词变换,排列组合

下面会逐一对上面九种常见面试题做分析

  1. 字典序
    按照两个字符串字符的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);
}
  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;
}
  1. 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;
}
  1. 字符计数(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——要求比较高的笔试

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值