【问题标题】:Calculate Keyword Density of each Unique Element in List in Java?计算Java列表中每个唯一元素的关键字密度?
【发布时间】:2021-07-05 15:27:34
【问题描述】:

我可以使用以下代码计算正文的关键字密度:

HashMap<String, Integer> frequencies = new HashMap<String, Integer>();

String[] splitTextArr = StringX.splitStrIntoWordsRtrnArr(text);

int articleWordCount = splitTextArr.length;

for (String splitText : splitTextArr) {


 if (frequencies.containsKey(splitText)) {

    Integer previousInt = frequencies.get(splitText);
    Integer newInt = ++previousInt;
    frequencies.put(splitText, newInt);
    } else {

    frequencies.put(splitText, 1);
    }
}

然后,我使用以下调用按照出现次数最多的关键字到出现次数最少的关键字的顺序对关键字密度列表进行排序:

    Map<String, Integer> sortedMap = new LinkedHashMapX<String, Integer>().sortByValueInDescendingOrder(frequencies);

上面的代码按预期工作,但是我现在必须实现一个独特的要求。

总结:给定一个标题条目列表,我需要提取足够多的关键字,以便列表中的每个标题都由一个关键字表示,然后计算该关键字表示的标题总数(例如见下文)。

示例:假设我有以下 5 个标题,格式为 LinkedHashMap&lt;String, String&gt;

title1: canon photo printer 
title2: canon camera canon
title3: wireless mouse
title4: wireless charger
title5: mouse trap

KEY 表示 titleID(即 title1title2 等),VALUE 表示实际标题。

每个关键字的原始出现次数(按降序排列,不区分大小写)如下:

佳能:2 |鼠标:2 |无线:2 |相机:1 |充电器:1 |照片:1 |打印机:1 |陷阱:1

注意:每个关键字在每个标题中只计算一次。因此,虽然关键字 canon 出现了 3 次,但由于它在同一个标​​题中出现了两次(即 title2),所以它只计算一次。

在之前的map 中,关键字canon 出现在title1title2 中。由于每个标题都需要用一个关键字来表示,所以两个标题都可以用关键字 canon 来表示。 title1title2 中的其他关键字(例如:photoprinter 和 camera),因为每个标题都应该由一个关键字(不是更多,也不是更少)表示。虽然我们可以选择使用关键字 photocamera(或 printer)来表示 title1title2 camera) - 因为这会增加代表所有标题所需的关键字总数,所以这是不希望的。换句话说,我们希望用尽可能少的关键字来表示所有标题

重要的部分是提取能够一次“代表”列表中所有标题的最少数量的关键字,并跟踪每个关键字链接到的标题数量和 titleID .如果不是 5 个标题,我们有一个包含 100 个标题的列表,其中关键字 photo 出现了 95 次(即,比关键字 canon 出现的次数更多)关键字 photo 将用于替换关键字 canon,而 title2 将由关键字 camera 表示。

如果两个或多个关键字可以代表相同数量的标题,我们将按字母顺序选择第一个。因此,关键字 mouse 将用于表示标题 title3title5,而不是关键字 wireless。类似地,为了表示 title4,关键字 charger 将用作字母表中字母 C 位于字母 W 之前(即使关键字 wireless 出现两次,而关键字 charger 只出现一次,因为包含关键字 wirelesstitle3 已经由关键字 表示鼠标而不是关键字无线,所以当两个关键字代表相同数量的标题时,我们恢复使用字母表中的第一个关键字)

在前面 5 个标题的示例中,预期输出将具有以下格式:

LinkedHashMap<String, LinkedHashMap<Integer, ArrayList<String>> map = new LinkedHashMap<String, LinkedHashMap<Integer, ArrayList<String>>();

其中String 表示关键字(例如canonmouse),Integer 表示该关键字所代表的唯一标题的数量(canon = 2,鼠标 = 2,充电器 = 1) 和 ArrayList&lt;String&gt; 是与该关键字链接的 titleIDs 列表。例如,关键字 canon 将链接到标题:title1title2。关键字 mouse 将链接到标题:title3title5,关键字 charger 将链接到标题: title4.

注意: wireless = 0, trap = 0 所以在最终结果中省略。

实现这一目标的最有效方法是什么?

谢谢

【问题讨论】:

  • 通过正确的术语来引用事物来消除混淆。而不是“第一个字符串”和“第二个字符串”,分别将它们称为 KEY 和 VALUE。
  • 另外,这个解释很复杂。尽量总结一下。
  • @hfontanez 感谢您的建设性反馈!我更新了术语并尝试增强摘要。希望它能让事情变得更清楚。
  • 没问题。我正在研究解决方案。如果没有中断,我应该尽快准备好。 :)
  • 我发布了我的解决方案。希望我正确理解了您的要求。

标签: java string data-structures hashmap find-occurrences


【解决方案1】:

基本上,您需要创建某种数据结构,您需要在其中跟踪关键字、链接到的标题数量以及与之关联的标题 ID;像这样:

[canon, {2, [ title1, title2 ] } ]

如果是这种情况,这至少有两个原因是不好的:

  1. 使用计数作为键是不好的,因为计数会在其他元素中重复
  2. 您可以通过返回给定关键字的标题 ID 列表的大小或长度来简单地计算计数。

我提出的解决方案有两个方面:使用一个类来捕获您的关键字和标题 ID 映射,然后是一组这些映射对象。

public class KeywordMapping implements Comparable<KeywordMapping> {
    
    private final String keyword;
    private TreeSet<String> titleIds = new TreeSet<String>();
    private int frequency;
    
    public KeywordMapping(String keyword, String titleId) {
        this.keyword = keyword;
        titleIds.add(titleId);
    }
    
    public String getKeyword () {
        return keyword;
    }
    
    public int getTitleCount () {
        return titleIds.size();
    }
    
    public void addTitleId (String titleId) {
        titleIds.add(titleId);
    }
    
    public String getFirstTitleId () {
        return titleIds.first();
    }
    
    public void incrementFrequency(String keyword) {
        if (this.keyword.equals(keyword)) {
            frequency++;            
        }
    }

    public int getFrequency() {
        return frequency;
    }

    public boolean containsTitle(String titleId) {
        return titleIds.contains(titleId);
    }

    @Override
    public int hashCode () {
        final int prime = 31;
        int result = 1;
        result = prime * result + ( (keyword == null) ? 0 : keyword.hashCode());
        result = prime * result + ( (titleIds == null) ? 0 : titleIds.hashCode());
        return result;
    }

    @Override
    public boolean equals (Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        KeywordMapping other = (KeywordMapping) obj;
        if (keyword == null) {
            if (other.keyword != null) return false;
        } else if (!keyword.equals(other.keyword)) return false;
        if (titleIds == null) {
            if (other.titleIds != null) return false;
        } else if (!titleIds.equals(other.titleIds)) return false;
        return true;
    }

    @Override
    public String toString () {
        return "KeywordMapping [keyword=" + keyword + ", titleIds=" + titleIds + ", frequency=" + frequency
            + "]\n";
    }

    @Override
    public int compareTo (KeywordMapping o) {
        return COMPARATOR.compare(this, o);
    }

    private static final Comparator<KeywordMapping> COMPARATOR =
        Comparator.comparing( (KeywordMapping keyMapping) -> keyMapping.keyword);
}

这个解决方案没问题。我宁愿使用 Builder Pattern 来构建它并使此类不可变,以便仅在将所有标题 ID 添加到类中时才构建此类的每个实例。但是,让我解释一下这个实现的一些细节。我使用TreeSet 来存储标题ID,以便它们按自然顺序(按字母顺序)排列。这对于存储标题 ID 并不重要,但稍后会很重要。最后,如果您需要将这些对象插入到可排序的集合中,该类必须实现 Comparable&lt;T&gt;

关键字映射的TreeMap

TreeMap<String, KeywordMapping> mappings = new TreeMap<>();

必须使用树形图来按键按自然顺序排列条目。

我创建了这个简单的 main 方法来演示这个实现

public static void main (String[] args) {
    List<String> titles = List.of("title2: canon camera canon",
        "title3: wireless mouse",
        "title1: canon photo printer",
        "title4: wireless charger", "title5: mouse trap");
    
    MasterMap mappings = new MasterMap(); // A class wrapping TreeMap<String, KeywordMapping> mappings (the code is shown later on)
    
    for (String title : titles) {
        String[] tokens = title.split(": ");
        Set<String> keywords = new HashSet<>(Arrays.asList(tokens[1].split(" ")));
        for (String keyword : keywords) {
            KeywordMapping km = mappings.get(keyword); // don't create duplicate keyword mapping objects
            if (km == null) {
                km = new KeywordMapping(keyword, tokens[0]);
            }
            km.addTitleId(tokens[0]);
            km.incrementFrequency(keyword);
            mappings.put(keyword, km); // update existing or new keyword mapping entry
        }
    }
    System.out.println(mappings);
    System.out.println("First entry: " + mappings.firstMapping());
    System.out.println("First title in first entry: " + mappings.firstMapping().getValue().getFirstTitleId());
}

这个程序的输出是:

camera=KeywordMapping [keyword=camera, titleIds=[title2], frequency=1]
canon=KeywordMapping [keyword=canon, titleIds=[title1, title2], frequency=2]
charger=KeywordMapping [keyword=charger, titleIds=[title4], frequency=1]
mouse=KeywordMapping [keyword=mouse, titleIds=[title3, title5], frequency=2]
photo=KeywordMapping [keyword=photo, titleIds=[title1], frequency=1]
printer=KeywordMapping [keyword=printer, titleIds=[title1], frequency=1]
trap=KeywordMapping [keyword=trap, titleIds=[title5], frequency=1]
wireless=KeywordMapping [keyword=wireless, titleIds=[title3, title4], frequency=2]

First entry: camera=KeywordMapping [keyword=camera, titleIds=[title2], frequency=1]
First title in first entry: title2

请注意,即使原始标题列表不是按自然顺序排列的,在插入KeywordMapping 对象后,支持标题 ID 列表的集合也会按顺序排列它们。这与打印树形图中的条目的顺序相同。

最后,每个标题的频率计数仅增加一次,因为标题 ID 列表由 Set 而不是 List 支持,并且 Java 中的集合不允许重复。因此,对于title2: canon camera canon 这样的情况,关键字canon 只计算一次,因为对于该标题,关键字集只有{cannon, camera}

包装 TreeMap 以增加功能

public class MasterMap {
    private TreeMap<String, KeywordMapping> mappings = new TreeMap<>();
    
    public KeywordMapping put(String key, KeywordMapping value) {
        return mappings.put(key, value);
    }
    
    public KeywordMapping get(String key) {
        return mappings.get(key);
    }
    
    public KeywordMapping firstMapping() {
        return mappings.firstEntry().getValue();
    }
    
    public String getKeywordForTitle (String titleId) {
        
        List<KeywordMapping> keywords = new ArrayList<>();
        
        Collection<KeywordMapping> values = mappings.values();
        Iterator<KeywordMapping> iter = values.iterator();
        
        while (iter.hasNext()) {
            KeywordMapping value = iter.next();
            if (value.containsTitle(titleId)) {
                keywords.add(value);
            }
        }
        
        KeywordMapping temp = keywords.stream()
            .max(Comparator.comparingInt(KeywordMapping::getFrequency)
                .thenComparing(KeywordMapping::getKeyword).reversed())
            .get();
        
        return temp.getKeyword();
    }
}

为了满足最后一个要求,我决定将TreeMap 包装到一个类中,以便我可以添加方法来执行最后一个操作:根据以下条件返回与给定标题关联的关键字:返回频率最高的关键字或按字母顺序排列的第一个关键字(如果频率相同)。实现的函数不会以任何方式操作KeywordMapping 对象或包装的树映射。它的作用是使用Comparator 来根据该条件查找匹配的关键字。

添加更改后(之前的代码已更新),将“title3”传递给函数会产生以下输出:

Keyword for title3: mouse

这是因为与“title3”{mouse, wireless} 关联的两个关键字具有相同的频率 (2),但“mouse”是集合中按字母顺序排列的第一个关键字。

【讨论】:

  • @感谢您非常详细和翔实的回复!我认为唯一剩下的要求是删除代表已经由另一个关键字表示的标题的关键字。例如,应删除关键字“photo”,因为 title1 由关键字“canon”表示。同样的想法适用于关键字“打印机”。仅当关键字表示的标题未由另一个关键字表示时,才应将关键字包含在结果中..(续)
  • @alpha1 我正要发布一个更新,如果您想要更多“元数据”功能,您应该将映射TreeMap 包装在一个类中并添加您想要的方法。例如,如果标题 ID 是 title2getKeywordsByTitle(String titleId) 应该返回 {camera, canon}。同样,您应该能够创建另一种方法来根据该要求减少或优化您的地图。
  • (cont) 如果两个或多个关键字可以代表同一个标题(canon vs photo),我们会选择出现频率更高的一个(canon)。如果频率相等,我们选择字母表中第一个出现的频率。出于这个原因,关键字“无线”不代表任何标题,因为标题3已经由关键字“鼠标”表示,因为它与关键字“无线”出现的频率相同,但它在字母表中排在第一位。所以“无线”可以仅代表 title4,但由于“charger”在字母表中排在第一位,因此 title4 由关键字“charger”而不是“wireless”表示。谢谢!
  • @alpha1 您提到的所有这些功能都属于另一个类(称它为 MasterMapping 包装了 TreeMap&lt;String, KeywordMapping&gt; mappings
  • @hfontanez 对您的惊人回复表示赞同。明天早上我将在我的本地机器上测试代码并接受并奖励赏金。感谢您的巨大努力!更新:我接受了答案,因为您为此付出了很多努力。不能再奖励 19 小时的赏金。早上我会在我的机器上测试代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-05
  • 2021-07-25
  • 2023-04-06
  • 2017-11-09
  • 2020-06-27
  • 2020-11-05
  • 1970-01-01
相关资源
最近更新 更多