冷たい面接官は、時間をつぶすために LRU キャッシュ除去アルゴリズムを手作業で書くように私に依頼しました。

冷たい面接官は、時間をつぶすために LRU キャッシュ除去アルゴリズムを手作業で書くように私に依頼しました。

[[384337]]

この記事はWeChatの公開アカウント「Xiaolangmazhida」から転載したもので、著者はSimon Langです。この記事を転載する場合は、Xiaolangmazhida の公開アカウントにご連絡ください。

背景

効率性を追求する私たちの世界では、何かを待つときにとてもイライラするようです。Web ページを更新できないとイライラしますし、コンピューターがプログラムを実行するのが遅いとイライラします。では、私たちは何をすべきでしょうか? 技術の創造は私たちに役立つのではないでしょうか? 今日は、キャッシュの技術についてお話しし、私たちがよく知っているデータ構造、つまりリンク リストを使用して LRU キャッシュ削除アルゴリズムを実装します。

リンク リストを使用して LRU キャッシュ削除アルゴリズムを実装する方法を学ぶ前に、いくつかの質問を提起しましょう。それらについて慎重に考えてみましょう。質問は次のとおりです。

  • キャッシュとは何ですか?また、キャッシュは何をしますか?
  • キャッシュ除去戦略は何ですか?
  • リンク リストを使用して LRU キャッシュ削除アルゴリズムを実装する方法、その特性、および最適化する方法を教えてください。

さて、上記の質問に答えて次の研究に進みましょう。

1. キャッシュとは何ですか? また、その機能は何ですか?

キャッシュとは、簡単に言えば、データのコピーを保存して、後ですぐにアクセスできるようにすることです。コンピュータの使用シナリオを例に挙げてみましょう。CPU がメモリ内のデータにアクセスする場合、まずキャッシュを検索します。キャッシュが見つかった場合は、直接使用されます。キャッシュが見つからない場合は、メモリを検索する必要があります。

同様に、データベース アクセスのシナリオでは、プロジェクト システムがデータベース内のデータの一部をクエリする必要がある場合、最初に要求でキャッシュをクエリできます。ヒットした場合は、キャッシュされた結果が直接返されます。ヒットしなかった場合は、データベースをクエリし、クエリ結果をキャッシュに格納します。次に要求されたときに、キャッシュがヒットした場合は、データベースを再度クエリせずに結果を直接返します。

上記の 2 つの例から、シナリオが何であっても、最初にキャッシュ、次にメモリ、次にキャッシュ、次にデータベースという順序があることがわかりました。しかし、キャッシュの存在はメモリ空間の一部を占有するため、キャッシュは空間を時間と交換する典型的な例であり、データのリアルタイム性を犠牲にしながらも、コンピュータの動作の高効率性を満たします。

よく考えてみると、私たちは日々の開発の中でキャッシュの例にかなり多く遭遇します。

  • オペレーティング システム キャッシュ

ディスクのやり取りを減らす

  • データベースキャッシュ

データベースクエリを削減

  • Web サーバー キャッシュ

アプリケーションサーバーへのリクエストを減らす

  • クライアントブラウザキャッシュ

ウェブサイトへの訪問を減らす

2. キャッシュ除去戦略は何ですか?

キャッシュの本質は、スペースを時間と交換することであるため、キャッシュの容量は制限される必要があります。キャッシュがいっぱいになった場合、キャッシュ内のどのデータをクリアし、どのデータを保持する必要がありますか? これには、キャッシュ削除戦略を決定する必要があります。

実際、一般的に使用されているキャッシュ削除戦略は 3 つあります。先入れ先出し FIFO、一定期間にアクセスが最も少ないページを削除する (最も頻繁に使用されない LFU)、最も長い間使用されていないページを削除する (最も最近使用されていない LRU)

これらのアルゴリズムは、異なるレベルのキャッシュで実行された場合に効率が異なるため、特定のシナリオに基づいて選択する必要があります。

2.1 FIFOアルゴリズム

FIFO アルゴリズムは先入れ先出しアルゴリズムであり、多くの場合キューを使用して実装されます。キャッシュの設計原則は、データが最初にキャッシュに入った場合は、最初に削除されるというものです。

FIFOアルゴリズム

新しくアクセスされたデータは FIFO キューの最後に挿入され、キュー内のデータはキューの先頭から順番に移動します。

キューがいっぱいになったら、キューの先頭にあるデータを削除します

