算法:求比指定数大且最小的“不重复数”问题的高效实现
问题:
给定任意一个正整数,求比这个数大且最小的“不重复数”,“不重复数”的含义是相邻两位不相同,例如1101是重复数,而1201是不重复数。
——引自 百度2014校招笔试题目题解
问题的提法:
为代码简便,将问题等价地改为,求大于等于指定正整数的不重复数。由find()函数实现。
调用:
find( i + 1u )
原型:
unsigned find( unsigned );
算法:
以19922884u为例。
首先确定高位是否是重复数。即依次判断
1u
19u
199u
1992u
19922u
199228u
1992288u
是否是重复数。
如高位不是重复数,则当前数不变,并判断当前数是否是重复数。
例如对19u,由于1u不是重复数(一位正整数不是重复数,是显而易见的事。(if ( n < 10u ) return n;),所以判断19u是否是重复数(通过简单地判断19u的个位和十位是否相同。n % 10u == n /10u %10u)。
当当前数为199u时,高位(19u)不是重复数,当前数本身(119u)是重复数。
此时,将当前数加1,问题变为求大于等于200u的不重复数。
由于200u的高位不是重复数,而200u本身是重复数,所以经过了
n:2
n:20
之后,问题变成了求大于等于201u的不重复数。
201u不是重复数,所以回到求大于等于1992u的不重复数时,由于对于1992u来说,由于高位是重复数(返回值大于1992u/10u。 if ( n/10u <(t = find( n/10u)) )n = t * 10;),所以问题变成了求2010u的不重复数(n = t * 10;)。
2010u是不重复数,求大于等于19922u的不重复数变成了求大于等于20100u的不重复数。
但由于20100u的高位不是重复数,20100u本身是重复数(个位和十位相同),所以问题又变成了求大于等于20101u的不重复数的问题( if ( n % 10u == n /10u %10u ) return find( n + 1u );)。
重复以上过程,可得结果为20101010。
代码:
1 #include <stdio.h> 2 3 unsigned find( unsigned ); 4 5 int main( void ) 6 { 7 unsigned i ; 8 9 //测试 10 for ( i = 19922884u ; i < 19922884u + 1u ; i++ ) 11 { 12 printf ( "%u %u\n" , i , find( i + 1u ) ); 13 } 14 15 return 0; 16 } 17 18 unsigned find( unsigned n ) 19 { 20 unsigned t; 21 22 printf( "n:%u\n" , n ) ; //演示一下调用路径 23 24 if ( n < 10u ) 25 return n; 26 27 if ( n / 10u < ( t = find( n / 10u ) ) ) 28 n = t * 10u ; 29 30 if ( n % 10u == n /10u % 10u ) 31 return find( n + 1u ); 32 33 return n ; 34 }
提前回复:
飞鸟_Asuka 网友大概又会问:“有没有不用递归的算法呢?”我提前回复,有。不过目前写得还很难看,实在拿不出手。等我改好了再拿出来献丑。
非递归写法
#include <stdio.h> typedef struct { unsigned char t[ 20 ] ; //这个空间应该够了 int top ; //记录第一位的下标 } Map ; unsigned find( unsigned ); void parse( Map * , unsigned ) ; int search ( const Map * ); void add_1( Map * , const int ); void clear( Map * , const int , const int ); unsigned combi( const Map * ); int main( void ) { unsigned iu ; //测试 for ( iu = 19922884u ; iu < 19922884u + 1u ; iu++ ) { printf ( "%u %u\n" , iu , find( iu + 1u ) ); } return 0; } unsigned combi( const Map * p_m ) { unsigned nu = 0u ; int i; for ( i = p_m->top ; i >= 0 ; i -- ) { nu *= 10u ; nu += p_m->t[i] ; } return nu; } 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 parse( Map * p_m , unsigned n ) { p_m->top = -1 ; while ( n > 0u ) { p_m->t[ ++ p_m->top ] = n % 10u ; n /= 10u ; } } unsigned find( unsigned n ) { Map map ; int end = 0 , b_point ; parse( &map , n ) ; //将n分解为数字 while ( ( b_point = search ( &map ) ) > -1 )//为-1时说明不是重复数 { add_1( &map , b_point ); //重复数部分加1 clear( &map , b_point , end ); //后面改为0 end = b_point ; //确定下次循环的处理范围 } return combi( &map ); }
算法描述:
以19922884为例,
数据结构:用一数组及所使用到的最大下标表示。
map:
4 8 8 2 2 9 9 1
7
find():
从19922884的高位开始查找不重复数,记录位置
b_point = search ( &map )
b_point :5
b_point以后部分加1:add_1( &map , b_point );
map:
4 8 8 2 2 0 0 2
7
从 end到b_point-1之间的元素清零:clear( &map , b_point , end );
map:
0 0 0 0 0 0 0 2
7
记录b_point作为下次循环的end。
end:5
第二次循环,
b_point = search ( &map )
b_point :5
b_point以后部分加1:add_1( &map , b_point );
map:
0 0 0 0 0 1 0 2
7
从 end到b_point-1之间的元素清零:由于此时end为5,所以没有任何元素清零
记录b_point作为下次循环的end。
end:5
第三次循环,
b_point = search ( &map )
b_point :3
b_point以后部分加1:add_1( &map , b_point );
map:
0 0 0 1 0 1 0 2
7
从 end到b_point-1之间的元素清零:由于此时end为5,b_point 为3,所以没有任何元素清零
记录b_point作为下次循环的end。
end:3
第四次循环,
b_point = search ( &map )
b_point :1
b_point以后部分加1:add_1( &map , b_point );
map:
0 1 0 1 0 1 0 2
7
从 end到b_point-1之间的元素清零:由于此时end为3,b_point 为1,所以没有任何元素清零
记录b_point作为下次循环的end。
end:1
第五次循环,
b_point = search ( &map )
b_point :-1
循环结束。
重新合成整数:return combi( &map );
问题得解。
总结
非递归写法很难写,就这个问题而言,效率方面也不比递归方法好。