Java 100道经典机试笔试题(06)——附可运行代码

本文深入解析了Java中实现安全密码规范检查的算法,包括密码强度验证和最小修改次数计算,以及使用Sieve of Eratosthenes算法高效求解指定范围内质数数量的方法。

导语

每篇将有两道经典Java机试题,每道题后面均为大家附上代码,每一道题目力求:

  • 能够在JDK11环境下编译
  • 在Eclipse JavaIDE中运行通过
  • 思路易想易懂易学
  • 重点代码有注释

第011题    安全密码规范(难度:★★★★☆)

题目描述:

小明在卡中心工作,用到的很多系统账号都需要设置安全密码。密码如果符合以下规范可以称为安全密码:
1、密码至少包含6个字符,至多包含20个字符;

2、至少包含一个小写字母,至少包含一个大写字母,至少包含一个数字;

3、不能出现连续3个及以上相同的字符。

请写一个检查密码是否为安全密码的函数。

输入为一个字符串作为密码,输出为将该密码改为安全密码的最小改变次数。如果它已经是安全密码,则返回0。

备注:插入、删除、或者替换一个字符,均视为改变一次。

输入示例:

aB3ab3

654321##############

6$$$$$$$$$$54321##############

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

aAAA...BBAAA#$%^!&AAAAAAa

输出示例:

0

4

14

16

6

思路

这道题难度还是有的。

