计算 Java 列表中每个唯一元素的关键字密度?

Calculate Keyword Density of each Unique Element in List in Java?

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

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<String, String>

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出现在title1 & title2。由于每个标题都需要用一个关键字来表示,所以两个标题都可以用关键字 canon 来表示。没有必要包含 title1title2 中的其他关键字(例如:photoprintercamera) 因为每个标题应该由一个关键字表示(不多也不少)。尽管我们可以选择使用关键字 photo 来表示 title1title2 camera(或 printercamera)- 因为这会增加表示所有内容所需的关键字总数标题,这是不希望的。换句话说,我们希望用尽可能少的关键字代表所有标题

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

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

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

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

其中String代表关键词(如canonmouse),Integer代表数量由该关键字表示的独特标题的数量(canon = 2,mouse = 2,charger = 1)并且 ArrayList<String>titleIDs[=115= 的列表] 链接到该关键字。例如,关键字 canon 将链接到标题:title1title2。关键字 mouse 将链接到标题:title3title5 以及关键字 charger 将链接到标题:title4.

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

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

谢谢

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

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

如果是这种情况,至少有两个原因很糟糕:

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

我建议的解决方案是 two-fold:使用 class 捕获您的关键字和标题 ID 映射,然后使用一组这些映射 objects。

Class

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

这个解决方案是可以的。我宁愿使用构建器模式来构建它并使它 class 不可变,以便仅当所有标题 ID 都已添加到 class 时才构建此 class 的每个实例。但是,让我解释一下这个实现的一些细节。我使用 TreeSet 来存储标题 ID,以便它们按自然顺序(按字母顺序)排列。这对于存储标题 ID 并不重要,但稍后会很重要。最后,如果您需要将这些 object 插入到可排序的 collection.

中,class 必须实现 Comparable<T>

关键字映射的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 object 后,支持标题 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包装成一个class,这样我就可以添加方法来执行最后一个动作:return关联的关键字具有基于以下标准的给定标题:return 具有最高频率的关键字或按字母顺序排列的第一个关键字(如果频率相同)。实现的函数不会以任何方式操纵 KeywordMapping objects 或包装树图。它的作用是使用 Comparator 来根据该条件找到匹配的关键字。

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

Keyword for title3: mouse

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