lettcode 上的几道哈希表与链表组合的数据结构题

目录

  • LRU缓存
  • LFU缓存
  • 全O(1)的数据结构

下面这几道题都要求在O(1)时间内完成每种操作。

LRU缓存

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

做法:

  • 使用先进先出的队列,队尾的元素即是可能要淘汰的。
  • 由于需要查找某个key在队列中的位置,需要一种数据结构快速定位,并且快速删除。
  • 使用链表来实现队列的功能,同时使用哈希表记录每个key对应的链表结点。
  • 可以手写一个双向哈希链表,也可以使用c++ std库中的list+unordered_map来代替。
typedef struct Node
{
    int key;
    int value;
    Node *prev;
    Node *next;
    Node(int k, int v):key(k), value(v), prev(NULL), next(NULL){}
}Node;
class HashDoubleLinkList
{
private:
    int size;
    Node *head;
    Node *tail;
    unordered_map<int, Node*> key_dict;

public:
    void init(){
        key_dict.clear();
        size = 0;
        head = newNode(0,0);
        tail = newNode(0,0);
        head->next = tail;
        tail->prev = head;
    }
    int getsize() { return size; }
    Node *newNode(int k,int v){
        return new Node(k, v);
    }
    //find node by key, if key not exist, return NULL
    Node *find(int key){
        auto it = key_dict.find(key);
        if(it == key_dict.end()) return NULL;
        return it->second;
    }
    //remove node by key, if key not exist, return NULL
    Node* removekey(int key){
        Node *node = find(key);
        if(!NULL) return NULL;
        remove(node);
        return node;
    }
    //remove node
    void remove(Node *node){
        node->prev->next = node->next;
        node->next->prev = node->prev;
        key_dict.erase(node->key);
        size--;
    }
    //pop the last element in list
    Node* pop_back(){
        if(size <= 0) return NULL;
        size--;
        Node *node = tail->prev;
        node->prev->next = tail;
        tail->prev = node->prev;
        key_dict.erase(node->key);
        return node;
    }
    //pop the first element in list
    Node* pop_front(){
        if(size <= 0) return NULL;
        size--;
        Node *node = head->next;
        head->next = node->next;
        node->next->prev = head;
        key_dict.erase(node->key);
        return node;
    }
    //insert before first element in list
    void push_front(Node *node){
        head->next->prev = node;
        node->next = head->next;
        head->next = node;
        node->prev = head;
        size++;
        key_dict[node->key] = node;
    }
    //insert after last element in list
    void push_back(Node *node){
        node->next = tail;
        node->prev = tail->prev;
        tail->prev->next = node;
        tail->prev = node;
        size++;
        key_dict[node->key] = node;
    }
};
class LRUCache {
    int cap;
    HashDoubleLinkList dl;
public:
    LRUCache(int capacity) {
        cap = capacity;
        dl.init();
    }

    int get(int key) {
        Node *node = dl.find(key);
        if(node == NULL) return -1;
        dl.remove(node);
        dl.push_front(node);
        return node->value;
    }

    void put(int key, int value) {
        Node *node = dl.find(key);
        if(node == NULL){
            if(dl.getsize() == cap){
                dl.pop_back();
            }
            dl.push_front(new Node(key, value));
        }else{
            dl.remove(node);
            node->value = value;
            dl.push_front(node);
        }
    }
};

LFU缓存

LFU是Least Frequency Used的缩写,即最少使用次数,次数相等时即等同于LRU缓存。每次选择访问次数最少的页面予以淘汰,若存在多个次数最少的页面,选择访问时间最远的页面淘汰。

做法:

  • 将访问次数相同的元素丢到一个链表中,这个链表的操作就跟LRU缓存一样了。
  • 用哈希表记录次数对应的链表, key对应的链表结点,和key对应的访问次数和值。

使用了三个哈希表+一个链表,LFU缓存比较耗费内存。

class LFUCache {
    int cap;
    int minFreq;
    unordered_map<int, list<int> > FreqKey;
    unordered_map<int, pair<int,int> > KeyFreqAndValue;
    unordered_map<int, list<int>::iterator> FreqKeyIter;
private:
public:
    LFUCache(int capacity) {
        cap = capacity;
        minFreq = 1;
    }
    int get(int key) {
        auto fv = KeyFreqAndValue.find(key);
        if(fv == KeyFreqAndValue.end()) return -1;
        FreqKey[fv->second.first].erase(FreqKeyIter[key]);
        fv->second.first++;
        if (FreqKey.find(fv->second.first) == FreqKey.end()){
            FreqKey[fv->second.first] = list<int>();
        }
        FreqKey[fv->second.first].push_front(key);
        FreqKeyIter[key] = FreqKey[fv->second.first].begin();
        if(FreqKey[minFreq].empty()) minFreq++;
        return fv->second.second;
    }

    void put(int key, int value) {
        if(cap <= 0) return ;
        if(get(key) != -1){
            KeyFreqAndValue[key].second = value;
            return ;
        }
        if(KeyFreqAndValue.size() == cap){
            int pop_key = *FreqKey[minFreq].rbegin();
            KeyFreqAndValue.erase(pop_key);
            FreqKeyIter.erase(pop_key);
            FreqKey[minFreq].pop_back();
        }
        minFreq = 1;
        FreqKey[1].push_front(key);
        KeyFreqAndValue[key] = make_pair(1, value);
        FreqKeyIter[key] = FreqKey[1].begin();
    }
};

全O(1)的数据结构

这道题有四种操作

  • Inc(key) - 插入一个新的值为 1 的 key。或者使一个存在的 key 增加一,保证 key 不为空字符串
  • Dec(key) - 如果这个 key 的值是 1,那么把他从数据结构中移除掉。否者使一个存在的 key 值减一。如果这个 key 不存在,这个函数不做任何事情。key 保证不为空字符串。
  • GetMaxKey() - 返回 key 中值最大的任意一个。如果没有元素存在,返回一个空字符串""。
  • GetMinKey() - 返回 key 中值最小的任意一个。如果没有元素存在,返回一个空字符串""。

做法:

  • 这里的O(1)应该是不包括计算key的哈希值的时间的。
  • 双向链表将值从小到大串连起来, 链表中每个结点维护一个哈希表存储所有值相同的key,
  • 维护一个哈希表记录值在链表中对应的结点,一个哈希表记录key对应的值
  • 求值最大/最小的key就是求链表的首尾即可。
class AllOne {
    unordered_map<string, int> values;
    unordered_map<int, list<unordered_set<string>>::iterator> count_iter;
    list<unordered_set<string> > count_keys;
public:
    /** Initialize your data structure here. */
    AllOne() {
        values.clear();
        count_iter.clear();
        count_keys.clear();
    }
    void remove(list<unordered_set<string>>::iterator iter, string key, int value){
                (*iter).erase(key);
                if((*iter).empty()){
                    count_iter.erase(value);
                    count_keys.erase(iter);
                }
    }
    /** Inserts a new key <Key> with value 1. Or increments an existing key by 1. */
    void inc(string key) {
        auto it = values.find(key);
        if(it == values.end()){
            values[key] = 1;
            if(count_iter.find(1) == count_iter.end()){
                count_keys.push_front({key});
                count_iter[1] = count_keys.begin();
            }else{
                (*count_iter[1]).insert(key);
            }
        }else{
            int value = it->second;
            //update values
            it->second++;

            //update value + 1
            auto iter = count_iter[value];
            iter++;
            if (count_iter.find(value + 1) == count_iter.end()){
                count_iter[value + 1] = count_keys.insert(iter, {key});
            }else{
                (*iter).insert(key);
            }
            //delte value
            remove(count_iter[value], key, value);
        }
    }

    /** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
    void dec(string key) {
        auto it = values.find(key);
        if(it == values.end()) return ;
        int value = it->second;
        //update values
        it->second--;
        //update value - 1
        auto iter = count_iter[value];
        if(it->second == 0){
            values.erase(key);
        }else{
            if (count_iter.find(value - 1) == count_iter.end()){
                count_iter[value - 1] = count_keys.insert(iter, {key});
            }else{
                (*count_iter[value - 1]).insert(key);
            }
        }
        remove(iter, key, value);
    }

    /** Returns one of the keys with maximal value. */
    string getMaxKey() {
        if(count_keys.empty()) return "";
       return *(count_keys.back().begin());
    }

    /** Returns one of the keys with Minimal value. */
    string getMinKey() {
        if(count_keys.empty()) return "";
        return *(count_keys.front().begin());
    }
};

/**
 * Your AllOne object will be instantiated and called as such:
 * AllOne* obj = new AllOne();
 * obj->inc(key);
 * obj->dec(key);
 * string param_3 = obj->getMaxKey();
 * string param_4 = obj->getMinKey();
 */

原文地址:https://www.cnblogs.com/jiachinzhao/p/11474085.html

时间: 2024-10-14 14:03:20

lettcode 上的几道哈希表与链表组合的数据结构题的相关文章

数组和广义表-第5章-《数据结构题集》答案解析-严蔚敏吴伟民版

