在讲这道题以前,我想先聊聊「技术面试到底是在考什么」这个问题。java
在人人都知道刷题的今天,面试官也都知道你们会刷题准备面试,代码你们都会写,那面试为何还在考这些题?那为何有些人代码写出来了还挂了?node
你们知道美国的大厂面试 80%是在考算法,这实际上是最近 5-10 年以谷歌、雅虎为首才兴起的;国内大厂对于算法的考察虽然没有这么狂热,但也愈来愈重视了。面试
那么算法面试真的只是在考算法吗?显然不是。本质上考的是思考问题的方式,分析、解决问题的能力,以及和同事沟通交流的能力,看你可否主动推动去解决问题。算法
套路就是:数据库
虽然说是套路,但未尝不是一个高效的工做方式?缓存
那拿到一个问题,首先应该是去 clarify 这个问题,由于工做就是如此,不像在刷题网站作题什么都给你定义好了,面试官一般都不会一次性给你全部条件,而是须要你思考以后去问他。那经过这个环节,面试官就知道了你遇到问题是怎么去思考的,你考虑的是否全面,怎么去和别人沟通的,从此和你一块儿工做的状态是怎样的。服务器
就像咱们平时工做时,须要和 product manager 不断的 clarify 需求,特别是没定义清楚的部分,反反复复的讨论,也是磨刀不误砍柴工。那这个过程,在我司可能就要 1-2 周,不会很着急的就开始,不然努力错了方向就是南辕北辙,得不偿失。那么面试时也是同样,代码都写完了面试官说这不是我想问的,那时候已经没时间了,买单的是咱们本身。数据结构
第二点分析思路就是重中之重了,也是本文的核心,会以 LRU Cache 这到经典题为例,展现我是如何思考、分析的。app
第三点写代码,没什么好说的,终究是须要落到实处的。模块化
第四点跑测试,不少同窗可能会忘,因此若是你能主动提出 run test cases,过几个例子检验一下,是很好的。
有些人说每道题我都作出来了,为何仍是挂了?那照着这四点对比一下,看看是哪一个环节出了问题。
另外这道题为何各大公司都喜欢考呢?
一是由于它可以多方面、多维度的考察 candidate:这道题考察的是基本功,考对数据结构理解使用,考能不能写出 readable 的代码。一场 45 分钟-60 分钟的面试,如何摸清楚 candidate 的真实水平,也是不容易的啊。
二是由于这道题可难可易,能够简单到像 Leetcode 上那样把 API 什么的都已经定义好了,也能够难到把 System Design 的内容都包含进来,聊一下 Redis 中的近似 LRU 算法。
因此 follow up 就能够无限的深刻下去,若是面试官想问的你都能回答的头头是道,那 strong hire 天然跑不掉。那有些同窗只会到第一层或者第二层,面试是优中选优的过程,其余同窗会的比你多,沟通交流能力又好,天然就是别人拿 offer 了。
那今天就以这道题为例,在这里浅谈一下个人思考过程,为你们抛砖引玉,欢迎在留言区分享你的想法。
LRU = Least Recently Used 最近最少使用
它是一种缓存逐出策略 cache eviction policies
LRU 算法是假设最近最少使用的那些信息,未来被使用的几率也不大,因此在容量有限的状况下,就能够把这些不经常使用的信息踢出去,腾地方。
好比有热点新闻时,全部人都在搜索这个信息,那刚被一我的搜过的信息接下来被其余人搜索的几率也大,就比前两天的一个过期的新闻被搜索的几率大,因此咱们把好久没有用过的信息踢出去,也就是 Least Recently Used 的信息被踢出去。
举个例子:咱们的内存容量为 5,如今有 1-5 五个数。
咱们如今想加入一个新的数:6
但是容量已经满了,因此须要踢出去一个。
那按照什么规则踢出去,就有了这个缓存逐出策略。好比:
FIFO (First In First Out)
这个就是普通的先进先出。LFU (Least Frequently Used)
这个是计算每一个信息的访问次数,踢走访问次数最少的那个;若是访问次数同样,就踢走很久没用过的那个。这个算法其实很高效,可是耗资源,因此通常不用。LRU (Least Recently Used)
这是目前最经常使用了。LRU 的规则是把很长时间没有用过的踢出去,那它的隐含假设就是,认为最近用到的信息之后用到的几率会更大。
那咱们这个例子中就是把最老的 1 踢出去,变成:
不断迭代...
简单理解就是:把一些能够重复使用的信息存起来,以便以后须要时能够快速拿到。
那至于它存在哪里就不必定了,最多见的是存在内存里,也就是 memory cache,但也能够不存在内存里。
使用场景就更多了,好比 Spring 中有 @Cacheable 等支持 Cache 的一系列注解。上个月我在工做中就用到了这个 annotation,固然是我司包装过的,大大减小了 call 某服务器的次数,解决了一个性能上的问题。
再好比,在进行数据库查询的时候,不想每次请求都去 call 数据库,那咱们就在内存里存一些经常使用的数据,来提升访问性能。
这种设计思想实际上是遵循了著名的“二八定律”。在读写数据库时,每次的 I/O 过程消耗很大,但其实 80% 的 request 都是在用那 20% 的数据,因此把这 20% 的数据放在内存里,就可以极大的提升总体的效率。
总之,Cache 的目的是存一些能够复用的信息,方便未来的请求快速得到。
那咱们知道了 LRU,了解了 Cache,合起来就是 LRU Cache 了:
当 Cache 储存满了的时候,使用 LRU 算法把老家伙清理出去。
说了这么多,Let's get to the meat of the problem!
这道经典题你们都知道是要用 HashMap + Doubly Linked List,或者说用 Java 中现成的 LinkedHashMap,可是,为何?你是怎么想到用这两个数据结构的?面试的时候不讲清楚这个,不说清楚思考过程,代码写对了也没用。
和在工做中的设计思路相似,没有人会告诉咱们要用什么数据结构,通常的思路是先想有哪些 operations,而后根据这些操做,再去看哪些数据结构合适。
那咱们来分析一下对于这个 LRU Cache 须要有哪些操做:
那第一个操做很明显,咱们须要一个可以快速查找的数据结构,非 HashMap
莫属,还不了解 HashMap 原理和设计规则的在公众号内发消息「HashMap」,送你一篇爆款文章;
但是后面的操做 HashMap 就不顶用了呀。。。
来来来,咱们来数一遍基本的数据结构:
Array, LinkedList, Stack, Queue, Tree, BST, Heap, HashMap
在作这种数据结构的题目时,就这样把全部的数据结构列出来,一个个来分析,有时候不是由于这个数据结构不行,而是由于其余的数据结构更好。怎么叫更好?忘了咱们的衡量标准嘛!时空复杂度,赶忙复习递归那篇文章,公众号内回复「递归」便可得到。
那咱们的分析以下:
Array, Stack, Queue 这三种本质上都是 Array 实现的(固然 Stack, Queue 也能够用 LinkedList 来实现。。),一会插入新的,一会删除老的,一会调整下顺序,array 不是不能作,就是得 O(n) 啊,用不起。
BST 同理,时间复杂度是 O(logn).
Heap 即使能够,也是 O(logn).
LinkedList,有点能够哦,按照从老到新的顺序,排排站,删除、插入、移动,均可以是 O(1) 的诶!可是删除时我还须要一个 previous pointer 才能删掉,因此我须要一个 Doubly LinkedList.
那么咱们的数据结构敲定为: HashMap + Doubly LinkedList
选好了数据结构以后,还须要定义清楚每一个数据结构具体存储的是是什么,这两个数据结构是如何联系的,这才是核心问题。
咱们先想个场景,在搜索引擎里,你输入问题 Questions,谷歌给你返回答案 Answer。
那咱们就先假设这两个数据结构存的都是 <Q, A>,而后来看这些操做,若是都很顺利,那没问题,若是有问题,咱们再调整。
那如今咱们的 HashMap 和 LinkedList 长这样:
而后咱们回头来看这四种操做:
操做 1,没问题,直接从 HashMap 里读取 Answer 便可,O(1);
操做 2,新加入一组 Q&A,两个数据结构都得加,那先要判断一下当前的缓存里有没有这个 Q,那咱们用 HashMap 判断,
但是,怎么找 LinkedList 的这个 node 呢?一个个 traverse 去找并非咱们想要的,由于要 O(n) 的时间嘛,咱们想用 O(1) 的时间操做。
那也就是说这样记录是不行的,还须要记录 LinkedList 中每一个 ListNode 的位置,这就是本题关键所在。
那天然是在 HashMap 里记录 ListNode 的位置这个信息了,也就是存一下每一个 ListNode 的 reference。
想一想其实也是,HashMap 里没有必要记录 Answer,Answer 只须要在 LinkedList 里记录就能够了。
以后咱们更新、移动每一个 node 时,它的 reference 也不须要变,因此 HashMap 也不用改动,动的只是 previous, next pointer.
那再一想,其实 LinkedList 里也不必记录 Question,反正 HashMap 里有。
这两个数据结构是相互配合来用的,不须要记录同样的信息。
更新后的数据结构以下:
这样,咱们才分析出来用什么数据结构,每一个数据结构里存的是什么,物理意义是什么。
那其实,Java 中的 LinkedHashMap 已经作了很好的实现。可是,即使面试时可使用它,也是这么一步步推导出来的,而不是一看到题目就知道用它,那一看就是背答案啊。
有同窗问我,若是面试官问我这题作没作过,该怎么回答?
答:实话实说。
真诚在面试、工做中都是很重要的,因此实话实说就行了。但若是面试官没问,就没必要说。。。
其实面试官是不 care 你作没作过这道题的,由于你们都刷题,基本都作过,问这个问题没有意义。只要你能把问题分析清楚,讲清楚逻辑,作过了又怎样?不少作过了题的人是讲不清楚的。。。
那咱们再总结一下那四点操做:
第一个操做,也就是 get() API,没啥好说的;
二三四,是 put() API,有点小麻烦:
画图的时候边讲边写,每一步都从 high level 到 detail 再到代码,把代码模块化。
当年我把这图画出来,面试官就没让我写代码了,直接下一题了...
那若是面试官还让你写,就写呗。。。
class LRUCache { // HashMap: <key = Question, value = ListNode> // LinkedList: <Answer> public static class Node { int key; int val; Node next; Node prev; public Node(int key, int val) { this.key = key; this.val = val; } } Map<Integer, Node> map = new HashMap<>(); private Node head; private Node tail; private int cap; public LRUCache(int capacity) { cap = capacity; } public int get(int key) { Node node = map.get(key); if(node == null) { return -1; } else { int res = node.val; remove(node); appendHead(node); return res; } } public void put(int key, int value) { // 先 check 有没有这个 key Node node = map.get(key); if(node != null) { node.val = value; // 把这个node放在最前面去 remove(node); appendHead(node); } else { node = new Node(key, value); if(map.size() < cap) { appendHead(node); map.put(key, node); } else { // 踢走老的 map.remove(tail.key); remove(tail); appendHead(node); map.put(key, node); } } } private void appendHead(Node node) { if(head == null) { head = tail = node; } else { node.next = head; head.prev = node; head = node; } } private void remove(Node node) { // 这里我写的可能不是最 elegant 的,可是是很 readable 的 if(head == tail) { head = tail = null; } else { if(head == node) { head = head.next; node.next = null; } else if (tail == node) { tail = tail.prev; tail.next = null; node.prev = null; } else { node.prev.next = node.next; node.next.prev = node.prev; node.prev = null; node.next = null; } } } } /** * Your LRUCache object will be instantiated and called as such: * LRUCache obj = new LRUCache(capacity); * int param_1 = obj.get(key); * obj.put(key,value); */
那再回到面试上来,为何不少面试是以算法考察为主的?这样的面试道理何在?算法题面试真的能衡量一我的的工做能力吗?(固然了,对于有些工做经验的人还会考察系统设计方面的内容。)
这是我一直在思考的问题,工做以后愈发以为,这样的面试真的是有效的。
由于咱们须要的是可以去解决未知的问题的能力,知识可能会被遗忘,可是思考问题的方式方法是一直跟随着咱们的,也是咱们须要不断提升的。那么在基本功扎实的前提下,有正确的方法和思路作指引,nothing is impossible.