你会使用哪种数据结构:TreeMap 还是 HashMap?(Java)

56

描述 | 一个Java程序,读取文本文件并按字母顺序打印每个唯一单词以及单词在文本中出现的次数。

该程序应声明一个类型为Map<String, Integer>的变量来存储单词和相应的出现频率。但具体使用哪种类型呢?TreeMap<String, Number>还是HashMap<String, Number>

输入应转换为小写。

一个单词不包含以下任何字符:\t\t\n]f.,!?:;\"()'

示例输出 |

 Word            Frequency
  a                 1
  and               5
  appearances       1
  as                1
         .
         .
         .

备注 | 我知道,我已经在Perl中看到了大约两行代码的优雅解决方案。但是,我想在Java中看到它。

编辑:哦对了,展示一个使用这些结构之一的实现(在Java中)会很有帮助。


哇,这是在明目张胆地钓作业问题的答案吗? - Spencer Kormos
26
实际上,作业问题要求实际实现这两个版本--它没有提到哪个数据结构更好。然而,在努力保持半真诚学习的失落艺术方面,我在拐弯抹角...只是试图获得一些见解! - JohnZaj
我怎么会因为上面那条评论得到六个点?我知道...这是一个关于元数据的问题。 - JohnZaj
我有类似的问题,所有内容都相同,我只想按值而不是键进行排序...最好的方法是什么? - ante.sabo
14个回答

63
对我来说,TreeMap 似乎是个不言而喻的选择 - 这仅仅是因为它需要“按字母顺序”排序。当你遍历 HashMap 时没有排序;TreeMap 则按自然键排序。

编辑:我认为 Konrad 的评论可能是在建议“使用 HashMap,然后进行排序。”这很好,因为尽管最初我们将有 N 次迭代,但由于重复,最后我们将只有 K ≤ N 个键。我们可以把昂贵的部分(排序)留到最后,当我们有比之前更少的键时再进行排序,而不是花费小但非固定的代价一边遍历一边排序。

话虽如此,就目前而言,我会坚持我的答案:因为这是实现目标最简单的方式。我们并不真正知道 OP 是否特别关注性能,但问题意味着他关心优雅和简洁。使用 TreeMap 使这变得非常简短,这很吸引我。我猜想,如果性能真的是一个问题,可能有更好的方法来解决它,而不是使用 TreeMapHashMap :)


7
Jon,不要高估排序。根据我的经验,即使是巨大的数据集排序也可以被忽略。如果有足够的项目,O(1)插入绝对优于O(logn)。但是像往常一样,这只是纯粹的猜测,没有进行性能分析。 - Konrad Rudolph
1
如果您可以实现O(1)插入,则总插入时间为O(n)。 如果您还建议在迭代之前没有更多的工作要做,那么您已经以某种方式实现了 n(相对任意)个项目的O(n)排序。这是如何运作的? - Jon Skeet
1
Jon,我知道这已经是古代历史了;P 但是这里有个解释。哈希表实际上在预期的摊销O(n)时间内实现了排序。与计算模型的比较模型相关的排序操作的已证明下限为n log n。哈希表通过利用传统计算机中随机访问存储器的顺序性来击败它。然而!在实践中,基于比较的排序几乎总是更快的;据说,一个好的哈希表实现比一个平庸的快速排序实现昂贵100倍的常数因子。 - Mark McKenna
1
@Mark:我不太确定你在解释什么,或者内存的顺序性如何避免N log N复杂度... - Jon Skeet
@Mark: 但字符串仍然具有任意长度,这似乎很困难。从您引用的维基百科文章中可以看出:“对于n个具有k位数或更少位数的键,基数排序的效率为O(k·n)。” 这相当重要,除非限制k,但我们在这里没有这样做。 我认为在这种情况下不能忽略k的值。 - Jon Skeet
显示剩余14条评论

18

TreeMap比HashMap更好,因为TreeMap已经为您排序。

但是,您可能希望考虑使用更合适的数据结构-一个bag。请参见Commons Collections,以及TreeBag类:

它具有优化的内部结构和API:

bag.add("big")
bag.add("small")
bag.add("big")
int count = bag.getCount("big")

编辑:Jon已回答了HashMap与TreeMap性能问题 - HashMap和排序可能更快(可以尝试!),但TreeBag更容易。相同的情况也适用于bags。有HashBag和TreeBag。基于实现(使用可变整数),bag应该优于等价的纯整数映射。要确定,唯一的方法是测试,就像任何性能问题一样。


2
是的,但是最后一次排序会比整个时间保持排序的成本更快吗? - Herms
Herms,这取决于所涉及的数据结构的效率。使用随机枢轴快速排序对数组进行原地排序是一个非常快的n log n,但Java的TreeMap也是一个相当快的n log n。Java的HashMap的工作速度相对较慢,为n。由于我们需要随机访问所选元素,因此我们在快速O(n log t)一次和慢速O(n)一次加上非常快的O(t log t)之间进行选择;其中t < n。后者将在n变大时最终超过前者,但我认为n必须先变得非常大。对于Google来说,哈希+排序会更好;对于学校来说,可能是treemap。 - Mark McKenna

11

我看到很多人说 "TreeMap 查找需要 O(n log n)"!! 为什么呢?

我不知道它是如何被实现的,但在我的理解中,它只需要 O(log n)

这是因为在树中查找可以在 O(log n) 内完成。每次插入一个元素时都不需要对整棵树进行排序。这就是使用树的整个想法!

因此,回到最初的问题,比较的数据如下:

HashMap 方法:平均情况下为 O(n + k log k),最坏情况可能会更大

TreeMap 方法:最坏情况下为 O(k + n log k)

其中n = 文本中的单词数,k = 文本中不同单词的数量。


3
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream.GetField;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;

public class TreeMapExample {

    public static void main (String args[]){
        Map<String,Integer> tm = new TreeMap<String,Integer>();
        try {

            FileInputStream fis = new FileInputStream("Test.txt");
            DataInputStream in = new DataInputStream(fis);
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line;
            int countValue = 1;
            while((line = br.readLine())!= null ){
                line = line.replaceAll("[-+.^:;,()\"\\[\\]]","");
                StringTokenizer st = new StringTokenizer(line, " ");    
                while(st.hasMoreTokens()){
                    String nextElement = (String) st.nextElement();

                    if(tm.size()>0 && tm.containsKey(nextElement)){
                        int val = 0;
                        if(tm.get(nextElement)!= null){
                        val = (Integer) tm.get(nextElement);
                        val = val+1;
                        }
                        tm.put(nextElement, val);
                    }else{
                    tm.put(nextElement, 1);
                    }

                }
            }
            for(Map.Entry<String,Integer> entry : tm.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
            }

        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

3

哈希表应该更快。您不应该根据最终想要的项目排列方式选择容器;只需在最后对(单词,频率)对列表进行排序即可。需要排序的这些对通常比文件中的单词要少,因此使用哈希表的渐近(和实际)性能会更好。


不一定。在最坏的情况下,哈希表的访问时间为O(n)。而自平衡二叉搜索树即使在最坏的情况下也始终是O(log n)。 - newacct
2
哈希表只有在负载因子较高或哈希函数较差的情况下才会出现最坏情况。Java默认提供了足够好的哈希和因子,因此这不是一个真实的担忧。 - Mark McKenna

2
在我看来,更好的方法是使用Apache Commons Collections中的HashBag,或者Guava中的HashMultiset,或者Eclipse Collections(以前是GS Collections)中的HashBag,或者以下任何类:
    Order    |  Guava           |   Apache  | Eclipse(GS) | JDK analog
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Not define   | HashMultiset     |   HashBag | HashBag     | HashMap<String, Integer>
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Sorted       | TreeMultiset     |   TreeBag | TreeBag     | TreeMap<String, Integer>
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Linked       |LinkedHashMultiset|     -     |     -       | LinkedHashMap<String, Integere>
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Concurrent & | ConcurrentHash-  |Synchroniz-|Synchroniz-  | Collections.synchronizedMap(
not define   | Multiset         |   edBag   | edBag       |       HashMap<String, Integer>)
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Concurrent   |         -        |Synchroniz-|Synchroniz-  | Collections.synchronizedSorted-
and sorted   |                  |edSortedBag| edSortedBag |       Map(TreeMap<>))
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Immutable and| ImmutableMultiset|Unmodifiab-|Unmodifiab-  | Collections.unmodifiableMap(
not define   |                  |   leBag   | leBag       | HashMap<String, Integer>)
─────────────┼──────────────────┼───────────┼─────────────┼─────────────
Immutable and| ImmutableSorted- |Unmodifiab-|Unmodifiab-  | Collections.unmodifiableSorted-
sorted       | Multiset         |leSortedBag| leSortedBag | Map(TreeMap<String, Integer>))
────────────────────────────────────────────────────────────────────────

例子:

1. 使用Apache的SynchronizedSortedBag

    // Parse text to separate words
    String INPUT_TEXT = "Hello World! Hello All! Hi World!";
    // Create Multiset
    Bag bag = SynchronizedSortedBag.synchronizedBag(new TreeBag(Arrays.asList(INPUT_TEXT.split(" "))));

    // Print count words
    System.out.println(bag); // print [1:All!,2:Hello,1:Hi,2:World!]- in natural (alphabet) order
    // Print all unique words
    System.out.println(bag.uniqueSet());    // print [All!, Hello, Hi, World!]- in natural (alphabet) order


    // Print count occurrences of words
    System.out.println("Hello = " + bag.getCount("Hello"));    // print 2
    System.out.println("World = " + bag.getCount("World!"));    // print 2
    System.out.println("All = " + bag.getCount("All!"));    // print 1
    System.out.println("Hi = " + bag.getCount("Hi"));    // print 1
    System.out.println("Empty = " + bag.getCount("Empty"));    // print 0

    // Print count all words
    System.out.println(bag.size());    //print 6

    // Print count unique words
    System.out.println(bag.uniqueSet().size());    //print 4

2. 在 Eclipse 中使用 TreeBag(GC)

    // Parse text to separate words
    String INPUT_TEXT = "Hello World! Hello All! Hi World!";
    // Create Multiset
    MutableSortedBag<String> bag =  TreeBag.newBag(Arrays.asList(INPUT_TEXT.split(" ")));

    // Print count words
    System.out.println(bag); // print [All!, Hello, Hello, Hi, World!, World!]- in natural order
    // Print all unique words
    System.out.println(bag.toSortedSet());    // print [All!, Hello, Hi, World!]- in natural order

    // Print count occurrences of words
    System.out.println("Hello = " + bag.occurrencesOf("Hello"));    // print 2
    System.out.println("World = " + bag.occurrencesOf("World!"));    // print 2
    System.out.println("All = " + bag.occurrencesOf("All!"));    // print 1
    System.out.println("Hi = " + bag.occurrencesOf("Hi"));    // print 1
    System.out.println("Empty = " + bag.occurrencesOf("Empty"));    // print 0

    // Print count all words
    System.out.println(bag.size());    //print 6

    // Print count unique words
    System.out.println(bag.toSet().size());    //print 4

3. 使用 Guava 中的 LinkedHashMultiset

    // Parse text to separate words
    String INPUT_TEXT = "Hello World! Hello All! Hi World!";
    // Create Multiset
    Multiset<String> multiset = LinkedHashMultiset.create(Arrays.asList(INPUT_TEXT.split(" ")));

    // Print count words
    System.out.println(multiset); // print [Hello x 2, World! x 2, All!, Hi]- in predictable iteration order
    // Print all unique words
    System.out.println(multiset.elementSet());    // print [Hello, World!, All!, Hi] - in predictable iteration order

    // Print count occurrences of words
    System.out.println("Hello = " + multiset.count("Hello"));    // print 2
    System.out.println("World = " + multiset.count("World!"));    // print 2
    System.out.println("All = " + multiset.count("All!"));    // print 1
    System.out.println("Hi = " + multiset.count("Hi"));    // print 1
    System.out.println("Empty = " + multiset.count("Empty"));    // print 0

    // Print count all words
    System.out.println(multiset.size());    //print 6

    // Print count unique words
    System.out.println(multiset.elementSet().size());    //print 4

更多例子可以在我的Github项目中找到


2
当一个键已经存在时,它的性能与HashMap相同是错误的。HashMap具有O(1)插入,而TreeMap具有O(n log n)。至少需要n log n次检查才能确定是否在表中!

2
您不能将TreeMap<String,Number>分配给类型为Map<String,Integer>的变量。您可以将DoubleLong等放入TreeMap<String,Number>中。当我从Map<String,Integer>获取一个值时,它必须是一个Integer。请忽略任何i18n问题、内存限制和错误处理,以下是内容:
class Counter {

  public static void main(String... argv)
    throws Exception
  {
    FileChannel fc = new FileInputStream(argv[0]).getChannel();
    ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
    CharBuffer cb = Charset.defaultCharset().decode(bb);
    Pattern p = Pattern.compile("[^ \t\r\n\f.,!?:;\"()']+");
    Map<String, Integer> counts = new TreeMap<String, Integer>();
    Matcher m = p.matcher(cb);
    while (m.find()) {
      String word = m.group();
      Integer count = counts.get(word);
      count = (count == null) ? 1 : count + 1;
      counts.put(word, count);
    }
    fc.close();
    for (Map.Entry<String, Integer> e : counts.entrySet()) {
      System.out.printf("%s: %d%n", e.getKey(), e.getValue());
    }
  }

}

是的,你说得对 - 我会返回并编辑它。我想要表达的是将其声明为 Map<String, Integer> 类型,而不是 Map<String, Number>,然后 TreeMap<String, Integer> 就是 Map<String, Integer> 的一个子类型。 - JohnZaj

1

我肯定会选择TreeMap:

  • TreeMap在插入新键时自动排序,无需后续排序。
  • 当键已存在时,它的性能与HashMap相同。

TreeSet内部使用TreeMap,因此为什么不直接使用TreeMap。


0
这是Java示例,用于读取文本文件,按键排序,然后根据单词在文件中出现的次数对值进行排序。
public class SortFileWords {

    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        ValueCompare vc = new ValueCompare(map);
        TreeMap<String, Integer> sorted_map = new TreeMap<String, Integer>(map);
        List<String> list = new ArrayList<>();
        Scanner sc;
        try {
            sc = new Scanner(new File("c:\\ReadMe1.txt"));
            while (sc.hasNext()) {
                list.add(sc.next());
            }
            sc.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        for (String s : list) {
            if (map.containsKey(s)) {
                map.put(s, map.get(s) + 1);
            } else
                map.put(s, 1);
        }

        System.out.println("Unsorted map: " + map);
        sorted_map.putAll(map);
        System.out.println("Sorted map on keys: " + sorted_map);

        TreeMap<String, Integer> sorted_value_map = new TreeMap<>(vc);
        sorted_value_map.putAll(map);
        System.out.println("Sorted map on values: " + sorted_value_map);
    }
}

class ValueCompare implements Comparator<String> {

    Map<String, Integer> map;

    public ValueCompare(Map<String, Integer> map) {
        this.map = map;
    }

    @Override
    public int compare(String s1, String s2) {
        if (map.get(s1) >= map.get(s2))
            return -1;
        else
            return 1;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接