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

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

11 检查字符串是否为回文


作为一个快速的提醒,回文(无论是字符串还是数字)在反转时看起来是不变的。这意味着可以从两个方向处理(读取)回文,并且将获得相同的结果(例如,单词madam是回文,而单词madam不是)。


一个易于实现的解决方案是用中间相遇的方法比较给定字符串的字母。基本上,此解决方案将第一个字符与最后一个字符进行比较,第二个字符与最后一个字符逐个进行比较,依此类推,直到到达字符串的中间。实现依赖于while语句:

public static boolean isPalindrome(String str) {
  int left = 0;
  int right = str.length() - 1;
  while (right > left) {
    if (str.charAt(left) != str.charAt(right)) {
      return false;
    }
    left++;
    right--;
  }
  return true;
}

以更简洁的方法重写上述解决方案将包括依赖于for语句而不是while语句,如下所示:

public static boolean isPalindrome(String str) {
  int n = str.length();
  for (int i = 0; i < n / 2; i++) {
    if (str.charAt(i) != str.charAt(n - i - 1)) {
      return false;
    }
  }
  return true;
}


但是这个解决方案可以简化为一行代码吗?答案是肯定的。


JavaAPI 提供了StringBuilder类,该类使用reverse()方法。顾名思义,reverse()方法返回相反的给定字符串。对于回文,给定的字符串应等于它的反向版本:

public static boolean isPalindrome(String str) {
  return str.equals(new StringBuilder(str).reverse().toString());
}


在 Java8 函数风格中,也有一行代码用于此。只需定义从 0 到给定字符串一半的IntStream,并使用noneMatch()短路终端操作和谓词,按照中间相遇的方法比较字母:

public static boolean isPalindrome(String str) {
  return IntStream.range(0, str.length() / 2)
    .noneMatch(p -> str.charAt(p) != 
      str.charAt(str.length() - p - 1));
}

现在,让我们讨论从给定字符串中删除重复字符。




12 删除重复字符


让我们从依赖于StringBuilder的这个问题的解决方案开始。解决方案主要应该循环给定字符串的字符,并构造一个包含唯一字符的新字符串(不可能简单地从给定字符串中删除字符,因为在 Java 中,字符串是不可变的)。


StringBuilder类公开了一个名为indexOf()的方法,该方法返回指定子字符串(在本例中是指定字符)第一次出现的给定字符串中的索引。因此,这个问题的一个潜在解决方案是,每次应用于当前字符的indexOf()方法返回 -1(这个负数意味着StringBuilder不包含当前字符)时,循环给定字符串的字符并将它们逐个添加到StringBuilder:


public static String removeDuplicates(String str) {
  char[] chArray = str.toCharArray(); // or, use charAt(i)
  StringBuilder sb = new StringBuilder();
  for (char ch : chArray) {
    if (sb.indexOf(String.valueOf(ch)) == -1) {
      sb.append(ch);
    }
  }
  return sb.toString();
}

下一个解决方案依赖于HashSetStringBuilder之间的协作。主要是,HashSet确保消除重复,而StringBuilder存储结果字符串。如果HashSet.add()返回true,那么我们也在StringBuilder中添加字符:

public static String removeDuplicates(String str) {
  char[] chArray = str.toCharArray();
  StringBuilder sb = new StringBuilder();
  Set<Character> chHashSet = new HashSet<>();
  for (char c: chArray) {
    if (chHashSet.add(c)) {
      sb.append(c);
    }
  }
  return sb.toString();
}


到目前为止,我们提供的解决方案使用toCharArray()方法将给定字符串转换为char[]。或者,两种解决方案也可以使用str.charAt(position)


第三种解决方案依赖于 Java8 函数式风格:


public static String removeDuplicates(String str) {
  return Arrays.asList(str.split("")).stream()
    .distinct()
    .collect(Collectors.joining());
}

首先,解决方案将给定的字符串转换为Stream<String>,其中每个条目实际上是一个字符。此外,该解决方案应用了有状态的中间操作distinct()。此操作将从流中消除重复项,因此它返回一个没有重复项的流。最后,该解决方案调用了collect()终端操作并依赖于Collectors.joining(),它只是将字符按照相遇顺序连接成一个字符串。




13 删除给定字符


依赖于 JDK 支持的解决方案可以利用String.replaceAll()方法。此方法将给定字符串中与给定正则表达式(在本例中,正则表达式是字符本身)匹配的每个子字符串(在本例中,每个字符)替换为给定的替换(在本例中,替换为空字符串,""):

public static String removeCharacter(String str, char ch) {
  return str.replaceAll(Pattern.quote(String.valueOf(ch)), "");
}

注意,正则表达式被包装在Pattern.quote()方法中。这是转义特殊字符所必需的,例如“<,(,[,{,\,^,-,=,$,!”!, |, ], }, ), ?、*、+、、和>。该方法主要返回指定字符串的文本模式字符串。


现在,让我们来看一个避免正则表达式的解决方案。这一次,解决方案依赖于StringBuilder。基本上,解决方案循环给定字符串的字符,并将每个字符与要删除的字符进行比较。每次当前字符与要删除的字符不同时,都会在StringBuilder中追加当前字符:


public static String removeCharacter(String str, char ch) {
  StringBuilder sb = new StringBuilder();
  char[] chArray = str.toCharArray();
  for (char c : chArray) {
    if (c != ch) {
      sb.append(c);
    }
  }
  return sb.toString();
}

最后,让我们关注 Java8 函数式方法。这是一个四步方法:

  1. 通过String.chars()方法将字符串转换成IntStream
  2. 过滤IntStream消除重复
  3. 将结果IntStream映射到Stream<String>
  4. 将此流中的字符串连接起来,并将它们收集为单个字符串


此解决方案的代码可以编写如下:

public static String removeCharacter(String str, char ch) {
  return str.chars()
    .filter(c -> c != ch)
    .mapToObj(c -> String.valueOf((char) c))
    .collect(Collectors.joining());
}

或者,如果我们想要移除 Unicode 代理项对,那么我们可以依赖于codePointAt()codePoints(),如下面的实现所示:

public static String removeCharacter(String str, String ch) {
   int codePoint = ch.codePointAt(0);
   return str.codePoints()
     .filter(c -> c != codePoint)
     .mapToObj(c -> String.valueOf(Character.toChars(c)))
     .collect(Collectors.joining());
 }

对于第三方库支持,请考虑 ApacheCommonsLang,StringUtils.remove()

现在,让我们来谈谈如何找到最形象的字符。




14 寻找最形象的字符


一个非常简单的解决方案依赖于HashMap。此解决方案包括三个步骤:

  1. 首先,循环给定字符串的字符,并将键值对放入HashMap,其中键是当前字符,值是当前出现的次数
  2. 其次,计算HashMap中表示最大出现次数的最大值(例如,使用Collections.max()
  3. 最后,通过循环HashMap条目集,得到出现次数最多的字符


工具方法返回Pair<Character, Integer>,其中包含出现次数最多的字符和出现次数(注意,忽略了空格)。如果你不喜欢这个额外的类,也就是说,Pair,那就依赖Map.Entry<K, V>

public static Pair<Character, Integer> maxOccurenceCharacter(
  String str) {
  Map<Character, Integer> counter = new HashMap<>();
  char[] chStr = str.toCharArray();
  for (int i = 0; i < chStr.length; i++) {
    char currentCh = chStr[i];
    if (!Character.isWhitespace(currentCh)) { // ignore spaces
      Integer noCh = counter.get(currentCh);
      if (noCh == null) {
        counter.put(currentCh, 1);
      } else {
        counter.put(currentCh, ++noCh);
      }
    }
  }
  int maxOccurrences = Collections.max(counter.values());
  char maxCharacter = Character.MIN_VALUE;
  for (Entry<Character, Integer> entry: counter.entrySet()) {
    if (entry.getValue() == maxOccurrences) {
      maxCharacter = entry.getKey();
    }
  }
  return Pair.of(maxCharacter, maxOccurrences);
}


如果使用HashMap看起来很麻烦,那么另一个解决方案(稍微快一点)就是依赖 ASCII 代码。此解决方案从 256 个索引的空数组开始(256 是扩展 ASCII 表代码的最大数目;更多信息可在“查找第一个非重复字符”部分中找到)。此外,此解决方案循环给定字符串的字符,并通过增加此数组中的相应索引来跟踪每个字符的出现次数:


private static final int EXTENDED_ASCII_CODES = 256;
...
public static Pair<Character, Integer> maxOccurenceCharacter(
  String str) {
  int maxOccurrences = -1;
  char maxCharacter = Character.MIN_VALUE;
  char[] chStr = str.toCharArray();
  int[] asciiCodes = new int[EXTENDED_ASCII_CODES];
  for (int i = 0; i < chStr.length; i++) {
    char currentCh = chStr[i];
    if (!Character.isWhitespace(currentCh)) { // ignoring space
      int code = (int) currentCh;
      asciiCodes[code]++;
      if (asciiCodes[code] > maxOccurrences) {
        maxOccurrences = asciiCodes[code];
        maxCharacter = currentCh;
      }
    }
  }
  return Pair.of(maxCharacter, maxOccurrences);
}


我们将在这里讨论的最后一个解决方案依赖于 Java8 函数式风格:

public static Pair<Character, Long> 
    maxOccurenceCharacter(String str) {
  return str.chars()
    .filter(c -> Character.isWhitespace(c) == false) // ignoring space
    .mapToObj(c -> (char) c)
    .collect(groupingBy(c -> c, counting()))
    .entrySet()
    .stream()
    .max(comparingByValue())
    .map(p -> Pair.of(p.getKey(), p.getValue()))
    .orElse(Pair.of(Character.MIN_VALUE, -1L));
}


首先,这个解决方案收集不同的字符作为Map中的键,以及它们的出现次数作为值。此外,它使用 Java8Map.Entry.comparingByValue()和max()终端操作来确定映射中具有最高值(最高出现次数)的条目。因为max()是终端操作,所以解决方案可以返回Optional<Entry<Character, Long>>,但是这个解决方案增加了一个额外的步骤,并将这个条目映射到Pair<Character, Long>。




15 按长度对字符串数组排序


排序时首先想到的是比较器的使用。


在这种情况下,解决方案应该比较字符串的长度,因此通过调用给定数组中每个字符串的String.length()来返回整数。因此,如果整数被排序(升序或降序),那么字符串将被排序。


JavaArrays类已经提供了一个sort()方法,该方法使用数组进行排序和一个比较器。在这种情况下,Comparator<String>应该完成这项工作。


在 Java7 之前,实现比较器的代码依赖于compareTo()方法。这种方法的常见用法是计算x1 - x2类型的差值,但这种计算可能导致溢出。这使得compareTo()相当乏味。从 Java7 开始,Integer.compare()就是一条路(没有溢出风险)。


下面是一个依赖于Arrays.sort()方法对给定数组排序的方法:


public static void sortArrayByLength(String[] strs, Sort direction) {
  if (direction.equals(Sort.ASC)) {
    Arrays.sort(strs, (String s1, String s2) 
      -> Integer.compare(s1.length(), s2.length()));
  } else {
    Arrays.sort(strs, (String s1, String s2) 
      -> (-1) * Integer.compare(s1.length(), s2.length()));
  }
}


原始数字类型的每个包装器都有一个compare()方法。

从 Java8 开始,Comparator接口被大量有用的方法丰富了。其中一个方法是comparingInt(),它接受一个函数,该函数从泛型类型中提取int排序键,并返回一个Comparator<T>值,将其与该排序键进行比较。另一个有用的方法是reversed(),它反转当前的Comparator值。

基于这两种方法,我们可以授权Arrays.sort()如下:


public static void sortArrayByLength(String[] strs, Sort direction) {
  if (direction.equals(Sort.ASC)) {
    Arrays.sort(strs, Comparator.comparingInt(String::length));
  } else {
    Arrays.sort(strs, 
      Comparator.comparingInt(String::length).reversed());
  }
}


比较器可以用thenComparing()方法链接。


我们在这里给出的解决方案返回void,这意味着它们对给定的数组进行排序。为了返回一个新的排序数组而不改变给定的数组,我们可以使用 Java8 函数样式,如下代码片段所示:

public static String[] sortArrayByLength(String[] strs, 
    Sort direction) {
  if (direction.equals(Sort.ASC)) {
    return Arrays.stream(strs)
      .sorted(Comparator.comparingInt(String::length))
      .toArray(String[]::new);
  } else {
    return Arrays.stream(strs)
      .sorted(Comparator.comparingInt(String::length).reversed())
      .toArray(String[]::new);
  }
}


因此,代码从给定的数组中创建一个流,通过有状态的中间操作对其进行排序,并将结果收集到另一个数组中。




16 检查字符串是否包含子字符串


一个非常简单的一行代码解决方案依赖于String.contains()方法。

此方法返回一个boolean值,指示字符串中是否存在给定的子字符串:

String text = "hello world!";
String subtext = "orl";
// pay attention that this will return true for subtext=""
boolean contains = text.contains(subtext);

或者,可以通过依赖于String.indexOf()(或String.lastIndexOf()来实现解决方案,如下所示:

public static boolean contains(String text, String subtext) {
  return text.indexOf(subtext) != -1; // or lastIndexOf()
}


另一种解决方案可以基于正则表达式实现,如下所示:

public static boolean contains(String text, String subtext) {
  return text.matches("(?i).*" + Pattern.quote(subtext) + ".*");
}

注意,正则表达式被包装在Pattern.quote()方法中。这是转义特殊字符(如“<”)([{^-=$)所必需的!|]})?*+. 给定子串中的>。


对于第三方库支持,请考虑 ApacheCommonsLang,StringUtils.containsIgnoreCase()。




17 计算字符串中子字符串的出现次数


计算一个字符串在另一个字符串中出现的次数是一个问题,它至少有两种解释:

  • 11/111 发生 1 次
  • 11/111 发生 2 次



在第一种情况下(111 中的 11 发生 1 次),可以依赖于String.indexOf()方法来解决。此方法的一种风格允许我们从指定的索引(如果没有这样的索引,则为 -1)开始获取指定子字符串第一次出现的字符串中的索引。基于此方法,该解决方案可以简单地遍历给定的字符串并计算给定子字符串的出现次数。遍历从位置 0 开始,直到找不到子字符串为止:



public static int countStringInString(String string, String toFind) {
  int position = 0;
  int count = 0;
  int n = toFind.length();
  while ((position = string.indexOf(toFind, position)) != -1) {
    position = position + n;
    count++;
  }
  return count;
}


或者,该溶液可以使用String.split()方法。基本上,该解决方案可以使用给定的子字符串作为分隔符来拆分给定的字符串。结果String[]数组的长度应等于预期出现的次数:

public static int countStringInString(String string, String toFind) {
  int result = string.split(Pattern.quote(toFind), -1).length - 1;
  return result < 0 ? 0 : result;
}


在第二种情况下(111 中的 11 出现了 2 次),解决方案可以在一个简单的实现中依赖于PatternMatcher类,如下所示:

public static int countStringInString(String string, String toFind) {
  Pattern pattern = Pattern.compile(Pattern.quote(toFind));
  Matcher matcher = pattern.matcher(string);
  int position = 0;
  int count = 0;
  while (matcher.find(position)) {
    position = matcher.start() + 1;
    count++;
  }
  return count;
}


很好!让我们继续讨论字符串的另一个问题。





18 检查两个字符串是否为异序词


两个具有相同字符但顺序不同的字符串是异序词。一些定义强制要求字谜不区分大小写和/或应忽略空格(空格)。


因此,与应用的算法无关,解决方案必须将给定的字符串转换为小写并删除空格(空格)。除此之外,我们提到的第一个解决方案通过Arrays.sort()对数组进行排序,并通过Arrays.equals()检查它们的相等性。


一旦它们被排序,如果它们是字谜,它们将相等(下图显示了两个字谜):


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ktjouv9K-1657077108923)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/ebed6c7d-49fc-4c77-b4cf-f97de703e88e.png)]


此解决方案(包括其 Java8 函数式版本)在本书附带的代码中提供。这两种解决方案的主要缺点是排序部分。以下解决方案消除了这一步骤,并依赖于 256 个索引的空数组(最初仅包含 0)(字符的扩展 ASCII 表代码更多信息可在“查找第一个非重复字符”部分中找到)。


算法非常简单:


   对于第一个字符串中的每个字符,此解决方案将此数组中对应于 ASCII 代码的值增加 1

   对于第二个字符串中的每个字符,此解决方案将此数组中对应于 ASCII 代码的值减少 1


代码如下:

private static final int EXTENDED_ASCII_CODES = 256;
...
public static boolean isAnagram(String str1, String str2) {
  int[] chCounts = new int[EXTENDED_ASCII_CODES];
  char[] chStr1 = str1.replaceAll("\\s", 
    "").toLowerCase().toCharArray();
  char[] chStr2 = str2.replaceAll("\\s", 
    "").toLowerCase().toCharArray();
  if (chStr1.length != chStr2.length) {
    return false;
  }
  for (int i = 0; i < chStr1.length; i++) {
    chCounts[chStr1[i]]++;
    chCounts[chStr2[i]]--;
  }
  for (int i = 0; i < chCounts.length; i++) {
    if (chCounts[i] != 0) {
      return false;
    }
  }
  return true;
}

在遍历结束时,如果给定的字符串是异序词,那么这个数组只包含 0。


相关文章
|
2月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
2月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
90 12
|
2月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
239 2
|
3月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
3月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
2月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
2月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
76 3
|
2月前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
91 4
|
3月前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
99 1

热门文章

最新文章