Java - 从字符串中删除重复项

时间:2015-09-02 10:51:32

标签: java regex

我有一个字符串,其中的值列表以分号分隔。我需要一些最佳方法来删除重复项。我遵循正则表达式:

\b(\w+);(?=.*\b\1;?)

这有效,但是当有空格时它会失败。例如aaa bbb;aaa bbb;aaa bbb创建aaa aaa aaa bbb而不是aaa bbb

4 个答案:

答案 0 :(得分:6)

可能最简单的解决方案是使用集合 - 不允许重复的集合。在分隔符上拆分字符串,并将其放在set中。

在Java 8中,您的代码可能如下所示:

String result = Stream.of(yourText.split(";"))          //stream of elements separated by ";"
                      .distinct()                       //remove duplicates in stream
                      .collect(Collectors.joining(";"));//create String joining rest of elements using ";"

Pre Java 8解决方案可能如下所示:

public String removeDuplicates(String yourText) {
    Set<String> elements = new LinkedHashSet<>(Arrays.asList(yourText.split(";")));

    Iterator<String> it = elements.iterator();

    StringBuilder sb = new StringBuilder(it.hasNext() ? it.next() : "");
    while (it.hasNext()) {
        sb.append(';').append(it.next());
    }

    return sb.toString();
}

答案 1 :(得分:1)

这可以通过多种方式实现。如前所述,HashSet是正确的方法。当你声明你需要一个&#34;最佳&#34;解决方案我花时间对几个实现进行优化和基准测试。

我们从Pshemo的Java 8之前的解决方案开始:

public static String distinct0(String yourText) {
    Set<String> elements = new LinkedHashSet<>(Arrays.asList(yourText.split(";")));
    Iterator<String> it = elements.iterator();
    StringBuilder sb = new StringBuilder(it.hasNext() ? it.next() : "");
    while (it.hasNext()) {
        sb.append(';').append(it.next());
    }
    return sb.toString();
}

此实现使用String.split()创建一个字符串数组。然后将此数组转换为List,并将其添加到LinkedHashSet中。 LinkedHashSet通过维护其他链接列表来保留添加元素的顺序。接下来,迭代器用于枚举集合中的元素,然后将其与StringBuilder连接。

我们可以通过实现在迭代输入字符串中的各个元素的同时构建结果来稍微优化此方法。因此,不必存储关于已找到不同字符串的顺序的信息。这消除了对LinkedHashSet(和Iterator)的需求:

public static String distinct1(String elements){
    StringBuilder builder = new StringBuilder();
    Set<String> set = new HashSet<String>();
    for (String value : elements.split(";")) {
        if (set.add(value)) {
            builder.append(set.size() != 1 ? ";" : "").append(value);
        }
    }
    return builder.toString();
}

接下来,我们可以摆脱String.split(),从而避免创建一个包含输入字符串中所有元素的中间数组:

public static String distinct2(String elements){

    char[] array = elements.toCharArray();
    StringBuilder builder = new StringBuilder();
    Set<String> set = new HashSet<String>();
    int last = 0;
    for (int index=0; index<array.length; index++) {
        if (array[index] == ';') {
            String value = new String(array, last, (index-last));
            if (set.add(value)) {
                builder.append(last != 0 ? ";" : "").append(value);
            }
            last = index + 1;
        }
    }
    return builder.toString();
}

最后,我们可以通过不为各个元素构造String对象来消除不必要的内存分配,因为构造函数String(array,offset,length)(String.split()也使用它)将调用Arrays。 copyOfRange(...)分配一个新的char []。为了避免这种开销,我们可以在输入char []周围实现一个包装器,它实现给定范围的hashCode()和equals()。这可用于检测结果中是否已包含某个字符串。另外,这个方法允许我们使用StringBuilder.append(array,offset,length),它只是从提供的数组中读取数据:

public static String distinct3(String elements){

    // Prepare
    final char[] array = elements.toCharArray();
    class Entry {
        final int from;
        final int to;
        final int hash;

        public Entry(int from, int to) {
            this.from = from;
            this.to = to;
            int hash = 0;
            for (int i = from; i < to; i++) {
                hash = 31 * hash + array[i];
            }
            this.hash = hash;
        }

        @Override
        public boolean equals(Object object) {
            Entry other = (Entry)object;
            if (other.to - other.from != this.to - this.from) {
                return false;
            }
            for (int i=0; i < this.to - this.from; i++) {
                if (array[this.from + i] != array[other.from + i]) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public int hashCode() {
            return hash;
        }
    }

    // Remove duplicates
    StringBuilder builder = new StringBuilder();
    Set<Entry> set = new HashSet<Entry>();
    int last = 0;
    for (int index=0; index<array.length; index++) {
        if (array[index] == ';') {
            Entry value = new Entry(last, index);
            if (set.add(value)) {
                builder.append(last != 0 ? ";" : "").append(array, last, index-last);
            }
            last = index + 1;
        }
    }
    return builder.toString();
}

我将这些实现与以下代码进行了比较:

public static void main(String[] args) {

    int REPETITIONS = 10000000;
    String VALUE = ";aaa bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;"+
                   "aaa bbb;;aaa bbb;aaa;bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb;"+
                   "aaa bbb;aaa bbb;aaa bbb;aaa;bbb;aaa bbb;aaa bbb;aaa bbb;aaa bbb";

    long time = System.currentTimeMillis();
    String result = null;
    for (int i = 0; i < REPETITIONS; i++) {
        result = distinct0(VALUE);
    }
    System.out.println(result + " - " + (double) (System.currentTimeMillis() - time) /
                                        (double) REPETITIONS + " [ms] per call");
}

在使用JDK 1.7.0_51在我的机器上运行时,它给了我以下结果:

  • distinct0:每次通话0.0021881 [ms]
  • distinct1:每次通话0.0018433 [ms]
  • distinct2:每次通话0.0016780 [ms]
  • distinct3:每次通话0.0012777 [ms]

虽然无疑比原始版本复杂得多且可读性低得多,但优化后的实现速度几乎快了两倍。如果需要一个简单易读的解决方案,我会选择第一个或第二个实现,如果需要快速实现,我会选择最后一个实现。

答案 2 :(得分:0)

您可以使用

(?<=^|;)([^;]+)(?=(?:;\\1(?:$|;))+)

请参阅demo

用空格替换aaa bbb;aaa bbb;aaa bbb会产生aaa bbb

所有多个连续的;必须用2个后处理步骤替换:

  • .replaceAll("^;+|;+$", "") - 删除前导/尾随分号
  • .replaceAll(";+",";") - 将所有多个;合并为1 ;

以下是最终IDEONE demo

String s = "ccc;aaa bbb;aaa bbb;bbb";
s = s.replaceAll("(?<=^|;)([^;]+)(?=(?:;\\1(?:$|;))+)", "").replaceAll("^;+|;+$", "").replaceAll(";+",";");
System.out.println(s); 

答案 3 :(得分:0)

如果最佳方法==计算复杂度较低那么

从开始,按值解析字符串,并使用您找到的值创建并行HashSet。 如果集合中存在值,则忽略它并转到下一个。 如果集合中不存在值,则将其发出并添加到集合中。

在HashSet中查找和添加O(1)操作,因此该算法应为O(n)。

内存消耗也是O(n),根据输入可能需要考虑。