簡體   English   中英

編寫此算法的更有效方法?

[英]More efficient way to write this algorithm?

目前正在從事圖書館模擬器的工作。 一切工作正常,但我只是想了解一些信息。

在此程序中,共有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 ++編程方法來實現此功能?

  1. 如果大小不是很大,請使用std::vector

由於涉及間接性,指針總是要付出一定的代價。 查找地址並在內存中訪問地址可能無法通過編譯器優化,因此將涉及訪問內存的成本。 內存訪問通常是系統性能的瓶頸,因此最好嘗試使內存中的內容相互靠近,並嘗試構建程序,以使訪問內存最少。

  1. 如果數據過大,請使用SQL之類的數據庫系統。

另一方面,我們可以放棄所有繁瑣的工作,而使用已建立的數據庫庫或程序。 諸如MySQL之類的東西可以使用出色的編程語言輕松地管理大量數據,從而也可以訪問和管理它們。 某些數據庫(例如PostgreSQL)可以擴展到大量數據。 熟悉它也很有幫助。 例如,甚至某些移動應用程序可能都將MySQL用於Android。

  1. for循環迭代語法,請使用現代的C ++ 11或更高版本。

當前的for循環語法非常不透明,可能有很多不足之處。 C ++ 11引入了一種更清晰for循環語法,可在諸如mapvector標准庫容器中進行迭代。 使用: for(auto it : vector_name) 如果需要修改每個參數,請為it使用參考限定符-迭代器。

  1. 使用預遞增語法可能會最小化加速。

++ii++略有不同。 ++i只是直接修改對象在表達式中的位置,然后再繼續對其求值。 i++創建對象的副本,將其返回並遞增原始對象。 在C ++中,創建值或對象的副本會產生成本,因此避免在某些情況下會有所幫助,並且無論如何都是一個好習慣。

  1. 通過const &傳遞。 不只是定期參考。

在C ++中,默認情況下,函數參數按值傳遞。 這意味着C ++只會復制對象。 但是,當有重復的突變應用於對象時,例如,使用函數隨時間更改整數值,您可能希望通過引用傳遞。 引用基本上會傳遞“真實”對象,這意味着您對引用所做的任何更改都是在“真實”對象上完成的。

現在,為什么要傳遞不可修改的對象? 因為它可以導致更好的優化。 通過常量引用傳遞可使編譯器對您的代碼進行更強的假設(例如,由於引用不能在程序過程中更改,因此在函數中多次引用相同的引用不需要重新加載參數的值再來一次,因為它在函數內部時不應更改)。

  1. 使用std::unique_ptrstd::shared_ptr

智能指針也是C ++ 11引入的一項不錯的功能,它涉及通過將生命周期附加到作用域來自動釋放自身的指針。 換句話說,無需使用newdelete ,只需創建指針即可, 不必跟蹤何時釋放內存。 在某些情況下,這可能會變得復雜,但是總的來說,使用智能指針可以提高安全性,並減少內存管理問題的變化,這就是為什么首先將它們引入標准庫的原因。

我認為有兩個問題需要回答。 第一個是:此算法能否更有效? 另一個是:我在c ++中實現算法的效率更高嗎?

對於第一個問題,我不會回答。 基於這個問題,在我看來,您沒有比O(n ^ 2)更好的信息。

如評論中所述,您可以遍歷每個人,並按截止日期對他們的書進行排序。 實際上,這可以節省一些時間,但從理論上講,書本查找仍然是線性時間O(n)。 另外,您還增加了使算法現在排序為O(mnlog(n))的開銷,其中m是用戶數,n是書本數。 如果您知道您的顧客很少,每人都有很多書籍,那么分類可能會有所幫助。 如果您有很多主顧而很少有書,那將沒有太多好處。

至於第二個問題:盡管我認為大多數時候它們是不必要的,但是有一些小的調整(和一些大的調整)可以使您的代碼更高效。 我注意到的一件主要事情是,您在第一次for循環的每次迭代中都會重新創建向量對象。 這樣做會造成不必要的開銷。 嘗試改用以下偽代碼:

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

可能需要大修的另一個優化是刪除Vector庫。 C ++中的向量具有即時調整大小的能力以及其他不必要的開銷(用於您的任務)。 盡管等效地節省了時間,但簡單地使用數組將提高存儲效率。

您提到在第一學期就讀,因此您可能尚未被Big O標記法介紹。

如果那是您要優化的唯一操作,則保留一個tuple <int, Book *, Patron * >的向量,並按表示evey檢出書的到期日期的int排序,然后迭代直到到期日期大於當前申請日期罰款相關的贊助人。

如果你已經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)

您可以考慮將vector替換為std::map容器。 地圖存儲為分類樹。 如果定義比較器功能來比較結帳長度(或更可能是“到期日期”),則無需每次都掃描整個列表。

一種更復雜的解決方案是將所有書籍存儲在按其過期時間排序的單個指針樹中。 然后,您根本不必遍歷成員。 而是遍歷書籍,直到找到一本尚未過期的書籍。

這更加復雜,因為現在為每個成員添加/刪除書籍(甚至遍歷一個成員擁有的所有書籍)更加困難,並且可能需要作為當前方法為每個用戶維護一個指針向量(除了全局書籍地圖之外) )。

自從我使用C ++已經有一段時間了,但是幾乎總是標准庫會比您自己的實現更快。 供您參考,請檢查與std :: vector關聯的標准函數(此站點非常有用)。

您可能可以通過其他一些過濾邏輯來ptr.size() ,這樣您就不必遍歷那些沒有遲到書籍的人(也許對書籍及其到期日期進行了排序?)

現在,您要修改O(n)的罰款(n為getCheckOutLength()。size()),但您可以在O(log(n))中進行罰款,因為您只需要一些較晚的書而不是它們的對象即可進行細化,如果您有該數字,則可以將其乘以.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);
   }
} 

讓我們退后一步,看看需求。 當您去圖書館看書時,可能會問圖書館員欠什么。 圖書管理員會查詢您的帳戶並告訴您。 在這一點上,您應該計算費用。 您現在正在做的是在每個午夜重新計算費用(我假設是)。 那是效率低下的部分。

取而代之的是這個用例:

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

我以一種易於更改費用公式的方式編寫了此代碼。 某些類型的材料產生的罰款與其他類型的材料不同。 罰款可能會被限制在一定數量。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM