程序的基本概念,常量,变量,表达式
1、解释执行的语言相比编译执行的语言有什么优缺点?
由于少了编译过程,解释型语言开发调试的周期更短;由于不需要生成机器指令,解释型语言平台无关性更好;解释型语言的执行效率不如编译型语言,因为在运行时还要解释源代码或中间代码,而编译型语言的程序在运行时没有这个负担。
1、总结前面介绍的转义序列的规律,想想在printf的格式化字符串中怎么表示一个%字符?写个小程序试验一下。
printf("%%") 根据转义序列的\规则类比可知printf("%%");
假设变量x和n是两个正整数,我们知道 x/n 这个表达式的结果是取Floor,例 如x是17,n是4,则结果是4。如果希望结果取Ceiling应该怎么写表达式呢?例 如x是17,n是4,则结果是5,而x是16,n是4,则结果是4。
答案是(x+n-1)/n。 这题很重要,多给点时间让学生想。一个班差不多有一两个人能想出来。有些人自以为学得不错,不好好听课,就可以拿这题打击打击。有些人用if/else或三目运算符做出来,应该discourage他们。这种写法以后很常见,绝非奇技淫巧,比如以后讲到对齐会见到这样的情况,分配的内存必须是4字节的整数倍。 给学生解释可以这么说:先拆成x/n+(n-1)/n来看,如果x/n还能余出一点儿来,合写成(x+n-1)/n就可以把余出来的那一点儿给(n-1),从而使(n-1)/n等于1。
简单函数
如果在一个程序中调用了printf函数却不包含头文件,例如int main(void) { printf("\n"); },编译时会报警告:warning: incompatible implicit declaration of built-in function ‘printf’。请分析错误原因。
没有包含头文件就没有printf函数的声明,编译器要做隐式声明,返回值int,参数一个,而真正的printf原型返回值是int,参数是可变个数的,所以incompatible。
定义变量时可以把同样类型的变量列在一起,而定义参数却不可以。其实这里的这个规定也不算十分的例外,并不是C语言的设计者故意找茬,而是不得不这么规定,读者想想为什么 呢?
假如使用该规则, 当有多个类型的变量即int add(int a, b, double c, d); 难以检查语法正确性.
分支语句
1、以下程序段编译能通过,执行也不出错,但是执行结果不正确(根据第 3 节 “程序的调试”的定义,这是一个语义错误),请分析一下哪里错了。还有,既然错了为什么编译能通过呢?
int x = -1; if (x > 0); printf("x is positive.\n");
if判断后有个分号;(空语句), 因为这不是语法错误,所以能够编译通过。
1、写两个表达式,分别取整型变量x的个位和十位。
1) x%10
2)x%100/10
2、写一个函数,参数是整型变量x,功能是打印x的个位和十位。
void print_x(int x) {
printf("个位: %d 十位: %d \n", x%10, x%100/10);
}
1、把代码段 if (x > 0 && x < 10); else printf("x is out of range.\n"); 改写成下面这种形式:
if (____ || ____) printf("x is out of range.\n");
__应该怎么填?
x<=0 x>=10
德摩根律
2、把代码段:
if (x > 0) printf("Test OK!\n"); else if (x <= 0 && y > 0) printf("Test OK!\n"); else printf("Test failed!\n");
改写成下面这种形式: if ( && ) printf("Test failed!\n"); else printf("Test OK!\n"); __应该怎么填?
(不确定是否最优) y<=0 x<=0 x+¬x*y=x+y,德摩根律
3、有这样一段代码:
if (x > 1 && y != 1) { ... } else if (x < 1 && y != 1) { ... } else { ... }
要进入最后一个else,x和y需要满足条件 || 。这里应该怎么填?
x==1 y==1 x(y+z)=xy+x*z,德摩根律
4、以下哪一个if判断条件是多余的可以去掉?这里所谓的“多余”是指,某种情况下如果本来应该打印Test OK!,去掉这个多余条件后仍然打印Test OK!,如果本来应该打印Test failed!,去掉这个多余条件后仍然打印Test failed!。
if (x<3 && y>3) printf("Test OK!\n"); else if (x>=3 && y>=3) printf("Test OK!\n"); else if (z>3 && x>=3) printf("Test OK!\n"); else if (z<=3 && y>=3) printf("Test OK!\n"); else printf("Test failed!\n");
第四个条件else if (z<=3 && y>=3) 可以去掉。 因为在第二个判断假如不符合条件,可以确定y一定小于3。那么而第三个条件不符合,可以得到z>3,换言之,第四个条件一定为假,没有必要。 xy+¬xz+yz=xy+¬x*z
深入理解函数
1、编写一个布尔函数int is_leap_year(int year),判断参数year是不是闰年。如果某年份能被4整除,但不能被100整除,那么这一年就是闰年,此外,能被400整除的年份也是闰年。
code
2、编写一个函数double myround(double x),输入一个小数,将它四舍五入。例如myround(-3.51)的值是-4.0,myround(4.49)的值是4.0。可以调用math.h中的库函数ceil和floor实现这个函数。
code
在上面的例子中,如果循环结束条件就要写成 i<n ,还要结果正确,那么前面应该怎么改?
n += 1;
2、在上一节中讲过怎样把 for 语句写成等价的 while 语句,但也提到如果循环体中有 continue 语句,这两种形式就不等价了,想一想为什么不等价了?
因为for里有执行语句,当continue后会执行(比如++i),而while里则没有。
1、编写递归函数求两个正整数a和b的最大公约数(GCD,Greatest Common Divisor),使用Euclid算法:
- 如果a除以b能整除,则最大公约数是b。
- 否则,最大公约数等于b和a%b的最大公约数。 Euclid算法是很容易证明的,请读者自己证明一下为什么这么算就能算出最大公约数。
int GCD(int a, int b) {
if(a%b == 0)
return b;
else
return GCD(b, a%b);
}
2、编写递归函数求Fibonacci数列的第n项,这个数列是这样定义的: fib(0)=1 fib(1)=1 fib(n)=fib(n-1)+fib(n-2) 这个递归函数做两次递归调用,会形成树状的调用关系,需要画图解释。
int Fibonacci(int n) {
if(n==0 || n==1)
return 1;
else
return Fibonacci(n-1) + Fibonacci(n-2);
}
循环语句
1、用循环解决第 3 节 “递归”的所有习题,体会递归和循环这两种不同的思路。
int w_GCD(int a, int b) {
while(a%b != 0) {
int temp = a;
a = b;
b = temp%b;
}
return b;
}
int w_Fibonacci(int n) {
int i, sum=1, before=1;
for(i=1; i<n; ++i) {
sum += before;
before = sum;
}
return sum;
}
2、编写程序数一下1到100的所有整数中出现多少次数字9。在写程序之前先把这些问题考虑清楚:
- 这个问题中的循环变量是什么?
- 这个问题中的累加器是什么?用加法还是用乘法累积?
- 在第 2 节 “if/else语句”的习题1写过取一个整数的个位和十位的表达式,这两个表达式怎样用到程序中?
1.数字 1-100 2.i,加法累加
int count(void) {
int i, count=1;
for(i=10; i<=100; ++i) {
if(i/9==0 || i%10 == 9) {
++count;
}
}
return count;
}
其实没有必要从2一直检查到n-1,只要从2检查到⌊sqrt(n)⌋,如果全都不能整除就足以证明n是素数了,解释一下为什么
对称性.
求素数这个程序只是为了说明break和continue的用法才这么写的,其实完全可以不用break和continue,请读者修改一下循环的结构,去掉break和continue而保持功能不变
#include <stdio.h>
int is_prime(int n)
{
int i;
for (i = 2; i < n; i++)
if (n % i == 0)
return 0;
return 1;
}
int main(void)
{
int i;
for (i = 1; i <= 100; i++) {
if (is_prime(i))
printf("%d\n", i);
}
return 0;
}
1、上面打印的小九九有一半数据是重复的,因为89和98的结果一样。请修改程序打印这样的小九九: 1
2 4
3 6 9
4 8 12 16
5 10 15 20 25
6 12 18 24 30 36
7 14 21 28 35 42 49
8 16 24 32 40 48 56 64
9 18 27 36 45 54 63 72 81
void print_numbers(void)
{
int i, j;
for(i=1; i<10; ++i) {
for(j=1; j<=i; ++j) {
printf("%d\t", i*j);
}
printf("\n");
}
}
2、编写函数diamond打印一个菱形。如果调用diamond(3, '*')则打印:
*
* * *
*
如果调用diamond(5, '+')则打印:
+
+ + +
+ + + + +
+ + +
+
如果用偶数做参数则打印错误提示。
void diamod(int num, char graph)
{
if(! num&1) {// %2 == 0
printf("wrong\n");
return;
}
int i, j, count;
count = -1;
for(i=num/2; i>=0; --i) {
count += 2;
for(j=i; j>0; --j)
printf("\t");
for(j=count; j>0; --j)
printf("%c\t", graph);
printf("\n");
}
for(i=num/2+1; i<num; ++i) {
for(j=i-num/2; j>0; --j)
printf("\t");
for(j=count-2; j>0; --j)
printf("%c\t", graph);
printf("\n");
count -= 2;
}
}
/* 宋老师答案 */
void diamond(int n, char c)
{
int i, j;
if (n <= 0 || !(n % 2))
return;
for (i = 1; i <= n/2+1; i++) {
for (j = 1; j <= n/2-i+1; j++)
printf("\t");
for (j = 1; j <= 2*i-1; j++)
printf("%c\t", c);
printf("\n");
}
for (i = 1; i <= n/2; i++) {
for (j = 1; j <= i; j++)
printf("\t");
for (j = 1; j <= n-2*i; j++)
printf("%c\t", c);
printf("\n");
}
}
以下代码编译没有问题, 但运行结果却和预期不符合, 不能打印出other number, 请分析原因.
#include <stdio.h> int main(void){ int n = 3; switch(n) { case 1: printf("1\n"); break; case 2: printf("2\n"); break; default: printf("other number\n"); } return 0; }
好像这道题目出错了? 能够正常运行并且打印的.
结构体
1、在本节的基础上实现一个打印复数的函数,打印的格式是x+yi,如果实部或虚部为0则省略,例如:1.0、-2.0i、-1.0+2.0i、1.0-2.0i。最后编写一个main函数测试本节的所有代码。想一想这个打印函数应该属于上图中的哪一层?
存储表示层
void print_complex(struct complex_struct z) {
if(z.real()==0 && z.imag()==0)
printf("0\n");
else if(z.real() == 0)
printf("%fi", z.imag());
else if(z.imag() == 0)
printf("%f", z.real());
else
printf("%f+%fi", z.real(), z.iamg());
}
2、实现一个用分子分母的格式来表示有理数的结构体rational以及相关的函数,rational结构体之间可以做加减乘除运算,运算的结果仍然是rational 注意要约分为最简分数,例如1/8和-1/8相减的打印结果应该是1/4而不是2/8,可以利用第 3 节 “递归”练习题中的Euclid算法来约分。在动手编程之前先思考一下这个问题实现了什么样的数据抽象,抽象层应该由哪些函数组成。
struct Ragtional make_rational(int numerator, int denominator)
{
struct Ragtional a;
a.numerator = numerator;
a.denominator = denominator;
return a;
}
struct Ragtional add_rational(struct Ragtional a, struct Ragtional b)//need to fix -
{
struct Ragtional result;
int max, gcm;
max = GCD(a.denominator, b.denominator);
gcm = (a.denominator*b.denominator) / max;
printf("gcm is %d\n", gcm);
result.denominator = gcm;
result.numerator = (a.numerator*gcm/a.denominator) + (b.numerator*gcm/b.denominator);
printf("numerator is %d\n", result.numerator);
return result;
}
struct Ragtional mul_rational(struct Ragtional a, struct Ragtional b)
{
struct Ragtional result;
result.numerator = a.numerator * b.numerator;
result.denominator = a.denominator * b.denominator;
return result;
}
struct Ragtional div_rational(struct Ragtional a, struct Ragtional b)
{
struct Ragtional trans;
trans.numerator = b.denominator;
trans.denominator = b.numerator;
return mul_rational(a, trans);
}
void print_rational(struct Ragtional a)
{
int max;
max = GCD(a.numerator, a.denominator);
if(a.denominator/max == 1)
printf("%d\n", a.numerator/max);
else
printf("%d/%d\n", a.numerator/max, a.denominator/max);
}
1、本节只给出了make_from_real_img和make_from_mag_ang函数的实现,请读者自己实现real_part、img_part、magnitude、angle这些函数。
double real_part(struct complex_struct z) {
if(z.t==RECTANGULAR)
return z.a;
else
return z.a*cos(z.b);
}
double imag_part(struct complex_struct z) {
if(z.t==RECTANGULAR)
return z.b;
else
return z.a*sin(z.b);
}
double magnitude(struct complex_struct z) {
if(z.t==RECTANGULAR)
return sqrt(z.a*z.a + z.b*z.b);
else
return z.a;
}
double angle(struct complex_struct z) {
if(z.t==RECTANGULAR) {
double PI = acos(-1.0);
if(z.a > 0)
return atan(z.b/z.a);
else
return atan(z.b/z.a) + PI;
} else
return z.b;
}
2、编译运行下面这段程序:
#include <stdio.h> enum coordinate_type { RECTANGULAR = 1, POLAR }; int main(void) { int RECTANGULAR; printf("%d %d\n", RECTANGULAR, POLAR); return 0; }
结果是什么?并解释一下为什么是这样的结果。
结果是 -1217339392 2, 原因是main函数中的未赋值RECTANGULAR变量覆盖了全局的enum变量。
数组
1、编写一个程序,定义两个类型和长度都相同的数组,将其中一个数组的所有元素拷贝给另一个。既然数组不能直接赋值,想想应该怎么实现。
void copy(char *a, char *b) {
int i;
for(i=0; b[i]; ++i)
a[i] = b[i];
}
1、用rand函数生成10~20之间的随机整数,表达式应该怎么写?
rand()%11 + 10
1、补完本节直方图程序的main函数,以可视化的形式打印直方图。例如上一节统计20个随机数的结果是: 0 1 2 3 4 5 6 7 8 9
* * * * *
int j, sum;
for(i=0; i<=9; i++)
printf("%d ", i);
printf("\n");
for(i=0; i<=9; i++)
printf("%d ", histogram[i]);
printf("\n");
do {
sum = 0;
for(i=0; i<=9; i++)
sum += histogram[i];
for(j=0; j<=9; ++j) {
if(histogram[j]!=0) {
printf("* ");
--histogram[j];
}else
printf(" ");
}
printf("\n");
} while(sum != 0);
return 0;
2、定义一个数组,编程打印它的全排列。比如定义:
#define N 3
int a[N] = { 1, 2, 3 }; 则运行结果是: $ ./a.out 1 2 3 1 3 2 2 1 3 2 3 1 3 2 1 3 1 2 1 2 3
code
完成了上述要求之后再考虑第二个问题:如果再定义一个常量M表示从N个数中取几个数做排列(N == M时表示全排列),原来的程序应该怎么改? 最后再考虑第三个问题:如果要求从N个数中取M个数做组合而不是做排列,就不能用原来的递归过程了,想想组合的递归过程应该怎么描述,编程实现它。
code
留给读者思考的问题是:(man - computer + 4) % 3 - 1这个神奇的表达式是如何比较出0、1、2这三个数字在“剪刀石头布”意义上的大小的? 胜 负 平 胜 负 man-computer -2 -1 0 1 2 man-computer+4 2 3 4 5 6 (man-computer+4)%3 2 0 1 2 0 (man-computer+4)%3-1 1 -1 0 1 -1
剪刀石头布相生相克,形成一个环,凡是具有环的特性的数学模型都可以考虑用取模运算,首先确定了man-computer和%3,然后再调整其它常数得到normalized的结果。
编码风格
gdb
看下面的程序:
#include <stdio.h> int main(void) { int i; char str[6] = "hello"; char reverse_str[6] = ""; printf("%s\n", str); for (i = 0; i < 5; i++) reverse_str[5-i] = str[i]; printf("%s\n", reverse_str); return 0; }
首先用字符串"hello"初始化一个字符数组str(算上'\0'共6个字符)。然后用空字符串""初始化一个同样长的字符数组reverse_str,相当于所有元素用'\0'初始化。然后打印str,把str倒序存入reverse_str,再打印reverse_str。然而结果并不正确: $ ./main hello 我们本来希望reverse_str打出来是olleh的,结果什么都没有。重点怀疑对象肯定是循环,那么简单验算一下,i=0时,reverse_str[5] = str[0],也就是'h',i=1时,reverse_str[4] = str[1],也就是'e',依此类推,i=0,1,2,3,4,共5次循环,正好把h,e,l,l,o五个字母给倒过来了,哪里不对了? 请调试修正这个Bug。
程序把'\0'也换到开头来了。应该改成 reverse_str[4-i] = str[i];
排序与查找
实现选择排序算法
code
1、为了便于初学者理解,本节的merge函数写得有些啰嗦,你能想出哪些办法将它化简?
code
2、快速排序是另外一种采用分而治之策略的排序算法,在平均情况下的时间复杂度也是Θ(nlgn),但比归并排序有更小的时间常数。它的基本思想是这样的: int partition(int start, int end) { 从a[start..end]中选取一个pivot元素(比如选a[start]为pivot); 在一个循环中移动a[start..end]的数据,将a[start..end]分成两半, 使a[start..mid-1]比pivot元素小,a[mid+1..end]比pivot元素大,而a[mid]就是pivot元素; return mid; } void quicksort(int start, int end) { int mid; if (end > start) { mid = partition(start, end); quicksort(start, mid-1); quicksort(mid+1, end); } } 请补完partition函数,这个函数有多种写法,请选择时间常数尽可能小的实现方法。想想快速排序在最好和最坏情况下的时间复杂度是多少?快速排序在平均情况下的时间复杂度分析起来比较复杂,有兴趣的读者可以参考[算法导论]。
int partition(int a[], int start, int end)
{
int pivot = a[end];
while(start < end) {
while(start<end && a[start]<=pivot)
++start;
a[end] = a[start];
while(start<end && a[end]>=pivot)
--end;
a[start] = a[end];
}
a[start] = pivot;
return start;
}
1、实现一个算法,在一组随机排列的数中找出最小的一个。你能想到的最直观的算法一定是Θ(n)的,想想有没有比Θ(n)更快的算法?
没有更快的算法,理由同上。
2、在一组随机排列的数中找出第二小的,这个问题比上一个稍复杂,你能不能想出Θ(n)的算法?
secondmin.c
3、进一步泛化,在一组随机排列的数中找出第k小的,这个元素称为k-th Order Statistic。能想到的最直观的算法肯定是先把这些数排序然后取第k个,时间复杂度和排序算法相同,可以是Θ(nlgn)。这个问题虽然比前两个问题复杂,但它也有平均情况下时间复杂度是Θ(n)的算法,将上一节习题1的快速排序算法稍加修改就可以解决这个问题: / 从start到end之间找出第k小的元素 / int order_statistic(int start, int end, int k) { 用partition函数把序列分成两半,中间的pivot元素是序列中的第i个; if (k == i) 返回找到的元素; else if (k > i) 从后半部分找出第k-i小的元素并返回; else 从前半部分找出第k小的元素并返回; } 请编程实现这个算法。
int order_statistic(int a[], int start, int end, int k)
{
int i;
if(end >= start) {
i = order_partition(a, start, end, k);
if(k == i)
return a[i];
else if(k > i)
return order_statistic(a, i+1, end, k);
else
return order_statistic(a, start, i-1, k);
}
}
ordstat.c。可以分析一下为什么时间复杂度是Θ(n),在最好情况下每次丢掉一半的元素,n+n/2+n/4+n/8+...=2n,平均情况下的分析比较复杂,但快速排序类似,时间复杂度和最好情况下一致。
1、本节的折半查找算法有一个特点:如果待查找的元素在数组中有多个则返回其中任意一个,以本节定义的数组int a[8] = { 1, 2, 2, 2, 5, 6, 8, 9 };为例,如果调用binarysearch(2)则返回3,即a[3],而有些场合下要求这样的查找返回a[1],也就是说,如果待查找的元素在数组中有多个则返回第一个。 请修改折半查找算法实现这一特性。
int binary_search(int a[], int low, int high, int element)
{
int mid = -2;
while(low <= high) {
mid = (low+high)/2;
if(a[mid] == element) {
while(a[mid-1] == element)/* return first element */
--mid;
break;
}
else if(element < a[mid])
high = mid-1;
else
low = mid+1;
}
if(low > high)
return -2;
return mid;
}
2、编写一个函数double mysqrt(double y);求y的正平方根,参数y是正实数。我们用折半查找来找这个平方根,在从0到y之间必定有一个取值是y的平方根,如果我们查找的数x比y的平方根小,则x2
y,我们可以据此缩小查找范围,当我们查找的数足够准确时(比如满足|x2-y|<0.001),就可以认为找到了y的平方根。思考一下这个算法需要迭代多少次?迭代次数的多少由什么因素决定? double mysqrt(double y) { double low=0, high=y, mid = -2; while(low < high) { mid = (low+high)/2; if(abs(pow(mid, 2)-y) < 0.00001) break; else if(pow(mid, 2) < y) low = mid+0.00001; else high = mid-0.00001; } return mid; }
迭代次数取决于精度(可以用对数函数表示)
3、编写一个函数double mypow(double x, int n);求x的n次方,参数n是正整数。最简单的算法是: double product = 1; for (i = 0; i < n; i++) product *= x; 这个算法的时间复杂度是Θ(n)。其实有更好的办法,比如mypow(x, 8),第一次循环算出x·x=x2,第二次循环算出x2·x2=x4,第三次循环算出4·x4=x8。这样只需要三次循环,时间复杂度是Θ(lgn)。思考一下如果n不是2的整数次幂应该怎么处理。请分别用递归和循环实现这个算法。
从以上几题可以看出,折半查找的思想有非常广泛的应用,不仅限于从一组排好序的元素中找出某个元素的位置,还可以解决很多类似的问题。[编程珠玑]对于折半查找的各种应用和优化技巧有非常详细的介绍。
栈和队列
1、修改本节的程序,要求从起点到终点正向打印路线。你能想到几种办法?
dfs.c
2、本节程序中predecessor这个数据结构占用的存储空间太多了,改变它的存储方式可以节省空间,想想该怎么改。
计算出row*MAX_ROW+col,把结果保存到predecessor数组,可节省一半的存储,需要座标时可以用/MAX_ROW和%MAX_ROW运算把row和col分离出来
3、上一节我们实现了一个基于堆栈的程序,然后改写成递归程序,用函数调用的栈帧替代自己实现的堆栈。本节的DFS算法也是基于堆栈的,请把它改写成递归程序,这样改写可以避免使用predecessor数据结构,想想该怎么做。
new
1、本节的例子直接在队列元素中加一个指针成员表示前趋,想一想为什么上一节的例 12.3 “用深度优先搜索解迷宫问题”不能采用这种方法表示前趋?
因为出栈的元素被新入栈的元素覆盖了
2、在讲解例 12.1 “用堆栈实现倒序打印”时我们说“top总是指向栈顶元素的下一个元素”是堆栈 操作的Class Invariant,那么本节实现的队列操作的Invariant应该怎么描述?
xy
2、本节例子中给队列分配的存储空间是512个元素,其实没必要这么多,那么解决这个问题至少要分配多少个元素的队列空间呢?跟什么因素有关?
每个点最多入队一次,迷宫中有多少个点就最多需要多大的队列空间
1、现在把迷宫问题的要求改一下,只要求程序给出最后结论就可以了,回答“有路能到达终点”或者“没有路能到达终点”,而不需要把路径打印出来。请把例 12.4 “用广度优先搜索解迷宫问题”改用环形队列实现,然后试验一下解决这个问题至少需要分配多少个元素的队列空间。
bfs_circular.c
注意用%运算移动head和tail,具有环的特性的数学模型都可以考虑用取模运算。队列空的判断条件是head == tail;,队列满的判断条件是head == (tail + 1) % QSIZE;,必须保留一个元素未用,否则无法区分队空和队满。
1、将例 12.4 “用广度优先搜索解迷宫问题”改用环形队列实现。然后回答: 运行原来的程序要求queue数组至少有多长?不用跟踪程序的运行过程,你能很快答上来 吗? 改为环形队列之后要求queue数组至少有多长?
code