簡體   English   中英

Java-從字符串中刪除重復項

[英]Java - Remove duplicates from a string

我有一個字符串,其中包含用分號分隔的值列表。 我需要一些最佳方法來刪除重復項。 我有以下正則表達式:

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

這可以工作,但是在有空格的情況下會失敗。 例如aaa bbb;aaa bbb;aaa bbb創建aaa aaa aaa bbb而不是aaa bbb

可能最簡單的解決方案是使用Sets-不允許重復的集合。 在分隔符上分割您的字符串,並將其放在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 ";"

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();
}

這可以以多種方式實現。 如前所述,HashSet是正確的方法。 當您聲明需要“最佳”解決方案時,我花了一些時間來優化和基准化多個實現。

我們從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()創建字符串數組。 然后將該數組轉換為一個列表,該列表將添加到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在我的機器上運行它時,給了我以下結果:

  • 與眾不同的0:每次通話0.0021881 [ms]
  • 與眾不同的1:每次通話0.0018433 [ms]
  • unique2:每次通話0.0016780 [ms]
  • 與眾不同3:每次通話0.0012777 [ms]

盡管無疑比原始版本要復雜得多,可讀性也要差得多,但是優化的實現幾乎快了一倍。 如果需要一個簡單易讀的解決方案,我將選擇第一個或第二個實現,如果需要一個快速的解決方案,則將選擇最后一個實現。

您可以使用

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

觀看演示

用空格替換aaa bbb;aaa bbb;aaa bbb得到aaa bbb

所有的連續詞; s必須替換為2個后處理步驟:

  • .replaceAll("^;+|;+$", "") -刪除前導/尾隨的分號
  • .replaceAll(";+",";") -合並所有多個; 成1 ;

這是最終的IDEONE演示

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

如果最優方法==較小的計算復雜度,則

從頭開始按值解析字符串,並使用找到的值創建並行HashSet。 當值存在於集合中時,您將忽略它並轉到下一個。 如果集合中不存在任何值,則將其發出並添加到集合中。

在HashSet上查找和添加O(1)操作,因此此算法應為O(n)。

內存消耗也為O(n),可能要根據輸入情況進行考慮。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM