算法竞赛入门经典学习笔记(四)——函数和递归

1. 自定义函数和结构体

  • 函数的参数和返回值最好是“一等公民”,如int、char或者double等。其他“非一等公民”作为参数和返回值要复杂一些。如果函数不需要返回值,则返回类型应写成void。
  • main函数也是有返回值的!到目前为止,我们总是让它返回0,这个0是什么意思呢?尽管没有专门说明,读者应该已经发现了,main函数是整个程序的入口。换句话说,有一个“其他的程序”来调用这个main函数——如操作系统、IDE、调试器,甚至自动评测系统。这个0代表“正常结束”,即返回给调用者。在算法竞赛中,除了有特殊规定之外,请总是让其返回0,以免评测系统错误地认为程序异常退出了。
  • hypot(x, y)返回欧几里德范数 sqrt(x*x + y*y)
  • 结构体
struct Point{double x, y;};
double dist(struct Point a, struct Point b)
{
    return hypot(a.x-b.x, a.y-b.y);
}
typedef struct{
    double x, y;
}Point;
double dist(Point a, Point b){
    return hypot(a.x-b.x, a.y-b.y);
}
  • 为了使用方便,往往用“typedef struct { 域定义; }类型名;”的方式定义一个新类型名。这样,就可以像原生数据类型一样使用这个自定义类型。
  • 对复杂的表达式进行化简有时不仅能减少计算量,还能减少甚至避免中间结果溢出。

程序4-1 组合数

计算组合数。编写函数,参数是两个非负整数n和m,返回组合数 C n m = n ! m ! ( n m ) ! ,其中m≤n≤25。例如,n=25,m=12时答案为5200300。
n ! / m ! = ( m + 1 ) ( m + 2 ) ( n 1 ) n

#include<stdio.h>
#include<string.h>
#include<math.h>
long long factorial(int n){
    long long m=1;
    for(int i=1; i<=n; i++)
        m*=i;
    return m;
}
long long C(int n, int m){
    return factorial(n)/(factorial(m)*factorial(n-m));
}
long long C1(int n, int m){
    if(m<n-m) m=n-m;
    long long ans = 1;
    for(int i=m+1;i<=n;i++) ans*=i;
    for(int i=1;i<=n-m;i++) ans/=i;
    return ans;
}
int main() {
    int a,b;
    while(scanf("%d%d",&a,&b)!=EOF){
        printf("%lld\n",factorial(a));
        printf("%lld\n",C1(a,b));
//        printf("%lld\n",C(a,b));
    }
    return 0;
}

程序4-3 素数判定

素数判定。编写函数,参数是一个正整数n,如果它是素数,返回1,否则返回0。

//n=1或者n太大时请勿调用
int is_prime(int n)
{
    for(int i=2;i*i<=n;i++) {
        if(n%i==0){
            return 0;
        }
    }
    return 1;
}
注意这里用到了两个小技巧。一是只判断不超过sqrt(x)的整数i(想一想,为什么)。
二是及时退出:一旦发现x有一个大于1的因子,立刻返回0(假),只有最后才返回1(真)。
函数名的选取是有章可循的,“is_prime”取自英文“is it a prime?”(它是素数吗?)。

不要用在n=1或者n太大时调用。这是为什么呢?
n太小时不难解释:n=1会被错误地判断为素数(因为确实没有其他因子)。
n太大时的理由则不明显:i*i可能会溢出!
如果n是一个接近int的最大值的素数,则当循环到i=46340时,i*i=2147395600<n;
但i=46341时,i*i=2147488281,超过了int的最大值,溢出变成负数,仍然满足i*i<n。
若n不是太大,可能出现101128442溢出后等于2147483280,终止循环;
但如果n= 2147483647,循环将一直进行下去

程序4-4 素数判定(2)

int is_prime(int n)
{
    if(n<=1) return 0;
    int m=floor(sqrt(n)+0.5);
    for(int i=2;i<=m;i++) {
        if(n%i==0){
            return 0;
        }
    }
    return 1;
}
除了特判n≤1的情况外,程序中还使用了变量m,一方面避免了每次重复计算sqrt(n),另
一方面也通过四舍五入避免了浮点误差——正如前面所说,如果sqrt将某个本应是整数的值
变成了xxx.99999,也将被修正,但若直接写m = sqrt(n),“.99999”会被直接截掉。

2. 函数调用与参数传递

  • 调用栈
    调用栈描述的是函数之间的调用关系。它由多个栈帧(Stack Frame)组成,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还很自然地保证了不同函数间的局部变量互不相干——因为不同函数对应着不同的栈帧。

用指针作参数

程序4-6 用函数交换变量

#include<stdio.h>
void swap(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}
int main() {
    int a=3, b=4;
    swap(&a, &b);
    printf("%d %d\n", a, b);
    return 0;
}
  • 变量名前面加“&”得到的是该变量的地址。
  • C语言的变量都是放在内存中的,而内存中的每个字节都有一个称为地址(address)的编号。
  • 用int* a声明的变量a是指向int型变量的指针。
  • 赋值a = &b的含义是把变量
    b的地址存放在指针a中,表达式*a代表a指向的变量,既可以放在赋值符号的左边(左值),也可以放在右边(右值)。
  • *a是指“a指向的变量”,而不仅是“a指向的变量所拥有的值”。
  • *a = *a + 1就是让a指向的变量自增1。甚至可以把它写成(*a)++。
  • 千万不要滥用指针,这不仅会把自己搞糊涂,还会让程序产生各种奇怪的错误。事实上,本书的程序会很少使用指针。
  • 再次回到对正确swap程序的调试。在swap程序中,a和b都是局部变量,在函数执行完毕以后就不复存在了,但是a和b里保存的地址却依然有效——它们是main函数中的局部变量a和b的地址。在main函数执行完毕之前,这两个地址将始终有效,并且分别指向main函数的局部变量a和b。程序交换的是*a和*b,也就是main函数中的局部变量a和b。

数组作为参数和返回值

程序4-7 计算数组的元素和(错误)

int sum(int a[]) {
    int ans = 0;
    for(int i=0; i<sizeof(a); i++) {
        ans += a[i];
    }
    return ans;
}
  • 这个函数是错误的,因为sizeof(a)无法得到数组的大小。为什么会这样?因为把数组作为参数传递给函数时,实际上只有数组的首地址作为指针传递给了函数。换句话说,在函数定义中的int a[]等价于int *a。在只有地址信息的情况下,是无法知道数组里有多少个元素的。
  • 正确的做法是加一个参数,即数组的元素个数。

程序4-8 计算数组的元素和(正确)

#include<stdio.h>
int sum(int* a,int n) {
    int ans = 0;
    for(int i=0; i<n; i++) {
        ans += a[i];
    }
    return ans;
}
int main() {
    int a[] = {1,2,3,4};
    int ans = sum(a, 4);
    printf("%d\n",ans);
    return 0;
}
  • 在上面的代码中,直接把参数a写成了int* a,暗示a实际上是一个地址。
  • 以数组为参数调用函数时,实际上只有数组首地址传递给了函数,需要另加一个参数表示元素个数。除了把数组首地址本身作为实参外,还可以利用指针加减法把其他元素的首地址传递给函数。
  • 指针a+1指向a[1],即2这个元素(数组元素从0开始编号)。因此函数sum“看到”{2, 3, 4}这个数组,因此返回9。
  • 一般地,若p是指针,k是正整数,则p+k就是指针p后面第k个元素,p-k是p前面的第k个元素,而如果p1和p2是类型相同的指针,则p2-p1是从p1到p2的元素个数(不含p2)。

猜你喜欢

转载自blog.csdn.net/u011741311/article/details/80957900