首先要明白难点在哪,密码修改有三种方式:插入、删除、替换,题目要求将输入的密码改为安全密码的最小改变次数,而难点就在“最小”二字。有些密码可能只需要插入,有些密码只需要删除,有些密码只需要替换。但是更多的情况时,三种改密码的方式随机使用,且使用的种类还不是单一方式,这就在求“最小”次数时带来了麻烦。举个例子:AAAAA,这个密码不是规范密码,在于:①长度不够(长度只有5);②没有小写字母;③没有数字;④出现连续5个相同的字符。最容易想到的方式是:首先添加一个字符,凑足长度(AAAAA#),1次;再添加一个小写字母和一个数字(AAAAA#s2),2次;最后由于不能出现连续相同的字母3次及以上,所以要删除3个A(AA#s2),3次;但是此时,长度又不够了,所以再添加一个字符(AA#S2$)1次,一共7次。但是显然这不是“最小”次数。 

进一步的想法:在上述的操作步骤中,有些步骤其实是“一箭双雕”,比如在添加字母和数字的时候,它就本身起到了增长的作用,即在满足②③时,会有可能同时满足了①;其次,对于AAAAA这种重复的字符处理,直接删除,不如直接替换中间的A所用的次数少,即把AAAAA改成AA#AA;所以这样:添加一个小写字母和一个数字(AAAAAs2),2次;再替换成AA#AAs2,1次,一共3次。大大降低了修改次数。

那么,3次是否是“最小”呢?我们再进一步想,在将AAAAA修改成AA#AA时,我们不如直接将其换成AA2AA(或者AAsAA),消除了③和④(消除了要求②和④)然后,再增加一个小写字母AA2AAs(或一个数字AAsAA2),消除了①和②(消除了①和③),这样2次就可以把不规范密码修改成了规范密码。

那么2次是否是“最小”呢?我们思索一番发现,由于改密码中没有小写字母和数字,所以必须要添加上,这就已经至少要2次了,所以2次就是“最小”的次数。


要判断是否为规范密码已经如何修改使其变成规范密码,前提是要首先判断出改密码不符合规范密码中的哪些要求,于是我们可以写出如下规则:某一用户输入的密码字符串中:

  • 规则1:长度小于6;
  • 规则2:长度大于20;
  • 规则3:至少有一个数字;
  • 规则4:至少包含一个小写字母;
  • 规则5:至少包含一个大写字母;
  • 规则6:出现连续三个及以上相同的字符;

以上规则,我们将其写在isFit(String string, String result) 函数中,调用即可判断。其中,string是传入的密码字符串,result是判断的结果。我采取的方式是,将判断结果返回为一个字符串result,每有一种规则不满足,就在result中添加不满足的规则序号,初始为空:(规则判断采用的是Java正则表达式判断)

String isFit(String string, String result) {
	String regex1 = ".{0,5}";                  //少于6个字符 ('+')
	String regex2 = ".{21,}";                  //多于20个字符('-')
	String regex3 = ".*[0-9]+.*";              //至少包含一个数字('+')
	String regex4 = ".*[a-z]+.*";              //至少包含一个小写字母('+')
	String regex5 = ".*[A-Z]+.*";              //至少包含一个大写字母('+')
	String regex6 = ".*(.)\\1{2,}.*";          //出现连续三个及以上相同的字符

	if(string.matches(regex1)) result += "1";  //满足规则1
	if(string.matches(regex2)) result += "2";  //满足规则2
	if(!string.matches(regex3)) result += "3"; //不满足规则3
	if(!string.matches(regex4)) result += "4"; //不满足规则4
	if(!string.matches(regex5)) result += "5"; //不满足规则5
	if(string.matches(regex6)) result += "6";  //满足规则6

	return result;
}

通过以上分析,我们还发现,有些步骤不能省略,必须要单独增加一次操作。而有些步骤在进行操作时,可以同时满足很多要求。要求最小次数,不会在那些“必须”步骤中节省次数,而是在这些“一箭N雕”的情况中节省次数。于是我们可以得出以下结论:

当result中不包含6时:

  1. result中包含“2”,则必须进行删除操作,删除次数为至少(string.length() - 20)次;
  2. result中包含“3”,则必须进行添加操作,添加次数至少1次;
  3. result中包含“4”,则必须进行添加操作,添加次数至少1次;
  4. result中包含“5”,则必须进行添加操作,添加次数至少1次;
  5. result中包含“1”,需分三种情况:如果原始字符串规则3、4、5都不符合,只有原始字符串长度小于3时,才需再次增加次数;如果原始字符串regex3、4、5中其中两条不符合,只有原始字符串长度小于4时,才需再次增加次数;如果原始字符串regex3、4、5中只有一条不符合,只有原始字符串长度小于5时,才需再次增加次数。

(这里举个例子就很容易明白:比如②:5##,它的result是145,因为有45,两条不符合,所以根据第3第4条结论,已经添加了一个小写字母和大写字母,是必须增加的次数,此时要考虑,增加之后,其长度是否达到了6,所以要分以上情况。增加后(5##aD)由于其原始长度小于4,所以还需再增加次数,因为要次数最少,所以增加的次数为(6 - 增加后密码的长度)。)

//符合regex1的,且不符合regex6的
/*
String com1 = ".*345.*";             //三种都不含
String com2 = ".*34.*|.*35.*|.*45.*";//只有一种
String com3 = ".*3.*|.*4.*|.*5.*";   //有两种
*/
if(result.contains("1") && !result.contains("6")) {
	//如果原始字符串regex3、4、5都不符合
	if(result.matches(com1)) {
		//只有原始字符串长度小于3时,才需再次增加次数
		if(s.length() < 3) { 
			count = count + 3 - s.length();
		}
	}
	//如果原始字符串regex3、4、5中其中两条不符合
	else if(result.matches(com2)) {
	//只有原始字符串长度小于4时,才需再次增加次数
	if(s.length() < 4) {
	    count = count + 4 - s.length();
		}
	}
	//如果原始字符串regex3、4、5中只有一条不符合
	else if(result.matches(com3)){
		//只有原始字符串长度小于5时,才需再次增加次数
		if(s.length() < 5) {
			count = count + 5 - s.length();
		}
	}
}

当result中包含6时:

  1. result中包含“3”,则必须进行添加操作,添加次数至少1次;
  2. result中包含“4”,则必须进行添加操作,添加次数至少1次;
  3. result中包含“5”,则必须进行添加操作,添加次数至少1次;
  4. result中包含“2”,是所有情况中,最复杂的一种,需要做预处理(下面会解释什么是预处理),即要优先处理
  5. result中包含“1”,此时情况较少,只有AAA、AAAA、AAAAA、AAA#、AAA##几种情况,但是它们会在预处理中就会变成上述其它情况,所以无需考虑。

预处理

预处理是比较重要的一步,对应程序代码中的String DeleteMinRepeatString(String s)函数,它是用来处理result中既包含6,又包含2的情况。

//当regex2和regex6同时满足时,从连续长度最小的字符中删除一个
static String DeleteMinRepeatString(String s) {
	int len1 = s.length(), len2 = len1;
	int pos1 = 0, pos2 = 0;
	Pattern pt=Pattern.compile("(.)\\1{2,}");
	Matcher mt=pt.matcher(s);
	//找出所有连续出现3次及以上的字符串中长度最小的
	while(mt.find()){
		len2 = mt.group(0).length();//当前符合连续出现3次及以上的字符串的长度
		pos2 = mt.start();          //当前符合连续出现3次及以上的字符串第一个字符在整个字符串中的位置
		if(len1 > len2) {
			len1 = len2;
			pos1 = pos2;
		}
	}
	//删除:例如:1222333345678910121314变成122333345678910121314,因为222最短,比3333短
	s = s.substring(0, pos1) + s.substring(pos1 + 1, s.length());
	return s;
}

//下面是说明在何处调用DeleteMinRepeatString(String s)函数
/*public static void main(String[] args){
    ……
    //符合regex2且符合regex6的,这种情况要首先处理
	//直至2和6有一个条件不满足
	while(result.contains("2") && result.contains("6")){
		int len = s.length();
		//调用DeleteMinRepeatString()函数
		s = DeleteMinRepeatString(s);
		count = count + len - s.length();
		result =""; //初始化result
		result = isFit(s, result);//再次判断
	}
}
*/

解释一下上述函数的意思,拿输入示例aAAA...BBAAA#$%^!&AAAAAAa来说,出现连续三个及以上的字符串有AAA、...、AAA、AAAAAA,现要找出最短的一个,这里有三组长度为3的,比如AAA,那么就将其删除一个变成AA。通过反复调用,使得字符串的result只包含2和6中的其中一个。

原因:对于一个长度超过20的密码,必须要进行删除操作,而在长度超过20的密码中出现连续相同的字符,最少的次数就是优先删除重复的字符,这样可以尽可能的同时满足既减少长度,又会使连续的字符个数减少。至于为什么选择长度最短的,原因也很简单,一开始讨论过,AAAAAAA修改成符合要求的方式有2种:删掉5个A(AA)或者替换掉两个A(AA#AA#A),显然是后者次数少,而后者能够实现的前提是该密码长度已经符合6-20了,否则,替换之后还要删除或者添加去满足长度要求,徒劳而已;但是短的就不一样了,比如AAA,删(AA),改(AA#),效果都是一样的,都只使用了一次修改,在长度超过20的情况下,当然优先使用删除,会有一箭双雕的效果。 

对result包含2和6的密码,要优先调用函数处理成包含2或6的情况:

  • 包含2,不包含6:上面分类讨论时已提到;
  • 包含6,不包含2:与上面包含6和1的情况类似:如果原始字符串regex3、4、5都不符合,只有countRepeat大于3时,才需再次增加次数;如果原始字符串regex3、4、5中其中两条不符合,只有countRepeat大于2时,才需再次增加次数;如果原始字符串regex3、4、5中只有一条不符合,只有countRepeat大于1时,才需再次增加次数;如果原始字符串regex3、4、5都符合,则直接增加countRepeat次只不过此时变成了countRepeat。
//符合regex6且不符合regex2的情况下(不论符不符合regex1)
if(result.contains("6") && !result.contains("2")) {
	//如果原始字符串regex3、4、5都不符合
	if(result.matches(com1)) {
		//只有countRepeat大于3时,才需再次增加次数
		if(countRepeat > 3) { 
			count = count + countRepeat - 3;
		}
	}
	//如果原始字符串regex3、4、5中其中两条不符合
	else if(result.matches(com2)) {
		//只有countRepeat大于2时,才需再次增加次数
		if(countRepeat  > 2) {
			count = count + countRepeat - 2;
		}
	}
	//如果原始字符串regex3、4、5中只有一条不符合
	else if(result.matches(com3)){
		//只有countRepeat大于1时,才需再次增加次数
		if(countRepeat > 1) {
			count = count + countRepeat - 1;
		}
	}
	//如果原始字符串regex3、4、5都符合,则直接增加countRepeat次
	else {
		count = count + countRepeat;
	}
}

这里的countRepeat的作用是计数器,计算字符串中需要去掉的重复字符个数:AAAAAAAAAAAA,所以此时countRepeat = 4。有关countRepeat计算的代码见下:

//计算处理过后的字符串中,连续重复三个以上的需进行修改删除的字符个数
for (int i = 1; i < s.length() - 1; i++) {
	if(s.charAt(i) == s.charAt(i - 1) && s.charAt(i) == s.charAt(i + 1)) {
		countRepeat ++;
		i += 2;
	}
}

我已经尽最大努力去阐述我的思考过程,如有没看明白的,或者你有更好的解题思路,欢迎在下方评论与我联系,我看到后会及时回复。(我给出的几个输入示例,很具有代表性,是我在解题过程中不断修正我思路的例子,大家可以反复研究这几个输入示例)

 代码

import java.util.Scanner;
import java.util.regex.*;
 
public class Test{
	//判断是否符合规范
	static String isFit(String string, String result) {
		String regex1 = ".{0,5}";                  //少于6个字符 ('+')
		String regex2 = ".{21,}";                  //多于20个字符('-')
		String regex3 = ".*[0-9]+.*";              //至少包含一个数字('+')
		String regex4 = ".*[a-z]+.*";              //至少包含一个小写字母('+')
		String regex5 = ".*[A-Z]+.*";              //至少包含一个大写字母('+')
		String regex6 = ".*(.)\\1{2,}.*";          //出现连续三个及以上相同的字符(36/46/56:1;346/356/456:2

		if(string.matches(regex1)) {
			result += "1";
		}
		if(string.matches(regex2)) {
			result += "2";
		}
		if(!string.matches(regex3)) {
			result += "3";
		}
		if(!string.matches(regex4)) {
			result += "4";
		}
		if(!string.matches(regex5)) {
			result += "5";
		}
		if(string.matches(regex6)) {
			result += "6";
		}
		return result;
	}
	//当regex2和regex6同时满足时,从连续长度最小的字符中删除一个
	static String DeleteMinRepeatString(String s) {
		int len1 = s.length(), len2 = len1;
		int pos1 = 0, pos2 = 0;
		Pattern pt=Pattern.compile("(.)\\1{2,}");
		Matcher mt=pt.matcher(s);
		//找出所有连续出现3次及以上的字符串中长度最小的
		while(mt.find()){
			len2 = mt.group(0).length(); //当前符合连续出现3次及以上的字符串的长度
			pos2 = mt.start();           //当前符合连续出现3次及以上的字符串第一个字符在整个字符串中的位置
			if(len1 > len2) {
				len1 = len2;
				pos1 = pos2;
			}
		}
		//删除:例如:1222333345678910121314变成122333345678910121314,因为222最短,比3333短
		s = s.substring(0, pos1) + s.substring(pos1 + 1, s.length());
		return s;
	}
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		String s = input.next();
		String result = "";
		int count = 0;
		result = isFit(s, result);
		
		String com1 = ".*345.*";     //三种都不含
		String com2 = ".*34.*|.*35.*|.*45.*";//只有一种
		String com3 = ".*3.*|.*4.*|.*5.*";   //有两种
		int countRepeat = 0;
		
		//符合regex2且符合regex6的,这种情况要首先处理
		//直至2和6有一个条件不满足
		while(result.contains("2") && result.contains("6")){
			int len = s.length();
			//调用DeleteMinRepeatString()函数
			s = DeleteMinRepeatString(s);
			count = count + len - s.length();
			result =""; //初始化result
			result = isFit(s, result);//再次判断
		}
		//计算处理过后的字符串中,连续重复三个以上的需进行修改删除的字符个数
		for (int i = 1; i < s.length() - 1; i++) {
			if(s.charAt(i) == s.charAt(i - 1) && s.charAt(i) == s.charAt(i + 1)) {
				countRepeat ++;
				i += 2;
			}
		}
		//不符合regex3的,至少要添加一个
		if(result.contains("3")) {
			count++;
		}
		//不符合regex4的,至少要添加一个
		if(result.contains("4")) {
			count++;
		}
		//不符合regex5的,至少要添加一个
		if(result.contains("5")) {
			count++;
		}
		//符合regex2且不符合regex6的,直接删除超过长度的个数
		if(result.contains("2") && !result.contains("6")) {
			count = count + s.length() - 20;
		}
		//符合regex6且不符合regex2的情况下(不论符不符合regex1)
		if(result.contains("6") && !result.contains("2")) {
			//如果原始字符串regex3、4、5都不符合
			if(result.matches(com1)) {
				//只有countRepeat大于3时,才需再次增加次数
				if(countRepeat > 3) { 
					count = count + countRepeat - 3;
				}
			}
			//如果原始字符串regex3、4、5中其中两条不符合
			else if(result.matches(com2)) {
				//只有countRepeat大于2时,才需再次增加次数
				if(countRepeat  > 2) {
					count = count + countRepeat - 2;
				}
			}
			//如果原始字符串regex3、4、5中只有一条不符合
			else if(result.matches(com3)){
				//只有countRepeat大于1时,才需再次增加次数
				if(countRepeat > 1) {
					count = count + countRepeat - 1;
				}
			}
			//如果原始字符串regex3、4、5都符合,则直接增加countRepeat次
			else {
				count = count + countRepeat;
			}
		}
		//符合regex1的,且不符合regex6的
		if(result.contains("1") && !result.contains("6")) {
			//如果原始字符串regex3、4、5都不符合
			if(result.matches(com1)) {
				//只有原始字符串长度小于3时,才需再次增加次数
				if(s.length() < 3) { 
					count = count + 3 - s.length();
				}
			}
			//如果原始字符串regex3、4、5中其中两条不符合
			else if(result.matches(com2)) {
				//只有原始字符串长度小于4时,才需再次增加次数
				if(s.length() < 4) {
					count = count + 4 - s.length();
				}
			}
			//如果原始字符串regex3、4、5中只有一条不符合
			else if(result.matches(com3)){
				//只有原始字符串长度小于5时,才需再次增加次数
				if(s.length() < 5) {
					count = count + 5 - s.length();
				}
			}
		}	
		System.out.println(count);
	}
}

运行结果

        

      

值得注意的是,最后一个测试结果:“aAAA...BBAAA#$%^!&AAAAAAa”把它稍稍调换一下顺序“aAAAAAA...BBAAA#$%^!&AAAa”,如果程序正确,其结果应该是一样的:


第012题    求解质数(难度:★★☆☆☆)

题目描述:

小南瓜上课时学到了质数这一节,现在小南瓜想知道:当他输入一个数字时,比它小的质数有几个。要求输入一个整数N,输出比它小的质数的个数。并要求算法时间复杂度尽可能低。

输入示例:

10

输出示例:

4(分别是2,3,5,7)

思路

如果题目不要求时间性能,那么第一个想到的方法是:将某个数n分别与2~n- 1相除,只余数均不为零,则是质数。但显然,这种性能及其低下。于是进一步想法,无需2~n-1,只需2~n/2,这样最起码效率要高一些。但是感觉时间还是不够高效。下面介绍一种比较高效的方法,思路一旦清楚,程序很简单。 

首先将2~n都列出来,并赋予一个默认标记false

23456789101112

n-1

n
falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse

然后,从2开始往后,划掉2的倍数,被划掉的数标为true

23456789101112n-1n
falsefalsetruefalsetruefalsetruefalsetruefalsetruefalsefalse

再从下一个标记不为true的数开始往后,这里是数字3,划掉3的倍数被划掉的数标为true

23456789101112n-1n
falsefalsetruefalsetruefalsetruetruetruefalsetruefalsefalse
依次类推,直到n,最后剩下标记为false的数字就是质数。

代码

import java.util.Scanner;

public class Test{
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		int n = input.nextInt();
		//开辟一个大小为n的布尔数组
		boolean prime [] = new boolean[n];
		//从2开始到n
		for (int i = 2; i < n; i++) {
			//如果该数标记为false,则将该数的倍数都标为true
			if(prime[i] == false) {
				int j = 2 * i;
				while(j < n) {
					prime[j] = true;
					j = j + i;
				}
			}
		}
		//找出标记为false的数,它们都是质数(0和1不是质数)
		int count = 0;
		for (int i = 2; i < n; i++) {
			if(prime[i] == false) {
				count++;
			}
		}
		//输出计数结果
		System.out.println(count);
	}
}

运行结果


以上是本次两道Java机试题

如有不足,欢迎批评指正

欢迎阅读上一篇:Java 100道典型机试笔试题(05)

欢迎阅读下一篇:Java 100道典型机试笔试题(07)


作者:小南瓜

日期:2019年4月17日11:06

完成日期:2019年7月19日23:04

(中间之所以有较大的跨度除了第11题的确难度耗费了博主大量精力外,博主还有其他各种事情耽误了,实在抽不开一个较为集中的时间来写。还请见谅。)

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值