差一错误是所有程序员的噩梦!
它看起来是一个琐碎的问题,却非常烦人,许多程序员对它是采取一种“轻蔑”的态度,即使被它虐了千百遍,还是不愿意正视这个问题。
其实,差一问题并不是一个小问题,我们应该对它给予足够的重视。
在《C陷阱与缺陷》中,对这个问题有详细的讨论,这里结合我的理解写一写解决这个问题的方法。
许多程序员在遇到差一问题时,常常采用的是“探测法”,一个位置应该写n还是应该写n-1,测试两次就够了,如果是两个位置,就要测试四次,三个位置,要测试八次。。。
其中还可能存在“重叠错误”,当有两个错误时,测试是正常的,当改正了一个错误,反而测试出错。
一次过写一段正确的代码,总比飞快得写一堆糟糕代码,再做一天debug好得多。
正确的指针和下标,是计算出来的,不是乱蒙出来的!
指针运算
在学会计算指针之前,先要正确理解数组和指针。
首先,地址就是一个整数,不是什么神奇的东西!
指针是地址变量,数组名是地址常量。
举个例子,
int arr[10];
编译器会分配10个整型大小的空间,首地址是arr,假设arr=500,那么这个数组就是这样的:
其中能索引到的元素是arr[0]到arr[9],arr[n]也就是arr[10]是没有的。
arr[0]有,arr[n]没有,这个叫“不对称边界”,它带来极大便利的同时,也引起了许多错误,不过,这种设计绝对是正确的。
可以用arr+5,表示第5个元素的地址(从0开始),但是不允许arr=arr+5,因为数组名是个常量。这里arr=500,但arr+5不等于505,而是等于520,因为在对地址量做加减时,它自动乘了一个sizeof(int)。
指针也一样,不同的是指针可以赋值。
地址量可以进行下列操作(p表示指针,i表示整型):
p=p+i; //指针后移i个位置
p=p-i; //指针前移i个位置
i=p1-p2; //两个指针之间的元素个数
要指向p1和p2中间那个元素,可以用p0=p1+(p2-p1)/2,
用p0=(p1+p2)/2似乎也可以,不过编译器不允许,因为p1+p2没有意义。
问题解决
《C陷阱与缺陷》中,差一问题的解决方法,
一个是理解“不对称边界”,
另一个是“边界计算”。
边界计算,简单举个例子。
在一个位置,不知道应该写n还是n-1,于是可以假设当n=2时,这里应该写1,所以,这个位置应该写n-1。
“从1到10有几个数?!”
“11个!”
“你给我想清楚!”
“11个!”
“那从1到2有几个数?”
就是这样了。
解决差一问题的方法绝不止这两种,我们可以在实际问题的过程中,想到各种不同的方法。
二分查找
下面以二分查找为例,讲解这个问题。
根据《编程珠玑》里的数据,在100个专业程序员中,90%的程序员写的二分查找是存在bug的。
在没有bug的二分查找代码中,也可能存在元素的重复比较。
我写的二分查找是这样的:
int* bs(int* arr,int n,int x){
⓪ if(n==0)return NULL;
① else if(x==arr[n/2])return arr+n/2;
② else if(x<arr[n/2])return bs(arr,n/2,x);
③ else return bs(arr+n/2+1,n-n/2-1,x);
}
元素按从小到大的顺序排,如果找到,返回的是元素的地址,找不到则返回NULL。(如果找到了bug,请告诉我)。
我写这段代码的过程是这样的:
先在纸上画出内存模型
⓪语句,当数组没有元素了,就返回NULL
①语句,比较x是否等于arr[n/2],(这里arr[n/2]是不是正中间的元素并不重要),如果等于,返回它的地址,arr+n/2
②语句,比较x是否小于arr[n/2],如果小于,应该继续查找的是位于arr[n/2]前面的那一段数组,不包括arr[n/2],因此,这段数组的首地址还是arr,它的最后一个元素是arr[n/2-1],长度是n/2
③语句,应该继续查找的是位于arr[n/2]后面的那一段数组,不包括arr[n/2],因此,这段数组的首地址是arr+n/2+1,那么长度应该怎么计算呢?
第1个方法:右边数组长度=原数组长度-左边数组长度-arr[n/2]这个元素
=n-n/2-1
第2个方法:右边数组的末地址=原数组的末地址
右边数组的首地址+长度=原数组的首地址+长度
arr+n/2+1+m=arr+n
m=n-n/2-1
所以,右边数组的长度为n-n/2-1(n-n/2不一定等于n/2,因为整数除法是向下取整的)
待定系数法
我想到的另一个方法,是待定系数法。
以对称矩阵的压缩存储为例,讲解这个方法。
对称矩阵的压缩存储是数据结构考试中的必备题目,在各大考试中频繁露面。
在不同的试题中,具体细节也不同,有的存储上三角,有的存储下三角,有的从0开始,有的从1开始,还有些是01混合。
上试题:
一个10阶对称矩阵A,采用行优先顺序压缩存储“下三角元素”,a[1,1]为第一个元素,其存储地址为数组B[0],每个元素占有1个存储地址空间,则a[ i, j ]的地址为_________。
解法:
先画出矩阵的一部分
设B[t]对应a[ i, j ],即对应方程为,选出四个元素
当i=1,j=1时,t=0
当i=2,j=1时,t=1
当i=3,j=1时,t=3
当i=3,j=2时,t=4
写成矩阵形式可以更好计算:
即,
则a[ i, j ]的地址为。
这里需要注意的是,选取元素时不能取同一条直线上的元素,否则在计算时矩阵不满秩,得不到最后结果。