1. 程式人生 > >幾個面試演算法題,附原始碼

幾個面試演算法題,附原始碼

昨天下午面試了一家,最後有道演算法題,當時沒想起來,就拍了張照,回來慢慢想,也算沒白去。

原題 :  有N個人圍成一圈,順序排號。從第一個人開始報數(1到3),凡是報到3的人推出圈子,問最後留下的是原來第幾號的那位?

先說下當時的思路,當時以為留下的人是有規律的,就想用數學歸納法,找到這個規律,但是下面寫了結果,發現並沒有什麼規律。

n=1 , 留下1 ; n=2 , 留下2 ; n=3 , 留下2 ; n=4 , 留下1 ; n=5 , 留下4 ; 

n=6 , 留下1 ; n=7 , 留下4 ; n=8 , 留下7 ; n=9 , 留下1 ; n=10 , 留下4 ; 

數學歸納法的方法不行。那就試試走程式吧。  

經過2個小時的除錯,終於出了結果,下面是原始碼,結合註釋看,

#include <stdio.h>

void fun(int n)
{

    int a[n];
    int i = 0 ;
    for(i=0;i<n;i++){
        a[i]=i+1;
        printf("%d\n",a[i]);

    }
    
    int m=0;//用來數123
    int k=0;//k記錄移動到的a陣列下標,k總是有效的陣列下標,
    int j=0;//記錄a陣列中大於0的個數,如果只剩一個說明遊戲結束。
    while(1){
        
        m++;
        printf("當前的m=%d   對應的元素  %d\n",m,a[k]);

        if(m==3){
            a[k]=-1;//把這個元素移除。
            m=0;//m歸零,重新開始下一輪計數
        }
              k++;
//尋找下一個元素,先向後尋找
            for(;k<n;k++){
            	if(a[k]>0){
            		printf("向後找到的下一個元素  %d\n",a[k]);
            		break;
            	}
            }
            
// 如果向後尋找失敗,那就得從頭開始繼續找
        if(k==n){
	for(i=0;i<n;i++){
		if(a[i]>0){
            		k=i;
            		printf("向前迴圈找到的下一個元素  %d\n",a[k]);
            		break;
           		}
            }
            
        }
        
        
        //判斷能否結束迴圈
        j=0;
        for(i=0;i<n;i++){
            if(a[i]>0){
                j++;
                
            }
        }
        printf("當前陣列中有效的元素個數是 %d\n",j);
        //j==1,說明可以結束了,k中的就是那個唯一剩下的元素了
        if(j==1){
            printf("最終留下的數字是 %d\n",a[k]);
            break;
        }
    }
    
    
    
}


int main(void) {
    fun(10);
    return 0;
}
下面是log資訊: 
1
2
3
4
5
6
7
8
9
10
當前的m=1   對應的元素  1
向後找到的下一個元素  2
當前陣列中有效的元素個數是 10
當前的m=2   對應的元素  2
向後找到的下一個元素  3
當前陣列中有效的元素個數是 10
當前的m=3   對應的元素  3
向後找到的下一個元素  4
當前陣列中有效的元素個數是 9
當前的m=1   對應的元素  4
向後找到的下一個元素  5
當前陣列中有效的元素個數是 9
當前的m=2   對應的元素  5
向後找到的下一個元素  6
當前陣列中有效的元素個數是 9
當前的m=3   對應的元素  6
向後找到的下一個元素  7
當前陣列中有效的元素個數是 8
當前的m=1   對應的元素  7
向後找到的下一個元素  8
當前陣列中有效的元素個數是 8
當前的m=2   對應的元素  8
向後找到的下一個元素  9
當前陣列中有效的元素個數是 8
當前的m=3   對應的元素  9
向後找到的下一個元素  10
當前陣列中有效的元素個數是 7
當前的m=1   對應的元素  10
向前迴圈找到的下一個元素  1
當前陣列中有效的元素個數是 7
當前的m=2   對應的元素  1
向後找到的下一個元素  2
當前陣列中有效的元素個數是 7
當前的m=3   對應的元素  2
向後找到的下一個元素  4
當前陣列中有效的元素個數是 6
當前的m=1   對應的元素  4
向後找到的下一個元素  5
當前陣列中有效的元素個數是 6
當前的m=2   對應的元素  5
向後找到的下一個元素  7
當前陣列中有效的元素個數是 6
當前的m=3   對應的元素  7
向後找到的下一個元素  8
當前陣列中有效的元素個數是 5
當前的m=1   對應的元素  8
向後找到的下一個元素  10
當前陣列中有效的元素個數是 5
當前的m=2   對應的元素  10
向前迴圈找到的下一個元素  1
當前陣列中有效的元素個數是 5
當前的m=3   對應的元素  1
向後找到的下一個元素  4
當前陣列中有效的元素個數是 4
當前的m=1   對應的元素  4
向後找到的下一個元素  5
當前陣列中有效的元素個數是 4
當前的m=2   對應的元素  5
向後找到的下一個元素  8
當前陣列中有效的元素個數是 4
當前的m=3   對應的元素  8
向後找到的下一個元素  10
當前陣列中有效的元素個數是 3
當前的m=1   對應的元素  10
向前迴圈找到的下一個元素  4
當前陣列中有效的元素個數是 3
當前的m=2   對應的元素  4
向後找到的下一個元素  5
當前陣列中有效的元素個數是 3
當前的m=3   對應的元素  5
向後找到的下一個元素  10
當前陣列中有效的元素個數是 2
當前的m=1   對應的元素  10
向前迴圈找到的下一個元素  4
當前陣列中有效的元素個數是 2
當前的m=2   對應的元素  4
向後找到的下一個元素  10
當前陣列中有效的元素個數是 2
當前的m=3   對應的元素  10
向前迴圈找到的下一個元素  4
當前陣列中有效的元素個數是 1
最終留下的數字是 4

 A,B,C,D,E五人在某天夜裡合夥去捕魚, 第二天凌晨時都已疲憊不堪,於是各自找地方睡覺。
 日上三杆
 A第一個醒來,他將魚分成五份,把多餘的一條魚扔掉,拿走自己的一份;
 B第二個醒來,也將魚分成五份,把多餘的一條魚扔掉,拿走自己的一份;
 C,D,E依次醒來,也同樣這樣做 。

 問他們合夥捕了多少條魚?

先假設一個數滿足了條件, 這些條件依次帶入 ,帶入5次後還沒有出現小數即為成功 . 列印這個數即可 . see 是每個人看見的總魚數. 

// 驗證這個數字是否滿足條件
func tryThisNum(tolte:Int)->Bool {
    
    var see = tolte
    for i in 0..<5 {
        if (see-1)%5==0 {
            see = (see-1)/5*4
        } else {
            return false
        }
    }
    // 迴圈了5次,還沒有返回,說明滿足條件了
    return true
    
}


for k in 0 ..< 10000 {
    if tryThisNum(tolte: k) {
        print( String(k) + "是滿足要求的魚數")
    }
}
3121是滿足要求的魚數
6246是滿足要求的魚數
9371是滿足要求的魚數


在 n 個數中找到 前 m 大的數字,除了排序還有什麼思路。

第一反應有點懵,後來想想,找出m個大數,那就需要一個數組b來存,然後拿b中最小值依次與n個數進行比較並更新b中的值即可。後來百度了,說有種堆排序的方法,適合大量資料。下面是思路:

維護一個大小為m的小根堆,依次掃描n個無序數,若掃描到的數比堆頂元素要大,用掃描到的數替換堆頂元素,然後調整堆。這個演算法的思想源自堆排序。時間複雜度是O(nlogm)。適用於海量資料,即它不需要所有的資料都載入進記憶體,只需要維護一個大小為m的小頂堆,這是其一個巨大的優勢。

#include <stdio.h>

// 紀錄最大值和最大值的下標
int minV=0,minX=0;
// 找出b中的最小值
void findMinInB(int *b){
	
	minV = b[0];
	minX = 0 ;
	for (int i=0;b[i];i++){
		if (minV>b[i]){
			minV=b[i];
			minX = i ;
		}
	}
	printf("最小值 %d 最小值下標 %d \n",minV ,minX);
}

void show(int * a){
	for (int i=0;a[i];i++){
		printf("%d \t",a[i]);
	}
	printf("\n");
}

int main()
{
   int a[10]={1,5,12,7,187,
			 6,78,45,11,26};
	int b[5]={0,0,0,0,0};
	// 給b中的值初始值
	for (int i=0;i<5;i++) {
		b[i] = a[i];
	}
	// 找到b中的最小者,紀錄下來座標和值
	findMinInB(b);
	// 用剩下的a中的值依次進行比較
	for (int i=5;a[i];i++){
		// 比b中最小值大的話就替換最小值,然後再次更新b中最小者
		if(a[i]>minV){
			b[minX]=a[i];
			findMinInB(b);
		}
	}
	show(a);
	show(b);
	
   return 0;
}

列印楊輝三角

#include <stdio.h>

int main(void) {

	
	int n=9;
	
	int a[n][2*n+1];
	for(int i=0;i<n;i++){
		for(int j=0;j<2*n+1;j++){
			a[i][j]=0;
		}
	}
	//最頂點的1,作為初始值,直接賦值,其餘的通過計算獲得
	a[0][n]=1;
	for(int i=1;i<n;i++){
	
		for(int j=0;j<2*n+1;j++){
			//陣列越界了,預設超出的值是0
			if(j-1<0){
				a[i][j]=0+a[i-1][j+1];
				continue;
			}
			if(j+1>=2*n+1){
				a[i][j]=a[i-1][j-1]+0;
				continue;
			}
			//普通的數 = 左上角 + 右上角
			a[i][j]=a[i-1][j-1]+a[i-1][j+1];
		}
		
		
	}


	for(int i=0;i<n;i++){
		for(int j=0;j<2*n+1;j++){
			if(a[i][j]==0){
				// 0 就不列印了,但是佔位還是要有的
				printf("   ") ;
				continue;
			}
			printf(" %d ",a[i][j]) ;
		}
		printf("\n");
	}






	return 0;
}


我設定的引數是9,列印9行,如果設定10,會有3位數出現,對齊有點問題,需要用\t來對齊,無傷大雅,就不折騰了結果:
                               1                               
                            1     1                            
                         1     2     1                         
                      1     3     3     1                      
                   1     4     6     4     1                   
                1     5     10     10     5     1                
             1     6     15     20     15     6     1             
          1     7     21     35     35     21     7     1          
       1     8     28     56     70     56     28     8     1       

兩個乒乓球隊進行比賽,各出3人。甲隊為A,B,C3人,甲隊為x,y,z3人。抽籤決定比賽名單。有人向隊員打聽比賽的名單,A說他不和x比,C說他不和x,z比。程式設計找出3對賽手的名單。第一眼以為是好幾個結果,但是動手後發現僅有一種符合,這種題其實不適合作為程式設計題。
#include <stdio.h>

int main(void) {
	// 初看以為會不止一種情況,但是自己動手算出了,發現只有一種情況
	// a-z  b-x  c-y
	// i 作為a的對手 j作為b的對手, k作為c的對手,求出ijk即可。
	char i,j,k;
	
	for(i='x';i<='z';i++) {
		// a的對手不是x
		if(i=='x') continue;
		
		for(j='x';j<='z';j++){
			if(i==j) continue;
			
			for(k='x';k<='z';k++){
				//c的對手不是x,z
				if(k=='x'||k=='z') continue;
				// 對手是抓對廝殺,不能一打N
				if(i!=j&&i!=k&&j!=k){
					printf("a--%c  b--%c  c--%c \n",i,j,k);
				}
				
			}
			
		}
	}

	return 0;
}
 a--z  b--x  c--y


給定一個字串,同時給定兩個字元,求出包含這兩個字元的最小子串的長度;比如輸入:addcddabc    ,a  ,c 那麼包含的子串為:addc,   cdda,   abc  —>最小的子串長度為3;注意, 像是這種有重複的需要處理,比如  addaaaddac,最小字串就是ac,前面重複的a是無效的,只有最後一個有效。
#include <stdio.h>


int main(void) {

	char * str="adadcdcdabc";
	int start=0;
	char sChar=' ';
	for(int i=0;str[i]!='\0';i++){
		
		if(str[i]!='a'&&str[i]!='c'){
			continue;
		}
		if(sChar==' '){
			start=i;
			sChar=str[i];
			printf("第一次遇到起始字元 %c  位置為%d \n",sChar,start);
			continue;
			
		} 
		if(str[i]==sChar){
			start=i;
			sChar=str[i];
			printf("遇到重複的起始字元 %c  位置需要更新為%d \n",sChar,start);
			
		} else {
			
			printf("遇到可以結束的字元 %c  位置為%d 長度為%d \n",str[i],i,i-start+1);
			
			sChar=str[i];
			start=i;
			printf("新的起始的字元 %c  位置為%d \n",str[i],i,i-start+1);
		}
		
		
		
	}

	return 0;
}

第一次遇到起始字元 a  位置為0 
遇到重複的起始字元 a  位置需要更新為2 
遇到可以結束的字元 c  位置為4 長度為3 
新的起始的字元 c  位置為4 
遇到重複的起始字元 c  位置需要更新為6 
遇到可以結束的字元 a  位置為8 長度為3 
新的起始的字元 a  位置為8 
遇到可以結束的字元 c  位置為10 長度為3 
新的起始的字元 c  位置為10 


這道題初看沒什麼思路,但是想起了把  這個數分解成兩個正整數數乘積的形式 就行,但是對於 2的情況需要特殊考慮,在這個基礎上,繼續思考,發現不止是2,所有的偶數都是比較特殊的,所以最後就是分為 偶數和奇數個 連續數來分別計算的。
#include <stdio.h>

int main(void) {
	
	
    int n=15;
    //從2開始找,3=1+2,最小的符合結果
    for(int i = 2; i<=n;i++){
    	
    	//i一定是偶數,下面還有一個i++,一次迴圈其實是 i+=2
	//結果分為偶數個 連續數,和奇數個 連續數
	// 先處理偶數個的 15=7+8  10=1+2+3+4這種
	// 15/2==2/2  10/4==4/2  
	// 也就是 n/偶數 == x.5這種的是可以的
        if(n%i==i/2){  
        	//查探一下最小的數是否是一個負數,負數就跳過
        	
        	//n/i得到平均數-0.5 , i/2得到連續數的一半,在+1就是最小的連續數
        	//比如 15/2-2/1+1 就是最小的數 7 
        	if(n/i-i/2+1 > 0){
	        	for(int j=0;j<i;j++){
        		
			 printf(" %d  ",n/i-i/2+1+j);  	
        		}
		        printf("\n");	
        	}
    
        }
        // 此時 i 肯定是一個奇數
        i++; 
        // 對奇數取餘==0,說明可以整數,商就是連續數的個數
        if(n%i==0){
        	
        	//同樣,計算連續數中的最小值,
        	//n/i是中間值,i/2,i是奇數,得到的值恰好是中位數左邊的數量
        	// 15/5-5/2=1
        	if(n/i-i/2>0){
		       for(int j=0;j<i;j++){
        		
			    printf(" %d  ",n/i-i/2+j);  	
        		}
		        printf("\n");	
        	}
        }
        
        
    }  	
    
    
    return 0;
}

結果 
 7   8  
 4   5   6  
 1   2   3   4   5  


