【问题标题】:Algorithm that will maintain top n items in the past k days?将保持过去 k 天前 n 项的算法?
【发布时间】:2017-08-08 12:39:30
【问题描述】:

我想实现一个数据结构,为排行榜维护一组S,它可以有效地回答以下查询,同时还可以节省内存:

  1. add(x, t) 添加一个得分为x 的新项目以设置S 和相关时间t

  2. query(u) 列出集合S 中的前n 项目(按分数排序),这些项目与t 相关联,因此t + k >= u。每个后续查询的u 不小于之前的查询。

在标准英语中,可以单独将高分添加到此排行榜中,我想要一种算法,可以有效地查询帖子k 天(其中k 和@ 987654335@ 是固定常数)。

n 可以假设比项目总数少得多,并且可以假设分数是随机的。

一种简单的算法是在将所有元素添加到按分数排序的平衡二叉搜索树中时存储它们,并在元素超过 k 天时从树中删除它们。检测超过 k 天的元素可以通过另一个按时间排序的平衡二叉搜索树来完成。该算法将产生O(log(h)) 的良好时间复杂度,其中h 是过去k 天添加的分数总数。但是,空间复杂度为O(h),很容易看出,即使接下来的k 天没有添加新分数,大部分保存的数据也不会在查询中报告。

如果n 为1,则只需一个简单的双端队列即可。在将新项目添加到队列的前面之前,请从前面删除分数低于新项目的项目,因为它们永远不会在查询中报告。在查询之前,从队列后面移除太旧的项目,然后返回留在队列后面的项目。所有操作都将摊销恒定的时间复杂度,并且我不会存储永远不会报告的项目。

n 大于 1 时,我似乎无法制定具有良好时间复杂度且仅存储可能被报告的项目的算法。具有时间复杂度O(log(h)) 的算法会很棒,但n 足够小,所以O(log(h) + n) 也是可以接受的。

有什么想法吗?谢谢!

【问题讨论】:

  • 你是否按 t 升序添加项目?
  • 大多数时候,但我不想排除网络延迟、需要更新第二台服务器以及其他可能导致不同步的事情的可能性。但如果你有一个算法,只有在你按升序添加项目时才有效,那也很好。
  • 一个想法:制作四叉树
  • 我们也可以认为k很小吗?
  • @MoTao 我知道这一点,所以我提到分数可能被认为是随机的。虽然最坏情况下的空间复杂度不会小于 O(h),但平均空间复杂度可能会小很多。

标签: algorithm sorting


【解决方案1】:

此解决方案基于双端队列解决方案,我假设 t 是升序的。

思路是,如果有n条记录的t和x都大于它,则可以删除一条记录,示例代码中Record.count实现。

由于每条记录最多会从S 移动到temp n 次,因此我们的平均时间复杂度为 O(n)。 空间复杂度很难决定。但是,它在模拟中看起来不错。当 h = 10000 且 n = 50 时,S.size() 约为 400。

#include <iostream>
#include <vector>
#include <queue>
#include <cstdlib>
using namespace std;

const int k = 10000, n = 50;

class Record {
public:
    Record(int _x, int _t): x(_x), t(_t), count(n) {}
    int x, t, count;
};

deque<Record> S;

void add(int x, int t)
{
    Record record(x, t);
    vector<Record> temp;
    while (!S.empty() && record.x >= S.back().x) {
        if (--S.back().count > 0) temp.push_back(S.back());
        S.pop_back();       
    }
    S.push_back(record);
    while (!temp.empty()) {
        S.push_back(temp.back());
        temp.pop_back();
    }
}

vector<int> query(int u)
{
    while (S.front().t + k < u)
        S.pop_front();
    vector<int> xs;
    for (int i = 0; i < S.size() && i < n; ++i)
        xs.push_back(S[i].x);
    return xs;
}

int main()
{
    for (int t = 1; t <= 1000000; ++t) {
        add(rand(), t);
        vector<int> xs = query(t);
        if (t % k == 0) {
            cout << "t = " << t << endl;
            cout << "S.size() = " << S.size() << endl;
            for (auto x: xs) cout << x << " ";
            cout << endl;
        }
    }

    return 0;
}

【讨论】:

  • 这看起来很棒!但我认为query 函数中的 for 循环还应该检查记录是否太旧,即如果S[i].t + k &lt; u 则忽略/丢弃记录。虽然最前面的记录可能是最近添加的,但 S 中的其他记录可能比它更旧。
  • @Bernard 检查S[i].t + k &lt; u 似乎没有必要,因为tS 中上升。顺便说一句,如果有帮助,记得接受这个答案。
  • 您能解释一下为什么会这样吗?我看不到t 可能会如何上升。 add() 函数仅确保xS 中升序。当n = 1 时,t 只保证在S 中升序。考虑以下x 的值,随着t 的增加插入:999999999998999997999996、...,直到最旧的记录 (999999) 几乎到期。然后插入1000000(比其他所有内容都大)。当前记录不会被删除,新记录将放在最前面。如果你在999999过期后查询,query()仍然会返回。
  • 一旦问题解决,我会接受答案,因为现在代码对我来说似乎不正确。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-09
相关资源
最近更新 更多