2.2 LRUアルゴリズム

LRU アルゴリズムは、データへの履歴アクセス数に基づいてデータを排除し、通常はリンク リストを使用して実装されます。キャッシュでは、データが最近アクセスされた場合、将来もアクセスされる可能性が高くなるという設計原則があります。

LRUアルゴリズム

  • 新しく追加されたデータはリンクリストの先頭に挿入されます
  • キャッシュ ヒットが発生するたびに (つまり、キャッシュされたデータにアクセスするたびに)、データはリンク リストの先頭に移動されます。
  • リンクリストがいっぱいになると、リンクリストの末尾のデータは破棄されます。

2.3 LFUアルゴリズム

LFU アルゴリズムは、データの過去のアクセス頻度に基づいてデータを削除します。したがって、LFU アルゴリズムの各データ ブロックには参照カウントがあります。すべてのデータ ブロックは参照カウントに従ってソートされ、同じ参照カウントを持つデータ ブロックは時間順にソートされます。キャッシュでは、データが何度もアクセスされると、将来さらに頻繁にアクセスされるという原則に基づいて設計されています。

LFUアルゴリズム

  • 新しく追加されたデータはキューの末尾に挿入されます (参照カウントは 1 です)。
  • キュー内のデータにアクセスした後、参照カウントが増加し、キューが並べ替えられます。
  • データを削除する必要がある場合は、ソートされたリスト内の最後のデータ ブロックを削除します。

3. リンク リストを使用してキャッシュ削除を実装する方法、その特徴、最適化する方法を教えてください。

上記の記事では、キャッシュと削除戦略の概念を理解しました。その中でも、LRUアルゴリズムは筆記試験/面接で頻繁にテストされます。秋に採用活動をしていたとき、多くの企業からこのアルゴリズムを手動で記述するように求められました。誰もが陥る落とし穴を避けるために、以下にLRUキャッシュ削除アルゴリズムを手動で記述してみましょう。

リンクリストには複数の形式があることは誰もが知っていますが、どれを選択すればよいのでしょうか?

3分ほど考えてみてください...

さあ、答えを発表しましょう!

実際、リンク リストは、接続構造の違いにより、単一リンク リスト、循環リンク リスト、二重リンク リストに分類できます。

  • 単方向リンクリスト
    • 各ノードには、後続ポインターという 1 つのポインターのみが含まれます。
    • 単一リンク リストには、最初のノードと末尾のノードという 2 つの特別なノードがあります。最初のノードのアドレスはリンク リスト全体を表し、末尾のノードの後続ポインターは空のアドレス null を指します。
    • パフォーマンス特性: ノードの挿入と削除の時間計算量は O(1) で、検索の時間計算量は O(n) です。
  • 循環リンクリスト
    • 末尾ノードの後続ポインターが最初のノードのアドレスを指していることを除いて、すべてが単一のリンク リストと一致しています。
    • ジョセフ問題などの周期的な特性を持つデータを保存するのに適しています。
  • 二重リンクリスト
    • データの保存に加えて、ノードには前のノードアドレス(前任者ポインタprev)と次のノードアドレス(後任ポインタnext)を指す2つのポインタもあります。
    • 最初のノードの先行ポインター prev と末尾ノードの後続ポインターは両方とも空のアドレスを指します。

二重リンクリストが単一リンクリストよりも優れている主な利点は、先行ノードの検索にかかる時間計算量が O(1) であるのに対し、単一リンクリストでは先頭ノードからゆっくりと下方向にしか検索できないため、依然として O(n) であることです。さらに、挿入と削除の最適化も行われます。

疑問が湧くかもしれません: 単一リンクリストの挿入と削除は O(1) ではないでしょうか?

はい、しかし一般的に、挿入や削除の操作を実行する場合、最初に検索してから挿入または削除する必要があります。実際には最初に O(n)、次に O(1) であることがわかります。

削除操作を実行する必要があるため、ノードを削除するには、ノード自体のポインタを取得するだけでなく、他の先行ノードのポインタを操作する必要があります。双方向リンクリストは先行ノードを直接見つけることができるため、操作時間の複雑さは O(1) になります。したがって、LRU キャッシュ削除アルゴリズムを実装するための構造として双方向リンクリストを使用する方が効率的です。

アルゴリズムのアイデア

キャッシュされたすべての値を格納するために二重リンク リストを維持し、最も古い値をリストの末尾に配置します。

