2016-09-30 2 views
8

Я пытаюсь сортировать строку по количеству вхождений каждого из ее символов, с наиболее частым в начале и самым редким в конце. После сортировки мне нужно будет удалить все повторяющиеся символы. Поскольку примеры всегда понятнее, программа должна сделать следующее:Эффективная сортировка строки по количеству вхождений каждого из ее символов

String str = "aebbaaahhhhhhaabbbccdfffeegh"; 
String output = sortByCharacterOccurrencesAndTrim(str); 

В этом случае, метод «sortByCharacterOccurrencesAndTrim» должен вернуться:

String output = "habefcdg" 

В случае, когда 2 символа имеют один и тот же возникновение, их порядок в возвращаемой строке не имеет значения. Так что «habefcdg» может также равняться «habfecgd», потому что «f» и «e» встречаются 3 раза, и оба «d» и «g» встречаются один раз.

"habefcdg" would effectively be the same as "habfecgd" 

Примечание: Я хотел бы отметить, что производительность имеет значение в этом случае, так что я предпочел бы наиболее эффективный метод возможно. Я говорю это, потому что длина строки может варьироваться от 1 до максимальной длины (что, я думаю, совпадает с Integer.MAX_VALUE, но не уверен), поэтому я хочу свести к минимуму любые потенциальные узкие места.

+0

Есть ограниченный набор символов? – shmosel

+0

Если строка слишком велика, то использование карт будет хорошим вариантом с точки зрения производительности, так как карта может легко сортироваться с помощью hashcode. –

+0

@shmosel Строка кодируется с помощью US_ASCII, поэтому любой доступный символ ascii. – TheMasterGabriel

ответ

5

«Карта и несколько петель», безусловно, самый простой способ, и, вероятно, будет очень быстро. Идея такова:

for each character 
    increment its count in the map 
Sort the map in descending order 
Output the map keys in that order 

Но 100 000 000 картографических поисков могут стать довольно дорогими. Вы могли бы ускорить его, создав массив из 65 536 целых чисел (или 128 символов, если это ASCII). Тогда:

for each character 
    array[(int)ch] += 1 

Затем вы проходите через этот массив и создать карту персонажей с ненулевых отсчетов:

for i = 0 to 65535 
    if array[i] > 0 
     map.add((char)i, array[i]) 

Затем сортировать карту, чтобы и выходных убыванию символы в таком порядке ,

Это может работать довольно быстро, просто потому, что индексирование в массив 100 000 000 раз, вероятно, намного быстрее, чем выполнение 100 000 000 поиска карт.

+0

Поиск 'HashMap' _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 0 – Andreas

+3

@ Аndreas: поиск O (1), да. Так что индексирование массива напрямую. Но помните, что O (1) не учитывает постоянные факторы. Индексирование в массив включает ссылку на косвенную память. Поиск в хэш-карте включает вызов функции, которая вычисляет хэш-значение из ключа, а затем индексирует в массив поддержки. Поиск таблицы хэш-таблиц асимптотически выполняется так же быстро, как индексирование в массив, но требует гораздо большего количества кода для выполнения. –

+1

Я тестировал производительность ваших решений. См. [Мой комментарий к вопросу] (http://stackoverflow.com/questions/39783311/39783734#comment66861860_39783311). Относительно говоря, разница в производительности 4 раза может считаться «довольно дорогой», но оба решения по-прежнему довольно быстро выполняются для строки в 100 000 000 символов. – Andreas

3

Просто для удовольствия (и я не утверждаю, что это наиболее эффективное решение): как насчет некоторых Java 8 lambdas + параллельных потоков?

public String sortByCharacterOccurrencesAndTrim(String str) { 

    // build a frequency map, for each code point store its count  
    Map<Integer, Long> frequencies = 
     str.codePoints() 
      .parallel() 
      .boxed() 
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); 

    // sort by descending frequency and collect code points into array 
    int[] output = 
     frequencies.entrySet() 
        .parallelStream() 
        .sorted(Map.Entry.<Integer, Long>comparingByValue().reversed()) 
        .mapToInt(Map.Entry::getKey) 
        .toArray(); 

    // create output string from code point array 
    return new String(output, 0, output.length); 

} 

Если вы хотите супер-эффективное решение, которое вы можете переписать выше алгоритм с использованием явных циклов, но это очень много кода, и это поздно для меня :). Идея, однако, будет одинаковой: постройте частотную карту символов, отсортируйте по частоте с использованием нисходящего порядка и постройте строку с символами.

+0

Не было бы более последовательным использование 'str.codePoints()'? – shmosel

+0

@shmosel было бы какое-то преимущество? –

+1

Это позволило бы использовать 3 или 4 байтовых символа, а не то, что это необходимо здесь. Но если вы используете конструктор кодовой точки, вы также можете использовать соответствующий метод потока. – shmosel

-1

Я ничего о потоках и лямбды не знаю, но я хотел бы сделать что-то вроде этого:

Map<Character, Integer> map; 
for (int i=0;i<str.length,i++){ 
    char c = s.charAt(i); 
    switch(c){ 
     case: map.containsKey(c): 
      int temp = map.get(c)++; 
      map.put(c, tmp); 
      break; 
     case: !map.containsKey(c): 
      map.put(c, tmp); 
      break; 
    } 
}   

для подсчета вхождений. Тогда это просто вопрос сортировки от наивысшего до самого низкого.

+2

Разве это даже компилируется? Я бы подумал, что вам понадобится 'map.put (c, 1);' –

+0

Ах, я хотел набрать (c, 1) для! Содержит случай ... – Gabe

+1

есть ошибки в строках (начиная с нуля) 4, 5, 6, 8, 9 в этом ответе – starikoff

4

Примечание: Это не ответ, а просто показывает производительность тестового кода ответов на Jim Mischel и Óscar López (с параллельными потоками в ответ на comment by OP).

import java.util.ArrayList; 
import java.util.Collections; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 
import java.util.Random; 
import java.util.function.Function; 
import java.util.stream.Collectors; 

public class Test { 
    public static void main(String[] args) { 
     long start = System.currentTimeMillis(); 
     String s = buildString(); 
     System.out.println("buildString: " + (System.currentTimeMillis() - start) + "ms"); 

     start = System.currentTimeMillis(); 
     String result1 = testUsingArray(s); 
     System.out.println("testUsingArray: " + (System.currentTimeMillis() - start) + "ms"); 

     start = System.currentTimeMillis(); 
     String result2 = testUsingMap(s); 
     System.out.println("testUsingMap: " + (System.currentTimeMillis() - start) + "ms"); 

     start = System.currentTimeMillis(); 
     String result3 = testUsingStream(s); 
     System.out.println("testUsingStream: " + (System.currentTimeMillis() - start) + "ms"); 

     start = System.currentTimeMillis(); 
     String result4 = testUsingParallelStream(s); 
     System.out.println("testUsingParallelStream: " + (System.currentTimeMillis() - start) + "ms"); 

     System.out.println(result1); 
     System.out.println(result2); 
     System.out.println(result3); 
     System.out.println(result4); 
    } 
    private static String buildString() { 
     Random rnd = new Random(); 
     char[] buf = new char[100_000_000]; 
     for (int i = 0; i < buf.length; i++) 
      buf[i] = (char)(rnd.nextInt(127 - 33) + 33); 
     return new String(buf); 
    } 
    private static String testUsingArray(String s) { 
     int[] count = new int[65536]; 
     for (int i = 0; i < s.length(); i++) 
      count[s.charAt(i)]++; 
     List<CharCount> list = new ArrayList<>(); 
     for (int i = 0; i < 65536; i++) 
      if (count[i] != 0) 
       list.add(new CharCount((char)i, count[i])); 
     Collections.sort(list); 
     char[] buf = new char[list.size()]; 
     for (int i = 0; i < buf.length; i++) 
      buf[i] = list.get(i).ch; 
     return new String(buf); 
    } 
    private static String testUsingMap(String s) { 
     Map<Character, CharCount> map = new HashMap<>(); 
     for (int i = 0; i < s.length(); i++) 
      map.computeIfAbsent(s.charAt(i), CharCount::new).count++; 
     List<CharCount> list = new ArrayList<>(map.values()); 
     Collections.sort(list); 
     char[] buf = new char[list.size()]; 
     for (int i = 0; i < buf.length; i++) 
      buf[i] = list.get(i).ch; 
     return new String(buf); 
    } 
    private static String testUsingStream(String s) { 
     int[] output = s.codePoints() 
         .boxed() 
         .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) 
         .entrySet() 
         .stream() 
         .sorted(Map.Entry.<Integer, Long>comparingByValue().reversed()) 
         .mapToInt(Map.Entry::getKey) 
         .toArray(); 
     return new String(output, 0, output.length); 
    } 
    private static String testUsingParallelStream(String s) { 
     int[] output = s.codePoints() 
         .parallel() 
         .boxed() 
         .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) 
         .entrySet() 
         .parallelStream() 
         .sorted(Map.Entry.<Integer, Long>comparingByValue().reversed()) 
         .mapToInt(Map.Entry::getKey) 
         .toArray(); 
     return new String(output, 0, output.length); 
    } 
} 
class CharCount implements Comparable<CharCount> { 
    final char ch; 
    int count; 
    CharCount(char ch) { 
     this.ch = ch; 
    } 
    CharCount(char ch, int count) { 
     this.ch = ch; 
     this.count = count; 
    } 
    @Override 
    public int compareTo(CharCount that) { 
     return Integer.compare(that.count, this.count); // descending 
    } 
} 

Пример вывода

buildString: 974ms 
testUsingArray: 48ms 
testUsingMap: 216ms 
testUsingStream: 1279ms 
testUsingParallelStream: 442ms 
UOMP<FV{KHt`(-q6;Gl'R9nxy+.Y[=2a7^[email protected],>|AD_\ILpJ}8sow"Z&bCmNW1$!Sd0c]~g3BjX#fz:Q*Tkui%/r)h 
UOMP<FV{KHt`(-q6;Gl'R9nxy+.Y[=2a7^[email protected],>|AD_\ILpJ}8sow"Z&bCmNW1$!Sd0c]~g3BjX#fz:Q*Tkui%/r)h 
UOMP<FV{KHt`(-q6;Gl'R9nxy+.Y[=2a7^[email protected],>|AD_\ILpJ}8sow"Z&bCmNW1$!Sd0c]~g3BjX#fz:Q*Tkui%/r)h 
UOMP<FV{KHt`(-q6;Gl'R9nxy+.Y[=2a7^[email protected],>|AD_\ILpJ}8sow"Z&bCmNW1$!Sd0c]~g3BjX#fz:Q*Tkui%/r)h 
+0

Еще один тестовый пример для использования TreeSet вместо ArrayList: private static String testUsingTreeSet (String s) { int [] count = new int [65536]; для (int i = 0; i set = новый TreeSet <>(); for (int i = 0; i <65536; i ++) if (count [i]! = 0) set.add (новый CharCount ((char) i, count [i])); char [] buf = новый char [set.size()]; int i = 0; для (CharCount chcnt: set) buf [i ++] = chcnt.ch; return new String (buf); } – bashnesnos

+1

Я бы ожидал, что решение 'TreeSet' будет несколько медленнее, чем решение' HashSet'. «TreeSet» - это O (n log k), где n - длина строки, а k - количество уникальных символов. Решение «HashSet» (и решение массива) - это O (n + k log k) (n для заполнения «HashSet», а затем k log k для сортировки элементов). Увидев, что n в этом случае на порядки больше k, (10^8 против 6x10^4), вы можете считать решение HashSet равным O (n). –

+0

@JimMischel тест для строки 100000000 показывает те же тайминги для 'TreeSet' и' HashSet' (вставка 'ArrayList' также O (n)). Поскольку мы вычисляем наихудший случай, для «ArrayList» (N для перемещения входа, K для заполнения карт/arrayList и K logK для сортировки) будет O (N + K + K logK). Для «TreeSet» это было бы просто O (N + K logK), мы вообще не меняем N-часть, мы просто меняем способ сортировки invidual char counts. Фактически, мы могли бы просто отсортировать массив после части N с помощью 'Arrays.sort' и вообще не использовать дополнительный список/карту. – bashnesnos

Смежные вопросы