1.前言
做了好几家公司的笔试编程题,其中涉及到了很多字符串的题,有做出来的也有没做出来后来上网学习大佬的代码的(有的还涉及到了最优解),于是在这里记录一下,加深自己的印象,题目是凭借的回忆,解决方法多数是参考的网络上其他的大佬。
2.字符串笔试题
腾讯笔试编程题的第二题(第三题是一个字符串的题,我太菜了实在是没有思路,最后放弃了),这个题难度不是很大,但是我自己的算法最后会提醒超出运行时间,只通过了百分之八十的测试用例,还是先记录一下,以后改了再更新博客。
题目的内容是:一排巧克力是由n个巧克力球组成的,有的巧克力球上面有榛果,现在要将这一排巧克力分成一个个小块,要求每个小块上有且只能有一个带榛果的小球,请问有多少种分法。输入有两行,第一行表示有n个巧克力球,第二行表示每一个巧克力求是否含有榛子,0代表没有,1代表有。输出有多少种分法。
思考:这个题我借鉴了之前的一道字符串划分IP地址的算法的思路,可以使用递归调用的方法,将巧克力分为已经分成小块的,还未分成小块的,再对还未划分成小块的巧克力递归调用划分算法,直到所有的巧克力都分完,方法数加1;问题的关键在于判断什么时候可以切分成小块。
我的思路是利用其没有榛果是0,有榛果是1的特性,可以遍历整个巧克力条,然后对巧克力球进行累加,累加和为0的时候继续累加;为1的时候继续累加,且此时可以切分,进行递归调用;为2的时候跳出循环。具体代码如下:
import java.util.Scanner;
public class Tengxun2 {
public static int count = 0 ;
public static void main(String[] args ){
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt() ;
int a[] = new int[n];
for( int i = 0 ; i < n ; i++ ){
a[i] = scanner.nextInt() ;
}
findways( 0 , a );
System.out.println(count);
}
public static void findways( int start , int a[]){
int temp = 0 ;
if( start == a.length ){
count++ ; //已经将所有的巧克力切分完成,方法数加1
}
else{ //从还未切分处进行遍历,累加巧克力球的值
for( int i =start ; i < a.length ; i ++ ){
temp+=a[i];
if( temp==1 ){
findways( i+1 , a); //切分巧克力,start从下一颗球开始
}
if( temp == 2){
break; //跳出循环
}
}
}
}
}
运行结果:
两种方式分别为 (10)(1)(1)以及(1)(01)(1)(1)
前面提到了字符串划分IP地址,这是字节跳动的笔试第三题(总共五个题)遇到的,输入是一行字符串,代表抹掉点的IP地址,输出是一个整数,表示原始可能的IP数量。
思考:我们可以将字符串划分为已经划分为IP地址的部分以及还未划分为IP地址的部位,然后遍历字符串,当数字的值是在0-255之间的时候,对剩余还未划分部分进行递归调用划分函数。同样当字符串划分到尾部的时候,方法数加1。还有一些小的细节具体见代码的注释:
import java.util.Scanner;
public class Test3 {
public static int count = 0 ;
public static void main (String []args){
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
find( 0 , 0 ,s );
System.out.println(count+"");
}
//start代表开始划分的位数,finish代表已经完成的数量
public static void find( int start , int finish , String s ){
//字符串长度超过了最大的值(3*还未完成的数量)
if( s.length()- start > 3*(4-finish)){
return;
}
//字符串长度小于最小的值(1*未完成的数量)
if( s.length()- start < 4-finish ){
return;
}
//当字符串遍历到了尾部,且已经划分为四个部分,方法数加1
if( s.length() ==start && finish ==4 ){
count++;
return;
}
//计算temp,防止数组越界
int temp = Math.min(s.length()-start , 3 );
int num = 0 ;
for( int i = start ; i<start+temp ; i ++ ){
num= num*10 + (s.charAt(i)-'0');//求和
if(num <=255){
//满足要求,从下一位开始划分,且完成的数量加1
find( i+1 , finish+1 ,s );
}
//一个0是满足要求的,但是两个0就不行了
if( num == 0 ){
break;
}
}
}
}
运行结果:
方法有且只有一种为10.0.0.1
字节跳动还有一道求字符串中无重复字符的最长字符串长度,这个题其实十分的常见,做法很多,但是存在优解,我在笔试的时候利用的是滑窗法(百分之百的用例通过率),具体实现则是利用了start和end两个指针分别指向字符串的最开始,然后遍历字符串,同时利用了set这一个数据结构(set中不包含有重复元素),当set中未包含有end指针所指向元素,将该元素加入到set当中,end指针后移;当set中包含了该元素,则删除start指针指向的元素,同时将start指针后移,直到未包含end指针指向的元素。整个步骤就像是一个在字符串上滑动的窗口,实现代码如下:
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class wuchongfuzichuan {
public static void main( String [] args ){
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
Set<Character> list = new HashSet<>();
int Max = 0 , start = 0 , end = 0 ;
while( start < s.length() && end <s.length() ){
if( !list.contains(s.charAt(end))){
list.add(s.charAt(end));
end ++ ;
Max =Math.max( Max , end -start );
}
else{
list.remove(s.charAt(start) );
start++;
}
}
System.out.println(Max+"");
}
}
运行结果:
这个题后来上百度查了一下,发现还有更简单的利用哈希表实现的时间复杂度为O(n)的方法,把字符作为键值,其在字符串中的位置作为值,储存在哈希表中,无重复字串的长度便是当前位置减去前一个该字符出现位置的值了,代码实现如下:
import java.util.HashMap;
import java.util.Scanner;
public class wuchongfuzichuan2 {
public static void main(String []args ){
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine() ;
HashMap<Character , Integer >map = new HashMap<>();
int Max = 0 ;
for( int i = 0 ; i < s.length() ; i++ ){
if( !map.containsKey(s.charAt(i))){
map.put(s.charAt(i) , i ) ;
}
else{
int temp = map.get(s.charAt(i)) ;
int tempRes = i - temp ;
Max = Math.max(Max,tempRes) ;
map.put(s.charAt(i) ,i );
}
}
System.out.println(Max);
}
}
同样还是字节跳动的笔试题,题目要求是求一个字符串中的最长非递减子序列,这是一道比较常见的动态规划的算法题,不考虑优化的思路:新建一个数组maxlist用来存储字符串中每个元素为末尾时候的最长非递减子序列的长度,遍历字符串,对于第i个元素,再一次遍历其前面的元素,遇到小于等于i元素的元素j时候,判断maxlist[i]与maxlist[j]+1的大小关系。
import java.util.Scanner;
public class LongNoDesc {
public static void main(String []args){
Scanner scanner = new Scanner(System.in) ;
String s = scanner.nextLine() ;
int a[] = new int[s.length()] ;
for ( int i = 0 ; i <s.length() ; i++ ){
a[i] = 1 ;
for( int j = 0 ; j <i ; j ++ ){
if( s.charAt(j) <= s.charAt(i) && a[j]+1 > a[i]){
a[i] = a[j]+1 ;
}
}
}
int Max = 0 ;
for( int i = 0 ; i < s.length() ; i++ ){
Max = Math.max(Max , a[i]);
}
System.out.println(Max);
}
}
运行结果:
子序列为aabcedf
该算法的时间复杂度为O(n^2)的平方(从代码中的双重循环可以很显然的得到该结论),此代码还存在着二分查找的优化版本,这里就不做优化了,同时如果想输出该最长序列的话,可以再创建一个数组用于储存前驱结点。而关于动态规划这一算法,以后还会专门再码一篇博客进行学习,到时候再留一个传送门。
华为的笔试第三题是一个大数相乘的算法题,这个题其实略显尴尬,因为直接利用Java中的Bigdecimal类,再调用其相乘方法就可以百分之百的测试用例通过率,但是我做的时候完全没有反应过来,而是选择了操作字符串,模拟了乘法的笔算过程来实现。乘法笔算的时候我们将两个子符串的所有位数两两想乘,并按照位数的顺序进行排列 ,最后从低位向高位进行进位的操作。具体实现见代码以及注释:
import java.math.BigDecimal;
import java.util.Scanner;
public class Huawei3 {
public static void main( String[] args){
Scanner scanner = new Scanner(System.in);
String a = scanner.nextLine();
String b = scanner.nextLine();
BigDecimal ba = new BigDecimal(a);
BigDecimal bb = new BigDecimal(b);
System.out.println(mutiply(a,b));
//利用BigDecimal可以直接实现,这里用于检查算法的正确性
BigDecimal bres = ba.multiply(bb);
if( bres.toString().equals(mutiply(a,b))){
System.out.println("True");
}
else{
System.out.println("False");
}
}
//该算法的关键在于理解我们平时的笔算乘法过程,比如说我们19*19,我们将两个字符串的每个位数两两相乘
// 得到 (0)(1*1)(1*9+9*1)( 9*9) = (0)(1)(18)(81) = (0)(1)(18+8)(1) =(0)(1+2)(6)(1) =361
public static String mutiply( String a , String b){
int flag = 0 ;
String resultString="";
//n位数乘n位数结果最多为n+n位数。
int result[] = new int[a.length()+b.length()];
for( int i =0 ; i < a.length() ; i ++ ){
for( int j = 0 ; j< b.length() ; j ++ ){
//两个字符串的每位数两两相乘,并储存在对应的位置
result[ i+ j+1 ] += (a.charAt(i)-'0')*(b.charAt(j)-'0');
}
}
//从最低位到最高位进行进位操作
for( int i = result.length-1 ; i>0 ; i -- ){
if( result[i]>=10 ){
result[i-1] += result[i]/10 ; //将十位进位
result[i] = result[i]%10 ; //留下个位
}
}
for( int i = 0 ; i< result.length ; i++ ){
//去除前缀0
if( result[i] != 0 ){
flag++ ;
}
if( flag!= 0 ){
resultString+=result[i]+"";
}
}
return resultString;
}
}
运行结果:
最后是一道顺丰的笔试题,这个题当时并没有做出来,后来上网查了查别人的做法。
我稍微将原博客的代码改了改,用java代码来实现
import java.util.ArrayList;
import java.util.Scanner;
public class Shunfeng2 {
public static void main(String args[]){
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
int n = scanner.nextInt();
String words = "";
ArrayList<String> list = new ArrayList<>();
ArrayList<String> resultWords = new ArrayList<>();
for( int i = 0 ; i < s.length() ; i ++ ){
if( s.charAt(i) != ','){
words+=s.charAt(i);
}
else{
list.add(words);
words="";
}
}
list.add(words); //将输入中的每个words提取出来,存储到list当中
int i = 0 ;
while ( i < list.size()){
String result ="";
int j = i , len = 0;
//计算判断加上下一个单词长度是否超过允许的最长长度,尽可能的为每一行放入更多单词(单词之间至少需要一个空格)
while ( j < list.size() && len+list.get(j).length()+ j - i <=n){
len += list.get(j).length() ;
j++ ;
}
int space = n - len ;//计算空格数量
for( int k = i ; k < j ; k++ ){
result += list.get(k); //放入单词
if( space > 0 ){ //需要放入空格
int temp=0 ;
if( j ==list.size() ){ //如果该行是单词的最后一行
if( j - k ==1 ){ //最后一个单词了,后面全部加空格
temp =space;
}
else{
temp = 1 ; //题目要求最后一行单词之间只能有一个空格
}
}
else{
if( j - k - 1 > 0 ){
if( space % ( j- k - 1 ) ==0){ //空格能够平均分配的时候
temp = space/(j - k - 1) ;
}
else{
temp = space/(j-k-1)+1; //左边要比右边的空格多 ;
}
}
else{
temp=space ;
}
}
//加上指定数量的空格
for( int m = 0 ; m < temp ; m++ ){
result+=" ";
}
space-=temp ;//减去已经放下的空格数量
}
}
resultWords.add(result);
i = j ;
}
for( int m = 0 ; m < resultWords.size()-1 ;m++ ){
System.out.println(resultWords.get(m));
}
System.out.print(resultWords.get(resultWords.size()-1));
}
}
运行结果:
3.总结
总的来说上面的几个题思路都还是比较常见,解决起来也没有用到什么复杂的算法(当然笔试题中还有其他很多毫无思路的难题,我这里并没有记录下来)。当然等我遇到了其他公司的笔试题的时候,还会持续更新这系列的博客。另外,上面中还用到了动态规划,这个我也打算再仔细了解一下。