我的第一篇IT博文:关于查找“吸血鬼数字”算法的讨论

本文介绍了一种寻找特定数学概念——吸血鬼数的有效算法。通过对比两种策略,选择了更高效的算法并进行了优化,最终实现了对不同位数吸血鬼数的快速查找。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

读本科时就对编程很感兴趣,最近在学习Java,据说学编程要写博文,那就来写写吧。

 

该问题源自《Thinking in Java》中的一道习题(找出4位数中所有的“吸血鬼”数字)。

所谓“吸血鬼”数字是这样定义的:

         1、位数为偶数;

         2、可由一对数字相乘得到,而这对数字各包含乘积的一半位数的数字,其选取的数字可采取任意顺序;

         3、以两个00结尾的数字排除。

例如:1260 = 21*60,,1827 = 21 * 87,2187 = 27 * 81。

 

算法可以采取两种策略:

1、选取一个4位数,利用其各位数字组合出两个2位数,比较这两个2位数的积与4位数是否相等;

2、选取两个2位数,计算它们的乘积,比较乘积与原来的两个2位数的组成数字是否一致。

 

大概考虑了一下,就放弃了第一种策略,不说“利用其各位数字组合出两个2位数”可能会比较复杂,单从循环执行次数来考虑的话,

采用策略1:需要执行9999-1000+1=9000次,而且由于不知道“吸血鬼”数字具备什么样的性质,只能排除以00结尾的数字共10*9=90个,其他8910数字都要做判断;

采取策略2,需要执行(99-10+1)(99-10+1)= 8100次,可以进一步优化到(89+1)*90/2=4050次,还可以排除掉二者乘积小于1000的情况以及乘积是00结尾的情况,进一步减少需要执行的次数。

没有对这两种策略进行实际测试,但网上看到有对两种策略算法的比较,认为策略2的算法好于策略1,这里就不讨论策略1的具体算法了,有兴趣的人可以自己搜一下。

策略1的算法还有一个问题是“利用其各位数字组合出两个2位数”这步操作,网上提供的对4位数的算法中是直接枚举结果的,因而难以扩展到其他情况,更一般情况的算法估计会比较复杂,所以从考虑可以查找其他位数的“吸血鬼”数字的角度也应优先考虑策略2。

 

确定了采用策略2之后,就来解决具体问题了。上代码:

	/**找出给定位数的数字中所有的吸血鬼数字
	 * @param digits int
	 * @return Set<Integer>
 	 */
	public static Set<Integer> findVampireNumbers(int digits) throws Exception {
		//按两数相乘算法,可能出现重复,6位数125460=204*615=246*510
		//避免重复应使用Set来保存结果
		if (digits % 2 != 0){
			throw new Exception("arg digits must be even!");
		}
		
		Set<Integer> vampires = new TreeSet<>();

		int min = IntHelper.minOfInt((byte)(digits/2));
		int max = IntHelper.maxOfInt((byte)(digits/2));
		int minOfProduct = IntHelper.minOfInt((byte)digits);

		label:
		for (int facter1 = min; facter1 <= max; facter1++){

			//可以不考虑facter1==facter2的情况
			for (int facter2 = max; facter2 > facter1; facter2--){
				int product = facter1 * facter2;

				if (product <= minOfProduct){
					continue label;
				}

				//添加一个新的过滤条件:(product - facter1 - facter2) % 9 != 0
				//运行时间可大大减少
				if (product % 100 == 0 || (product - facter1 - facter2) % 9 != 0){
					continue;
				}

				//此改进(改用统计个数的方法)可使运行时间大大减少
				//if (isVampire(facter1,facter2,digits)){
				//if (isVampire1(facter1,facter2)){
				if (isVampire2(facter1,facter2)){
					vampires.add(product);
				}
			}
		}

		return vampires;
	}


判断是否为“吸血鬼”数字的方法:

	public static boolean isVampire(int facter1, int facter2, int digits){
		List<Byte> factorsDigits = IntHelper.getAllDigits(facter1);
		factorsDigits.addAll(IntHelper.getAllDigits(facter2));
		List<Byte> productDigits = IntHelper.getAllDigits(facter1*facter2);
		Collections.sort(factorsDigits);
		Collections.sort(productDigits);
		int k; 
		for (k = 0; k <= digits-1; k++){
			if (factorsDigits.get(k) != productDigits.get(k)){
				break;
			}
		}

		if (k == digits){
			return true;
		}
		return false;
		
	}

下面是网上看到的算法:

	public static boolean isVampire1(int facter1, int facter2){
		//网上方法,思路与isVampire()一致,使用数组代替列表,但测试结果时间反而加倍
		String[] factorsDigits = (String.valueOf(facter1)+String.valueOf(facter2)).split("");    
		String[] productDigits = String.valueOf(facter1*facter2).split("");
		Arrays.sort(factorsDigits);
		Arrays.sort(productDigits);

		if (Arrays.equals(factorsDigits,productDigits)){
			return true;
		}
		return false;
		
	}