アクセスされた値がリンクリスト内にある場合: リンクリスト内の値が検索され、削除され、その値がリンクリストの先頭に再度追加されます (リンクリスト内の値の順序が新しいものから古いものへとなるようにします)

アクセスされた値がリンクリストにない場合: リンクリストがいっぱいの場合: リンクリストの最後の値を削除し、追加する値をリンクリストの先頭に置きます。リンクリストがいっぱいでない場合: リンクリストの先頭に直接追加します。

3.1 LRUキャッシュ除去アルゴリズム

Geek Time Wang Zheng の「The Beauty of Data Structure and Algorithm」では、順序付き単一リンク リストを使用した LRU キャッシュ削除アルゴリズムが紹介されています。コードは次のとおりです。

  1. パブリッククラスLRUBaseLinkedList<T> {
  2.  
  3. /**
  4. * デフォルトのリンクリスト容量
  5. */
  6. プライベートファイナルスタティック 整数DEFAULT_CAPACITY = 10;
  7.  
  8. /**
  9. * ヘッドノード
  10. */
  11. プライベート SNode<T> headNode;
  12.  
  13. /**
  14. * リンクリストの長さ
  15. */
  16. プライベート整数の長さ;
  17.  
  18. /**
  19. * リンクリスト容量
  20. */
  21. プライベート整数容量。
  22.  
  23. パブリックLRUBaseLinkedList() {
  24. this.headNode = 新しい SNode<>();
  25. this.capacity = DEFAULT_CAPACITY;
  26. this.length = 0;
  27. }
  28.  
  29. パブリックLRUBaseLinkedList(整数容量) {
  30. this.headNode = 新しい SNode<>();
  31. this.capacity = 容量;
  32. this.length = 0;
  33. }
  34.  
  35. パブリックvoid add (T データ) {
  36. SNode preNode = findPreNode(データ);
  37.  
  38. // リンクリストに存在する場合は、元のデータを削除し、リンクリストの先頭に挿入します
  39. preNode がnull場合
  40. 要素最適化を削除します(preNode);
  41. intsertElemAtBegin(データ);
  42. }それ以外{
  43. 長さ >= this.capacity の場合 {
  44. //末尾のノードを削除する
  45. 要素の終了位置を削除します。
  46. }
  47. intsertElemAtBegin(データ);
  48. }
  49. }
  50.  
  51. /**
  52. * preNodeノードの次の要素を削除します
  53. *
  54. * @param プレノード
  55. */
  56. プライベートvoid deleteElemOptim(SNode preNode) {
  57. SNode temp = preNode.getNext();
  58. preNode.setNext( temp .getNext());
  59. 一時= null ;
  60. 長さ- ;  
  61. }
  62.  
  63. /**
  64. * リンクリストの先頭にノードを挿入する
  65. *
  66. * @param データ
  67. */
  68. プライベートvoid intsertElemAtBegin(Tデータ) {
  69. SNodeの next = headNode.getNext();
  70. headNode.setNext(新しいSNode(データ、));
  71. 長さ++;
  72. }
  73.  
  74. /**
  75. * 見つかった要素の前のノードを取得します
  76. *
  77. * @param データ
  78. * @戻る 
  79. */
  80. プライベートSNode findPreNode(Tデータ) {
  81. SNode ノード = headNode;
  82. (node.getNext() != null ) の場合 {
  83. (data.equals(node.getNext().getElement()))の場合{
  84. ノードを返します
  85. }
  86. ノード = node.getNext();
  87. }
  88. 戻る ヌル;
  89. }
  90.  
  91. /**
  92. * 末尾のノードを削除する
  93. */
  94. プライベートvoid deleteElemAtEnd() {
  95. SNode ptr = headNode;
  96. // 空のリンクリストは直接返されます
  97. (ptr.getNext() == null )の場合{
  98. 戻る;
  99. }
  100.  
  101. // 最後から2番目のノード
  102. (ptr.getNext().getNext() != null ) の場合 {
  103. ptr = ptr.getNext();
  104. }
  105.  
  106. SNode tmp = ptr.getNext();
  107. ptr.setNext( null );
  108. tmp = null ;
  109. 長さ- ;  
  110. }
  111.  
  112. プライベートvoid printAll() {
  113. SNode ノード = headNode.getNext();
  114. while (ノード ​​!= null ) {
  115. システム.out.print (node.getElement() + "," );
  116. ノード = node.getNext();
  117. }
  118. System.out.println( ) ;
  119. }
  120.  
  121. パブリッククラスSNode<T> {
  122.  
  123. プライベート T 要素。
  124.  
  125. プライベート SNode;
  126.  
  127. パブリックSNode(T要素) {
  128. this.element = 要素;
  129. }
  130.  
  131. パブリックSNode(T要素、SNode){
  132. this.element = 要素;
  133. this.next =次へ;
  134. }
  135.  
  136. パブリックSNode() {
  137. this.next = null ;
  138. }
  139.  
  140. パブリックT getElement() {
  141. 要素を返します
  142. }
  143.  
  144. パブリックvoid setElement(T 要素) {
  145. this.element = 要素;
  146. }
  147.  
  148. パブリックSNode getNext() {
  149. 戻る ;
  150. }
  151.  
  152. パブリックvoid setNext(SNode next ) {
  153. this.next =次へ;
  154. }
  155. }
  156.  
  157. 公共 静的void main(String[] args) {
  158. LRUBaseLinkedList リスト = 新しい LRUBaseLinkedList();
  159. スキャナー sc = new Scanner( System.in );
  160. )の間{
  161. リストに追加します(sc.nextInt());
  162. リストを印刷します。
  163. }
  164. }
  165. }

このコードは、キャッシュがいっぱいかどうかに関係なく、リンク リストをトラバースする必要があるため、このリンク リストの実装に基づくキャッシュ アクセスの時間計算量は O(n) です。

3.2 ハッシュテーブルを使用して LRU を最適化する

実際、このアイデアはさらに最適化できます。単方向リンク リストを双方向リンク リストに置き換え、ハッシュ テーブルを導入することができます。

  • 双方向リンクリストは先行検索をサポートし、操作の時間計算量がO(1)であることを保証します。
  • 各データの場所を記録するハッシュテーブルを導入し、キャッシュアクセスの時間計算量をO(1)に削減する

ハッシュ テーブルは検索が高速ですが、データには固定の順序がありません。一方、リンク リストには順序があります。挿入と削除は高速ですが、検索は低速です。これらを組み合わせることで、新しいデータ構造 LinkedHashMap を形成できます。

ハッシュテーブル + 二重リンクリスト

Likou の質問 146 - LRU キャッシュ メカニズムを練習に使用できます。質問の画像は次のとおりです。

トピック:

既知のデータ構造を使用して、LRU (最近最も使われていない) キャッシュ メカニズムを設計および実装します。

  • LRUCache クラスを実装します。

LRUCache(int capacity) 正の整数を容量としてLRUキャッシュを初期化します。

int get(int key) キーワードキーがキャッシュ内に存在する場合はキーワードの値を返し、存在しない場合は -1 を返します。

void put(int key, int value) キーワードがすでに存在する場合は、そのデータ値を変更します。キーワードが存在しない場合は、「キーワード値」セットを挿入します。キャッシュの容量がいっぱいになると、新しいデータ値のためのスペースを確保するために、新しいデータを書き込む前に、最も最近使用されていないデータ値を削除する必要があります。

アイデア:

私たちのアイデアはハッシュテーブル+双方向リンクリストです

  • ハッシュテーブルはO(1)の時間計算量要件を満たすために使用され、二重リンクリストは順序を格納するために使用されます。
  • ハッシュテーブルキータイプ:
  • 値に加えて、二重リンク リストのノードにはキーも含める必要があります。これは、最も長い未使用データを削除するときに、リンク リストを使用してハッシュマップ内で削除する必要があるキーと値のペアを見つける必要があるためです。
  • いくつかの操作: 双方向リンク リストでは、後ろのノードは最後にアクセスされたノードです。
    • 新しく追加されたノードはリンクリストの末尾に配置されます。addNodeToLast(node)
    • 容量が上限に達した場合は、最も長い未使用データを削除します。removeNode(head.next)
    • データが新しくアクセスされた場合(新しい値で取得または配置された場合など)、ノードをリンクリストの末尾に移動します(moveNodeToLast(node))。
  • 操作の便宜上、双方向リンクリストの先頭と末尾にそれぞれヘッドノードとテールノードが定義されます。

