字串筆試題-演算法筆試筆記(一)
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.總結
總的來說上面的幾個題思路都還是比較常見,解決起來也沒有用到什麼複雜的演算法(當然筆試題中還有其他很多毫無思路的難題,我這裡並沒有記錄下來)。當然等我遇到了其他公司的筆試題的時候,還會持續更新這系列的部落格。另外,上面中還用到了動態規劃,這個我也打算再仔細瞭解一下。