对Alexia(minmin)网友代码的评论及对“求比指定数大且最小的‘不重复数’问题”代码的改进
应Alexia(minmin)网友之邀,到她的博客上看了一下她的关于“求比指定数大且最小的‘不重复数’问题”的代码(百度2014研发类校园招聘笔试题解答),并在评论中粗略地发表了点意见。
由于感觉有些看法在评论中无法详细表达,也由于为了更详细地说明一下我的 算法:求比指定数大且最小的“不重复数”问题的高效实现博文中没有说清楚的一些想法,并给出这个问题更加完美的代码,故制此文。欢迎Alexia(minmin)网友和其他网友指正。
Alexia(minmin)网友在其博文中对其算法思想描述得很清楚:
1. 给定N是一个正整数,求比N大的最小“不重复数”,这里的不重复是指没有两个相等的相邻位,如1102中的11是相等的两个相邻位故不是不重复数,而12301是不重复数。
算法思想:当然最直接的方法是采用暴力法,从N+1开始逐步加1判断是否是不重复数,是就退出循环输出,这种方法一般是不可取的,例如N=11000000,你要一个个的加1要加到12010101,一共循环百万次,每次都要重复判断是否是不重复数,效率极其低下,因此是不可取的。这里我采用的方法是:从N+1的最高位往右开始判断与其次高位是否相等,如果发现相等的(即为重复数)则将次高位加1,注意这里可能进位,如8921—>9021,后面的直接置为010101...形式,如1121—>1201,此时便完成“不重复数”的初步构造,但此时的“不重复数”不一定是真正的不重复的数,因为可能进位后的次高位变为0或进位后变成00,如9921—>10001,此时需要再次循环判断重新构造直至满足条件即可,这种方法循环的次数比较少,可以接受。
下面是Alexia(minmin)网友的代码:
// 求比指定数大且最小的“不重复数” #include <stdio.h> void minNotRep(int n)
{
// 需要多次判断
while(1)
{
int a[20], len = 0, i, b = 0;
// flag为true表示是“重复数”,为false表示表示是“不重复数”
bool flag = false; // 将n的各位上数字存到数组a中
while(n)
{
a[len++] = n % 10;
n = n / 10;
} // 从高位开始遍历是否有重复位
for(i = len - 1; i > 0; i--)
{
// 有重复位则次高位加1(最高位有可能进位但这里不需要额外处理)
if(a[i] == a[i - 1] && !flag)
{
a[i - 1]++;
flag = true;
}
else if(flag)
{
// 将重复位后面的位置为0101...形式
a[i - 1] = b;
b = (b == 0) ? 1 : 0;
}
} // 重组各位数字为n,如果是“不重复数”则输出退出否则继续判断
for(i = len - 1; i >= 0; i--)
{
n = n * 10 + a[i];
} if(!flag)
{
printf("%d\n", n);
break;
}
}
} int main()
{
int N; while(scanf("%d", &N))
{
minNotRep(N + 1);
} return 0;
}
我对这段代码的总体看法是,main()写的很好,因为很短,很容易看懂。
主要缺点是,main写在了源程序的后面,我个人认为这种风格欠佳——头重脚轻(参见《品悟C》p262,“贪小便宜——省略函数类型声明等问题”)。
理由是,看文章我们总是先看标题,同样的道理读代码也总是先读main()。把main()置于源代码的后部于人于己都不利于阅读。
这种写法唯一的好处是可以省写函数类型声明。这是初学者非常喜欢占的一个小便宜。但从长远以及稍微大一些规模的代码来看,这个小便宜微不足道得可以忽略不计。(我记得要么是在我以前发的博文中,要么就是在《品悟C》这本书里详细地讲过这件事。)
这段代码的另一个缺点是,minNotRep()太大。原因主要是minNotRep()这个函数不但完成了求不重复数,还顺便输出了这个不重复数。这很不好,函数的功能应该单一,而且函数应该越小越好。因此,minNotRep()不应该定义为
void minNotRep(int n);
的形式,更好一些的写法应该是返回不重复数
int minNotRep(int n);
或者
void minNotRep(int *n);
直接把原来的数改为不重复数,然后在main()中再考虑输出。
事情要一件一件地做,指望一个函数完成所有事情,代码显然不够从容,函数也必然臃肿。
所以,从整体上来说,代码这样安排为好
/*
诸函数类型声明:
输入N();
求最小不重复数();
输出();
*/ int main()
{
int N; //输入N();
//求最小不重复数();
//输出(); return 0;
} /*
诸函数定义:
输入N()
{
}
求最小不重复数();
{
}
输出();
{
}
*/
再来看minNotRep()函数。
在这个函数中,首先把n离散,然后将离散后的数字用一数组(int a[20])和数字的位数len表示。这个结构没有问题,很适合从高位到低位找重复数字要求(但是如果是我,则一定会把这两者构造成一个统一的数据结构。为什么?不解释。因为这是常识,)。问题在于这两个变量被放在了while(1)循环的内部,由于它们是局部auto变量,因而意味着每次循环都要重新建立这个数组和len变量。这显然是不妥的。这两个变量应该放在while(1)循环的外部,这样每次循环就不必重新建立这两个变量了。
与此类似,n的分解离散及合成也写在了while(1)循环之内,这就意味着每次循环都必须重新分解再重新合成,这也是无意义的多余动作。经与作者沟通交流发现,作者是因为没有很好地处理进位问题才不得不这样处理的。以19901212为例
首先,分解为 1、9、9、0、1、2、1、2存入数组,
while(n)
{
a[len++] = n % ;
n = n / ;
}
在数组中的顺序是:2 1 2 1 0 9 9 1
然后从高位到低位找重复数字
找到之后如果flag为false则加1
// 从高位开始遍历是否有重复位
for(i = len - ; i > ; i--)
{
// 有重复位则次高位加1(最高位有可能进位但这里不需要额外处理)
if(a[i] == a[i - ] && !flag)
{
a[i - ]++;
flag = true;
}
else if(flag)
{
// 将重复位后面的位置为0101...形式
a[i - ] = b;
b = (b == ) ? : ;
}
}
我不得不说,我很不喜欢这个flag,因为除了表现出一种别扭的思维,这里它没有别的用处。(参见flag标志什么?哦,它标志代码馊了 )这段代码完全可以这样写:
for(i = len - ; i > ; i--)
{
if(a[i] == a[i - ] )
{
a[i - ]++;
break ;
}
}
for ( 从 i- 到 )
{
// 将重复位后面的位置为0101...形式
}
无论从逻辑上还是形式上都更为简洁。
关于这个flag要说的另一件事情是,它是bool类型。这种类型C语言中是没有的(C99中有_Bool类型),作者恐怕是把C语言和C++混为一谈了。国内很多大学生都犯这个毛病,甚至专业程序员中也有很多人C和C++不分。这个错误很广泛,无疑首先是教材或书籍的责任。(参见《品悟C》p4,“C啊,多少C++假汝之名而行——C和C++不分”)
当然支持C99的编译器可以这样用,但前提是必须
#include <stdbool.h>
才行。可是在代码中我没有发现这条预处理命令。因此bool是误用无疑。
回到被打断的话题,加1之后,数组中变成了
2 1 2 1 0 9 1
由于作者没有及时处理这个10,所以才不得不在循环体内不断地分解与合成。其实这时只要对数组稍微处理一下,模拟一下进位,将数组改为
2 1 2 1 0 0 0 2
就用不着反复地分解、合成了。
紧接着,代码将0 0左侧的数组元素改写成了“0101...形式”:
0 1 0 1 0 2
这里的代码有两个问题。的问题是
第一,
a[i - 1] = b;
这句我认为是一个BUG。因为前面说的是a[i]与a[i-1]重复(并且有a[i - 1]++;),所以“// 将重复位后面的位置为0101...形式”应该是从a[i-2]而不是a[i - 1]开始改。但 a[i-2]也不对,因为所在循环for(i = len - 1; i > 0; i--)中的 i 最小可以为1,所以a[i-2]存在数组越界的问题。
第二问题是,由于加1之后重复位前面可能又出现了新的重复位,所以这里的“将重复位后面的位置为0101...形式”几乎是一个无意义的操作。这个动作仅仅是在最后一次才有意义,这就是我不肯接受这种写法的原因。写代码其实和下围棋一样,任何一个高手下围棋绝对不肯走一步显而易见没有用处的“废棋”。反对直接填写“0101...”的另一个原因是,这是人“算”的,不是程序“算”的。程序员的任务是用程序发出命令让计算机去做,而不是越俎代庖地替代程序和计算机。
我在这里的写法是将重复位后面各个位置上的数字改为0。而且为了不至于反复地进行无意义地重复写0,使用了一点小技巧。这个小技巧,就评论情况来看,目前还没有人看懂。
好,评论就到这里。下面讲一下我在这里的处理。依然是以以19901212为例,在数组中的顺序是:2 1 2 1 0 9 9 1。
我首先用 end = 0 这个变量规定了重复位后面改为0的最后一位。
用b_point = search ( &map )确定最前面的重复位,在这个例子里应该是5 (19) 。然后将199加1,并在数组中模拟了进位( add_1( &map , b_point ) ; ) ,
for ( i = from ; i < p_m->top ; i ++ ) //进位处理
{
p_m->t[i + ] += p_m->t[i] / 10u ;
p_m->t[i] %= 10u ;
} if ( p_m->t[p_m->top] > 9u ) //最高位有进位
{
p_m->t[p_m->top + ] = p_m->t[p_m->top] / 10u ;
p_m->t[p_m->top ++ ] %= 10u ;
}
(顺便说一句,这里的p_m->t[p_m->top ++ ] %= 10u ;一句一直是让我感到有些惴惴不安的,生怕“求道于盲”那样精通C语言的网友提出质疑。)
之后数组变为
2 1 2 1 0 0 0 2
然后将数组中从end到b_point-1的元素改为0(一共5个),数组变为
0 0 0 0 0 0 0 2
最后再将b_point的值赋给end,由于每次循环修改的是从b_point-1到end之间的元素,这样下次就不会再修改数组最左面那5个元素的值了。
我的失误:
我的失算之处是,最初也被题目中的“给定任意一个正整数”中的“正整数”三个字给迷惑了。直到写完代码我才意识到,这个题目跟正整数几乎没什么关系。把输入视为一个十进制形式正整数的字符序列,不但完全满足原来问题的要求,而且不限于整数类型的范围限制。为此,重新给出可处理最多100位正整数的代码如下:
#include <stdio.h> #define MAX 100
typedef struct
{
unsigned char t[ MAX + 1 ] ;
int top ; //记录第一位数的下标
}
Map ; void input( Map * );
void reverse( unsigned char [] , int );
void exchange( unsigned char * , unsigned char * );
void squeeze( Map * );
void find( Map * );
int search( const Map * );
void add_1( Map * , const int );
void clear( Map * , const int , const int );
void out( const Map * ); int main( void )
{
Map num ; input( & num ); //输入正整数
add_1( & num , 0 ); //加1
find ( & num ); //求不重复数
out ( & num ); //输出 return 0;
} void squeeze( Map * p_m )
{
while ( p_m -> t[ p_m -> top ] == 0 )
p_m -> top -- ;
} void exchange( unsigned char * p1 , unsigned char * p2 )
{
unsigned char c = * p1 ;
* p1 = * p2 ;
* p2 = c ;
} void reverse( unsigned char a[] , int n )
{
int i ; for ( i = 0 , n -- ; i < n ; i ++ , n -- )
exchange( a + i , a + n ); } void input( Map *p_m )
{
int c ; p_m -> top = -1 ; while ( ( c = getchar () ) != '\n' )
{
if ( c < '0' || c > '9' || p_m -> top > MAX )
break ; p_m -> top ++ ;
p_m -> t[ p_m -> top ] = c - '0' ;
} reverse( p_m -> t , p_m -> top + 1 );//颠倒次序 squeeze( p_m ); //去掉开头的0 } void clear( Map * p_m , const int from , const int to )
{
int i ; for ( i = from - 1 ; i > to - 1; i -- )
p_m->t[i] = 0u ;
} void add_1( Map * p_m , const int from )
{
int i ; p_m->t[from] ++; //最低位加1 for ( i = from ; i < p_m->top ; i ++ ) //进位处理
{
p_m->t[i + 1] += p_m->t[i] / 10u ;
p_m->t[i] %= 10u ;
} if ( p_m->t[p_m->top] > 9u ) //最高位有进位
{
p_m->t[p_m->top + 1] = p_m->t[p_m->top] / 10u ;
p_m->t[p_m->top ++ ] %= 10u ;
}
} int search( const Map * p_m )
{
int i ; for ( i = p_m->top ; i > 0 ; i-- )
{
if ( p_m->t[i] == p_m->t[i-1] )
break ;
} return i - 1 ;
} void find( Map * p_m )
{
int end = 0 , b_point ; while ( ( b_point = search ( p_m ) ) > -1 ) //为-1时说明不是不重复数
{
add_1( p_m , b_point ); //重复数部分加1
clear( p_m , b_point , end ); //后面改为0
end = b_point ; //确定下次循环的处理范围
}
} void out( const Map * p_m )
{
for (int i = p_m -> top ; i >= 0 ; i -- )
printf( "%u" , p_m -> t[i] ); putchar('\n');
}
对Alexia(minmin)网友代码的评论及对“求比指定数大且最小的‘不重复数’问题”代码的改进的更多相关文章
- 评playerc网友的"求比指定数大且最小的“不重复数”问题"
问题见:对Alexia(minmin)网友代码的评论及对“求比指定数大且最小的‘不重复数’问题”代码的改进 .算法:求比指定数大且最小的“不重复数”问题的高效实现 . playerc网友的代码如下(求 ...
- 代码中,使用__DATE__宏,获取程序编译时间,如何保证每次编译代码(非重新生成方式),都能更新__DATE__的值?
代码中,使用__DATE__宏,获取程序编译时间,如何保证每次编译代码(非重新生成方式),都能更新__DATE__的值? 解决:通过vs的预先生成命令中,添加批处理命令,删除对应的obj文件方式,强制 ...
- 帮初学者改代码——playerc之“练习:求完数问题”(下)
前文链接:帮初学者改代码——playerc之“练习:求完数问题”(上) 再来看看be_ferfect()应该如何改. be_ferfect()函数的功能是判断number是否为完数,同时把因子对写入d ...
- 帮初学者改代码——playerc之“练习:求完数问题”(上)
原文:“练习:求完数问题” 原代码: // #include <stdio.h> #include <stdlib.h> #include <math.h> #de ...
- 代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
- 漂浮广告代码兼容ie、firefox,多个漂浮不冲突,调用只需两行代码
原文:漂浮广告代码兼容ie.firefox,多个漂浮不冲突,调用只需两行代码 将广告内容放在div中,设置一个id,然后用下面方法调用var adcls=new AdMove("div的id ...
- 洛谷P1345 [USACO5.4]奶牛的电信Telecowmunication【最小割】分析+题解代码
洛谷P1345 [USACO5.4]奶牛的电信Telecowmunication[最小割]分析+题解代码 题目描述 农夫约翰的奶牛们喜欢通过电邮保持联系,于是她们建立了一个奶牛电脑网络,以便互相交流. ...
- python 控制语句基础---->代码块:以为冒号作为开始,用缩进来划分作用域,代表一个整体,是一个代码块,一个文件(模块)也称为一个代码块 | 作用域:作用的范围
# ### 代码块:以为冒号作为开始,用缩进来划分作用域,代表一个整体,是一个代码块,一个文件(模块)也称为一个代码块 # ### 作用域:作用的范围 print(11) print(12) prin ...
- 如何在PHP页面中原样输出HTML代码(是该找本php的数来看了)
如何在PHP页面中原样输出HTML代码(是该找本php的数来看了) 一.总结 一句话总结:字符串与HTML之间的相互转换主要应用htmlentities()函数来完成. 1.php中的html标签如何 ...
随机推荐
- linux 开机启动设置
操作系统:Ubuntu12.04硬件环境:HP CQ45 当用户使用sudo apt-get install安装完apache和mysql之后,这些服务默认是开机启动的,但是有的时候需要 ...
- iOS UPYUN(又拍云)使用总结
UPYUN,原来没用过,上个周用了一次,觉得蛮方便的,对于个人开发者,且没有服务器的,上传图片和文件,是个不二选择. 首先,先明白原理: 1.又拍云有一个上传空间,在这个空间里,有空间名称.密钥,其他 ...
- Fiddler-007-修改HTTP请求响应数据
前文简述了如何通过 Fiddler 修改 HTTP请求 的请求参数,详情请参阅:Fiddler-006-修改HTTP请求参数. 在进行 App 测试时,经常需要修改请求参数,以获得不同的显示效果,以查 ...
- 解决VS2010无法打开,提示无法找到atl100.dll的方法
这个问题是卸载VS2010一些组件造成的误删问题,且从网上下的atl100.dll通常与自己的VS2010不符 解决方法: 从路径:C:\Program Files\Microsoft Visual ...
- Jackson:fasterxml和codehaus的区别
Jackson fasterxml和codehaus的区别: 它们是jackson的两个分支.也是两个版本的不同包名.jackson从2.0开始改用新的包名fasterxml:1.x版本的包名是cod ...
- java 中集合和数组互相转换
package test; import java.util.Arrays;import java.util.List; /** * Created by Administrator on 2016/ ...
- Linux 进程间通信
[转]unix进程间的通信方式 (1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信. (2)命名管道(named pipe):命名管道克服 ...
- linux 安装apahce的configure: error: APR not found. Please read the documentation解决办法
1.下载所需软件包: 下载apr并配置 wget http://apache.freelamp.com/apr/apr-1.4.2.tar.gz 下载apr ./configure –prefix=/ ...
- Android 关于ListView中adapter调用notifyDataSetChanged无效的原因
话说这个问题已经困扰我很久了,一直找不到原因,我以为只要数据变了,调用adapter的notifyDataSetChanged就会更新列表,最近在做微博帐号管理这一块,想着动态更新列表,数据是变了,但 ...
- RAC GI安装,报"Task resolv.conf Integerity"验证失败
安装12.1.0.2 rac测试环境的时候,报"Task resolv.conf Integerity"验证失败 解决方案: 因为测试环境,没有使用DNS,删除resolv.con ...