一、目录
1.1、数据结构
- 数组:Array
- 栈和队列:Stack / Queue
- 优先队列(堆):Priority Queue(heap)
- 链表:LinkedList (Single/Double)
- 树(二叉树、二叉搜索树):Binary Tree
- Hash表:HashTable
- 集合:Disjoint Set
- 单词查找树:Trie
- 利用hash函数把数据映射到bit数组中:BloomFilter
- LRU Cache
1.2、算法
- 经典编程
- 中序、前序、后序遍历
- 贪心算法
- 递归/回溯算法
- 广度优先查找
- 深度优先查找
- 分治法
- 动态规划
- 二分查询
- 图
二、时间复杂度
- O(1):常数复杂度
- O(log n):对数复杂度
for(int i=1; i<n; i=i*2) {
System.out.println(i);
}
- O(n):线性时间复杂度
- O(n^2):平方
- O(n^3):立方
- O(2^n):指数
for(int i-1; i<=Math.pow(2,n); i++) {
System.out.println(i) //Math.pow(底数,几次方)
}
- O(n!):阶乘
for(int i=1; i<=factorial(n); i++) {
System.out.println(i); //factorial为阶乘函数
}
时间复杂度:只看最高复杂度。
O(1) < O(log(n)) < O(n) < O(nlog(n)) < O(n^2) < O(2 ^ n) < O(n!)
时间复杂度差一级,对于高并发系统差别非常大,这就是算法与数据结构重要的关键点。
例子:斐波那契数列:1,1,2,3,5,8,13,21,34,…
F(n) = F(n-1) + F(n-2)
public int fib(n) {
if (n==0 || n==1) {
return n;
}
return fib(n-1) + fib(n-2);
}
怎样看递归的时间复杂度呢?看n=5时计算多少次,n=10时计算多少次,这样就可以看出时间复杂度了。
以n=6可以看出:
实际的时间复杂度是O(2^n)
所以递归的时间复杂度非常差,虽然代码行数少。
所以面试时不要写递归算法。
常见算法的时间复杂度:
- 二分查找:O(log(n))
- 二叉树遍历:O(n):因为每个节点仅遍历一次
- 排序查找或排序二维矩阵查找:O(n) ,二维是O(n), 一维就可以是O(log(n))
- 各类排序(快排、归并排序等):O(nlog(n))
算法:三分学习七分练习:做常见算法但不太熟:如动态规划、搜索、递归等。
去Leetcode上练习。常看dicussion和solution
做题时,要把所有可能的解法都想出来,然后找个最佳的方案(时间、空间复杂度),千万不要想到一个解法就做题。
给出数列[2, 7, 11, 15]; target=9
因为num[0]+num[1]=9; 所以结果返回
[0,1]
机会永远留给有准备的人!
三、数据结构详解
3.1、数组和链表
3.1.1、数组:内存中连续的区域,可根据下标访问数组中的元素。
0~8是数组下标,可根据下标随意寻址找到内容,查询时间复杂度是O(1)
但数组的insert, delete的时间复杂度是O(n):
3.1.2、链表:本质上就是一个个的节点集合,然后通过节点上的指针next存储下一个节点的地址。
链表分为单链表和双链表
链表的插入过程:
链表的删除过程:
链表的插入和删除的时间复杂度都是O(1),但查询时间复杂度是O(n)
双链表:
3.1.3、习题
习题1、单链表反转
输入:1->2->3->4->5->NULL
输出:5->4->3->2->1->NULL
习题2、给定一个链表,反转相邻的两个节点
输入:1->2->3->4
输出:2->1->4->3
写的时候,可以画图辅助
习题3、给定一个链表,看其中是否有环
思路1:把每个走过的节点放到set, 直接判重。时间复杂度O(n)
思路2:快慢指针:快指针每次走2步;慢指针每次走1步。然后看快慢指针是否相遇。时间复杂度也是O(n), 空间复杂度比思路1更好。
3.2、栈和队列
3.2.1、概念
栈Stack:先入后出:数组或链表
队列queue:先进先出:数组或双向链表
3.2.2、习题
习题1、给定一个只包含大、中、小括号的字符串,判断字符串是否有效。
示例:输入“() [] {}", 输出:true
输入"{[)]", 输出:false
输入”((([])))", 输出:true
结题思路:使用栈:
左:push; 右:peek
左括号放到stack, 右括号跟栈顶元素看是否匹配。
时间复杂度是O(n),空间复杂度也是O(n)
习题2、只用stack实现queue, 只用queue实现stack
思路:负负得正。
1、用Stack实现queue:
要用两个stack:一个输入stack, 一个输出stack
输入stack:1->2->3
输出栈: 3->2->1
这样输出栈就实现了先入先出了。
3.3、优先队列(堆)
3.3.1、概念
Priority Queue:正常如,按优先级出。
3.3.2、实现机制
1、Heap (Binary, Binomial, Fibonacci)
2、Binary Search Tree
最小堆:最小的元素在上面,小于左孩子和右孩子。
最大堆:最大的元素在最上面,大于左孩子和右孩子。
各种堆的时间复杂度:
3.3.3、习题
3.3.3.1、实时判断数据流中第K大元素
示例:int k=3
int[] arr = [4,5,8,2]
思路1:保持前k个最大值,时间复杂度:N* klog(k)
思路2:最小堆:size=k,时间复杂度:N * log以2为底K的对数
Java中的最小堆,默认就是PriorityQueue:
Class KthLargest {
final PriorityQueue<Integer> q;
final int k;
public KthLargest(int k, int[] a) {
this.k=k;
q = new PriorityQueue<>(k);
for (int n : a) {
add(n);
}
}
public int add(int n) {
if (q.size() < k) {
q.offer(n);
} else if (q.peek() < n) {
q.poll();
q.offer(n);
}
return q.peek();
}
}
3.3.3.2、滑动窗口最大值
示例:输入:nums=[1,3,-1,-3,5,3,6],k=3(k是固定大小的窗口)
输出:[3,3,5,5,6]
思路1:优先队列:大顶堆。时间复杂度是N*log(k)
思路2:因为是固定大小的窗口,直接用双端队列dueue, 每次保留该窗口最大的k个值。时间复杂度O(N)
PriorityQueue: 实际上是一个堆(不指定Comparator时默认为最小堆),通过传入自定义的Comparator函数可以实现大顶堆。
PriorityQueue minHeap = new PriorityQueue(); //小顶堆,默认容量为11
PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(11,new Comparator<Integer>(){ //大顶堆,容量11
@Override
public int compare(Integer i1,Integer i2){
return i2-i1;
}
});
3.4、Map和Set:常用来做查询和计数
题目1、有效的字母异位词,不限字母次序
示例1:
输入 s=“anagram”, t=“nagaram”
输出 true
示例2:
输入 s=“rat”, t=“car”
输出 false
示例3:
输入 s=“rat”, t=“art”
输出 true
思路1:按字母排序,看排序是否相同,时间复杂度O(N*log(N))
思路2:数每个字母出现的次数,用Map计数,时间复杂度是O(N)
题目2、两数之和: 看数组中是否有两个元素的和等于target, 然后返回元素的下标
给定nums=[2,7,11,15], target=9
因为nums[0[ + num[1] = 9, 所以返回 [0, 1]
思路1:暴力求解,时间复杂度O(N^2)
思路2:采用Set存储x,时间复杂度O(n)
然后循环set看9-x是否在set中存在,时间复杂度O(1)
所以时间复杂度是O(n)
题目3、三数之和(高频面试题): 给定一个数组,看是否存在三个数的和等于target
给定数组 nums=[-1, 0, 1, 2, -1, -4],target=0
满足要求的三元组集合为:
[
[-1, 0, 1]
[-1, -1, 2]
]
思路1:暴力求解:三层嵌套循环,时间复杂度O(N^3)
思路2:c=0-(a+b), 看-(a+b)是否在set中存在,时间复杂度是O(N^2)
思路3:先排序,再查找:
先把整数数组排序,
遍历排序后的数组,在遍历的每个元素为a, a左边是b, 最右边是c
判断a+b+c<0, 那么b向右移动1位
a+b+c>0, 那么c向左移动1位
3.5、树、二叉树、二叉搜索树 & 图
树是单链表很像,树只是不只有一个next指针
完全二叉树:每个节点都有一个左孩子和右孩子。
图:可以从孩子指回父节点(面试不常考图)
链表、树、图的关系:
链表是特殊化的树;树是特殊化的图。
3.5.1、二叉搜索树
也叫有序二叉树,指一颗空树或具备如下特征的二叉树:
1、左子树上所有节点的值均小于它根节点的值
2、右子树上所有节点的值均大于他的根节点的值
3、递归:左右子树也分别为二叉查找树
二叉树用的最多的场景就是二叉搜索树。
这样每次查找,根据与根的大小比较,可以减少一半的查找量,把时间复杂度从O(N)变成了O(log(n))
实战中很多二叉搜索树都是用红黑树来实现的。
3.5.2、习题
题目1、验证二叉搜索树
示例1:
输入:[3,1,5,null, null, 2]
输出:true
示例2:
输入:[5,1,4,null, null, 3,6]
输出:false
思路1:中序遍历(中序过程中,看根节点是否大于左节点,否则return false),看结果是否与给定序列相同。时间复杂度O(N)
思路2:递归:左子树找最大值max,右子树找最小值min
然后判断max<root, 且min>root。时间复杂度O(N)
中序遍历:左、根、右。
前序遍历:根、左、右。
后续遍历:左、右、根。
题目2、二叉树/二叉搜索树两个节点的最近公共祖先
示例1:
输入: root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=8
输出:6
示例2:
输入:root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=4
输出:2
思路1:往上寻找,寻找最早重合的路径,时间复杂度O(N)
思路2:递归:findPorQ(root, p, q): 在root为根的子树中找p和q
3.6、二叉树遍历
前序遍历:中、左、右:
A,B,D,E,C,F,G
中序遍历:左、中、右
D,B,E,A,F,C, G
后续遍历:左、右、根
D,E,B,F,G,C,A
3.7、递归&分治
递归:自己调自己,但要防止死循环。递归可以加个level参数。
题目:求 n! = 1* 2* 3*… * n
public int factorial(int n) {
if (n <=1) {
return 1;
}
return n * factorial(n-1);
}
分治:把大问题拆分成n个子问题分别求解
题目:给定一个字符串:abcdefghij, 把每个字符变成大写。
解题思路:分治法:并行计算。
分治的好处是没有重复计算。
3.7.1、题目
题目1、pow(x, n),即x的n次幂
示例1:
输入: 2.00000, 10
输出:1024.00000
示例2:
输入:2.10000, 3
输出:9,26100
示例3:
输入:2.00000, -2
输出:0.25000
思路1:直接调库函数,但面试官不允许
思路2:暴力:循环,时间复杂度O(n)
思路3:分治法,时间复杂度O(logN), 可通过递归算法来实现,也可用非递归方式实现。
题目2、求众数:count(x) > n/2, x是数组中重复的元素,count(x)是x出现的次数,n是数组元素的个数。
示例1:
输入:[1,3,3,2,3]
输出:3
示例2:
输入:[1,1,1,0,2]
输出:1
思路1:暴力法:枚举所有的x,然后算出count(x), 时间复杂度是O(n^2)
思路2:用Map来计数,一次循环,把元素的个数都放到Map中,然后看Map中谁的count最大,谁就是结果。时间复杂度O(n)
思路3:先把数组排序,然后从左到右遍历,只要重复次数大于n/2, 就返回。时间复杂度是O(N*logN)
3.8、贪心算法
对问题求解时,总是做出当前看起来是最好的选择。
要凑36元,怎样找到纸币个数最少?
这里正好有这样面值的纸币,所以贪心才行,但如果面值不是20,10,5,1, 就不适合用贪心算法了。
面试时考贪心算法非常少,因为贪心算法使用场景非常少。
适用贪心算法:问题一定能拆分成相同的子问题,子问题的最优解就是全局问题的最优解。
通常来说,用贪心算法不能解的题,可考虑用动态规划法。
贪心算法与动态规划法的不同在于:
- 贪心算法对于每个子问题的解,不能回退
- 动态规划会保持以前的运算结果,并根据以前的运算结果对当前做出选择,有回退功能。
3.8.1、习题
题目1、买股票的最佳时机
输入:[7,1,5,3,6,4],输出:7
输入:[1,2,3,4,5], 输出:4
输入:[7,6,3,2], 输出:0
3.9、广度优先搜索
适用场景:在树(图)中找到特定节点。
3.10、深度优先搜索
深度优先与广度优先对比:
深度优先:推荐用递归写法
广度优先:非递归写法
3.10.1、习题
题目1、二叉树逐层遍历
给定二叉树[3,9,20,null,null,15,7]
返回逐层遍历结果:
[
[3],
[9,20],
[15,7]
}
思路1:广度优先
思路2:深度优先:先建好二维数组,然后深度优先,根据层数填充。
两种思路的时间复杂度都是O(n)
题目2、二叉树的最大和最小深度
给定二叉树:[3,9,20,null,null, 15, 7, null, 4]
返回他的最大深度4,最小深度2
思路1:递归
思路2:深度优先搜索,时间复杂度O(n)
思路3:广度优先搜索,时间复杂度O(n)
题目3、生成括号
给出n代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如:给出n=3, 生成结果为:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]
思路1:数学归纳法:先n=1:(), 再n=2:()(),(()), 再n=3,… 但这种方式写代码很复杂。
思路2:深度优先搜索,当n给定,字符串的长度也就给出了即2*n。把所有可能的组合,都列出来,然后判断是否合法。但时间复杂度不好O(2^n)
思路3:在思路2上改进:
1)局部组装出如果不合法,就不继续算了。
2)记录左括号用了几次,右括号用了几次, 虽然时间复杂度也是O(2^n),但比思路好快不少。
3.11、剪枝
把树中不需要的分支去掉。
3.11.1、习题
题目1、国际象棋:N皇后问题
如何将N个皇后放在N*N的棋盘上,并且使得皇后之间不能互相攻击
思路1:暴力
思路2:数组:
col[j] = 1
pie[i+j]=1
na[i-j]=1
在剪枝时,把横竖撇捺放在Set里,排除Set里的位置。
也可以用位运算,把横竖撇捺的值置为0
题目2、有效的数独
判断一个99的数独是否有效
只需要根据以下规则,验证已填入的数字是否有效即可。
1、数字1-9在每行只能出现一次
2、数字1-9在每列只能出现一次
3、数字1-9在每一个以粗实线分割的33宫内只能出现一次
比较难,解法略
3.12、二分法查找
二分查找的前提:
1、有序的数组
2、数组存在明确的上下界
3、可以通过索引来访问
所以有序数组适合二分查找,而链表不适合。
3.12.1、习题
题目1、实现一个求解平方根的函数 int sqrt(int x)
计算并返回x的平方根,其中x是非负整数,由于返回类型是整数,结果只保留整数部分,小数部分被舍弃。
思路1:二分法:left=0; right=5; mid=2.5
思路2:牛顿迭代法:也是不断的枚举,求他的切线,用它的根来逼近;再求切线,…
1.13、字典树Trie
搜索中的自动提示,用的就是Trie
Trie树,又称字典树、单词查找树、键树,是Hash树的变种。
其典型应用是统计和排序大量的字符串(但不限于字符串),所以常被搜索引擎用于文本词频统计。
他的优点是:最大限度地减少无畏的字符串比较,查询效率比Hash表高。
1.13.1、Trie树的核心思想
空间换时间,利用字符串的公用前缀来降低查询时间的开销,以达到提高性能的目的。
static final int ALPHABET_SIZE=256;
static class TrieNode {
TrieNode[] children = new TrieNode[ALPHABET_SIZE];
boolean isEndOfWord=false;
TrieNode() {
isEndOfWord=false;
for (int i=0; i < ALPHABET_SIZE; i++) {
children[i]=null;
}
}
}
Trie树的基本性质:
1、根节点不包含任何字符,除根节点每个节点只包含一个字符
2、从根节点到某一节点,路径上经过的字符连接起来,为节点对应的字符串
3、每个节点的所有子节点包含的字符都不相同。
1.13.2、习题
习题1、实现一个字典树
Class TrieNode {
public char val;
public boolean isWord;
public TrieNode[] children = new TrieNode[26]; // 只支持小写字符a~z
public TrieNode() {}
TrieNode (char c) {
TrieNode node = new TrieNode();
node.val = c;
}
}
public class Trie {
private TrieNode node;
public Trie() {
root = new TireNode();
root.val = " ";
}
public void insert(String word) {
TrieNode ws = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
// 判断该节点是否存在
if (ws.children[c - 'a'] == null) {
// 不存在则新建个节点
ws.children[c - 'a'] = new TrieNode();
}
ws = ws.children[c - 'a']
}
ws.isWord = true;
}
public boolean search(String word) {
TrieNode ws = root;
for (int i = 0; i < word.length; i++) {
char c = word.charAt(i);
if (ws.children[c - 'a'] == null) {
return false;
}
ws = ws.children[c - 'a'];
}
return ws.isWord;
}
public boolean startsWith(String prefix) {
TrieNode ws = root;
for (int i = 0; i < prefix.length; i++) {
char c = prefix.charAt(i);
if (ws.children[c - 'a'] == null) {
return false;
}
ws = ws.children[c - 'a'];
}
return true;
}
}
习题2、二维网格中的单词搜索问题
输入:候选词word = [“oath”, “pea”, “eat”, “rain”];
board=[
[‘o’, ‘a’, ‘a’, ‘n’],
[‘e’, ‘t’, ‘a’, ‘e’],
[‘i’, ‘h’, ‘k’, ‘r’],
[‘i’, ‘f’, ‘l’, ‘a’]
]; 相邻元素可组成单词
输出:[“eat”, “oath”]
思路1:深度优先搜索
思路2:Trie: 先根据word建立Trie树,然后把board可能出现的情况枚举,去Trie中搜索
3.14、位运算
3.14.1、什么是位运算
程序中所有数在内存中都是二进制形式存储的,位运算就是直接对整数在内存中的二进制进行操作。
例如,and本身就是个逻辑运算符,但整数与整数之间可以进行and运算,例如6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2。它是二进制对应位的逻辑运算的结果。(0表示false, 1表示true, 空位都当0处理)
由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
3.14.2、实战常用的位运算操作
x & 1 == 1 OR == 0 : 判断奇偶(x % 2 == 1)
x = x & (x - 1) : 清零最低位的1
x & -x : 得到最低位的1
3.14.2、题目:求位1的个数
输入:一个无符号整数
输出:返回其二进制表达式中数字位数为”1“的个数
思路1:x %2 == 1, 则count++, 然后x = x>>1
思路2:x = x & (x - 1)
3.15、动态规划(面试重点)
1、递归 + 记忆化 --> 递推
即先用递归 + 记忆化的方式来做题,最后再思考怎样做成递推的形式。
2、状态的定义:opt[n], dp[n], fib[n]
把状态定义成数组,然后用数组来推导出状态转移方程
3、状态转移方程:opt[n] = best_of (opt(n - 1), opt(n - 2), …)
根据前面的opt最优解得出 状态转移方程opt[n]的解
4、最优子结构
例1、斐波拉切数列
0,1,1,2,3,5,8,13,21,…
递推公式:f[n] = f[n-1] + f[n-2]
把计算过的值,存到缓存mem(也有就是记忆化),这样就把原来O(2^n)优化为O(n)
这样就用递归 + 记忆化做到了递推。
例2、一个人从起点走到终点,要么向右,要么向下,不能回退,实心的格子不能走,那么问从起点到终点有多少种走法?(格子高度为M,宽度为N)
状态转移方程:
每个格子的值等于他下面和右面格子值的和。
看左上角,其下面+右面为10 + 17 = 27,也就是共有27种走法。
时间复杂度是O(M*N)
这就是动态规划的最基础的题目。
总结:所谓动态规划,实际上就是动态递推的过程。
递推,最好不要递归+记忆化,递归+记忆化是比较初级阶段采用的方法。
动态规划 与 回溯算法 与 贪心算法 的区别和联系?
1)回溯(递归):重复计算 --> 不存在最优子结构的问题
2)贪心:永远局部最优 --> 局部最优并不一定能达到全局最优
3)动态规划:记录局部最优子结构/多种记录值 --> 通过缓存局部解,记录局部最优值,通过全面推导出全局最优解。
当贪心算法不是只看眼前利益,就变成了动态规划。
动态规划法很难,可能想不到思路,主要原因是功力还不够:解决方法是把常用的解法记下来,然后多多联系,直到达到看到题就有思路的程度。
3.15.1、习题
习题1、爬楼梯
输入:3
输出:3
解释:有三种方法可以爬到楼顶
方法1:1阶 + 1阶 + 1阶
方法2:1阶 + 2阶
方法3:2阶 + 1阶
思路1:回溯即递归 + 缓存子结果
f(n) = f(n-1) + f(n-2)
f(0) = f(1) = 1
思路2:动态规划
for i=2–>n:
f[n] = f[n-1] + f[n-2]
时间复杂度O(n)
动态规划思路的关键:
1)DP状态的定义:
f(n):定义为到第n阶的总走法个数
2)列出DP方程
f[n] = f[n-1] + f[n-2], 即斐波拉契数列。
习题2、三角形最小路径和
给定三角形:
[
[2]
[3,4]
[6,5,7]
[4,1,8,3]
]
自顶向下的最小路径和为11。即2+3+5+1=11
规则是他只能走他相邻的两个6下面的点,如3只能走6或5,不能走7。
思路1:回溯(递归)
思路2:动态规划
动态规划时,可以从下往上定义DP状态,通常是二维数组DP[i,j]
DP[i, j] = min( DP[i + 1, j], DP[i+1, j+1]) + self[i, j]
DP[m-1, j] = self[m-1, j]
3.16、并查集
3.16.1、概念
并查集(union and find):是一种树的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。
Find:确定元素属于哪一个子集,他可以用来确定两个元素是否属于同一个子集。
Union:将两个子集合并成一个集合。
生活中的例子:
小弟 --> 老大
帮派识别
两种优化方式
优化一:
优化二:
3.16.2、习题:岛屿的个数
给定一个由‘1’(陆地)和‘0’(水)组成的二维网络,计算岛屿的数量。一个岛屿被水包围,并且他是通过水平方向或垂直方向上相邻的陆地连接而成,你可以假设网络的四个边均被水包围。
示例:
输入:
11000
11000
00100
00011
输出:3
思路1:染色问题
思路2:并查集
3.17、LRU Cache
学习算法也要把常用的套路背会,即放到自己脑子这个cache里,这样考试时才能快速答处理,把O(N)变成O(1)。
LRU(least recently used):最近最少使用。
使用双向链表实现:如LinkedHashMap。
查询时间复杂度:O(1)
修改时间复杂度:O(1)
习题:实现一个LRU Cache
思路:用双向链表实现
3.18、布隆过滤器
在高并发系统、分布式系统用的非常多。
布隆过滤器用一个很长的二进制向量,和一个映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。
布隆过滤器的优点是时间效率和空间效率都远好于一般的算法,但缺点是有一定的错误识别率和删除困难。
布隆过滤器用于判断一个元素在还是不在一个集合,如果判断不在,那么一定不在;如果判断在,那么不一定在,还需要进一步校验(去内存或数据库中查询)。
布隆过滤器的实现,跟Hash表类似,只是布隆过滤器不是把查询映射到数组的某一个位置,而是散射到一个很长的二进制向量中
x, y, z是已有元素,w是查询是否存在
A,E是存在的,查询A, C, B是否存在,可以看到,B映射时,发现B存在,但实际B是不存在的。
布隆过滤器使用案例:
1、比特币:
用布隆过滤器判断是否存在,如果不存在就是不存在,如果存在,再去Redis中查询,降低了Redis的负载。
2、分布式系统(Map-Reduce)
四、总结
4.1、常用算法模板
递归模板:
二分:
DP模板:
4.2、练习与刷题
1、需要刻意练习自己不熟悉的算法和数据结构
2、做过的题目要反复练习
3、告别舒适区
4.3、面试答题四件套
在面试前,对前面的模板套路一定要形成机械记忆和条件反射!!!
1、搞清题目
弄清题目细节、边界条件、可能的极端错误情况
2、把所有可能的解法和面试官沟通一遍:不要把面试官当成监考老师,而是把他当成未来的同事,当成讨论问题。
1)每种解法的时间复杂度、空间复杂度
2)最优解
3、写代码
4、测试用例
1)正常用例
2)极端用例
3)想不到的用例
4.4、回到起点
https://blog.csdn.net/shenjian58/article/details/89850701
斐波那契:最好时间复杂度是O(log(n))
4.5、经验总结
1、三分学、七分练
2、算法和数据结构是内功修养,重在练习(修行)。