用Python手动实现LRU算法

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"说到LRU算法,就不得不提哈希链表。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"哈希链表","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在开始介绍哈希链表前,我们先简单的回顾一下哈希表。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 哈希表的读操作","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哈希表和我们现实世界中的“词典”很像,都是以一个“键值对(key-value)”的形式存在,比如我们像要查看“わたし”所对应的中文意思是什么,我们只需要通过“わたし”这个“键”在词典中找到与之对应的值即中文的“我”即可,而不需要从字典的第一页开始一页一页的翻,因此它的时间复杂度是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"O(1)。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"还记得自己学生时代,总是习惯在字典的侧面用笔将五十音图按照顺序涂上颜色,这样方便更快的查找。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们查找“わたし”这三个假名,还是有点慢,因为它是三个字符,但是如果我们将他转换成页数,假设每个单词单独一页。然后通过这个页码找单词是不是会更快一些呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回到编程的世界,则存在一个叫做","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"哈希函数","attrs":{}},{"type":"text","text":"的东西。在Python的语言中每一个对象都有一个与之对应的哈希值(hash值),无论什么类型的对象,它的hash值都是一个","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"整数对象","attrs":{}},{"type":"text","text":"。而值(value)则保存在一个数组里面。转换方式其实很简单:","attrs":{}}]},{"type":"katexblock","attrs":{"mathString":"index = hash(key) \\% size"}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"size就是我们保存值的数组的大小","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,由于直接取hash值,有可能会出现负数的情况,比如我们“わたし”这个key的hash值就是“-6935749861035834984”。用负数进行模运算是比较麻烦的一件事。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ed/edc0e677917637e313cf583b1dc47ffa.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我们是用","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"每一个元素的ASCII码相加","attrs":{}},{"type":"text","text":",再运行模运算。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如我们上面的那个例子,如果我们用来保存中文“我”的数组大小是8,而“わたし”的每个值的ASCII码分别是12431,12383,12375","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ef/ef3021eecf9d6e858ec59202e0499cdb.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我们得到的“わたし”在数组中的下标是","attrs":{}}]},{"type":"katexblock","attrs":{"mathString":"index = (12431+12383+12375) \\% 8 \n"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"katexblock","attrs":{"mathString":"index = 37189 \\% 8"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"katexblock","attrs":{"mathString":"index = 5"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样我们便可以通过这个下标在数组中更快的找到“我”这个元素。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"介绍完哈希表的的读(get),我们再看看怎么将一个值保存进哈希表,即写(put)的操作","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 哈希表的写操作","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如我们希望将YYDS这个网络词语添加到字典中。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"利用上面介绍的方法将YYDS转换成数组下标1","attrs":{}}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/57/57b38bf4de7526e5fbe8315e67e88866.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果数组中1的位置没有元素,则将“永远的神”保存在这个位置","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"但是如果,这个位置有内容了怎么办,这个中情况我们通常叫做","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"哈希冲突。","attrs":{}},{"type":"text","text":"通常有两种方式解决这个问题,一个是开放寻址方法(Python用的就是这种),另一个就是链表法。","attrs":{}}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"放寻址方法:从当前位置向后找,直到找到为空的位置,然后保存数据","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"链表法:通过修改链表的head和next来保存数据","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 哈希链表","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其实就是将hash和双向链表结合在了一起。这个更方便数据的操作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"时间复杂度期望值:查找,删除,更新, 插入都是O(1)。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即数据分布很平均,没有相同hash值","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"时间复杂度最坏值:查找,删除,更新, 插入都是O(n)。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即所有的hash值都是重复的","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"LRU算法实现","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在介绍玩hash表后,我们来看一个业务场景。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们需要对公司的所有员工信息进行管理,各个部门都会使用这个数据,因此为了减少数据的频繁访问,我们将数据保存在内存中的一个hash表中。这样数据的查询速度要快很多。但是随着人员增加,日积月累,在不扩展硬件的情况下,总有一天我们的内存会溢出,那么该怎么解决这个问题呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这便引出了我们今天的主角-----LRU算法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的核心思想是在有新数据添加进来,且预先分配的空间已满时删除访问最少的那个数据,从而保证数据的正常。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"这是员工数据在hash缓存中的,按照被访问的时间顺序,一次从右端开始插入","attrs":{}}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2f/2fb7fdeed936bff78cdeafeeafebc2bc.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"这时来了新员工,且分配的内存空间还充足的情况,就会变成下图所示","attrs":{}}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/9933daf0e13576bed54164e0e89bd732.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果我们访问二号员工,它就会先将二号删除,然后在右端重新插入","attrs":{}}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/24/24627bf9cbe6de7cdd545caac411b3da.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/32/32bfa97e23c10d27e8955ec0b44b8039.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"numberedlist","attrs":{"start":4,"normalizeStart":4},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"接着又来一个新员工,但是已经没有足够的内存了,因此我们需要将最左端好久没有访问的数据删除,然后将6号员工插入到右端。","attrs":{}}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/15/15c9ae08b2b84b9745f9f0abd9b891ab.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/82/828b2567f03ddc1409200a6521926424.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个便是我们的LRU算法的核心原理了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Python中的collections.OrderedDict其实已经对哈希链表进行了很好的实现,但是为了学习我们还是来手动实现一下。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"python"},"content":[{"type":"text","text":"class Node:\n def __init__(self, key, value):\n self.key = key\n self.value = value\n self.pre = None\n self.next = None\n\nclass LRUCache:\n def __init__(self, limit):\n self.limit = limit\n self.hash = {}\n self.head = None\n self.end = None\n\n def get(self, key):\n node = self.hash.get(key)\n if node is None:\n return None\n self.refresh_node(node)\n return node.value\n\n def put(self, key, value):\n node = self.hash.get(key)\n if node is None:\n # 如果key不存在,插入key-value\n if len(self.hash) >= self.limit:\n old_key = self.remove_node(self.head)\n self.hash.pop(old_key)\n node = Node(key, value)\n self.add_node(node)\n self.hash[key] = node\n else:\n # 如果key存在,刷新key-value\n node.value = value\n self.refresh_node(node)\n\n def refresh_node(self, node):\n # 如果访问的是尾节点,无需移动节点\n if node == self.end:\n return\n # 移除节点\n key = self.remove_node(node)\n self.hash.pop(key)\n # 重新插入节点\n self.put(node.key, node.value)\n\n def remove_node(self, node):\n if node == self.head and node == self.end:\n # 移除唯一的节点\n self.head = None\n self.end = None\n elif node == self.end:\n # 移除节点\n self.end = self.end.pre\n self.end.next = None\n elif node == self.head:\n # 移除头节点\n self.head = self.head.next\n self.head.pre = None\n else:\n # 移除中间节点\n node.pre.next = node.pre\n node.next.pre = node.next\n return node.key\n\n def add_node(self, node):\n if self.end is not None:\n self.end.next = node\n node.pre = self.end\n node.next = None\n self.end = node\n if self.head is None:\n self.head = node\n\n\nif __name__ == \"__main__\":\n lruCache = LRUCache(5)\n lruCache.put(\"001\", \"用户1信息\")\n lruCache.put(\"002\", \"用户2信息\")\n lruCache.put(\"003\", \"用户3信息\")\n lruCache.put(\"004\", \"用户4信息\")\n lruCache.put(\"005\", \"用户5信息\")\n print(\"----------初始化完成后的排序----------\")\n print(lruCache.hash.keys())\n print(\"----------查询完002后的排序----------\")\n print(lruCache.get(\"002\"))\n print(lruCache.hash.keys())\n print(\"----------添加新员工后的排序----------\")\n lruCache.put(\"006\", \"用户6信息\")\n print(lruCache.hash.keys())\n ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/15/15a39042e0c7319489d04af791b3ee78.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章