2.2 while循环和do-while循环

例题2-2 3n+1问题

猜想(4):对于任意大于1的自然数n,若n为奇数,则将n变为3n+1,否则变为n的一半。经过若干次这样的变换,一定会使n变为1。例如,3→10→5→16→8→4→2→1。

输入n,输出变换的次数。n≤109

样例输入:

3

样例输出:

7

【分析】

不难发现,程序完成的工作依然是重复性的:要么乘3加1,要么除以2,但和2.1节的程序又不太一样:循环的次数是不确定的,而且n也不是“递增”式的循环。这样的情况很适合用while循环来实现。

程序2-4 3n+1问题(有bug)


#include<stdio.h>
int main()
{
  int n, count = 0;
  scanf("%d", &n);
  while(n > 1)
  {
    if(n % 2 == 1) n = n*3+1;
    else n /= 2;
    count++;
  }
  printf("%d\n", count);
  return 0;
}

上面的程序有好几个值得注意的地方。首先是“=0”,意思是定义整型变量count的同时初始化为0。接下来是while语句。

提示2-8:while循环的格式为“while(条件)循环体;”。

此格式看上去比for循环更简单,可以用while改写for。“for(初始化;条件;调整)循环体;”等价于:


初始化;
while(条件)
{
  循环体;
  调整;
}

建议读者再次利用IDE或者gdb跟踪调试,看看执行流程是怎样的。

n/=2的含义是n=n/2,类似于前面介绍过的i++。很多运算符都有类似的用法,例如,a*=3表示a=a*3。

count++的作用是计数器。由于最终输出的是变换的次数,需要一个变量来完成计数。

提示2-9:当需要统计某种事物的个数时,可以用一个变量来充当计数器。

这个程序是否正确?先来测试一下:输入“987654321”,看看结果是什么。很不幸,答案等于1——这明显是错误的。题目中给出的范围是n≤109,这个987654321是合法的输入数据。

提示2-10:不要忘记测试。一个看上去正确的程序可能隐含错误。

问题出在哪里呢?若反复阅读程序仍然无法找到答案,就动手实验吧!一种方法是利用IDE和gdb跟踪调试,但这并不是本书所推荐的调试方法。一个更通用的方法是:输出中间结果。

提示2-11:在观察无法找出错误时,可以用“输出中间结果”的方法查错。

在给n做变换的语句后加一条输出语句printf("%d\n",n),将很快找到问题的所在:第一次输出为-1332004332,它不大于1,所以循环终止。如果认真完成了前面的所有探索实验,读者将立刻明白这其中的缘由:乘法溢出了。

下面稍微回顾一下数据类型的大小。在第1章中,通过实验得出了int整数的大小——很可能是-2147483648~2147483647,即-231~231-1。为什么叫“很可能”呢,因为C99中只规定了int至少是16位,却没有规定具体值(5)。是不是感觉有些别扭?的确如此,所以C99规定了一些固定长度的整数,例如int32_t、uint32_t(6)

好在算法竞赛的平台相对稳定,目前几乎在所有的比赛平台上,int都是32位整数。

提示2-12:C99并没有规定int类型的确切大小,但在当前流行的竞赛平台中,int都是32位整数,范围是-2147483648~2147483647。

回到本题。本题中n的上限109只比int的上界稍微小一点,因此溢出了也并不奇怪。只要使用C99中新增的long long即可解决问题,其范围是-263~263-1,唯一的区别就是要把输入时的%d改成%lld。但这也是不保险的——在MinGW的gcc(7)中,要把%lld改成%I64d,但奇怪的是VC2008里又得改回%lld。是不是很容易搞错?所以如果涉及long long的输入输出,常用C++的输入输出流或者自定义的输入输出方法,本书将在后面的章节对其进行深入讨论。

提示2-13:long long在Linux下的输入输出格式符为%lld,但Windows平台中有时为%I64d。为保险起见,可以用后面介绍的C++流,或者编写自定义输入输出函数。

最后给出long long版本的代码,它避开了对long long的输入输出,并且成功算出n=987654321时的答案为180。

程序2-5 3n+1问题


#include<stdio.h>
int main()
{
  int n2, count = 0;
  scanf("%d", &n2);
  long long n = n2;
  while(n > 1)
  {
    if(n % 2 == 1) n = n*3+1;
    else n /= 2;
    count++;
  }
  printf("%d\n", count);
  return 0;
}

例题2-3 近似计算

计算,直到最后一项小于10-6

【分析】

本题和例题2-2一样,也是重复计算,因此可以用循环实现。但不同的是,只有算完一项之后才知道它是否小于10-6。也就是说,循环终止判断是在计算之后,而不是计算之前。这样的情况很适合使用do-while循环。

程序2-6 近似计算


#include<stdio.h>
int main() {
  double sum = 0;
  for(int i = 0; ; i++) {
    double term = 1.0 / (i*2+1);
    if(i % 2 == 0) sum += term;
    else sum -= term;
    if(term < 1e-6) break;
  }
  printf("%.6f\n", sum);
  return 0;
}

提示2-14:do-while循环的格式为“do{循环体}while(条件);”,其中循环体至少执行一次,每次执行完循环体后判断条件,当条件满足时继续循环。