幾個面試演算法題,附原始碼
昨天下午面試了一家,最後有道演算法題,當時沒想起來,就拍了張照,回來慢慢想,也算沒白去。
原題 : 有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個小時的除錯,終於出了結果,下面是原始碼,結合註釋看,
下面是log資訊:#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; }
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;
}