【问题标题】:Sorting sets alphabetically, letters in sets separated by commas按字母顺序对集合进行排序,集合中的字母用逗号分隔
【发布时间】:2019-04-21 07:38:44
【问题描述】:
public static void main(String[] args) throws IOException
{

    HashSet set = new HashSet<String>();

    set.add("{}");
    set.add("{a}");
    set.add("{b}");
    set.add("{a, b}");
    set.add("{a, c}");

    sortedSet(set);
}

public static void sortedSet(HashSet set)
{
    List<String> setList = new ArrayList<String>(set);
    List<String> orderedByAlpha = new ArrayList<String>(set);

    //sort by alphabetical order
    orderedByAlpha = (List<String>) setList.stream()
        .sorted((s1, s2) -> s1.compareToIgnoreCase(s2))
        .collect(Collectors.toList());
    System.out.println(orderedByAlpha);
}

我正在尝试按字母顺序排序,但我得到的输出是这样的:

[{a, b}, {a, c}, {a}, {b}, {}]

但应该是:

[{a}, {a, b}, {a, c}, {b}, {}]

【问题讨论】:

  • 按字母顺序,, (0x2C) 在 } (0x7D) 之前。如果您希望得到不同的结果,则需要编写自己的自定义比较器。

标签: java sorting lambda java-8 comparator


【解决方案1】:

对它的处理(与the answer by Aomine 稍有相似)将去除导致String#compareTo() 失败的字符串,在这种情况下('{''}')。此外,空字符串 ("{}") 的特殊情况需要在其余部分之后进行排序。

下面的代码实现了这样一个比较器:

static final Comparator<String> COMPARE_IGNORING_CURLY_BRACES_WITH_EMPTY_LAST = (s1, s2) -> {
  Function<String, String> strip = string -> string.replaceAll("[{}]", "");
  String strippedS1 = strip.apply(s1);
  String strippedS2 = strip.apply(s2);
  return strippedS1.isEmpty() || strippedS2.isEmpty() ?
      strippedS2.length() - strippedS1.length() :
      strippedS1.compareTo(strippedS2);
};

当然,这不是最有效的解决方案。如果效率在这里真的很重要,我会像String#compareTo() 那样循环遍历字符,就像suggested by ETO 那样。

【讨论】:

    【解决方案2】:

    好吧,正如@Aomine 和@Holger 已经指出的那样,您需要一个自定义比较器。

    但恕我直言,他们的解决方案看起来设计过度。您不需要像 splitsubstring 这样的昂贵操作:

    • String.substring 创建一个新的 String 对象并在后台调用 System.arraycopy()
    • String.split 的成本更高。它遍历您的字符串并多次调用String.substring。此外,它创建一个ArrayList 来存储所有子字符串。如果子字符串的数量足够大,那么您的 ArrayList 将需要扩展其容量(可能不止一次),从而导致再次调用 System.arraycopy()

    对于您的简单情况,我会稍微修改内置 String.compareTo 方法的代码:

    Comparator<String> customComparator =
                (s1, s2) -> {
                    int len1 = s1.length();
                    int len2 = s2.length();
    
                    if (len1 == 2) return 1;
                    if (len2 == 2) return -1;
    
                    int lim = Math.min(len1, len2) - 1;
    
                    for (int k = 1; k < lim; k++) {
                        char c1 = s1.charAt(k);
                        char c2 = s2.charAt(k);
                        if (c1 != c2) {
                            return c1 - c2;
                        }
                    }
                    return len1 - len2;
                };
    

    它将比较复杂度O(n)的字符串,其中n是较短字符串的长度。同时它既不会创建任何新对象,也不会执行任何数组复制。

    同样的比较器可以使用Stream API实现:

    Comparator<String> customComparatorUsingStreams =
                (s1, s2) -> {
                    if (s1.length() == 2) return 1;
                    if (s2.length() == 2) return -1;
                    return IntStream.range(1, Math.min(s1.length(), s2.length()) - 1)
                            .map(i -> s1.charAt(i) - s2.charAt(i))
                            .filter(i -> i != 0)
                            .findFirst()
                            .orElse(0);
                };
    

    您可以像这样使用自定义比较器:

    List<String> orderedByAlpha = setList.stream()
                                         .sorted(customComparatorUsingStreams)
                                         .collect(Collectors.toList());
    System.out.println(orderedByAlpha);
    

    【讨论】:

    • 您忽略了 OP 使用了 compareToIgnoreCase 的事实。我不知道她是否会回来告诉我们,这是否重要。顺便说一句,如果只是为了避免substring操作的复制开销,你可以使用Comparator.comparing(s -&gt; CharBuffer.wrap(s, 1, s.length()-1))。但是,当热点优化器检测到substring 操作的结果完全在本地使用时,它可能会设法生成更好的代码。
    • @Holger 好吧,可以像.map(i -&gt; Character.toUpperCase(s1.charAt(i)) - Character.toUpperCase(s2.charAt(i))) 一样使用Character.toUpperCase 忽略大小写。至于使用CharBuffer.wrap,仍然没有避免创建其他对象。 (AFAIK,它在后台创建了一个 CharBuffer 的实例)。
    • wrap 返回的CharBuffer 是原始String 的轻量级包装器。它比在您的第二个变体中创建IntStream 便宜,后者创建IntStreamRangeIntSpliteratorTerminalOp,最后是OptionalInt,以及一些其他内部使用的对象。最后,只有与 String 创建相关的字符复制成本(即 O(n))才是真正昂贵的操作。有时,即使是这种差异也无关紧要,因此最简单的解决方案更可取。对于性能关键场景,您的循环可能会真正得到回报。
    • 是的,你是对的。第二种变体并不便宜。尽管在这种特殊情况下,我也会使用集合而不是字符串表示(正如您在下面评论的那样)。
    【解决方案3】:

    我建议您不要将源作为List&lt;String&gt;,而是将其作为List&lt;Set&lt;String&gt;&gt;,例如

    List<Set<String>> setList = new ArrayList<>();
    setList.add(new HashSet<>(Arrays.asList("a","b")));
    setList.add(new HashSet<>(Arrays.asList("a","c")));
    setList.add(new HashSet<>(Collections.singletonList("a")));
    setList.add(new HashSet<>(Collections.singletonList("b")));
    setList.add(new HashSet<>());
    

    然后将以下比较器与映射操作一起应用以产生预期结果:

    List<String> result = 
         setList.stream()
             .sorted(Comparator.comparing((Function<Set<String>, Boolean>) Set::isEmpty)
                            .thenComparing(s -> String.join("", s),
                            String.CASE_INSENSITIVE_ORDER))
             .map(Object::toString)
             .collect(Collectors.toList());
    

    然后打印出来:

    [[a], [a, b], [a, c], [b], []]
    

    请注意,目前的结果是一个字符串列表,其中每个字符串都是给定集合的字符串表示形式。但是,如果您希望结果为 List&lt;Set&lt;String&gt;&gt;,则只需删除上面的 map 操作即可。

    编辑:

    设法根据您最初的想法得到一个可行的解决方案....

    因此,首先,您需要一个全新的比较器,而不仅仅是 (s1, s2) -&gt; s1.compareToIgnoreCase(s2),因为这还不够。

    给定输入:

    Set<String> set =  new HashSet<>();
    
    set.add("{}");
    set.add("{a}");
    set.add("{b}");
    set.add("{a, b}");
    set.add("{a, c}");
    

    以及以下流管道:

    List<String> result = set.stream()
                .map(s -> s.replaceAll("[^A-Za-z]+", ""))
                .sorted(Comparator.comparing(String::isEmpty)
                        .thenComparing(String.CASE_INSENSITIVE_ORDER))
                .map(s -> Arrays.stream(s.split(""))
                                .collect(Collectors.joining(", ", "{", "}")))
                .collect(Collectors.toList());
    

    那么我们会得到以下结果:

    [{a}, {a, b}, {a, c}, {b}, {}]
    

    【讨论】:

    • 您对字符串进行排序的解决方案不能正确处理具有多个字符的元素。但是既然OP的问题只是花括号引起的,为什么不使用.map(s -&gt; s.substring(1, s.length()-1)) .sorted(…) .map(s -&gt; '{'+s+'}')呢?或者,仅为比较而进行更改,set.stream() .sorted(Comparator.comparing(s -&gt; s.substring(1, s.length()-1), Comparator.comparing(String::isEmpty) .thenComparing(String.CASE_INSENSITIVE_ORDER))) .collect(Collectors.toList());
    • @Holger 好喊,我猜我把事情复杂化了。我会让 OP 考虑你的两个有效点,而不是再次更新答案。 :)
    • @Holger 您的两个解决方案都创建了新字符串。为什么不简单地在字符串上循环呢?代码会更长一点,速度会更快。
    • @ETO 因为 OP 没有要求最快的解决方案。如果是为了我的项目,我没有使用字符串表示似乎是一个集合,所以问题一开始就没有出现。
    【解决方案4】:

    您的输出与您的代码不匹配。您正在显示 2D 数组列表,但您转换为 1D 数组列表没有意义。

    public static void main(String[] args)
    {
        test(Arrays.asList("a", "d", "f", "a", "b"));
    }
    
    static void test(List<String> setList)
    {
        List<String> out = setList.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).collect(Collectors.toList());
        System.out.println(out);
    }
    

    这是正确排序一维数组,所以你是正确的。

    您可能需要实现自己的比较器来比较二维数组列表以对它们进行排序。

    【讨论】:

      猜你喜欢
      • 2011-03-30
      • 1970-01-01
      • 2021-12-18
      • 2011-01-15
      • 1970-01-01
      • 1970-01-01
      • 2017-02-07
      • 2010-10-26
      • 2015-03-01
      相关资源
      最近更新 更多