习题集解析部分 第5章 数组和广义表 ——<数据结构题集>-严蔚敏.吴伟民版        源码使用说明  链接??? <数据结构-C语言版>(严蔚敏,吴伟民版)课本源码+习题集解析使用说明        课本源码合辑  链接??? <数据结构>课本源码合辑        习题集全解析  链接??? <数据结构题集>习题解析合辑       本习题文档的存放目录:数据结构\▼配套习题解析\▼05 数组和广义表       文档中源码的存放目录:数据结构\▼配

Redis源码研究—哈希表

Redis源码研究-哈希表 Category: NoSQL数据库 View: 10,980 Author: Dong 作者:Dong | 新浪微博:西成懂 | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明 网址:http://dongxicheng.org/nosql/redis-code-hashtable/ 本博客的文章集合:http://dongxicheng.org/recommend/ 本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享

MIT算法导论——第七讲.哈希表

从作用上来讲,构建哈希表的目的是把搜索的时间复杂度降低到O(1),考虑到一个长度为n的序列,如果依次去比较进行搜索的话,时间复杂度是θ(n),或者对其先进行排序然后再搜索会更快一些,但这两种方法都不是最快的方法. 哈希表也叫散列表,他通过一个哈希函数H,把要存储的内容取一个键值,经过H的运算,把键值映射到一个有m个槽的表中去,最简单的例子就是手机里存储别人的电话号码,键值就是名字,内容就是电话号码等个人信息.这样的表有一个最大的好处就是一旦要查找某个值,比如说"张三"的电话号码,我们把

什么叫哈希表(Hash Table)

散列表(也叫哈希表),是根据关键码值直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表. - 数据结构中,有个时间算法复杂度O(n)的概念来衡量某种算法在时间效率上的优劣.哈希表的理想算法复杂度为O(1),也就是说利用哈希表查找某个值,系统所使用的时间在理想情况下为定值,这就是它的优势.那么哈希表是如何做到这一点的呢? - 我们定义一个很大的有序数组,想要得到位于该数组第n个位置的值,它的算法复杂度

Redis源码研究:哈希表 - 蕫的博客

[http://dongxicheng.org/nosql/redis-code-hashtable/] 1. Redis中的哈希表 前面提到Redis是个key/value存储系统,学过数据结构的人都知道,key/value最简单的数据结果就是哈希表(当然,还有其他方式,如B-树,二叉平衡树等),hash表的性能取决于两个因素:hash表的大小和解决冲突的方法.这两个是矛盾的:hash表大,则冲突少,但是用内存过大:而hash表小,则内存使用少,但冲突多,性能低.一个好的hash表会权衡这两个

数据结构与算法--------哈希表

1.概述: 数据结构:哈希表 插入时间复杂度:O(1) 删除时间复杂度:O(1) 查找时间复杂度: 优点: 哈希表的操作速度比较快,如插入.删除的时间复杂度都是常量O(1),可以在一秒内查找上千条记录 哈希表的编程实现相对容易 缺点: 哈希表不能被填满.哈希表被基本填满的时候,性能会急剧下降,所以为了保证性能,你一定要确保你哈希表的容量的大小是足够的 哈希表到其他哈希表的数据迁移过程是一个费时的过程(如定期将数据迁移到更大的哈希表中去会很费时).如果一开始没有预估好你的数据量的大小,初始时创建的

哈希表与哈希函数 C实现

<pre name="code" class="cpp"><pre name="code" class="cpp"><pre name="code" class="cpp"> <strong>散列表</strong>(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通

程序员,你应该知道的数据结构之哈希表

哈希表简介 哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器. 哈希表也有自己的缺点,哈希表是基于数组的,我们知道数组创建后扩容成本比较高,所以当哈希表被填满时,性能下降的比较严重. 哈希表采用的是一种转换思想,其中一个中要的概念是如何将键或者关键字转换成数组下标?在哈希表中,这个过程有哈希函数来完成,但是并不是每个键或者关键字都需

来吧!一文彻底搞定哈希表!

哈希表是个啥? 小白: 庆哥,什么是哈希表?这个哈希好熟悉,记得好像有HashMap和HashTable之类的吧,这是一样的嘛??? 庆哥: 这个哈希确实经常见??,足以说明它是个使用非常频繁的玩意儿,而且像你说的HashMap和HashTable之类的与哈希这个词肯定是有关系的,那哈希是个啥玩意啊,这个咱们还是得先来搞明白啥是个哈希表.?? 我们看看百科解释吧: 散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构.也就是说,它通过计算一个关于键值的