【问题标题】:More efficient way to write this algorithm?编写此算法的更有效方法?
【发布时间】:2015-12-02 04:23:26
【问题描述】:

目前正在处理图书馆模拟器任务。一切正常,但我想知道一些事情只是为了了解它。

在这个程序中有 3 个类:Book、Patron 和 Library。图书馆类包含 3 个私有数据成员:一个指向书籍的指针向量、一个指向读者指针的向量和一个 currentDate int。

有问题的函数如下:

void Library::incrementCurrentDate()
{
  currentDate++;

  for (int i = 0; i < members.size(); i++)
  {
    vector<Book*> ptr = members.at(i)->getCheckedOutBooks();

     for (int j = 0; j < ptr.size(); j++)
      {
        if (currentDate > ptr.at(j)->getCheckOutLength())
            members.at(i)->amendFine(.10);
      }
  }
} 

对功能的要求是这样的:

增加当前日期;每借阅一本逾期图书,每位读者的罚款增加 10 美分(使用 amendFine)

我上面写的方式现在可以正常工作了。因为我刚刚在我的计算机科学课程的第一学期,我们不能使用我们没有涵盖的任何东西,我知道很多。话虽如此,是否有更有效的方法来使用更高级的 c++ 编程方法来实现此功能?

【问题讨论】:

  • 保持书籍按截止日期排序,这样您就不会检查每一本书
  • @NickD const std::vector&lt;Book*&gt;&amp; ptr
  • 使用 [] 而不是 at,因为您已经在进行范围检查
  • 统计逾期图书数量,最后调整一次罚款,可能比每次调用函数都快。
  • Everything is working fine - 插入Code Review

标签: c++ performance function


【解决方案1】:
  1. 如果大小不是特别大,请使用std::vector。

由于涉及到间接性,指针总是有与之相关的成本。在内存中查找地址并访问它可能无法被编译器优化出来,因此会涉及访问内存的成本。内存访问通常是系统性能的瓶颈,因此最好尝试将内存中的内容彼此靠近,并尝试构建程序以使访问内存最少。

  1. 如果数据变得非常大,请使用 SQL 等数据库系统。

另一方面,我们可以放弃所有繁琐的工作并使用已建立的数据库库或程序。像 MySQL 这样的东西可以使用出色的编程语言轻松管理大量数据,以便访问和管理它。某些数据库,如 PostgreSQL,可以扩展到大量数据。熟悉它也很有帮助。例如,甚至一些移动应用程序也可能使用 MySQL for Android。

  1. 使用现代 C++11 或更高版本的 for 循环迭代语法。

当前的for 循环语法非常不透明,可能有很多麻烦。 C++11 引入了更简洁的 for 循环语法来遍历标准库容器,例如 map 或 vector。使用:for(auto it : vector_name)。如果您需要修改每一个,请使用 it 的引用限定符--迭代器。

  1. 使用预增量语法以尽可能降低加速。

++i 和 i++ 略有不同。 ++i 只是在继续评估它之前直接修改它出现在表达式中的对象。 i++ 创建对象的副本,将其返回并递增原始对象。在 C++ 中创建值或对象的副本是有代价的,因此在某些情况下避免这样做可能会有所帮助,无论如何,这样做是一个很好的约定。

  1. 经过const &amp;。不仅仅是常规参考。

函数参数在 C++ 中默认按值传递。这意味着 C++ 只是制作对象的副本。但是,当对对象重复应用突变时,例如,使用函数随时间更改整数的值,您可能希望通过引用传递。引用基本上传递“真实”对象,这意味着您对引用所做的任何更改都是在“真实”对象上完成的。

现在,为什么要传递一个不可修改的对象?因为它可以带来更好的优化。通过常量引用传递允许编译器对您的代码做出更强的假设(例如,因为引用不能在程序过程中改变,在函数中多次引用相同的引用不需要重新加载参数的值重来一遍,因为它不应该在函数内部发生变化)。

  1. 使用std::unique_ptr 或std::shared_ptr。

智能指针也是 C++11 引入的一个很好的特性,它涉及通过将生命周期附加到范围来自动释放自身的指针。换句话说,不需要使用new 或delete——只需创建指针,您不应该必须跟踪何时释放内存。这在某些情况下可能会变得复杂,但总的来说,使用智能指针会带来更好的安全性和更少的内存管理问题变化,这就是它们首先被引入标准库的原因。

【讨论】:

  • 4 尚无定论。编译器将在for-loop 中进行optimize 前后递增和递减。
  • 第 7 项:是的,空格会使程序变慢,但不如那些讨厌的 cmets 慢
  • @GlennTeitelbaum 是的,我刚刚删除了它。这比无意义的垃圾更无意义。
  • 智能指针不会让代码运行得更快,只是更安全、更容易维护
  • @erip 我之所以这么说是因为这是一个很好的经验法则。必须处理具有 i++ 并创建奇怪行为的表达式可能很难找到,因为您认为它只是 i+1,而实际上它只是 i,然后是 i+1。我知道我以前绊倒过它,所以我把它放在那里。
【解决方案2】:

我认为这里有几个问题需要回答。第一个是:这个算法可以更高效吗?另一个是:我在 c++ 中实现算法的效率更高吗?

对于第一个问题,我会回答不。基于这个问题,在我看来,你没有进一步的信息可以让你做得比 O(n^2) 更好。

如 cmets 中所述,您可以遍历每个人并按到期日对他们的书进行排序。在实践中,这可以节省一些时间,但理论上书籍查找仍然是线性时间,O(n)。另外,您添加了排序的开销,使您的算法现在 O(mnlog(n)),其中 m 是顾客的数量,n 是书籍的数量。如果您知道您的顾客很少,每个人都有很多书,那么分类可能是有益的。如果您有很多顾客,但书籍很少,那将少得多。

至于第二个问题:有一些小的调整(和一些大的调整)可以使您的代码更高效,尽管我认为绝大多数情况下它们是不必要的。 我注意到的一件主要事情是,您在第一次 for 循环的每次迭代中都重新创建了一个向量对象。这样做会造成不必要的开销。试试这个伪代码:

currentDate++;
vector<Book*> ptr = members.at(i)->getCheckedOutBooks();
for(....)

另一个可能是大修的优化是删除向量库。 c++ 中的向量能够动态调整大小以及其他不必要的开销(对于您的任务)。简单地使用数组会更节省内存,尽管时间效率相当。

你提到你在第一学期,所以你可能还没有被介绍给Big O notation。

【讨论】:

  • 你确定你的O()用法吗?
  • 正确,我们还没有被介绍到大 O 表示法,但感谢您的链接!我只想在有空闲时间的时候开始吸收所有可以吸收的东西
  • @GlennTeitelbaum 相对肯定(尽管开放讨论)。我想说原始算法是 O(mn) 可能更正确,其中 m 是顾客数量,n 是书籍数量。在排序列表中查找与在未排序(线性)中查找相同,因为最坏的情况是每本书都已过期,因此我们遍历所有书籍。众所周知,排序充其量是 O(nlog(n))。所以是的,我支持我的 Big O 用法。
  • 无论有多少顾客,您都可以找到N 的书籍
  • 啊我明白你的意思,我应该更清楚。它是 O(mn),其中 m 是顾客数量,n 是每个顾客的书籍数量。
【解决方案3】:

如果这是您要优化的唯一操作,则保留一个由int 排序的tuple &lt;int, Book *, Patron * &gt; 向量,代表每一个签出书的到期日期 然后迭代,直到到期日期大于当前日期,对相关赞助人处以罚款。

【讨论】:

  • 很有趣,我们还没有介绍过,但我一定会尝试一下,谢谢!
  • 您是否考虑过您可能还想在成员签入时从该列表中删除图书?从这样的列表中删除会很昂贵,因为您不会从最后删除。
  • @hellogoodbye 优化通常是操作之间的权衡。如果应用罚款(通常是具有大 N 的批处理操作)是最关键的,那么正如 OP 所发布的,这将是我的解决方案。大多数图书馆将添加和删除书籍限制为一个小的 N,因此性能可能不那么重要。在平衡操作时,带有侧索引的链表可能总体上是最佳的,但这不是问题
  • @HelloGoodbye 是的,实际上,图书馆类中有一个函数可以在顾客归还图书时从顾客已签出书列表中删除一本书
  • @GlennTeitelbaum “平衡操作”是什么意思?
【解决方案4】:

如果您有n 已签出的书籍,其中m 已过期,您的算法需要O(n) 时间来添加罚款。这是因为你的数据结构存储这样的信息

member -> list(checked out books)
book -> check-out length // presumably the due date for returning the book

如果除了您的members 集合之外,您还存储以下信息:

check-out length -> list(checked out books with that due date)
book -> member who checked it out

然后您可以使用一个按到期日期存储所有已签出图书的排序树来查找O(log n) 中的所有过期图书。因此,您的算法的总渐近运行时间将从 O(n) 减少到 O(log n + m)。

【讨论】:

  • 我和你在同一条轨道上。您将使用哪种容器类型来存储具有特定到期日期的所有已签出书籍列表?因为这样的列表将取决于截止日期,所以您需要有许多不同类型的列表。
  • 没错,但是列表的数量是&lt;= n。我建议使用std::set 来存储由到期日期和在该日期到期的所有书籍的列表组成的结构。然后你需要为这些结构提供一个&lt; 操作符;在这里,您只需比较到期日。最后,您可以使用std::set::upper_bound 检索在某个日期之前到期的所有书籍。
  • 请注意,根据cplusplus.com/reference/set/set,std::set 通常是使用二叉搜索树实现的。
