目录
146. LRU 缓存
要让 put
和 get
方法的时间复杂度为 O(1),可以总结出 cache
这个数据结构必要的条件:
1、 cache
中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。
2、我们要在 cache
中快速找某个 key
是否已存在并得到对应的 val
;
3、每次访问 cache
中的某个 key
,需要将这个元素变为最近使用的,也就是说 cache
要支持在任意位置快速插入和删除元素。
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。
class LRUCache {
public:
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
LRUCache(int capacity) {
this->_capacity = capacity;
this->_size = 0;
// 使用伪头部和伪尾部节点
_head = new DLinkedNode();
_tail = new DLinkedNode();
_head->next = _tail;
_tail->prev = _head;
}
int get(int key) {
if (!_cache.count(key)) return -1;
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = _cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
// 如果 key 不存在,创建一个新的节点
if (!_cache.count(key)) {
DLinkedNode* node = new DLinkedNode(key, value);
_cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++_size;
// 如果超出容量,删除双向链表的尾部节点
if (_size > _capacity) {
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
_cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--_size;
}
}
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
else {
DLinkedNode* node = _cache[key];
node->value = value;
moveToHead(node);
}
}
// 封装双向链表的操作
void addToHead(DLinkedNode* node) {
node->prev = _head;
node->next = _head->next;
_head->next->prev = node;
_head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = _tail->prev;
removeNode(node);
return node;
}
private:
unordered_map<int, DLinkedNode*> _cache;
DLinkedNode* _head;
DLinkedNode* _tail;
int _size;
int _capacity;
};
460. LFU 缓存
LFU 算法把数据按照访问频次进行排序。如果多个数据拥有相同的访问频次,删除最早插入的那个数据。用哈希表以键 key为索引存储缓存,建立一个平衡二叉树 S 来保持缓存根据 (cnt,time) 双关键字。【可以利用set实现】
- get(key) 操作:查看哈希表 key_table 是否有 key 这个键即可,有的话需要同时更新哈希表和集合中该缓存的使用频率以及使用时间,否则返回 -1。
- put(key, value) 操作:首先需要查看 key_table 中是否已有对应的键值。如果有的话操作基本等同于 get(key),不同的是需要更新缓存的 value 值。如果没有的话相当于是新插入一个缓存,这时候需要先查看是否达到缓存容量 capacity,如果达到了的话,需要删除最近最少使用的缓存,即平衡二叉树中最左边的结点,同时删除 key_table 中对应的索引,最后向 key_table 和 S 插入新的缓存信息即可。
struct Node {
int cnt, time, key, value;
Node(int _cnt, int _time, int _key, int _value) :cnt(_cnt), time(_time), key(_key), value(_value) {}
bool operator < (const Node& rhs) const {
return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
}
};
class LFUCache {
private:
// 缓存容量,时间戳
int _capacity, _time;
unordered_map<int, Node> _key_table;
set<Node> _S;
public:
LFUCache(int capacity) {
_capacity = capacity;
_time = 0;
_key_table.clear();
_S.clear();
}
int get(int key) {
if (_capacity == 0) return -1;
auto it = _key_table.find(key);
// 如果哈希表中没有键 key,返回 -1
if (it == _key_table.end()) return -1;
// 从哈希表中得到旧的缓存
Node cache = it->second;
// 从平衡二叉树中删除旧的缓存
_S.erase(cache);
// 将旧缓存更新
cache.cnt += 1;
cache.time = ++_time;
// 将新缓存重新放入哈希表和平衡二叉树中
_S.insert(cache);
it->second = cache;
return cache.value;
}
void put(int key, int value) {
if (_capacity == 0) return;
auto it = _key_table.find(key);
if (it == _key_table.end()) {
// 如果到达缓存容量上限
if (_key_table.size() == _capacity) {
// 从哈希表和平衡二叉树中删除最近最少使用的缓存
_key_table.erase(_S.begin()->key);
_S.erase(_S.begin());
}
// 创建新的缓存
Node cache = Node(1, ++_time, key, value);
// 将新缓存放入哈希表和平衡二叉树中
_key_table.insert(make_pair(key, cache));
_S.insert(cache);
}
else {
// 这里和 get() 函数类似
Node cache = it->second;
_S.erase(cache);
cache.cnt += 1;
cache.time = ++_time;
cache.value = value;
_S.insert(cache);
it->second = cache;
}
}
};
前缀树
Trie 树又叫字典树、前缀树、单词查找树,是一种二叉树衍生出来的高级数据结构,主要应用场景是处理字符串前缀相关的操作。
Trie 树本质上就是一棵从二叉树衍生出来的多叉树,TrieNode
中 children
数组的索引是有意义的,代表键中的一个字符。比如说 children[97]
如果非空,说明这里存储了一个字符 'a'
,因为 'a'
的 ASCII 码为 97,一个节点有 256 个子节点指针。(来源:labuladong)
208. 实现 Trie (前缀树)
class Trie {
public:
// 每建立一个新结点,就会自动建立26个children(含义为索引值)为空的新结点
Trie(): children(26), isEnd(false) {}
void insert(string word) {
Trie* node = this;
for (auto ch : word) {
// 1.子节点不存在
if (node->children[ch - 'a'] == nullptr) {
node->children[ch - 'a'] = new Trie();
}
// 1.子节点存在,转到子结点上
node = node->children[ch - 'a'];
}
// 字符结束标记
node->isEnd = true;
}
bool search(string word) {
Trie* node = this->SearchPrefix(word);
return node && node->isEnd;
}
bool startsWith(string prefix) {
return this->SearchPrefix(prefix);
}
private:
// 每个节点包含以下字段:
// 指向子节点的指针数组{children}, 数组长度为26,即小写英文字母的数量
// 布尔字段isEnd,表示该节点是否为字符串的结尾。
vector<Trie*> children;
bool isEnd;
Trie* SearchPrefix(string prefix) {
Trie* node = this;
for (auto ch : prefix) {
// 1. 子节点不存在
if (node->children[ch - 'a'] == nullptr) return nullptr;
// 2. 子节点存在,转到子节点继续查找
node = node->children[ch - 'a'];
}
return node;
}
};
211. 添加与搜索单词 - 数据结构设计
本题增加的难度在于符号“.”可以代表任何一个小写字母,因此不能无脑递归查找。脑海中有抽象二叉树结构后,可以采用DFS的方法查找。
class WordDictionary {
public:
WordDictionary(): children(26), isEnd(false) {}
void addWord(string word) {
WordDictionary* node = this;
for (auto ch : word) {
ch -= 'a';
if (node->children[ch] == nullptr) {
node->children[ch] = new WordDictionary();
}
node = node->children[ch];
}
node->isEnd = true;
}
bool search(string word) {
return DFS(word, 0, this);
}
private:
vector<WordDictionary*> children;
bool isEnd;
bool DFS(const string& word, int index, WordDictionary* node) {
// 递归返回条件
if (index == word.size()) {
return node->isEnd;
}
char ch = word[index];
// 是“.”,需要遍历26个小写字母
if (ch == '.') {
for (size_t i = 0; i < 26; ++i) {
WordDictionary* child = node->children[i];
if (child && DFS(word, index + 1, child)) {
return true;
}
}
}
// 是小写字母,只需继续深度递归
else {
WordDictionary* child = node->children[ch - 'a'];
if (child && DFS(word, index + 1, child)) {
return true;
}
}
return false;
}
};
648. 单词替换
string replaceWords(vector<string>& dictionary, string sentence) {
// 将前缀存入前缀树
Trie priTree;
for (string prifix : dictionary) {
priTree.insert(prifix);
}
// 查找每个单词是否存在前缀
string word;
string res;
istringstream is(sentence);
while (is >> word) {
size_t i = 1;
for (; i <= word.size(); ++i) {
if (priTree.search(word.substr(0, i))) break;
}
res.append(word.substr(0, i) + ' ');
}
res.pop_back();
return res;
}
注:本题采用上述方法可能会超时。
677. 键值映射
class MapSum {
public:
MapSum() : children(26), val(0), isEnd(false) {}
void insert(string key, int val) {
MapSum* node = this;
for (auto str : key) {
str -= 'a';
if (node->children[str] == nullptr) {
node->children[str] = new MapSum();
}
node = node->children[str];
}
node->isEnd = true;
// 只有非空含键节点才有值
node->val = val;
}
int sum(string prefix) {
MapSum* node = this->SearchPrefix(prefix);
if (!node) return 0;
// 从前缀节点开始深度搜索,获取键值和
int res = 0;
DFS(node, res);
return res;
}
private:
// 每个节点包含以下字段:
// 指向子节点的指针数组{children}, 数组长度为26,即小写英文字母的数量
// 布尔字段isEnd,表示该节点是否为字符串的结尾。
vector<MapSum*> children;
int val;
bool isEnd;
MapSum* SearchPrefix(string prefix) {
MapSum* node = this;
for (auto ch : prefix) {
// 1. 子节点不存在
if (node->children[ch - 'a'] == nullptr) return nullptr;
// 2. 子节点存在,转到子节点继续查找
node = node->children[ch - 'a'];
}
return node;
}
void DFS(MapSum* node, int& sum) {
if (!node) return;
sum += node->val;
for (size_t i = 0; i < 26; ++i) {
DFS(node->children[i], sum);
}
}
};
295. 数据流的中位数
class MedianFinder {
public:
MedianFinder() {
}
void addNum(int num) {
// num小于等于中位数,新的中位数将小于等于原来的中位数
// 因此需要将queMin中最大的数移动到queMax中
// 当累计添加的数的数量为 0 时,也将 num 添加到 queMin 中
if (queFront.empty() || num < queFront.top()) {
queFront.push(num);
// 始终保持两个优先队列数量相等 / queMin比queMax多一个元素
if (queFront.size() > queBack.size() + 1) {
int temp = queFront.top();
queFront.pop();
queBack.push(temp);
}
}
else {
queBack.push(num);
if (queBack.size() > queFront.size()) {
int temp = queBack.top();
queBack.pop();
queFront.push(temp);
}
}
}
double findMedian() {
if (queBack.size() == queFront.size()) {
return (queBack.top() + queFront.top()) / 2.0;
}
else {
return queFront.top();
}
}
private:
// 从小到大的有序数组的前半部分,大顶堆
priority_queue<int, vector<int>, less<int>> queFront;
// 从小到大的有序数组的后半部分,小顶堆
priority_queue<int, vector<int>, greater<int>> queBack;
};
优先级队列:利用二叉堆实现
核心操作:
sink
(下沉)和swim
(上浮)用途:堆排序、优先级队列