目次
LRU は、
Least Recently Used
Least Recent Used の頭字語です。その理論的根拠は
時間的局所性
です。「最近使用されたデータは将来も一定期間使用され、長期間使用されなかったデータは将来長期間使用されない可能性が高い。」
トピックの紹介
146. LRU キャッシュ - LeetCode は、
習得したデータ構造を使用して、LRU (最も最近使用されていない) キャッシュ メカニズムを設計および実装します。LRUCache クラスを実装します。
- LRUCache(int Capacity) 容量として正の整数を使用して LRU キャッシュを初期化します。
- int get(int key) キー key がキャッシュに存在する場合はキーの値を返し、そうでない場合は -1 を返します。
- void put(int key, int value) キーが既に存在する場合はそのデータ値を変更し、キーが存在しない場合は「キーと値」のセットを挿入します。キャッシュ容量が上限に達すると、新しいデータを書き込む前に最も古い未使用のデータ値を削除して、新しいデータ値のためのスペースを確保する必要があります。
get 関数と put 関数は、平均時間計算量 O(1) で実行する必要があります。
トピック分析
O(1)
理想的な LRU は、20 分以内にデータの一部を読み取ったり、データを更新したりできる必要があります。つまり、関数 get と put は平均時間計算量 O(1) で実行する必要があります。ハッシュ テーブルを考えるのは簡単です。データのキーに応じて、o(1)
複雑な時間内で読み取りおよび書き込みの速度を簡単に達成できます。また、ハッシュテーブルのみを使用した場合、アクセス時間が最も早いデータを特定するには、すべてのテーブルを走査して見つける必要があるため、アクセス時間によるソートを維持し、アクセスが最も多いデータを見つける必要があります。時間計算量内で最近使用されたデータo(1)
、、最後に使用された要素 (アクセス頻度が最も低いものを削除)、要素の削除 (ノードの高速削除)。これを踏まえて、ハッシュテーブル+二重リンクリストの実装を検討します。その論理構造図は次のとおりです。
簡単に説明すると、キャッシュとは、キャッシュされたデータのキーを介して二重リンクリスト内の位置にマッピングされるハッシュテーブル (HashMap) を表します。二重リンク リストには、これらのキーと値のペアが使用される順序で格納されます。先頭に近いキーと値のペアが最も最近使用され、末尾に近いキーと値のペアが最も古いものになります。
注: 二重リンク リストの実装では、ダミー ヘッド (ダミー ヘッド) とダミー テール (ダミー テール) を使用して境界をマークします。そのため、ノードの追加および削除時に隣接するノードが存在するかどうかを確認する必要はありません。ノード。
この構造に基づいて、そのデータ構造は次のように実装されます。
public class LRUCache {
// 双向链表的节点元素
class DLinkedNode {
int key; // 关键字
int value; // 对应的值
DLinkedNode prev; // 双向链表的前驱指针
DLinkedNode next; // 双向链表的后继指针
}
// 使用java自带的 HashMap 来模拟 Cache表
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size; // 目前缓存中有效数据的个数
private int capacity; // LRU缓存容量大小
private DLinkedNode head, tail; // 双向链表的头结点和尾结点
}
説明、これは146 用に設計された構造です。LRU キャッシュ - Lituoをより広く使用したい場合は、パラダイムの使用を検討できます。そして、Cacheのハッシュテーブルを自分で実装します(配列+赤黒ツリー:チェーンアドレス法で競合を解決し、時間計算量で要素を見つけるために赤黒ツリーでリンクリストを最適化します)
o(log(n))
。
図の例
上記のデータ構造に基づいて、容量 4 の LRU キャッシュをシミュレートします。さまざまな状況に応じて、分析は次のようになります。
キャッシュテーブルがいっぱいではなく、データを挿入した後にミスする場合
最初に 4 つの要素(k1,value1)
、(k2,value2)
、(k3,value3)
、を追加します(k4,value4)
。これら 4 つの挿入はCache表
完全ではなく、ハッシュ値の計算時に競合が発生しないためです。したがって、状況は同じです。ここで説明するput(k4, value4)
例。まず、現在のキャッシュ内の有効なデータがいっぱいではないと判断し、次にキーk4
のハッシュ値を計算しHash(k4)
、競合がないことが判明したため、二重リンクを作成します。 list ノードを作成し、データを (key, value) にロードします。次に、二重リンク リストの先頭ポインタを見つけて、head
先頭に要素を追加し、Hash(k4)
キャッシュ内の対応するポインタ フィールドを要素ノードにポイントします。
これら 4 つの要素を挿入した後、構造内のデータに対応する論理構造は次のようになります。
キャッシュテーブルがいっぱいで、データの挿入後にミスした場合
4つの要素を追加した後k1, k2, k3, k4
、この時点でput(k5, value5)
要素を追加することでテーブルの容量を超えていることが分かりますCache
が、ハッシュ値を計算すると競合がないことが分かり、引き続き二重リンクリストノードが作成されます、k5
ノードのデータを入力し、双方向で要素ノードを連結リストの先頭に追加します。k5
ノードを追加すると容量を超えるため、二重リンク リストの末尾ポインタの先行ノードが削除され、最も長く未使用の要素がキャッシュから残るようになります。(ここでポイントがあります。つまり、Cache
テーブルの処理については、ハッシュ テーブルであり、競合はチェーン アドレス方式で処理されるため、ハッシュ テーブル内の要素を削除できます。他の方式であれば、 、ハッシュ テーブル要素内の要素を削除する方法)。処理フローは次のとおりです。ラベルは実行順序を示します。
データ挿入後のミスの場合
k1, k2, k3, k4
4 つの要素を追加した後、put(k3, value3)
要素を追加し続けます (ここで要素をすべて追加する必要はありません)。テーブル内にCache
計算がHash(k3)
存在することを確認します。したがって、テーブルを変更する必要はありません。代わりに、テーブル内の二重リンクリスト内の対応するノードが先頭に移動されます。k3
Cache
Cache
Cache
k3
上記 3 つの状況の分析に基づいて、次のように結論付けることができます。
- 二重リンクリストに要素を追加するたびに先頭から追加されます。
- 要素が削除されるたびに、末尾から削除されます
- 削除する場合は、ハッシュテーブルから対応するキーも同時に削除する必要があります
- 再度アクセスする要素をリンクリストの先頭に移動する必要がある
アルゴリズムの実装
主に 3 つのメソッドを実装するために使用されます。
LRUCache(int capacity)
:の容量のcapacity
バッファを初期化します。int get(int key)
:key
キーワードがキャッシュに存在する場合はキーワードを返し、存在しvalue
ない場合はキーワードを返します-1
。void put(int key, int value)
:key
キャッシュに保存され、LRU
の特性が維持されます。
取得操作
get
この操作では、まず次のkey
ものが存在するかどうかを判断します。
key
存在しない場合は返します-1
- 存在する場合
key
、key
対応するノードは最後に使用されたノードです。ハッシュ テーブルを通じて二重リンク リスト内のノードの位置を特定し、それを二重リンク リストの先頭に移動し、最後にノードの値を返します。
public int get(int key) {
DLinkedNode node = cache.get(key); //从Cache中获取对应双向链表中的结点地址
// key不存在
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部,先删除:
node.prev.next = node.next;
node.next.prev = node.prev;
// 再插入到头部
node.next = head.next;
head.next.prev = node;
node.prev = head;
head.next = node;
return node.value;
}
置く操作
key
存在しない場合は、key
と を使用しvalue
て新しい二重リンク リスト ノードを作成し、そのノードを二重リンク リストの先頭に追加し、そのkey
ノードのアドレスをハッシュ テーブルに追加します。次に、二重リンクリストのノード数が容量を超えているかどうかを判断し、容量を超えている場合は、二重リンクリストの末尾ノードを削除し、ハッシュテーブル内の対応する項目を削除します。- 存在する場合
key
、これはget
操作と同様であり、最初にハッシュ テーブルを通じて検索し、次に対応するノードの値を に更新しvalue
、そのノードを二重リンク リストの先頭に移動します。
public void put(int key, int value) {
DLinkedNode node = cache.get(key); //从Cache中获取对应双向链表中的结点地址
// 如果 key 不存在
if (node == null) {
// 创建一个新的双向链表节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
newNode.next = head.next;
head.next.prev = newNode;
newNode.prev = head;
head.next = newNode;
++size;//容量 + 1
// 如果超出容量,删除双向链表的尾部节点
if (size > capacity) {
DLinkedNode temp = tail.prev;
// 删除结点
temp.prev.next = tail;
tail.prev = temp.prev;
// 删除哈希表中对应的项
cache.remove(temp.key);
--size;
}
} else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
node.prev.next = node.next;
node.next.prev = node.prev;
// 再插入到头部
node.next = head.next;
head.next.prev = node;
node.prev = head;
head.next = node;
}
}
コードを再構築する
公式の回答によると、いくつかの単純な操作をカプセル化していることがわかります。つまり、put
との操作中にget
、二重リンクリストの処理には主に删除结点
、从头结点插入
、从尾结点删除
、が含まれます移动结点到头结点中
。
//删除结点
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
//从头结点插入
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
//从尾结点删除
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
//移动结点到头结点中
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
二重リンクリストの操作ロジックを次の図に示します。
ノードを削除します。
先頭ノードから挿入します。
末尾ノードから削除します。
ノードをヘッド ノードに移動します。
ハッシュマップ拡張機能
上記の内容では、java
組み込みのコレクションを使用してHashMap
実現しています。そこで、ハッシュ値計算後は一定の確率で競合が発生するため、競合要素についてはチェーンアドレス方式で競合を解決できると説明します。はい、図に示すように、次のように維持されます。
java
しかし、これでは連結リストが長くなりすぎて検索効率が低下しやすいため、単一連結リストをバランスの取れた二分木(赤黒木が内部に維持される)として維持することができますo(lg(n))
。要素は時間計算量の条件下で見つけることができます。今すぐ: