本章包括 39 个涉及字符串、数字和数学运算的问题。我们将从研究字符串的一系列经典问题开始,例如计算重复项、反转字符串和删除空格。然后,我们将研究专门用于数字和数学运算的问题,例如两个大数求和和和运算溢出,比较两个无符号数,以及计算除法和模的下限。每个问题都要经过几个解决方案,包括 Java8 的函数风格。此外,我们将讨论与 JDK9、10、11 和 12 有关的问题。
在本章结束时,您将知道如何使用一系列技术,以便您可以操纵字符串并应用、调整它们以适应许多其他问题。你还将知道如何解决可能导致奇怪和不可预测的结果的数学角落的情况。
问题
使用以下问题来测试您的字符串操作和数学角大小写编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
- 重复字符计数:编写一个程序,对给定字符串中的重复字符进行计数。
- 寻找第一个非重复字符:编写一个程序,返回给定字符串中的第一个非重复字符。
- 反转字母和单词:编写一个反转每个单词字母的程序,以及一个反转每个单词字母和单词本身的程序。
- 检查字符串是否只包含数字:编写一个程序检查给定字符串是否只包含数字。
- 计数元音和辅音:编写一个程序,计算给定字符串中元音和辅音的数量。对于英语,有五个元音(a、e、i、o 和 u)。
- 计数某个字符的出现次数:编写一个程序,对给定字符串中某个字符的出现次数进行计数。
- 将String转换成int、long、float或double:编写一个程序,将给定的String对象(代表数字)转换成int、long、float或double。
- 删除字符串中的空格:编写一个程序,删除给定字符串中的所有空格。
- 用一个分隔符连接多个字符串:编写一个程序,用给定的分隔符连接给定的字符串。
- 生成所有排列:编写一个程序,生成给定字符串的所有排列。
- 检查字符串是否为回文:编写一个程序,确定给定的字符串是否为回文。
- 删除重复字符:编写一个程序,从给定字符串中删除重复字符。
- 删除给定字符:编写一个从给定字符串中删除给定字符的程序。
- 查找出现次数最多的字符:编写一个程序,在给定的字符串中查找出现次数最多的字符。
- 按长度排序字符串数组:编写按给定字符串数组长度排序的程序。
- 检查字符串是否包含子字符串:编写程序检查给定字符串是否包含给定子字符串。
- 计算子串在字符串中出现的次数:编写一个程序,计算给定字符串在另一个给定字符串中出现的次数。
- 检查两个字符串是否是:编写一个检查两个字符串是否是异序词的程序。假设一个字符串的一个异序词是这个字符串的一个排列,忽略了大小写和空格。
- 声明多行字符串(文本块):编写声明多行字符串或文本块的程序。
- 连接同一字符串n次:编写一个程序,将同一字符串连接给定次数。
- 删除前导和尾随空格:编写一个程序,删除给定字符串的前导和尾随空格。
- 查找最长公共前缀:编写一个程序,查找给定字符串的最长公共前缀。
- 应用缩进:编写几个代码片段,对给定的文本应用缩进。
- 转换字符串:写几段代码将一个字符串转换成另一个字符串。
- 计算两个数的最小值和最大值:编写一个程序,返回两个数的最小值和最大值。
- 两个大int/long数的求和运算溢出:编写一个程序,对两个大int/long数求和,运算溢出时抛出算术异常。
- 作为基数的字符串中的无符号数:编写一个程序,将给定字符串解析为给定基数中的无符号数(int或long)。
- 无符号数字的转换:编写一个程序,将给定的int数字无符号转换成long。
- 比较两个无符号数字:编写一个程序,将给定的两个数字作为无符号数字进行比较。
- 无符号值的除法和模:编写一个程序,计算给定无符号值的除法和模。
- double/float是否是有限浮点值:编写一个程序来确定给定的double/float值是否是一个有限浮点值。
- 对两个布尔表达式应用逻辑 AND/OR/XOR:编写一个程序,对两个布尔表达式应用逻辑 AND/OR/XOR。
- 将BigInteger转换成原始类型:编写程序,从给定的BigInteger中提取原始类型值。
- 将long转换成int:编写一个将long转换成int的程序。
- 计算下限除法和模:编写程序,计算给定除法(x)和除法(y)的下限除法和下限模。
- 下一个浮点值:编写一个程序,返回给定的float/double值在正负无穷方向上相邻的下一个浮点值。
- 两个大int/long值相乘运算溢出:编写一个程序,将两个大int/long值相乘,运算溢出时抛出算术异常。
- 融合乘加(FMA):编写一个程序,取三个float/double值(a、b、c),高效计算ab+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,最后一步是对字符进行分组和计数。步骤如下:
对原始字符串调用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
。
下面的一些问题将提供包括 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()。