导语
每篇将有两道经典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时:
- result中包含“2”,则必须进行删除操作,删除次数为至少(string.length() - 20)次;
- result中包含“3”,则必须进行添加操作,添加次数至少1次;
- result中包含“4”,则必须进行添加操作,添加次数至少1次;
- result中包含“5”,则必须进行添加操作,添加次数至少1次;
- 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时:
- result中包含“3”,则必须进行添加操作,添加次数至少1次;
- result中包含“4”,则必须进行添加操作,添加次数至少1次;
- result中包含“5”,则必须进行添加操作,添加次数至少1次;
- result中包含“2”,是所有情况中,最复杂的一种,需要做预处理(下面会解释什么是预处理),即要优先处理;
- 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:
2 3 4 5 6 7 8 9 10 11 12 … n-1
n false false false false false false false false false false false … false false 然后,从2开始往后,划掉2的倍数,被划掉的数标为true:
2 3 45 67 89 1011 12… n-1 n false false true false true false true false true false true … false false 再从下一个标记不为true的数开始往后,这里是数字3,划掉3的倍数被划掉的数标为true:
依次类推,直到n,最后剩下标记为false的数字就是质数。
2 3 45 67 891011 12… n-1 n false false true false true false true true true false true … false 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题的确难度耗费了博主大量精力外,博主还有其他各种事情耽误了,实在抽不开一个较为集中的时间来写。还请见谅。)