原文:Java Coding Problems
协议:CC BY-NC-SA 4.0
贡献者:飞龙
本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。
本章包括 39 一个涉及字符串、数字和数学操作的问题。我们将从计算重复项、系列经典问题,如计算重复项、经典问题开始。然后,我们将研究专门用于数字和数学操作的问题,如两个大数求和和和和操作溢出,比较两个无符号,以及计算除法和模具的下限。每个问题都有几个解决方案,包括 Java8 函数风格。另外,我们将讨论和讨论 JDK9、10、11 和 12 相关问题。
在本章的结尾,您将知道如何使用一系列技术,以便您可以操作字符串并应用它们,并调整它们以适应许多其他问题。您还将知道如何解决可能导致奇怪和不可预测结果的数学角落。
问题
使用以下问题来测试您的字符串操作和数学角编程能力。我强烈建议您在使用解决方案和下载示例程序之前尝试每个问题:
-
:编写一个计数给定字符串中重复字符的程序。
-
:编写程序,返回给定字符串中的第一个非重复字符。
-
:编写反转每个单词字母的程序,以及反转每个单词字母和单词本身的程序。
-
:编写程序检查给定字符串是否只包含数字。
-
:编写一个计算给定字符串中元音和辅音数量的程序。英语有五个元音(a、e、i、o 和 u)。
-
:编写一个程序,计算给定字符串中某个字符的出现次数。
-
:编写个程序,给定
String
对象(代表数字)转换为int
、long
、float
或double
。 -
:编写程序,删除给定字符串中的所有空间。
-
:编写程序,用给定的分隔符连接给定的字符串。
-
:编写一个程序,生成给定字符串的所有排列。
-
:编写程序,确定给定的字符串是否为回文。
-
:从给定给定字符串中删除重复字符的程序。
-
:编写一个从给定字符串中删除给定字符的程序。
-
:编写一个程序,在给定的字符串中找到最常见的字符。
-
:编写按给定字符串数组长度排序的程序。
-
:编写程序,检查给定字符串是否包含给定子字符串。
-
:编写一个程序,计算给定字符串在另一个给定字符串中的次数。
-
:编写一个程序来检查两个字符串是否是异序词。假设一个字符串的异序词是字符串的排列,忽略了大小写和空间。
-
:编写声明多行字符串或文本块的程序。
-
:编写一个程序,将相同的字符串连接到给定的次数。
-
:编写一个程序,删除给定字符串的前导和跟间。
-
:编写一个程序,找到给定字符串的最长公共前缀。
-
:编写几个代码片段,缩进给定的文本应用程序。
-
:写几段代码,将一个字符串转换成另一个字符串。
-
:编写一个程序,返回两个数的最小值和最大值。
-
:写一个程序,两个大的
int
/long
计算溢出时抛出计算异常。 -
编写程序,将给定字符串分析为给定基数中的无符号(
int
或long
)。 -
:编写个程序,给定
int
数字无符号转换成long
。 -
:编写一个程序,比较给定的两个数字作为无符号数字。
-
:编写一个,计算给定无符号值的除法和模具。
-
:编写一个程序来确定给定
double
/float
值是否为有限浮点值? -
:为两个布尔表达式应用逻辑编写一个程序 AND/OR/XOR。
-
:从给定的
BigInteger
中提取原始类型值。 -
:编写一个将
long
转换成int
的程序。 -
:编写程序,计算给定除法(
x
)和除法(y
)下限除法和下限模。 -
:编写程序并返回给定的程序
float
/double
下一个浮点值相邻于正负无限方向。 -
:写一个程序,两个大的
int
/long
值相乘,运算溢出时抛出算术异常。 -
():写一个程序,拿三个
float
/double
值(a
、b
、c
),高效计算a
b c*。 -
:编写一个程序,编写数字 1000000 格式化为 1M和 1ML(意大利)。此外,将字符串中的字符 1M 和 1MLN 分析为数字。
解决方案
以下部分介绍了上述问题的解决方案。请记住,通常没有正确的方法来解决特定的问题。此外,请记住,这里显示的解释只包括解决问题所需的最有趣和最重要的细节。您可以从此页面下载示例解决方案,查看更多详细信息并尝试程序。
1 重复字符计数
计算字符串中的字符(包括特殊字符,如#、$和%)解决方案意味着取每个字符并与其他字符进行比较。在比较过程中,通过数字计数器保持计数状态。每次找到当前字符,都会增加一个计数器。
解决这个问题有两种方法。
第一种解决方案迭代字符串,并使用Map
存储字符作为键,存储出现的次数作为值。当前字符从未添加到Map
,则将其添加为(character, 1)
。如果当前字符存在于Map
中,则只需将其出现次数增加 1,例如(character, occurrences+1)
。如下代码所示:
public Map<Character, Integer> countDuplicateCharacters(String str) {
Map<Character, Integer> result = new HashMap<>();
// or use for(char ch: str.toCharArray()) { ... }
for (int i = 0; i<str.length(); i++) {
char ch = str.charAt(i);
result.compute(ch, (k, v) -> (v == null) ? 1 : ++v);
}
return result;
}
另一个解决方案依赖于 Java8 的流特性。这个解决方案有三个步骤。前两步是将给定的字符串转换成Stream<Character>
,最后一步是对字符进行分组和计数。步骤如下:
- 对原始字符串调用
String.chars()
方法。这将返回IntStream
。这个IntStream
包含给定字符串中字符的整数表示。 - 通过
mapToObj()
方法将IntStream
转换成字符流(将整数表示转换成人性化的字符形式)。 - 最后,将字符分组(
Collectors.groupingBy()
)并计数(Collectors.counting()
)。
以下代码片段将这三个步骤粘在一个方法中:
public Map<Character, Long> countDuplicateCharacters(String str) {
Map<Character, Long> result = str.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(c -> c, Collectors.counting()));
return result;
}
Unicode 字符呢?
我们非常熟悉 ASCII 字符。我们有 0-31 之间的不可打印控制码,32-127 之间的可打印字符,128-255 之间的扩展 ASCII 码。但是 Unicode 字符呢?对于每个需要操作 Unicode 字符的问题,请考虑本节
因此,简而言之,早期的 Unicode 版本包含值小于 65535(0xFFFF)的字符。Java 使用 16 位char
数据类型表示这些字符。只要i
不超过 65535,调用charAt(i)
就可以正常工作。但随着时间的推移,Unicode 添加了更多字符,最大值达到了 1114111(0x10FFFF)。这些字符不适合 16 位,因此 UTF-32 编码方案考虑 32 位值(称为码位)。
不幸的是,Java 不支持 UTF-32!尽管如此,Unicode 还是提出了一个解决方案,仍然使用 16 位来表示这些字符。此解决方案意味着:
- 16 位高位代理:1024 个值(U+D800 到 U+DBFF)
- 16 位低位代理:1024 个值(U+DC00 到 U+DFFF)
现在,一个高代理后跟一个低代理定义了所谓的。代理项对用于表示 65536(0x10000
)和 1114111(0x10FFFF
)之间的值。因此,某些字符(称为 Unicode 补充字符)被表示为 Unicode 代理项对(一个字符(符号)适合于一对字符的空间),这些代理项对被合并到一个代码点中。Java 利用了这种表示,并通过一组方法来公开它,如codePointAt()
、codePoints()
、codePointCount()
,和offsetByCodePoints()
(请查看 Java 文档了解详细信息)。调用codePointAt()
而不是charAt()
,codePoints()
而不是chars()
等等,可以帮助我们编写覆盖 ASCII 和 Unicode 字符的解决方案。
例如,众所周知的双心符号是 Unicode 代理项对,可以表示为包含两个值的char[]
:\uD83D
和\uDC95
。此符号的码位为128149
。要从此代码点获取一个String
对象,请调用String str = String.valueOf(Character.toChars(128149))
。str
中的码点计数可以通过调用str.codePointCount(0, str.length())
来完成,即使str
长度为 2,也返回 1;调用str.codePointAt(0)
返回128149
,调用str.codePointAt(1)
返回56469
。调用Character.toChars(128149)
返回 2,因为需要两个字符来表示此代码点是 Unicode 代理项对。对于 ASCII 和 Unicode 16 位字符,它将返回 1。
因此,如果我们尝试重写第一个解决方案(迭代字符串并使用Map
将字符存储为键,将出现次数存储为值)以支持 ASCII 和 Unicode(包括代理项对),我们将获得以下代码:
public static Map<String, Integer>
countDuplicateCharacters(String str) {
Map<String, Integer> result = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
int cp = str.codePointAt(i);
String ch = String.valueOf(Character.toChars(cp));
if(Character.charCount(cp) == 2) {
// 2 means a surrogate pair
i++;
}
result.compute(ch, (k, v) -> (v == null) ? 1 : ++v);
}
return result;
}
突出显示的代码也可以编写如下:
String ch = String.valueOf(Character.toChars(str.codePointAt(i)));
if (i < str.length() - 1 && str.codePointCount(i, i + 2) == 1) {
i++;
}
最后,尝试重写 Java8 函数式解决方案以覆盖 Unicode 代理项对,可以执行以下操作:
public static Map<String, Long> countDuplicateCharacters(String str) {
Map<String, Long> result = str.codePoints()
.mapToObj(c -> String.valueOf(Character.toChars(c)))
.collect(Collectors.groupingBy(c -> c, Collectors.counting()));
return result;
}
对于第三方库支持,请考虑 Guava:Multiset<String>
。
下面的一些问题将提供包括 ASCII、16 位 Unicode 和 Unicode 代理项对的解决方案。它们是任意选择的,因此,通过依赖这些解决方案,您可以轻松地为没有提供此类解决方案的问题编写解决方案。
2 找到第一个非重复字符
找到字符串中第一个不重复的字符有不同的解决方案。主要地,这个问题可以通过字符串的一次遍历或更完整/部分的遍历来解决。
在单遍历方法中,我们填充一个数组,该数组用于存储字符串中恰好出现一次的所有字符的索引。使用此数组,只需返回包含非重复字符的最小索引:
private static final int EXTENDED_ASCII_CODES = 256;
...
public char firstNonRepeatedCharacter(String str) {
int[] flags = new int[EXTENDED_ASCII_CODES];
for (int i = 0; i < flags.length; i++) {
flags[i] = -1;
}
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (flags[ch] == -1) {
flags[ch] = i;
} else {
flags[ch] = -2;
}
}
int position = Integer.MAX_VALUE;
for (int i = 0; i < EXTENDED_ASCII_CODES; i++) {
if (flags[i] >= 0) {
position = Math.min(position, flags[i]);
}
}
return position == Integer.MAX_VALUE ?
Character.MIN_VALUE : str.charAt(position);
}
此解决方案假定字符串中的每个字符都是扩展 ASCII 表(256 个代码)的一部分。如果代码大于 256,则需要相应地增加数组的大小。只要数组大小不超过char
类型的最大值,即Character.MAX_VALUE
,即 65535,该解决方案就可以工作。另一方面,Character.MAX_CODE_POINT
返回 Unicode 码位的最大值 1114111。为了覆盖这个范围,我们需要另一个基于codePointAt()
和codePoints()
的实现
由于采用了单次遍历的方法,所以速度非常快。另一种解决方案是循环每个字符的字符串并计算出现的次数。每出现一次(重复)就会打断循环,跳到下一个字符,并重复算法。如果到达字符串的结尾,则返回当前字符作为第一个不可重复的字符。本书附带的代码中提供了此解决方案。
这里介绍的另一个解决方案依赖于LinkedHashMap
。这个 Java 映射是一个插入顺序映射(它保持了键插入到映射中的顺序),对于这个解决方案非常方便。LinkedHashMap
以字符作为键,以出现次数作为值填充。一旦LinkedHashMap
完成,它将返回值等于 1 的第一个键。由于插入顺序功能,这是字符串中第一个不可重复的字符:
public char firstNonRepeatedCharacter(String str) {
Map<Character, Integer> chars = new LinkedHashMap<>();
// or use for(char ch: str.toCharArray()) { ... }
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
chars.compute(ch, (k, v) -> (v == null) ? 1 : ++v);
}
for (Map.Entry<Character, Integer> entry: chars.entrySet()) {
if (entry.getValue() == 1) {
return entry.getKey();
}
}
return Character.MIN_VALUE;
}
在本书附带的代码中,前面提到的解决方案是用 Java8 函数式编写的。此外,支持 ASCII、16 位 Unicode 和 Unicode 代理项对的函数式解决方案如下:
public static String firstNonRepeatedCharacter(String str) {
Map<Integer, Long> chs = str.codePoints()
.mapToObj(cp -> cp)
.collect(Collectors.groupingBy(Function.identity(),
LinkedHashMap::new, Collectors.counting()));
int cp = chs.entrySet().stream()
.filter(e -> e.getValue() == 1L)
.findFirst()
.map(Map.Entry::getKey)
.orElse(Integer.valueOf(Character.MIN_VALUE));
return String.valueOf(Character.toChars(cp));
}
要更详细地理解这段代码,请考虑“Unicode 字符是什么?计数重复字符”部分的小节
3 反转字母和单词
首先,让我们只反转每个单词的字母。这个问题的解决方案可以利用StringBuilder
类。第一步包括使用空格作为分隔符(Spring.split(" ")
)将字符串拆分为单词数组。此外,我们使用相应的 ASCII 码反转每个单词,并将结果附加到StringBuilder
。首先,我们将给定的字符串按空格拆分。然后,我们循环得到的单词数组,并通过charAt()
按相反的顺序获取每个字符来反转每个单词:
private static final String WHITESPACE = " ";
...
public String reverseWords(String str) {
String[] words = str.split(WHITESPACE);
StringBuilder reversedString = new StringBuilder();
for (String word: words) {
StringBuilder reverseWord = new StringBuilder();
for (int i = word.length() - 1; i >= 0; i--) {
reverseWord.append(word.charAt(i));
}
reversedString.append(reverseWord).append(WHITESPACE);
}
return reversedString.toString();
}
在 Java8 函数样式中获得相同的结果可以如下所示:
private static final Pattern PATTERN = Pattern.compile(" +");
...
public static String reverseWords(String str) {
return PATTERN.splitAsStream(str)
.map(w -> new StringBuilder(w).reverse())
.collect(Collectors.joining(" "));
}
请注意,前面两个方法返回一个字符串,其中包含每个单词的字母,但单词本身的初始顺序相同。现在,让我们考虑另一种方法,它反转每个单词的字母和单词本身。由于内置的StringBuilder.reverse()
方法,这非常容易实现:
public String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
对于第三方库支持,请考虑 ApacheCommonsLang,StringUtils.reverse()
。
4 检查字符串是否只包含数字
这个问题的解决依赖于Character.isDigit()
或String.matches()
方法
依赖于Character.isDigit()
的解决方案是非常简单和快速地循环字符串,如果此方法返回false
,则中断循环:
public static boolean containsOnlyDigits(String str) {
for (int i = 0; i < str.length(); i++) {
if (!Character.isDigit(str.charAt(i))) {
return false;
}
}
return true;
}
在 Java8 函数式中,前面的代码可以使用anyMatch()
重写:
public static boolean containsOnlyDigits(String str) {
return !str.chars()
.anyMatch(n -> !Character.isDigit(n));
}
另一种解决方案依赖于String.matches()
。此方法返回一个boolean
值,指示此字符串是否与给定的正则表达式匹配:
public static boolean containsOnlyDigits(String str) {
return str.matches("[0-9]+");
}
请注意,Java8 函数式和基于正则表达式的解决方案通常比较慢,因此如果速度是一个要求,那么最好使用第一个使用Character.isDigit()
的解决方案。
避免通过parseInt()
或parseLong()
解决此问题。首先,捕捉NumberFormatException
并在catch
块中进行业务逻辑决策是不好的做法。其次,这些方法验证字符串是否为有效数字,而不是仅包含数字(例如,-4 有效)。 对于第三方库支持,请考虑 ApacheCommonsLang,StringUtils.isNumeric()
。
5 元音和辅音计数
以下代码适用于英语,但取决于您所涵盖的语言种类,元音和辅音的数量可能会有所不同,因此应相应调整代码。
此问题的第一个解决方案需要遍历字符串并执行以下操作:
- 我们需要检查当前字符是否是元音(这很方便,因为英语中只有五个纯元音;其他语言有更多元音,但数量仍然很小)。
- 如果当前字符不是元音,则检查它是否位于
'a'
和'z'
之间(这意味着当前字符是辅音)。
注意,最初,给定的String
对象被转换为小写。这有助于避免与大写字符进行比较。例如,仅对'a'
进行比较,而不是对'A'
和'a'
进行比较。
此解决方案的代码如下:
private static final Set<Character> allVowels
= new HashSet(Arrays.asList('a', 'e', 'i', 'o', 'u'));
public static Pair<Integer, Integer>
countVowelsAndConsonants(String str) {
str = str.toLowerCase();
int vowels = 0;
int consonants = 0;
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (allVowels.contains(ch)) {
vowels++;
} else if ((ch >= 'a' && ch <= 'z')) {
consonants++;
}
}
return Pair.of(vowels, consonants);
}
在 Java8 函数式中,可以使用chars()
和filter()
重写此代码:
private static final Set<Character> allVowels 标签:
连接器df1ecdf37nc连接器