データ構造---LRUキャッシュ

LRUとは何ですか

これまでの研究により、コンピュータがタスクを処理する際には、まずハードディスクからデータを取り出してメモリにロードし、次にメモリ上のデータをCPUにロードして計算することがわかっていますが、ここには問題があります。 . CPU の計算速度は非常に速いですが、メモリへのデータの読み込み速度は非常に遅いため、マシン全体の作業効率を向上させるためには、メモリと CPU コンピュータにキャッシュと呼ばれるものを追加する必要があります。狭義のキャッシュは、CPU とメイン メモリ (キャッシュ) の間にある高速 RAM を指します。通常、システム メイン メモリのような DRAM テクノロジは使用されず、高価ですが高速な SRAM テクノロジが使用されます。広義のキャッシュとは、速度差の大きい2種類のハードウェアの間に位置し、両者のデータ転送速度の差を調整するために使用される構造を指します。CPUとメインメモリ間のキャッシュに加えて、メモリとハードディスクの間にもキャッシュがあり、ハードディスクとインターネット一時フォルダまたはネットワークコンテンツと呼ばれるネットワークの間にも、ある意味でのキャッシュがあります。キャッシュなど ただし、キャッシュの容量には制限があるため、キャッシュの容量が使い果たされて新しいコンテンツを追加する必要がある場合は、新しいコンテンツ用のスペースを確保するために、元のコンテンツの一部を選択して破棄する必要があります。次に、データのどの部分が削除されるかは LRU によって決まります。LRU を設計するのは難しくありませんが、効率的な LRU を設計するのは非常に困難です。ここでの効率とは、あらゆる操作の時間計算量が o( 1) であることを意味します。 LRU キャッシュの置換原則は、最も最近使用されていないコンテンツを置換することです。実際、LRU を「最長未使用」と翻訳した方がより鮮明です。アルゴリズムが長期間使用されていないコンテンツを置き換えるたびに、質問を使用して実装をシミュレーションすることになるからです。 LRUの質問 内容は以下の通りです
ここに画像の説明を挿入します
タイトルに記載されているコードは以下の通りです。

class LRUCache {
    
    
public:
    LRUCache(int capacity) {
    
    

    }
    
    int get(int key) {
    
    

    }
    