輸出一下內容:



經過觀察,# 號 在中間列必然出現,其餘的 # 和行數有關, 做一個二維陣列來儲存,剩下的工作交給除錯。

#include <stdio.h>

int main(void) {
	
	int h = 8 ;
	int w = 15;
	char a[h][w];
	for (int i = 0;i<h;i++){
		for(int j = 0 ;j<w;j++){
			a[i][j]=' ';
			// 最重要的一行,如果   7-i<=j<=7+i,那麼符合條件
			if(h-1+i>=j && h-1-i<=j){
				a[i][j]='#';
			}
			printf("%c",a[i][j]);
		}
		printf("\n");
	}





	return 0;
}


給2個正整數,求2數的最大公約數和最小公倍數:

額~一開始 是懵B的,只是知道能求,只記住了名字叫輾轉相除法,至於怎麼求,完全不會,還好有百度,

用歐幾里德演算法(輾轉相除法)求兩個數的最大公約數的步驟如下:
先用小的一個數除大的一個數,得第一個餘數;
再用第一個餘數除小的一個數,得第二個餘數;
又用第二個餘數除第一個餘數,得第三個餘數;
這樣逐次用後一個數去除前一個餘數,直到餘數是0為止。那麼,最後一個除數就是所求的最大公約數(如果最後的除數是1,那麼原來的兩個數是互質數)。
#include <stdio.h>

void func(int n,int m){
	int a=n;
	int b=m;
	// 保證 a是比b大大那個數
	if(a<b){
		int temp = a;
		a = b;
		b = temp;
	}
	int r=0;//r用來儲存餘數
	while(b!=0){
		r=a%b;
		a=b;
		b=r;
	}
	printf("最大公約數是 %d 最小公倍數是 %d\n",a,n*m/a);
	
	
	
}

int main(void) {
	

	func(3,4);

	return 0;
}


將一個正整數分解質因數,如50分解為2*5*5:

瞬間想到的是遞迴,但是看別人的都是用的迴圈,但是還是感覺遞迴好一點。在i<=n這裡小坑了一下,紙上談兵和真正的除錯還是差很多啊。

#include <stdio.h>

void func(int n) {
	
	if(n==1){
		return ;
	}
	
	for(int i = 2; i<=n;i++){
		if(n%i==0){
			printf(" %d ",i);
			func(n/i);
			return;
		}
	}
	
	
}


int main(void) {
	
	func(98);
	return 0;
}
 2  7  7 


輸入一個字串,輸出字串對應的整數,如“5144”變成int的5144:

細細想來,很多邊界條件沒有判斷啊,負數沒判斷。如果字串中像“123aaa213”這樣怎麼辦。

#include <stdio.h>

int main(void) {
	
	char * a="4722";
	
	
	int sum= a[0]-48 ;
	for(int i=1;a[i]!='\0';i++){
		sum = sum *10+a[i]-48;
	}

	printf("%d \n",sum);

	return 0;
}

輸入一個數字,輸出字串,如5144變成 字串的 “5144”:

用對10取餘獲得每一個數字,然後把數字轉成對應的char,把char拼起來,但是這樣的char是倒序的,那麼就倒序列印就好。

#include <stdio.h>

int main(void) {
	
	int a = 5186214;
	
	char str[100];
	int i=0;
	while(a>0){
		
		 int r = a%10;
		 a=a/10;
		 str[i] = r+48;
		 i++;
	}
	for(;i>=0;i--){
		printf("%c",str[i]);
	}
	
	return 0;
}



反轉字串,如“123456”變成“654321”
#include <stdio.h>
#include <string.h>

int main(void) {
	
	char * a= "123456";
	
	int len = strlen(a);
	char b[len+1];
	char temp;
	for(int i = len-1,j=0;i>=0;i--,j++ ){
	
		b[j]=a[i];
		
	}
	b[len]='\0';
	printf("%s   %s\n",a,b);

	return 0;
}


查詢子串是否存在,如“cd” 在 “abcdef”中是否出現。

記得有一個KMP演算法是目前的最優解,演算法的時間複雜度最低,效率最高。但是這演算法挺難理解的。寫個一般的,但是好理解的。

#include <stdio.h>

int main(void) {
	
	char * a = "aaabcabd";
	char * b = "abc";
	
	for(int i=0,j=0 ;a[i]!='\0';i++){
		if(a[i]==b[j]){
			j++;
			if(b[j]=='\0'){
				printf("子串存在\n");
				return 0;
			}
		} else{
			i=i-j;
			j=0;
		}
	}

	printf("子串不存在\n");
	return 0;
}


一個數組,下表從0-n,元素為從0-n的整數,判斷這裡面是否有重複元素:

看到題目瞬間就想到了桶排序,申明一個長度為n的陣列,遍歷原陣列,把原陣列的元素加入到新陣列中,遍歷結束後,列印新陣列的個數,超過1說明了有重複。

#include <stdio.h>

int main(void) {
	
	int a[10] = {1,2,2, 4,6,7, 8,1,3, 5};
	
	
	
	int b[10] = {0,0,0, 0,0,0, 0,0,0, 0};
	for(int i=0;i<10;i++){
		b[ a[i] ] ++;
	}
	for(int j=0;j<10;j++){
		printf("%d 出現了 %d 次 \n",j,b[j]);
	}
	
	
	return 0;
}

0 出現了 0 次 
1 出現了 2 次 
2 出現了 2 次 
3 出現了 1 次 
4 出現了 1 次 
5 出現了 1 次 
6 出現了 1 次 
7 出現了 1 次 
8 出現了 1 次 
9 出現了 0 次 


給一個正整數,找出數字1出現的位數,如 421134,1出現在3,4位上:

#include <stdio.h>

int main(void) {
	
	int a = 4512113;
	int n=0;
	while(a>0){
		int r=a%10;
		a=a/10;
		n++;
		if(r==1){
			printf("1在%d位上\n",n);
		}	
		
	}
	
	return 0;
}


有一張一元紙幣換成1分,2分,5分,每種至少一枚,有多少種換法:

一開始想到了暴力列舉解決,但是這樣有點傻,應該有更好的方式:

#include <stdio.h>

int main(void) {
	
	for(int i=1;i<20;i++){
		
		for(int j=1;j<50;j++){
			
			for(int k=1;k<94;k++){
				if(i *5 + j*2 + k == 100){
					printf("5分 %d 個 , 2分 %d個 , 1分 %d 個\n",i,j,k) ;
				}
			}
			
		}
		
		
	}

	return 0;
}
5分 1 個 , 2分 1個 , 1分 93 個
5分 1 個 , 2分 2個 , 1分 91 個
5分 1 個 , 2分 3個 , 1分 89 個
5分 1 個 , 2分 4個 , 1分 87 個
......
5分 19 個 , 2分 2個 , 1分 1 個

超經典的,記得當初學c語言就有這個演算法,判斷一個數是不是質數:

按照定義求就行了,從2開始遍歷,如果這個數對i取餘為0,說明是合數,遍歷到i *i 還沒有,那就說明是質數。

為什麼是i*i <n 就能判斷了, 因為 n=根號n * 根號n, 如果n是合數,那麼這2個乘數肯定一個比根號n大,一個比根號n小,現在遍歷到了根號n,這個數還沒有找到,那麼就能說明,比根號n大的另一個乘數是不存在的,n為質數。

#include <stdio.h>

int func(int n){
	// 其實1 既不是質數,也不是合數。沒寫那麼細
	if(n<=2){
		return 1;
	}
	
	for(int i =2 ;i*i<=n;i++){
		if(n%i==0){
			printf("是合數");
			return 0;
		}
	}

	printf("是質數");
	return 1;
	
	
}

int main(void) {
	

	func(18);

	return 0;
}