コード

  1. クラスLRUCache {
  2. プライベートint容量;
  3. プライベート HashMap< Integer , ListNode> ハッシュマップ;
  4. プライベートListNodeヘッド;
  5. プライベートListNodeテール;
  6.  
  7. プライベートクラスListNode{
  8. 整数 ;
  9. 整数値;
  10. リストノード前;
  11. ListNode;
  12. パブリックリストノード(){
  13. }
  14. パブリックListNode( int  キー int値){
  15. this.key =キー;
  16. this.val = val;
  17. }
  18. }
  19.  
  20. パブリックLRUCache( int容量) {
  21. this.capacity = 容量;
  22. ハッシュマップ = 新しい HashMap<>();
  23. ヘッド = 新しいListNode();
  24. 末尾 = 新しいListNode();
  25. ヘッドの次= テール;
  26. 末尾.prev = 先頭;
  27. }
  28.  
  29. プライベートvoid removeNode(ListNodeノード){
  30. ノードを次のノードにドラッグします
  31. ノードを次のノードにドラッグします。
  32. }
  33.  
  34. プライベート void addNodeToLast(ListNode ノード){
  35. ノードを末尾に追加します。
  36. ノードを次のノードにドラッグします。
  37. ノードの次の行にポインタを置きます。
  38. tail.prev=ノード;
  39. }
  40.  
  41. プライベート void moveNodeToLast(ListNode ノード){
  42. ノードを削除します。
  43. ノードを最後に追加します。
  44. }
  45.      
  46. 公共  int get( int  ) {
  47. if(hashmap.containsKey(キー)){
  48. ListNode ノード = hashmap.get(キー);
  49. ノードを最後のノードに移動します。
  50. node.valを返します
  51. }それ以外{
  52. -1 を返します
  53. }
  54. }
  55.      
  56. パブリックvoid put( int  キー int値){
  57. if(hashmap.containsKey(キー)){
  58. ListNode ノード = hashmap.get(キー);
  59. ノード.val = 値;
  60. ノードを最後のノードに移動します。
  61. 戻る;
  62. }
  63. if(ハッシュマップのサイズ() == 容量){
  64. ハッシュマップを削除します( head.next.key );
  65. ノードを削除します( head.next );
  66. }
  67.  
  68. ListNode ノード = 新しい ListNode(キー、 値 );
  69. hashmap.put(キー, ノード );
  70. ノードを最後に追加します。
  71. }
  72. }

巨人の肩の上:

[1] データ構造とアルゴリズムの美しさ - 王正

[2] Likku-LRUキャッシュメカニズム(146問)

[3]https://blog.csdn.net/yangpl_tale/article/details/44998423

[4]https://leetcode-cn.com/problems/lru-cache/solution/146-lru-huan-cun-ji-zhi-ha-xi-biao-shuan-l3um/

<<:  署名アルゴリズムに基づくシンプルで安全なAPI認証メカニズム

>>:  現在最も興味深い AI は、実は系図会社から生まれたものなのでしょうか?

ブログ    

推薦する

清華大学とアリババDAMOアカデミーが開発した業界初の少数サンプルNERデータセット

この記事はAI新メディアQuantum Bit(公開アカウントID:QbitAI)より許可を得て転載...

将来、音声認識はどのような商業シナリオに適用される可能性がありますか?

Companies and Markets の評価レポートでは、世界の音声認識市場は今後さらに多様...

AI + データサイエンス: スポーツ業界を変える6つの方法

[[329380]]テクノロジーの発展に伴い、人工知能とデータサイエンスはスポーツの分野でますます重...

...

...

人工知能の導入により AR/VR はどこへ向かうのでしょうか?

[51CTO.com からのオリジナル記事] 2015 年 1 月、Microsoft は長年「革...

企業がビッグデータの可能性を最大限に引き出す方法

専門家は、2025 年までにデータ ユニバース、つまりデータ ユニバースの規模が 180 ゼタバイト...

...

ジェネレーティブ AI がデータ センターの要件をどのように変えるか

データ センターとは何ですか。どのように使用しますか。具体的には、データ センターにはどのような種類...

...

...

スマートテクノロジーは高齢化問題の解決に役立つでしょうか?

世界保健機関によれば、2050年までに世界中で約20億人が60歳以上になると予想されています。これら...

AIがソフトウェアエンジニアリングをどのように強化できるかについて知っておくべきことすべて

翻訳者 |李睿レビュー | Chonglou AI 拡張ソフトウェア エンジニアリングは、人工知能と...

ChatGPTは人気を集めており、OpenAIはAIソフトウェア用のアプリストアの作成を検討している

今年 5 月、OpenAI はすべての ChatGPT Plus ユーザー向けにネットワーキングおよ...

...