    void put(int key, int value) {
    
    

    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

LRUの実装をシミュレーションする

上記の説明から、get 関数はキャッシュ内のデータを検索することがわかります。見つかった場合はデータの内容を返します。見つからなかった場合は -1 を返します。そして、put 関数には次の 2 つの関数があります。 1 つはコンテナーにデータを挿入すること、もう 1 つは更新することです。コンテナー内のデータを、上記の機能を実現するためにハッシュ テーブルを追加することで、データを見つける時間の計算量は o( 1)、データの削除の時間計算量も o(1)、データの更新の時間計算量も o(1) ですが、これには問題があります。ここでの時間計算量は要件を満たしていますが、ここで実装したいのは LRU ですが、データを削除するときにどちらの優先度が高いかはどのようにしてわかりますか? 優先度は低いですか? 簡単に言えば、どのデータを削除するべきかをどうやって知ることができるのでしょうか? したがって、ハッシュ テーブルだけでは要件を満たせないため、この際、実装を支援するためにプライオリティ キューを追加できないかという質問がありました。答えは「いいえ」です。プライオリティ キューは確かにデータの優先度を並べ替えることができます。誰の優先度が最も高く、誰が最も低いかをすぐに知ることができます。しかし、ここには問題があります。データを更新すると、優先度は次のようになります。変調が最も高いので、データ優先キュー内のその要素をどのように調整しますか? 調整できるとしても、時間計算量が o(1) になることは保証できますか? したがって、この方法は絶対に不可能です。現時点では、友人の中には、参照カウントの形で実装できるかどうか尋ねる人もいます。答えはイエスですが、それでも実装するのは面倒です。私たちは非常に簡単な方法を持っています。リンクされたリストを追加することです。これを支援するために、ここでのコードは次のとおりです。

class LRUCache {
    
    
public:
    LRUCache(int capacity) {
    
    

    }
    
    int get(int key) {
    
    

    }
    
    void put(int key, int value) {
    
    

    }
private:
    unordered_map<int, int> _hashmap;
    list<pair<int,int>> _l1;
};

検索と追加は計算量O(1)のハッシュテーブルで行うことができ、削除時はリンクリスト内の要素の位置に基づいて優先度を判断することができ、要素が更新されるとリンクリストはつまり、リンク リストの先頭にあるノードの優先度が高く、逆にリンク リストの最後にあるノードの優先度が低くなります。リンクされたリストの最後にあるものが最初に削除されます。これは合理的に思えますが、更新時間の計算量は o(1) になる可能性がありますか? 不可能だと思われますよね?リンク リストはランダム アクセスをサポートしていないため、更新するノードを見つけるためにループする必要があります。その場合、このステップの時間計算量は O(1) ではなく、ましてや後続のステップなので、ここを改善する必要があります。ハッシュ テーブルは、時間計算量 O(1) の中で必要なデータを見つけるのに役立ちます。問題を更新したい場合は、ハッシュ テーブル内のデータを見つけて、リンクリストに対応するデータの位置も同時に求めるので、ハッシュテーブルに記録されている要素の型を元のint型からリストクラスのイテレータ型に変更する方法です。次のように:

unordered_map<int, list<pair<int,int>>::iterator> _hashmap;
list<pair<int,int>> _l1;

ハッシュ テーブル内の各要素には、リンク リスト内の対応する要素の位置を記録する要素があるため、ハッシュ テーブル内の要素を見つけるのに O(1) 時間を使用でき、次の方法でリンク リスト内の要素も見つけることができます。コンストラクターは、「容量という名前の変数は、このコンテナーが保存できるデータの数を表します。後でデータを削除しやすくするために、LRU の容量を表す別のデータをここに追加し、typedef を使用します。」コードの長さを短縮するために名前を変更します。その後、コンストラクターで、capacity 変数を初期化するだけで済みます。このときのコードは次のようになります。

class LRUCache {
    
    
public:
typedef list<pair<int,int>>::iterator LiIter;
    LRUCache(int capacity) 
        :_capacity(capacity)
    {
    
    }
    
    int get(int key) {
    
    

    }
    
    void put(int key, int value) {
    
    

    }
private:
    unordered_map<int,LiIter> _hashmap;
    list<pair<int,int>> _l1;
    size_t _capacity;
};

get 関数の実装は非常に簡単です。まず、find 関数を使用して現在のデータが存在するかどうかを確認します。次に、find 関数を使用してこれを実現します。find 関数の戻り値がデータの終わりである場合は、ハッシュ テーブルの場合、現在のデータが存在しないことを意味します。終了でない場合は、まず操作 -> によって 2 番目の内部データを取得しますが、ここのデータはまだリンク リストを指すイテレータであるため、ここで次のようにします。 2 番目のデータを取得するには、演算子 -> を使用する必要があります。ここでのコードは次のとおりです。

int get(int key) {
    
    
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
    
    
        return ret->second->second;
    }
    else
    {
    
    
        return -1;
    } 
}

しかし、ここでデータを検索したので、ここで終わりではなく、このデータの優先順位を変更する必要があります、つまり、要素が配置されているリンクされたリストの位置がリストに追加されます。一つ目は、現在の要素を記録し、その要素を削除してリンクリストの先頭に同じデータを挿入し、最後にイテレータのポイントを修正する方法ですが、この方法は面倒です。コンテナ内の splice 関数を直接使用してノードを転送します。この関数のパラメータ形式と機能は次のように紹介されます:
ここに画像の説明を挿入します
ここに画像の説明を挿入します
独自のリンク リスト内のノードを独自のリンク リストの先頭位置に転送できます。以下のとおりであります:

int get(int key) {
    
    
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
    
    
        LiIter it =ret->second;
        _l1.splice(_l1.begin(),_l1,it);
        return it->second;
    }
    else
    {
    
    
        return -1;
    }
}

put関数は追加する場合と挿入する場合に分かれますが、まず現在のデータが存在するかどうかを判断しましょう。

void put(int key, int value) {
    
    
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
    
    
        //元素存在那么这里就是更新
    }
    else
    {
    
    
        //元素不存在这里是插入
    }
}

現在挿入している場合はここで判断する必要がありますが、ここで判断する場合、リスト内のサイズは逐次走査であるため、リストのサイズ関数は使用できず、ハッシュのサイズ関数が使用されます。複雑度は O(N)、ハッシュ サイズ関数は です ハッシュ内のサイズは内部データを直接返すため、オブジェクトがいっぱいになった場合はデータを削除する必要があります まず、オブジェクトの末尾のデータを記録する変数を作成しますリンク リストを削除し、ハッシュ消去関数を使用してデータを削除し、最後に Pop_back 関数を使用してリンク リストを削除します。データの末尾の場合、ここでのコードは次のとおりです。

void put(int key, int value) {
    
    
   auto ret=_hashmap.find(key);
   if(ret!=_hashmap.end())
   {
    
    
       //元素存在那么这里就是更新
   }
   else
   {
    
    
       //元素不存在这里是插入
       if(_capacity==_hashmap.size())
       {
    
    
           //满了就要删除
           pair<int,int> tmp=_l1.back();
           _l1.pop_back();
           _hashmap.erase(tmp.second);
       }
   }
}

データを削除した後、リンク リストの先頭にデータを挿入し、次にハッシュ テーブルにデータを挿入し、要素の 2 番目の要素をコンテナの先頭に初期化します。このコードは次のようになります。

void put(int key, int value) {
    
    
    auto ret=_hashmap.find(key);
    if(ret!=_hashmap.end())
    {
    
    
        //元素存在那么这里就是更新
    }
    else
    {
    
    
        //元素不存在这里是插入
        if(_capacity==_hashmap.size())
        {
    
    
            //满了就要删除
            pair<int,int> tmp=_l1.back();
            _l1.pop_back();
            _hashmap.erase(tmp.second);
        }
        _l1.push_front(make_pair(key,value));
        _hashmap[key]=_l1.begin();
    }
}

現在のオブジェクトがいっぱいでない場合は、 value の値と、リンクされたリスト内の現在の要素の位置を更新する必要があります。ここでの考え方は前のものと同じですが、格納されている値を変更する必要があります。これは次のとおりです。

    void put(int key, int value) {
    
    
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
    
    
            //元素存在那么这里就是更新
             LiIter it =ret->second;
             it->second=value;
            _l1.splice(_l1.begin(),_l1,it);
        }
        else
        {
    
    
            //元素不存在这里是插入
            if(_capacity==_hashmap.size())
            {
    
    
                //满了就要删除
                pair<int,int> tmp=_l1.back();
                _l1.pop_back();
                _hashmap.erase(tmp.second);
            }
            _l1.push_front(make_pair(key,value));
            _hashmap[key]=_l1.begin();
        }
    }

これを書くとコードは完成するので、完全なコードは次のようになります。

class LRUCache {
    
    
public:
typedef list<pair<int,int>>::iterator LiIter;
    LRUCache(int capacity) 
        :_capacity(capacity)
    {
    
    }
    
    int get(int key) {
    
    
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
    
    
            LiIter it =ret->second;
            _l1.splice(_l1.begin(),_l1,it);
            return it->second;
        }
        else
        {
    
    
            return -1;
        }
    }
    
    void put(int key, int value) {
    
    
        auto ret=_hashmap.find(key);
        if(ret!=_hashmap.end())
        {
    
    
            //元素存在那么这里就是更新
             LiIter it =ret->second;
             it->second=value;
            _l1.splice(_l1.begin(),_l1,it);
        }
        else
        {
    
    
            //元素不存在这里是插入
            if(_capacity==_hashmap.size())
            {
    
    
                //满了就要删除
                pair<int,int> tmp=_l1.back();
                _l1.pop_back();
                _hashmap.erase(tmp.first);
            }
            _l1.push_front(make_pair(key,value));
            _hashmap[key]=_l1.begin();
        }
    }
private:
    unordered_map<int,LiIter> _hashmap;
    list<pair<int,int>> _l1;
    size_t _capacity;
};

質問テストの結果は次のとおりです。
ここに画像の説明を挿入します
つまり、コードに問題がないことを意味し、この記事はここで終了します。

Supongo que te gusta

Origin blog.csdn.net/qq_68695298/article/details/131965629
Recomendado
Clasificación