数据结构类的题一般是考察对算法的设计能力,还有对基础数据结构的能力能力,如何根据题意才能设计出最优的结构。 也是考察对一些数据结构设计的理解,比如说Java直接就支持哈希链表(LinkedHashMap)的结构,但有些语言是没有的,如果你是Java开发一般会问哈希链表是如何设计的;如果不是Java开发的话可能会让你从头设计一个LRU结构。
常见的数据结构也有很多,比如:LRU、LFU、大顶堆,二叉堆、最大频率栈、前缀树、单调栈等等。
以LRU进行举例: 146. LRU 缓存 LRU算法就是一种缓存淘汰策略,即(最近最少使用)缓存。iOS中的YYCache就是使用LUR策略实现的。Cache的容量是有限的,当Cache的空间被占满后,如果再次发生缓存失效,就必须选择哪个最长时间未被使用的块替换。这种方法比较好地反映了局部性规律。
这个题中有两个方法,一个put一个get。要让put和get方法的时间复杂度为O(1),可以总结出有以下必要条件: 1,cache元素必须有序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾出位置来。 2,要在cache中快速找某个key是否已存在并得到对应的val 3,每次访问cache中的某个key,需要将这个元素变为最近使用的,也就是说cache要支持在任意位置快速插入和删除元素。 哈希表查找快,链表有顺序插入删除快,但查找慢。所以结合起来形成一种新的数据结构:哈希链表 LinkedHashMap。
对于一个Cache的操作无非三种:插入、替换、查找。
- 插入:当Cache未满时,新的数据项只需插到双链表头部即可。
- 替换:当Cache已满时,将新的数据项插到双链表头部,并删除双链表的尾结点即可。
- 查找:每次数据项被查询到时,都将此数据项移动到链表的头部。
具体代码实现如下:
class DLikedNode {
var val: Int
var next: DLikedNode?
var pre: DLikedNode?
var key: Int?
init(val: Int, key: Int? = nil, next: DLikedNode? = nil, pre: DLikedNode? = nil) {
self.val = val
self.key = key
self.next = next
self.pre = pre
}
}
class LRUCache {
// 容量
var capacity = 0
var count = 0
let first = DLikedNode(val: -1)
let last = DLikedNode(val: -1)
var hash = [Int: DLikedNode]()
init(_ capacity: Int) {
self.capacity = capacity
first.next = last
first.pre = nil
last.pre = first
last.next = nil
}
// 挪到第一位
func moveToHead(node: DLikedNode) {
let pre = node.pre
let next = node.next
pre?.next = next
next?.pre = pre
node.next = first.next
first.next?.pre = node
first.next = node
node.pre = first
}
func removeLastNode() {
let node = last.pre
node?.pre?.next = last
last.pre = node?.pre
count -= 1
hash[node!.key!] = nil
}
func addNewNode(key: Int, node: DLikedNode) {
moveToHead(node: node)
count += 1
hash[key] = node
}
func get(_ key: Int) -> Int {
let node = hash[key]
if let n = node {
moveToHead(node: n)
return n.val
}else {
return -1
}
}
func put(_ key: Int, _ value: Int) {
let node = hash[key]
if let n = node {
// 挪到第一位
n.val = value
moveToHead(node: n)
}else {
let newNode = DLikedNode(val: value, key: key)
// 判断容量
if count >= capacity {
// 移除最后一个节点
removeLastNode()
// 新节点插入头节点后
addNewNode(key: key, node: newNode)
}else {
// 新节点插入头节点后
addNewNode(key: key, node: newNode)
}
}
}
}
至此,以上就是LRU算法的简单封装,调用函数可以避免直接操作链表和哈希表。
其他经典的设计类的算法题还有:460. LFU缓存、380. O(1)时间插入、删除和获取随机元素、295. 数据流的中位数、895. 最大频率栈