面试官问你斐波那契数列的时候不要高兴得太早 搞懂C语言函数指针 搜索引擎还可以这么玩? 那些相见恨晚的搜索技巧
面试官问你斐波那契数列的时候不要高兴得太早
前言
假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了。如果真这么想,那就危险了。
递归求斐波那契数列
递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
斐波那契数列的计算表达式很简单:
F(n) = n; n = 0,1
F(n) = F(n-1) + F(n-2),n >= 2;
因此,我们能很快根据表达式写出递归版的代码:
/*fibo.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列递归版*/
unsigned long fibo(unsigned long int n)
{
if(n <= 1)
return n;
else
return fibo(n-1) + fibo(n-2);
}
int main(int argc,char *argv[])
{
if(1 >= argc)
{
printf("usage:./fibo num\n");
return -1;
}
unsigned long n = atoi(argv[1]);
unsigned long fiboNum = fibo(n);
printf("the %lu result is %lu\n",n,fiboNum);
return 0;
}
关键代码为3~9行。简洁明了,一气呵成。
编译:
gcc -o fibo fibo.c
运行计算第5个斐波那契数:
$ time ./fibo 5
the 5 result is 5
real 0m0.001s
user 0m0.001s
sys 0m0.000s
看起来并没有什么不妥,运行时间也很短。
继续计算第50个斐波那契数列:
$ time ./fibo 50
the 50 result is 12586269025
real 1m41.655s
user 1m41.524s
sys 0m0.076s
计算第50个斐波那契数的时候,竟然花了一分多钟!
递归分析
为什么计算第50个的时候竟然需要1分多钟。我们仔细分析我们的递归算法,就会发现问题,当我们计算fibo(5)的时候,是下面这样的:
|--F(1)
|--F(2)|
|--F(3)| |--F(0)
| |
|--F(4)| |--F(1)
| |
| | |--F(1)
| |--F(2)|
| |--F(0)
F(5)|
| |--F(1)
| |--F(2)|
| | |--F(0)
|--F(3)|
|
|--F(1)
为了计算fibo(5),需要计算fibo(3),fibo(4);而为了计算fibo(4),需要计算fibo(2),fibo(3)……最终为了得到fibo(5)的结果,fibo(0)被计算了3次,fibo(1)被计算了5次,fibo(2)被计算了2次。可以看到,它的计算次数几乎是指数级的!
因此,虽然递归算法简洁,但是在这个问题中,它的时间复杂度却是难以接受的。除此之外,递归函数调用的越来越深,它们在不断入栈却迟迟不出栈,空间需求越来越大,虽然访问速度高,但大小是有限的,最终可能导致栈溢出。
在linux中,我们可以通过下面的命令查看栈空间的软限制:
$ ulimit -s
8192
可以看到,默认栈空间大小只有8M。一般来说,8M的栈空间对于一般程序完全足够。如果8M的栈空间不够使用,那么就需要重新审视你的代码设计了。
迭代解法
既然递归法不够优雅,我们换一种方法。如果不用计算机计算,让你去算第n个斐波那契数,你会怎么做呢?我想最简单直接的方法应该是:知道第一个和第二个后,计算第三个;知道第二个和第三个后,计算第四个,以此类推。最终可以得到我们需要的结果。这种思路,没有冗余的计算。基于这个思路,我们的C语言实现如下:
/*fibo1.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列迭代版*/
unsigned long fibo(unsigned long n)
{
unsigned long preVal = 1;
unsigned long prePreVal = 0;
if(n <= 2)
return n;
unsigned long loop = 1;
unsigned long returnVal = 0;
while(loop < n)
{
returnVal = preVal +prePreVal;
/*更新记录结果*/
prePreVal = preVal;
preVal = returnVal;
loop++;
}
return returnVal;
}
/**main函数部分与fibo.c相同,这里省略*/
编译并计算第50个斐波那契数:
$ gcc -o fibo1 fibo1.c
$ time ./fibo1 50
the 50 result is 12586269025
real 0m0.002s
user 0m0.001s
sys 0m0.002s
可以看到,计算第50个斐波那契数只需要0.002s!时间复杂度为O(n)。
尾递归解法
同样的思路,但是采用尾递归的方法来计算。要计算第n个斐波那契数,我们可以先计算第一个,第二个,如果未达到n,则继续递归计算,尾递归C语言实现如下:
/*fibo2.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列尾递归版*/
unsigned long fiboProcess(unsigned long n,unsigned long prePreVal,unsigned long preVal,unsigned long begin)
{
/*如果已经计算到我们需要计算的,则返回*/
if(n == begin)
return preVal+prePreVal;
else
{
begin++;
return fiboProcess(n,preVal,prePreVal+preVal,begin);
}
}
unsigned long fibo(unsigned long n)
{
if(n <= 1)
return n;
else
return fiboProcess(n,0,1,2);
}
/**main函数部分与fibo.c相同,这里省略*/
效率如何呢?
$ gcc -o fibo2 fibo2.c
$ time ./fibo2 50
the 50 result is 12586269025
real 0m0.002s
user 0m0.001s
sys 0m0.002s
可见,其效率并不逊于迭代法。尾递归在函数返回之前的最后一个操作仍然是递归调用。尾递归的好处是,进入下一个函数之前,已经获得了当前函数的结果,因此不需要保留当前函数的环境,内存占用自然也是比最开始提到的递归要小。时间复杂度为O(n)。
递归改进版
既然我们知道最初版本的递归存在大量的重复计算,那么我们完全可以考虑将已经计算的值保存起来,从而避免重复计算,该版本代码实现如下:
/*fibo3.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列,避免重复计算版本*/
unsigned long fiboProcess(unsigned long *array,unsigned long n)
{
if(n < 2)
return n;
else
{
/*递归保存值*/
array[n] = fiboProcess(array,n-1) + array[n-2];
return array[n];
}
}
unsigned long fibo(unsigned long n)
{
if(n <= 1)
return n;
unsigned long ret = 0;
/*申请数组用于保存已经计算过的内容*/
unsigned long *array = (unsigned long*)calloc(n+1,sizeof(unsigned long));
if(NULL == array)
{
return -1;
}
array[1] = 1;
ret = fiboProcess(array,n);
free(array);
array = NULL;
return ret;
}
/**main函数部分与fibo.c相同,这里省略*/
效率如何呢?
$ gcc -o fibo3 fibo3.c
$ time ./fibo3 50
the 50 result is 12586269025
real 0m0.002s
user 0m0.002s
sys 0m0.001s
可见效率是不逊于其他两种优化算法的。但是特别注意的是,这种改进版的递归,虽然避免了重复计算,但是调用链仍然比较长。
其他解法
其他两种时间复杂度为O(logn)的矩阵快速幂解法以及通项表达式解法本文不介绍。欢迎留言补充。
总结
总结一下递归的优缺点:
优点:
- 实现简单
- 可读性好
缺点:
- 递归调用,占用空间大
- 递归太深,易发生栈溢出
- 可能存在重复计算
可以看到,对于求斐波那契数列的问题,使用一般的递归并不是一种很好的解法。
所以,当你使用递归方式实现一个功能之前,考虑一下使用递归带来的好处是否抵得上它的代价。
搞懂C语言函数指针
前言
函数指针是什么?如何使用函数指针?函数指针到底有什么大用?本文将一一介绍。
如何理解函数指针
如果有int *类型变量,它存储的是int类型变量的地址;那么对于函数指针来说,它存储的就是函数的地址。函数也是有地址的,函数实际上由载入内存的一些指令组成,而指向函数的指针存储了函数指令的起始地址。如此看来,函数指针并没有什么特别的。我们可以查看程序中函数的地址:
#include <stdio.h>
int test()
{
printf("this is test function");
return 0;
}
int main(void)
{
test();
return 0;
}
编译:
gcc -o testFun testFun.c
查看test函数相对地址(并非实际运行时的地址):
$ nm testFun |grep test #查看test函数的符号表信息
0000000000400526 T test
如何声明函数指针
声明普通类型指针时,需要指明指针所指向的数据类型,而声明函数指针时,也就要指明指针所指向的函数类型,即需要指明函数的返回类型和形参类型。例如对于下面的函数原型:
int sum(int,int);
它是一个返回值为int类型,参数是两个int类型的函数,那么如何声明该类型函数的指针呢?很简单,将函数名替换成(*pf)形式即可,即我们把sum替换成(*fp)即可,fp为函数指针名,结果如下:
int (*fp)(int,int);
这样就声明了和sum函数类型相同的函数指针fp。这里说明两点,第一,*和fp为一体,说明了fp为指针类型,第二,*fp需要用括号括起来,否则就会变成下面的情况:
int *fp(int,int);
这种情况下,意思就大相径庭了,它声明了一个参数为两个int类型,返回值为int类型的指针的函数,而不再是一个函数指针了。
在经常使用函数指针之后,我们很快就会发现,每次声明函数指针都要带上长长的形参和返回值,非常不便。这个时候,我们应该想到使用typedef,即为某类型的函数指针起一个别名,使用起来就方便许多了。例如,对于前面提到的函数可以使用下面的方式声明:
typedef int (*myFun)(int,int);//为该函数指针类型起一个新的名字
myFun f1; //声明myFun类型的函数指针f1
上面的myFun就是一个函数指针类型,在其他地方就可以很方便地用来声明变量了。typedef的使用不在本文的讨论范围,但是特别强调一句,typedef中声明的类型在变量名的位置出现,理解了这一句,也就很容易使用typedef了。因而下面的方式是错误的:
typedef myFun (int)(int,int); //错误
typedef (int)(int,int) *myFun; //错误
为函数指针赋值
赋值也很简单,既然是指针,将对应指针类型赋给它既可。例如:
#include<stdio.h>
int test(int a,int b)
{
/*do something*/
return 0
}
typedef int(*fp)(int,int);
int main(void)
{
fp f1 = test; //表达式1
fp f2 = &test;//表达式2
printf("%p\n",f1);
printf("%p\n",f2);
return 0;
}
在这里,声明了返回类型为int,接受两个int类型参数的函数指针f1和f2,分别给它们进行了赋值。表达式1和表达式2在作用上并没有什么区别。因为函数名在被使用时总是由编译器把它转换为函数指针,而前面加上&不过显式的说明了这一点罢了。
调用
调用也很容易,把它看成一个普通的函数名即可:
#include<stdio.h>
int test(int a,int b)
{
/*do something*/
printf("%d,%d\n",a,b);
return 0
}
typedef int(*fp)(int,int);
int main(void)
{
fp f = test;
f(1,2);//表达式1
(*f)(3,4);//表达式2
return 0;
}
在函数指针后面加括号,并传入参数即可调用,其中表达式1和表达式2似乎都可以成功调用,但是哪个是正确的呢?ANSI C认为这两种形式等价。
函数指针有何用
函数指针的应用场景比较多,以库函数qsort排序函数为例,它的原型如下:
void qsort(void *base,size_t nmemb,size_t size , int(*compar)(const void *,const void *));
看起来很复杂对不对?拆开来看如下:
void qsort(void *base, size_t nmemb, size_t size, );
拿掉第四个参数后,很容易理解,它是一个无返回值的函数,接受4个参数,第一个是void*类型,代表原始数组,第二个是size_t类型,代表数据数量,第三个是size_t类型,代表单个数据占用空间大小,而第四个参数是函数指针。这第四个参数,即函数指针指向的是什么类型呢?
int(*compar)(const void *,const void *)
很显然,这是一个接受两个const void*类型入参,返回值为int的函数指针。
到这里也就很清楚了。这个参数告诉qsort,应该使用哪个函数来比较元素,即只要我们告诉qsort比较大小的规则,它就可以帮我们对任意数据类型的数组进行排序。
在这里函数指针作为了参数,而他同样可以作为返回值,创建数组,作为结构体成员变量等等,它们的具体应用我们在后面的文章中会介绍,本文不作展开。本文只介绍一个简单实例。
实例介绍
我们通过一个实例来看函数指针怎么使用。假设有一学生信息,需要按照学生成绩进行排序,该如何处理呢?
#include <stdio.h>
#include <stdlib.h>
#define STU_NAME_LEN 16
/*学生信息*/
typedef struct student_tag
{
char name[STU_NAME_LEN]; //学生姓名
unsigned int id; //学生学号
int score; //学生成绩
}student_t;
int studentCompare(const void *stu1,const void *stu2)
{
/*强转成需要比较的数据结构*/
student_t *value1 = (student_t*)stu1;
student_t *value2 = (student_t*)stu2;
return value1->score-value2->score;
}
int main(void)
{
/*创建三个学生信息*/
student_t stu1 = {"one",1,99};
student_t stu2 = {"two",2,77};
student_t stu3 = {"three",3,88};
student_t stu[] = {stu1,stu2,stu3};
/*排序,将studentCompare作为参数传入qsort*/
qsort((void*)stu,3,sizeof(student_t),studentCompare);
int loop = 0;
/**遍历输出*/
for(loop = 0; loop < 3;loop++)
{
printf("name:%s,id:%u,score:%d\n",stu[loop].name,stu[loop].id,stu[loop].score);
}
return 0;
}
我们创建了一个学生信息结构,结构成员包括名字,学号和成绩。main函数中创建了一个包含三个学生信息的数组,并使用qsort函数对数组按照学生成绩进行排序。qsort函数第四个参数是函数指针,因此我们需要传入一个函数指针,并且这个函数指针的入参是cont void *类型,返回值为int。我们通过前面的学习知道了函数名本身就是指针,因此只需要将我们自己实现的studentCompare作为参数传入即可。
最终运行结果如下:
name:two,id:2,score:77
name:three,id:3,score:88
name:one,id:1,score:99
可以看到,最终学生信息按照分数从低到高输出。
总结
本文介绍了函数指针的声明和简单使用。更多使用将在后面的文章介绍,本文总结如下:
- 函数指针与其他指针类型无本质差异,不过它指向的是函数的地址罢了。
- 声明函数指针需要指明函数的返回类型和形参类型。
- 函数名在被使用时总是由编译器把它转换为函数指针。
- 要想声明函数指针,只需写出函数原型,然后将函数名用(*fp)代替即可。这里fp是声明的函数指针变量。
- typedef中声明的类型在变量名的位置出现。
搜索引擎还可以这么玩?
今日分享:没有任何事情比群众的想法更为多变,也没有任何事情能够像群众对他们昨天还赞扬的事情今天便给予痛骂的做法更为常见。--《乌合之众》
前言
我们平常看到的搜索结果都是普普通通,并没有什么特别。但实际上有很多关键字会搜索出意想不到的结果,我们一起来看看有哪些有趣的搜索结果吧。
百度篇
黑洞
搜索黑洞,会出现搜索结果都被吸入,最后又重新出现:
翻转
搜索翻转,搜索页面会出现翻转:
抖动/摇一摇/跳跃
分别搜索以上关键字,页面都会出现相应的动作抖动,不便截图,可自行尝试。
Google篇
the Answer to Life, the Universe and Everything is
对生命,宇宙和一切的回答是什么?google 会告诉你下面的答案。
对于为何是这个答案,也有很多有意思的讨论。
画立体图
搜索Graph for 函数,可以搜索出函数的图形,立体图甚至可以旋转,例如:
还有下面的搜索:
sqrt(cos(x))*cos(300x)+sqrt(abs(x))-0.7)*(4-x*x)^0.01,sqrt(6-x^2), -sqrt(6-x^2) from -4.5 to 4.5
2sqrt(-abs(abs(x)-1)*abs(3-abs(x))/((abs(x)-1)*(3-abs(x))))(1+abs(abs(x)-3)/(abs(x)-3))sqrt(1-(x/7)^2)+(5+0.97(abs(x-.5)+abs(x+.5))-3(abs(x-.75)+abs(x+.75)))(1+abs(1-abs(x))/(1-abs(x))),-3sqrt(1-(x/7)^2)sqrt(abs(abs(x)-4)/(abs(x)-4)),abs(x/2)-0.0913722(x^2)-3+sqrt(1-(abs(abs(x)-2)-1)^2),(2.71052+(1.5-.5abs(x))-1.35526sqrt(4-(abs(x)-1)^2))sqrt(abs(abs(x)-1)/(abs(x)-1))+0.9
do a barrel roll
搜索do a barrel roll,页面会发生旋转:
递归
搜索Recursion(或递归),它会提示你是不是要找Recursion(或递归),真正给你演示了什么叫递归。
Google Gravity
在google 搜索栏中输入Google Gravity,然后点击手气不错:
就会出现下面页面掉落:
鼠标可以拖动上面破碎的方块。
Solitaire
搜索Solitaire就可以出现接龙游戏啦:
Snake game
没错,搜索Snake game会出现经典的贪吃蛇游戏:
Zerg Rush
搜索Zerg Rush。拼手速的时候到了,在Zerg“吃掉”页面之前,把它干掉吧!
Atari Breakout
在google 图片中搜索Atari Breakout,也会出现一个很有意思的游戏:
pacman
搜索pacman,经典的吃豆人游戏就出现啦!
tic tac toe
搜索圈圈叉叉或者tic tac toe,就会出现经典的OOXX游戏:
spinner
搜索spinner,会出现一个转盘:
fun facts
每次搜索fun facts都会出现一个有意思的知识,并且每次搜索结果可能不一样:
flip a coin
不好决策,没有硬币?丢个硬币吧:
最后说两句
搜索引擎使用技巧可参考《那些相见恨晚的搜索技巧》。你还发现了哪些有趣的搜索结果?欢迎留言分享。
前言
搜索引擎我们经常使用,但是我们最常用的可能就是把要搜索的内容直接复制到搜索框,而很多时候这样搜索出来的结果有很多的冗余信息,对于获取自己所需要的内容甚至存在阻碍。我们如何更加快速地获取我们想要的信息呢?这里就有一些常用的搜索技巧。以下技巧多数同样适用于其他搜索引擎。
按文件类型搜索-filetype
我们常常需要在网络上找文件,但是搜索出来的却可能是一堆不相关的东西。怎么办?使用filetype。
例如想要搜索《c primer plus》的pdf版本,只需要搜索输入:
c primer plus中文版 filetype:pdf
从结果中可以看到,前面列出的结果都是pdf文件。当然文件类型并不限于pdf。你可以搜索其他类型,例如doc,mobi等。
过滤关键字-减号
如果我们在搜索结果中不想看到某个关键字,可以使用-关键字。例如,搜索linux常用命令,但是不想搜索结果出现CSDN,可以使用下面的方式:
linux常用命令 -CSDN
可以看到,包含CSDN的结果不会出现在最终搜索结果中。不加的时候,效果如何,可以试试。
必须包含某关键字-加号
同样地,搜索结果中必须包含某个关键字时,可以使用+关键字。例如,搜索linux常用命令,且必须包含centos:
linux常用命令 +centos
搜索结果中就会包含centos。
搜索指定网站-site
当然了,如果我们只需要看某个网站的结果,可以使用site:网站地址。例如,我要搜索linux常用命令,但限定个人博客主页www.huyanbing.me中的结果,可以使用下面的方式:
linux常用命令 site:www.huyanbing.me
可以看到,最后只展示了www.huyanbing.me中的相关结果。
inurl也会有同样的效果。
链接中包含字符串-inurl
例如,我们需要搜索链接中包含”zhihu“的结果:
完全匹配搜索结果
有时候搜索的结果实在是太多了,而且搜索出来的内容可能和你需要的不一致,这个使用可以采用完全匹配的方式搜索,只需要在搜索的内容中加上英文双引号即可,例如:
”作为编辑器之神的vim,初安装时,就像十几岁的孩子,潜力无限但能力有限。“
搜索结果如下:
可以看到非常精确地搜索出了结果,没有任何地冗余信息。
搜索标题-intitle
我们要搜索的内容可能是标题,也可能是内容,如果我们只想搜索标题内容,可以使用intile,例如:
intitle:C语言入坑指南
与intitle不同,intext用于搜索内容。有兴趣的可以自己尝试一下。
或者OR
默认搜索,中间空格是与(AND),而使用OR,可以达到或的效果。例如,你搜索:
C语言 经典书籍
搜索结果会既包含C语言,又包含计算机经典书籍。而如果使用:
C语言 OR 经典书籍
搜索时,它的结果包含C语言,或者包含经典书籍。
查看定义 define
例如,我们想要查询冯诺依曼体系的定义,只需要像下面这样搜索即可:
冯诺依曼体系 define
翻译 translate
例如需要翻译:it is never too late to learn
计算数学式子
如果需要做计算,可以直接输入式子:
30*8+25
单位转换-in
例如分钟和秒之间的转换:
minute in second
搜索可遍历目录-index of
index of可搜索允许目录浏览的web网站,例如:
点击第一条结果:
是不是隐隐地感觉它能干大事?
总结
以上所列举的技巧基本都可以组合使用。实际上,搜索引擎的搜索技巧还有很多很多,这里不一一介绍,并且不同的搜索引擎可能有些差异。但是无论如何,了解这些实用的技巧,能让我们可以更好地利用搜索引擎,帮助我们快速找到我们需要的信息。
当然了,如果不想记这些麻烦的语法,完全可以使用各大搜索引擎自带的高级搜索,这里也可以设置很多的条件: