Java 编程问题:一、字符串、数字和数学

简介: Java 编程问题:一、字符串、数字和数学

本章包括 39 个涉及字符串、数字和数学运算的问题。我们将从研究字符串的一系列经典问题开始,例如计算重复项、反转字符串和删除空格。然后,我们将研究专门用于数字和数学运算的问题,例如两个大数求和和和运算溢出,比较两个无符号数,以及计算除法和模的下限。每个问题都要经过几个解决方案,包括 Java8 的函数风格。此外,我们将讨论与 JDK9、10、11 和 12 有关的问题。


在本章结束时,您将知道如何使用一系列技术,以便您可以操纵字符串并应用、调整它们以适应许多其他问题。你还将知道如何解决可能导致奇怪和不可预测的结果的数学角落的情况。



问题


使用以下问题来测试您的字符串操作和数学角大小写编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:


  1. 重复字符计数:编写一个程序,对给定字符串中的重复字符进行计数。


  1. 寻找第一个非重复字符:编写一个程序,返回给定字符串中的第一个非重复字符。


  1. 反转字母和单词:编写一个反转每个单词字母的程序,以及一个反转每个单词字母和单词本身的程序。


  1. 检查字符串是否只包含数字:编写一个程序检查给定字符串是否只包含数字。


  1. 计数元音和辅音:编写一个程序,计算给定字符串中元音和辅音的数量。对于英语,有五个元音(a、e、i、o 和 u)。


  1. 计数某个字符的出现次数:编写一个程序,对给定字符串中某个字符的出现次数进行计数。


  1. 将String转换成int、long、float或double:编写一个程序,将给定的String对象(代表数字)转换成int、long、float或double。


  1. 删除字符串中的空格:编写一个程序,删除给定字符串中的所有空格。


  1. 用一个分隔符连接多个字符串:编写一个程序,用给定的分隔符连接给定的字符串。


  1. 生成所有排列:编写一个程序,生成给定字符串的所有排列。


  1. 检查字符串是否为回文:编写一个程序,确定给定的字符串是否为回文。


  1. 删除重复字符:编写一个程序,从给定字符串中删除重复字符。


  1. 删除给定字符:编写一个从给定字符串中删除给定字符的程序。


  1. 查找出现次数最多的字符:编写一个程序,在给定的字符串中查找出现次数最多的字符。


  1. 按长度排序字符串数组:编写按给定字符串数组长度排序的程序。


  1. 检查字符串是否包含子字符串:编写程序检查给定字符串是否包含给定子字符串。


  1. 计算子串在字符串中出现的次数:编写一个程序,计算给定字符串在另一个给定字符串中出现的次数。


  1. 检查两个字符串是否是:编写一个检查两个字符串是否是异序词的程序。假设一个字符串的一个异序词是这个字符串的一个排列,忽略了大小写和空格。


  1. 声明多行字符串(文本块):编写声明多行字符串或文本块的程序。


  1. 连接同一字符串n次:编写一个程序,将同一字符串连接给定次数。


  1. 删除前导和尾随空格:编写一个程序,删除给定字符串的前导和尾随空格。


  1. 查找最长公共前缀:编写一个程序,查找给定字符串的最长公共前缀。


  1. 应用缩进:编写几个代码片段,对给定的文本应用缩进。


  1. 转换字符串:写几段代码将一个字符串转换成另一个字符串。


  1. 计算两个数的最小值和最大值:编写一个程序,返回两个数的最小值和最大值。


  1. 两个大int/long数的求和运算溢出:编写一个程序,对两个大int/long数求和,运算溢出时抛出算术异常。


  1. 作为基数的字符串中的无符号数:编写一个程序,将给定字符串解析为给定基数中的无符号数(int或long)。


  1. 无符号数字的转换:编写一个程序,将给定的int数字无符号转换成long。


  1. 比较两个无符号数字:编写一个程序,将给定的两个数字作为无符号数字进行比较。


  1. 无符号值的除法和模:编写一个程序,计算给定无符号值的除法和模。


  1. double/float是否是有限浮点值:编写一个程序来确定给定的double/float值是否是一个有限浮点值。


  1. 对两个布尔表达式应用逻辑 AND/OR/XOR:编写一个程序,对两个布尔表达式应用逻辑 AND/OR/XOR。


  1. 将BigInteger转换成原始类型:编写程序,从给定的BigInteger中提取原始类型值。


  1. 将long转换成int:编写一个将long转换成int的程序。


  1. 计算下限除法和模:编写程序,计算给定除法(x)和除法(y)的下限除法和下限模。


  1. 下一个浮点值:编写一个程序,返回给定的float/double值在正负无穷方向上相邻的下一个浮点值。


  1. 两个大int/long值相乘运算溢出:编写一个程序,将两个大int/long值相乘,运算溢出时抛出算术异常。


  1. 融合乘加(FMA):编写一个程序,取三个float/double值(a、b、c),高效计算ab+c*。


  1. 紧凑数字格式化:编写一个程序,将数字 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()。