【解决方案5】:

您可以考虑将vector 替换为std::map 容器。地图存储为排序树。如果您定义比较结帐长度(或更可能是“过期日期”)的比较器函数,则无需每次都扫描整个列表。

一个更复杂的解决方案是将所有本书存储在一个按到期时间排序的指针树中。然后,您根本不必遍历成员。而是遍历书籍,直到找到尚未过期的书籍。

这更复杂,因为现在为每个成员添加/删除书籍(甚至迭代成员拥有的所有书籍)更加困难,并且可能需要为每个用户维护一个指针向量作为您当前的方法(除了全球图书地图)。

【讨论】:

  • 排序后的vector 走得更快
  • @GlennTeitelbaum:但添加条目的成本更高。所以假设用户借书和还书的频率,一棵保存所有书的树可能会更好。当然取决于涉及的书籍数量。由于内存效率和避免指针追逐,对于小数字向量更有效。
  • @nimrodm 现在不是映射一种链表形式吗?如果我记得,不久前有一个 CPPCon 演示文稿。
  • @VermillionAzure std::map 和相关容器通常实现为平衡二叉树。 C++ 标准规定了链表无法满足的某些操作的复杂性要求。
  • @Blastfurnace 我认为二叉树通常被构造为类似链表的结构。关键是他们使用指针。对吗?
【解决方案6】:

我使用 C++ 已经有一段时间了,但几乎所有标准库都会比您自己的实现更快。供您参考,查看与std::vector 相关的标准函数(这个站点非常有用)。

您也许可以通过其他一些过滤逻辑来精简ptr.size(),这样您就不必遍历那些没有迟交书籍的人(也许对书籍及其截止日期进行排序?)

【讨论】:

  • 这确实很有意义,感谢您的输入
【解决方案7】:

现在您在 O(n) 中修改罚款(n 是 getCheckOutLength().size()),但您可以在 O(log(n)) 中进行修改,因为您只需要迟交书籍的数量,而不是它们的罚款对象,如果你有这个数字,那么你将它乘以 0.01 并使用一个修正罚款函数来完成这一切。

所以这是我建议的方式:
如果您将 getCheckedOutBooks() 按其 getCheckOutLength() 排序在一个向量中,那么您可以通过在向量中查找 std::upper_bound 来找到哪些日期超过 curDate,该向量为您提供大于 currentDate 的第一个元素,所以从那个元素索引到向量的末尾是应该被罚款的书的数量,这里是代码:

int checkedDateComparator(Patron & leftHand, Patron & rightHand){
    return leftHand.getCheckedOutLength() < rightHand.getCheckOutLength();  
}
bool operator==(Patron & a, Patron & b){
    return a.getCheckedOutLength() < b.getCheckOutLength();
}
void Library::incrementCurrentDate()
{
    currentDate++;

    for (int i = 0; i < members.size(); i++)
    {
        vector<Book*> ptr = members.at(i)->getCheckedOutBooks();
        Book dummy; //dummy used for find the fines 
        dummy.setCheckedOutLength(currentDate);
        int overdue = ptr.end() - upper_bound(ptr.begin(), ptr.end(), dummmy, checkedDateComparator);
        members.at(i)->amendFine(overdue* .01);
   }
} 

【讨论】:

    【解决方案8】:

    让我们退后一步,看看需求。当您去图书馆并有一些可能延迟借出的书籍时,您可能会问图书管理员您欠什么。图书管理员查看您的帐户并告诉您。那时您应该计算费用。您现在正在做的是每天午夜重新计算费用(我假设)。这就是效率低下的部分。

    让我们改用这个用例:

    1. 图书馆员试图检查顾客的书籍
    2. 系统计算费用
    3. 赞助人支付任何未结费用
    4. 图书管理员检查书籍

    与您的问题相关的部分是第 2 步。这是伪代码:

    float CalculateFees(Patron patron)
    {
        float result = 0;
        foreach(checkedOutBook in patron.GetCheckedOutBooks())
        {
            result += CalculateFee(checkedOutBook.CheckOutDate(), today);
        }
        return result;
    }
    
    float CalculateFee(Date checkOutDate, Date today)
    {
        return (today.Day() - checkOutDate.Day()) * 0.10;
    }
    

    整个用例可以很简单:

    void AttemptCheckout(Patron patron, BookList books)
    {
        float fees = CalculateFees(patron);
        if(fees == 0 || (fees > 0 && PatronPaysFees(patron, fees)))
        {
            Checkout(patron, books);
        }
        else
        {
            RejectCheckout(patron);
        }
    }
    

    我以一种可以轻松更改费用公式的方式编写此内容。某些类型的材料产生的罚款与其他类型的材料不同。罚款可能有一定的上限。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-04
      • 2012-01-06
      相关资源
      最近更新 更多