思路基本是一致的,网上的算法采用的String数组来实现的,我是采用List<Integer>来实现的,从我在自己的电脑上测试的情况来说,List<Integer>实现的方法要快一些。


由于一开始没注意循环条件写的有误,导致程序进入死循环,让我误以为这个算法效率低,于是开始考虑是否有更高效的方法。这里对“吸血鬼”数字的验证,是通过获得各位数字之后,先进行排序,再一一比较而得出的结果,其实我们并不关心排序,排序只是为了能够进行一一比较,那么考虑另一种思路:我们完全不必去获取各位数字,只需要统计0-9的数字的出现次数,一样可以验证结果。下面是改进后的算法:

	public static boolean isVampire2(int facter1, int facter2){
		//采用统计0-9数字出现次数的方法
		int[] facter1Digits = IntHelper.countDigits(facter1);
		int[] facter2Digits = IntHelper.countDigits(facter2);
		int[] productDigits = IntHelper.countDigits(facter1 * facter2);
		for (int i = 0; i <= facter1Digits.length-1; i++){
			facter1Digits[i] += facter2Digits[i]; 
		}
		if(Arrays.equals(facter1Digits, productDigits)){
			return true;
		}
		return false;
		
	}

经本地测试,该算法的速度更快,应该是一个比较不错的算法了,暂时就考虑到这里了。

这是我的辅助类:

class IntHelper {
	
	/**输出给定位数的整数的最小值
	 * 处理范围为1-Integer.MAX_VALUE(2147483647)
	 * 超出范围返回-1
	 * @param digit byte value of 1-10
	 */
	public static int minOfInt(byte digit){
		if (digit < 1 || digit > 10){
			return -1;	//结果超出int范围
		}
		int min = 1;
		while (--digit > 0){
			min *= 10;
		}
		return min;	
	}

	/**输出给定位数的整数的最大值
	 * 处理范围为1-Integer.MAX_VALUE(2147483647)
	 * 超出范围返回-1
	 * @param digit byte value of 1-9
	 */
	public static int maxOfInt(byte digit){
		if (digit < 1 || digit > 9){
			return -1;	//结果超出int范围
		}
		int max = 9;
		while (--digit > 0){
			max = max * 10 + 9;
		}
		return max;
	}

	/**输出给定数字的各位数字
	 * 给定数字小于等于0时返回空List
	 * @param num int
	 * @return List<Byte>
	 */
	public static List<Byte> getAllDigits(int num){
		List<Byte> digits = new ArrayList<>();
		while (num > 0){
			digits.add((byte)(num % 10));
			num /= 10;
		}
		return digits;
	}

	/**输出给定数字的各位数字出现次数的统计结果
	 * 给定数字小于等于0时返回全为0的数组
	 * @param num int
	 * @return int[] 
	 */
	public static int[] countDigits(int num){
		int[] digits = new int[10];
		Arrays.fill(digits,0);
		while (num > 0){
			digits[num % 10]++;
			num /= 10;
		}
		return digits;
	}


本地测试比较结果:

运行时间msdigits=4digits=6digits=8
采用isVampire()15-3278-945421-5860
采用isVampire1()16-32188-20315672-16156
采用isVampire2()0-1631-471610-2031

关于此问题其他一些小点:

1、从网上看到的,对需要判断的数字进行筛除的另一个条件:(product - facter1 - facter2) % 9 != 0。

原因:若product为一个4位“吸血鬼”数字,则product可写成1000a+100b+10c+d,而facter1+facter2可写成xa+yb+wc+zd(其中x,y,w,z为10或1),则product -facter1 - facter2=(1000-x)a+(100-y)b+(10-w)c+(1-z)d,(1000-x)为999或990,(100-y)为99或90,(10-w)为9或0,(1-z)为0或-9,均可被9整除。

添加该筛除条件后,算法执行时间有明显减少,digits=4,6,8时采用isVampire2()不添加该筛除条件时的运行时间分别为0-16,125-141,12969-13547ms。

2、也是网上看到的,在执行二重循环时,facter2可以从facter1+1开始,不必从facter1开始,因为平方数不可能是“吸血鬼”数字。

3、采用策略2的算法存在的一个问题,会出现重复的结果,在6位数中存在一个:125460=204*615=246*510,开始时使用List来保存结果就出现问题了(查找6位“吸血鬼”数字时,总数为142,实际应为141),后来换成TreeSet(结果可以按序输出),执行起来会比List慢,未验证使用List获得结果后再剔除重复值的方法是否会比TreeSet快,考虑到虚拟机对TreeSet可能有内部优化,应该还是使用TreeSet好一些,而且Set本就是设计用来保存不重复值的,除非确定不可能出现重复值。

4、在对乘积太小、位数不符合要求的情况(如10*11=110 < 1000)做筛除时,我采用的是if条件判断,然后continue label跳到外层循环,网上的方法是通过

int from = Math.max( 1000 / facter1, facter1 + 1);
int to = Math.min( 1000*10 / facter1, 99);

提前设定facter2的范围来实现的,运行效果差别不大。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值