相关文章
|
2天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
3天前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
6天前
|
Java 开发者
【Java编程新纪元】JDK 22:超级构造函数来袭,super(...) 前导语句改写编程规则!
【9月更文挑战第6天】JDK 22的超级构造函数特性是Java编程语言发展史上的一个重要里程碑。它不仅简化了代码编写,还提升了代码的可读性和维护性。我们有理由相信,在未来的Java版本中,还将有更多令人兴奋的新特性等待我们去发现和应用。让我们共同期待Java编程新纪元的到来!
|
6天前
|
Oracle Java 关系型数据库
【颠覆性升级】JDK 22:超级构造器与区域锁,重塑Java编程的两大基石!
【9月更文挑战第6天】JDK 22的发布标志着Java编程语言在性能和灵活性方面迈出了重要的一步。超级构造器和区域锁这两大基石的引入,不仅简化了代码设计,提高了开发效率,还优化了垃圾收集器的性能,降低了应用延迟。这些改进不仅展示了Oracle在Java生态系统中的持续改进和创新精神,也为广大Java开发者提供了更多的可能性和便利。我们有理由相信,在未来的Java编程中,这些新特性将发挥越来越重要的作用,推动Java技术不断向前发展。
|
3天前
|
算法 Oracle Java
Java字符串拼接技术演进及阿里巴巴的贡献
本文主要讲述了Java字符串拼接技术的演进历程,以及阿里巴巴贡献的最新实现 PR 20273。
|
3天前
|
安全 Java 数据安全/隐私保护
- 代码加密混淆工具-Java 编程安全性
在Java编程领域,保护代码安全与知识产权至关重要。本文探讨了代码加密混淆工具的重要性,并介绍了五款流行工具:ProGuard、DexGuard、Jscrambler、DashO 和 Ipa Guard。这些工具通过压缩、优化、混淆和加密等手段,提升代码安全性,保护知识产权。ProGuard 是开源工具,用于压缩和混淆Java代码;DexGuard 专为Android应用程序设计,提供强大加密功能;Jscrambler 基于云,保护Web和移动应用的JavaScript及HTML5代码;DashO 支持多种Java平台和
16 1
|
3天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
5天前
|
Java 开发者
Java中的多线程编程基础与实战
【9月更文挑战第6天】本文将通过深入浅出的方式,带领读者了解并掌握Java中的多线程编程。我们将从基础概念出发,逐步深入到代码实践,最后探讨多线程在实际应用中的优势和注意事项。无论你是初学者还是有一定经验的开发者,这篇文章都能让你对Java多线程有更全面的认识。
14 1
|
2天前
|
安全 Java UED
Java并发编程:解锁多线程的潜力
在Java的世界里,并发编程如同一场精心编排的交响乐,每个线程扮演着不同的乐手,共同奏响性能与效率的和声。本文将引导你走进Java并发编程的大门,探索如何在多核处理器上优雅地舞动多线程,从而提升应用的性能和响应性。我们将从基础概念出发,逐步深入到高级技巧,让你的代码在并行处理的海洋中乘风破浪。
|
2天前
|
Java 程序员
Java编程中的对象和类: 初学者指南
【9月更文挑战第9天】在Java的世界中,对象和类构成了编程的基石。本文将引导你理解这两个概念的本质,并展示如何通过它们来构建你的程序。我们将一起探索类的定义,对象的创建,以及它们如何互动。准备好了吗?让我们开始这段Java的旅程吧!