我认为您可以使用cache replacement policy 的实现来实现这一点。
这些算法可帮助计算机处理器 (CPU) 确定将主内存 (RAM) 的哪些部分(“页”)保存在缓存中(L1、L2 等)——CPU 可以访问哪些部分很多 比 RAM 快。但它们可以很容易地适应您的问题。
根据最近使用情况对项目进行排序的算法类似于缓存策略 LRU 过期/替换 Least-R ecently-U缓存已满时的页面。
按最频繁使用(此处“频繁”实际上是指“使用次数”)对项目进行排序的算法类似于缓存策略 LFU 过期/替换 Least-F频繁-U缓存已满时的页面。
有几种策略可以按照您要求的方式显式或隐式结合这两个概念。有些还涉及“时间”(根据实际的计算机时钟时间或简单的页面请求的递增计数器等)以获得更好的页面访问“年龄”或“频率”的感觉(而不是简单的使用次数)。
这些变得更加复杂和难以实现,特别是如果您需要一个非常有效的算法。但是对于与用户界面相关的大多数用途,即使是低效的算法也应该足够快,因为项目的数量很少,用户很少会修改列表。
一个可能适合您的算法示例是 LRFU(最近最少/经常使用)策略,该策略直接寻求结合 LRU 和 LFU 以根据结合了新近度的公式来过期页面和使用频率。您可以在上面列出的同一页面上找到对此的参考。你也可以看到被举报的scholarly article。
本文使用堆和链表数据结构的组合来实现它,并包含一些实现的伪代码。
对于您的使用,您可能可以相当容易地编写一个更简单但效率较低的算法。
例如,您可以简单地存储具有 2 个属性的对象数组:
- 价值(您关心的东西——例如网站或文件名等)
- 分数(下文讨论——在文章中称为“CRF”)
只要用户选择了一个值,您就可以按如下方式修改列表:
- 通过将每个分数乘以恒定加权因子
USAGE_WEIGHT(介于 0.5 和 1.0 之间的数字,下文将详细讨论)来更新列表中所有现有项目的分数。
- 搜索列表以查看最近选择的项目是否已存在。如果是这样,只需将 1.0 添加到其现有分数。如果没有,则创建一个初始分数为 1.0 的新项目并将其添加到列表中。如果列表中没有更多空间(意味着它已经包含您要显示的最大 MRU 项目数),则首先删除列表中具有 最低 分数的项目(始终位于列表的末尾,由于下一步)。
- 现在按分数降序重新排序列表。 (最高分应首先出现在 MRU 列表中)。
USAGE_WEIGHT 因素决定了“新近度”与“频率”(即使用次数)相比的重要性。值 0.5 将导致列表具有完全 LRU 行为(只有最近才重要),而值 1.0 将导致列表具有完全 LFU 行为(只有使用计数很重要)。中间值(例如 0.9)将导致列表具有 LRU 和 LFU 行为的混合,如下面的示例输出所示。
在下面的每个场景中,“值”都是字母,它们按以下顺序添加:
A B C B A A D A C D A B D E C B A
每次添加后,我都会在引号中列出添加的字母以及当前 MRU 列表(例如“DBA”)。最大 MRU 大小为 3。我还列出了更详细的列表表示形式,以 { Letter, Score } 的形式显示每个项目的 Value(字母)和 Score。
USAGE_WEIGHT = 1.0(完全 LFU - 只有使用次数很重要)
1. (Added A) "A" [ { A, 1.0 } ]
2. (Added B) "AB" [ { A, 1.0 } { B, 1.0 } ]
3. (Added C) "ABC" [ { A, 1.0 } { B, 1.0 } { C, 1.0 } ]
4. (Added B) "BAC" [ { B, 2.0 } { A, 1.0 } { C, 1.0 } ]
5. (Added A) "BAC" [ { B, 2.0 } { A, 2.0 } { C, 1.0 } ]
6. (Added A) "ABC" [ { A, 3.0 } { B, 2.0 } { C, 1.0 } ]
7. (Added D) "ABD" [ { A, 3.0 } { B, 2.0 } { D, 1.0 } ]
8. (Added A) "ABD" [ { A, 4.0 } { B, 2.0 } { D, 1.0 } ]
9. (Added C) "ABC" [ { A, 4.0 } { B, 2.0 } { C, 1.0 } ]
10. (Added D) "ABD" [ { A, 4.0 } { B, 2.0 } { D, 1.0 } ]
11. (Added A) "ABD" [ { A, 5.0 } { B, 2.0 } { D, 1.0 } ]
12. (Added B) "ABD" [ { A, 5.0 } { B, 3.0 } { D, 1.0 } ]
13. (Added D) "ABD" [ { A, 5.0 } { B, 3.0 } { D, 2.0 } ]
14. (Added E) "ABE" [ { A, 5.0 } { B, 3.0 } { E, 1.0 } ]
15. (Added C) "ABC" [ { A, 5.0 } { B, 3.0 } { C, 1.0 } ]
16. (Added B) "ABC" [ { A, 5.0 } { B, 4.0 } { C, 1.0 } ]
17. (Added A) "ABC" [ { A, 6.0 } { B, 4.0 } { C, 1.0 } ]
USAGE_WEIGHT = 0.5(完全 LRU - 只有最近才重要)
1. (Added A) "A" [ { A, 1.0 } ]
2. (Added B) "BA" [ { B, 1.0 } { A, 0.5 } ]
3. (Added C) "CBA" [ { C, 1.0 } { B, 0.5 } { A, 0.25 } ]
4. (Added B) "BCA" [ { B, 1.25 } { C, 0.5 } { A, 0.125 } ]
5. (Added A) "ABC" [ { A, 1.0625 } { B, 0.625 } { C, 0.25 } ]
6. (Added A) "ABC" [ { A, 1.5313 } { B, 0.3125 } { C, 0.125 } ]
7. (Added D) "DAB" [ { D, 1.0 } { A, 0.7656 } { B, 0.1563 } ]
8. (Added A) "ADB" [ { A, 1.3828 } { D, 0.5 } { B, 0.0781 } ]
9. (Added C) "CAD" [ { C, 1.0 } { A, 0.6914 } { D, 0.25 } ]
10. (Added D) "DCA" [ { D, 1.125 } { C, 0.5 } { A, 0.3457 } ]
11. (Added A) "ADC" [ { A, 1.1729 } { D, 0.5625 } { C, 0.25 } ]
12. (Added B) "BAD" [ { B, 1.0 } { A, 0.5864 } { D, 0.2813 } ]
13. (Added D) "DBA" [ { D, 1.1406 } { B, 0.5 } { A, 0.2932 } ]
14. (Added E) "EDB" [ { E, 1.0 } { D, 0.5703 } { B, 0.25 } ]
15. (Added C) "CED" [ { C, 1.0 } { E, 0.5 } { D, 0.2852 } ]
16. (Added B) "BCE" [ { B, 1.0 } { C, 0.5 } { E, 0.25 } ]
17. (Added A) "ABC" [ { A, 1.0 } { B, 0.5 } { C, 0.25 } ]
USAGE_WEIGHT = 0.9(LRFU -- LRU 和 LFU 的混合)
1. (Added A) "A" [ { A, 1.0 } ]
2. (Added B) "BA" [ { B, 1.0 } { A, 0.9 } ]
3. (Added C) "CBA" [ { C, 1.0 } { B, 0.9 } { A, 0.81 } ]
4. (Added B) "BCA" [ { B, 1.81 } { C, 0.9 } { A, 0.729 } ]
5. (Added A) "ABC" [ { A, 1.6561 } { B, 1.629 } { C, 0.81 } ]
6. (Added A) "ABC" [ { A, 2.4905 } { B, 1.4661 } { C, 0.729 } ]
7. (Added D) "ABD" [ { A, 2.2414 } { B, 1.3195 } { D, 1.0 } ]
8. (Added A) "ABD" [ { A, 3.0173 } { B, 1.1875 } { D, 0.9 } ]
9. (Added C) "ABC" [ { A, 2.7156 } { B, 1.0688 } { C, 1.0 } ]
10. (Added D) "ADB" [ { A, 2.444 } { D, 1.0 } { B, 0.9619 } ]
11. (Added A) "ADB" [ { A, 3.1996 } { D, 0.9 } { B, 0.8657 } ]
12. (Added B) "ABD" [ { A, 2.8796 } { B, 1.7791 } { D, 0.81 } ]
13. (Added D) "ADB" [ { A, 2.5917 } { D, 1.729 } { B, 1.6012 } ]
14. (Added E) "ADE" [ { A, 2.3325 } { D, 1.5561 } { E, 1.0 } ]
15. (Added C) "ADC" [ { A, 2.0993 } { D, 1.4005 } { C, 1.0 } ]
16. (Added B) "ADB" [ { A, 1.8893 } { D, 1.2604 } { B, 1.0 } ]
17. (Added A) "ADB" [ { A, 2.7004 } { D, 1.1344 } { B, 0.9 } ]
在第一个示例中(USAGE_WEIGHT=1.0),现有项目的分数在添加新项目时不会改变(这是因为我们在每一步都乘以 1.0)。这导致每次使用后将分数简单地增加 1.0,因此分数直接表示使用计数。请注意,项目始终按使用次数递减的顺序列出。
在第二个示例中(USAGE_WEIGHT=0.5),每次添加一个项目时,现有项目的分数减半(这是因为我们每一步都乘以 0.5)。这导致列表中的所有分数都低于最近添加的项目(得分为 1.0)的属性,而且在第 N 步添加的项目总是比在第 N 步添加的项目具有更高的分数。 N 之前的任何步骤,无论该项目被重新添加多少次。这正是产生 LRU 策略的属性。
最后,当 (USAGE_WEIGHT=0.9) 我们可以看到这两种策略是如何混合的。第三个示例开始看起来像 LRU(即“新近度”很重要)。但是随着某些项目的使用次数开始增加,它们开始产生影响并改变行为。这可以从 LRU 列出“DAB”的第 7 步中看出,但由于 A 和 B 的使用率较高,示例 3 显示为“ABD”。然后示例 3 看起来更像 LFU 几个步骤,但有趣的事情发生在步骤10. 这是 LRFU 开始大放异彩的地方。至此,A 被添加了 4 次,而“B”、“C”和“D”分别被添加了 2 次。 LRU 显示“DCA”,因为“D”和“C”是最近添加的,但它忽略了用户选择“A”的可能性是“D”或“C”的两倍这一事实。 LFU 显示“ABD”,除了用户在选择“B”后两次选择“D”外,这表明“D”的使用正在“升温”,因此比“B”更有可能。 LRFU 通过显示“ADB”来正确处理。当然,这有点主观,其他读者可能不同意这是一个更好的选择。毕竟,我们试图根据之前的历史来预测用户未来的选择,所以没有完美的解决方案。但是使用 LRFU,您至少可以“调整” USAGE_WEIGHT 参数以在给定情况下找到 LRU 与 LFU 的正确平衡。
事实上,对于某些应用程序,甚至可能更可取的是动态随着程序的进展而更改 USAGE_WEIGHT 以改进基于历史数据的预测。这对于用户界面 MRU 列表可能没有意义,但对于预测大量或高频事件更有意义。
仅供参考,在本文讨论的 LRFU 算法中,Score 被称为“CRF”。他们讨论的算法还存储了计算每个分数的“时间”(或步数)。通过存储分数和时间,可以只更新正在添加的项目的分数以及项目的一小部分——而不是整个列表。此外,排序顺序由堆和链表数据结构组合维护,因此该算法比我在此处描述的使用简单数组并重新计算和重新计算的算法要高效得多每次添加后对列表进行排序。但这种实现方式简单明了、易于理解,并且适用于用户界面 MRU 列表。
这是一个非常基本的 Java 中 LRFU 列表的实现。在性能方面可以做很多改进,但它充分证明了 LRFU 的结果:
public static void main( String[] args ) {
double[] weights = { 1.0, 0.5, 0.9 };
for(double weight : weights) {
System.out.println("USAGE_WEIGHT = " + weight);
testMRU(weight);
System.out.println();
}
}
private static void testMRU(double weight) {
PrintStream p = System.out;
MRUList<String> list = new MRUList<>(3, weight);
String[] lettersAdded = "A B C B A A D A C D A B D E C B A".split(" ");
for(int i = 0; i < lettersAdded.length; i++) {
String value = lettersAdded[i];
list.add(value);
p.printf("%3s. (Added %s) \"", i, value);
for(MRUItem<String> item : list.list)
p.print(item.Value);
p.print("\"\t[ ");
for(MRUItem<String> item : list.list) {
p.printf("{ %s, %.5s } ", item.Value, item.Score);
}
p.println("]");
}
}
private static class MRUList<T> {
public static final double SCORE_INIT = 1.0;
private double usageWeight; // factor that balances LRU vs LFU
private int maxSize; // maximum number of MRU items.
public ArrayList<MRUItem<T>> list;
public MRUList(int maxItemCount) { this(maxItemCount, 0.9); }
public MRUList(int maxItemCount, double usageWeightFactor) {
maxSize = maxItemCount;
usageWeight = usageWeightFactor;
list = new ArrayList<>(maxSize);
}
// Add an item each time the user chooses it.
public void add(T value) {
// Update the score of all existing items
for(MRUItem<T> item : list)
item.Score *= usageWeight; // age the items (this does not affect sort order)
// Search for the item in the list.
MRUItem<T> existing = find(value);
if (existing==null) {
existing = new MRUItem<>(value, SCORE_INIT);
if (list.size()<maxSize) {
// we have room -- add the item.
list.add(existing);
} else {
// no more room -- replace last item.
list.set(list.size() - 1, existing);
}
} else {
// increment the score of the item if it already existed in the list.
existing.Score += SCORE_INIT;
}
// Sort the items for display.
// Collections.sort uses the Comparable interface of MRUItem.
Collections.sort(list);
}
// Get a copy of the list of items, in the correct display order.
public List<T> getItems() {
ArrayList<T> copy = new ArrayList<>();
for(MRUItem<T> item : list)
copy.add(item.Value);
return copy;
}
// return an item if it's Value is already present in the list.
private MRUItem<T> find(T value) {
for(MRUItem<T> item : list)
if (Objects.equals(item.Value, value))
return item;
return null;
}
}
private static class MRUItem<T> implements Comparable<MRUItem<T>> {
public T Value;
public double Score;
public MRUItem(final T value, final double score) {
Score = score;
Value = value;
}
// Sorts by Score in descending order (due to - sign)
@Override
public int compareTo(final MRUItem<T> other) {
return -Double.compare(Score, other.Score);
}
}