摊还分析法
栈
常见题目:实现浏览器前进后退功能,表达式求解,括号匹配
队列
队列可用在池化技术中,用于请求的排队等待
循环队列
实现的要点是确定好队满和队空的判定条件
阻塞队列
生产者-消费者模型
并发队列
基于CAS实现无锁并发队列
递归
满足以下三个条件的问题可以使用递归:
一个问题可以分解为多个子问题
该问题与子问题的数据规模不一样,但解决思路一样
存在递归终止条件
我们找出问题与子问题的关系,写出递推公式,找出终止条件,根据这两项就可以直接得出代码。
我们思考递归时,不用去想每一层的调用关系和每一个步骤,因为这样的思考人脑很难去抽象。我们需要去假定子问题已经被解决,在子问题被解决的基础上去思考新问题。
递归是借助栈来实现的,所以用递归实现的地方都能改为迭代循环的方式来实现。
常见题目:斐波那契数列、跳蛙跳台阶、二叉树遍历
位运算
a&(a-1):相当于将a最右边的1变成0
常见题目:统计二进制中1的个数、判断是否2的幂次、m变成n需要变动几个二进制
数组
当我们对数组内的元素进行删除时,可以不去删除和搬移元素,可以先标记该元素已被删除,等到数组没有更多空间时再一起处理。
这个思想与JVM标记清除的GC算法一样。
链表
写完链表代码后,需要检查边界条件是否正确:
- 链表为空
- 链表只有1个节点
- 链表只有2个节点
- 代码处理头尾节点是否正确
单链表
单链表的操作比较简单,遍历只能单向从头到尾。
快慢指针常作为单链表解决问题的辅助手段。
常见题目:链表反转,环的检测,求入环点,有序链表合并,求倒数第k个节点,回文单链表,求链表中间节点
循环链表
常见问题:约瑟夫问题
双向链表
工程中双向链表较为常用,因为它能在O(1)时间找到前驱节点。
例如Java中的LinkedList、LinkedHashMap中都涉及到双向链表。
常见题目:LRU缓存(LinkedHashMap与LRU缓存原理是一样的,实现上有个小技巧:双端链表加一个header,双端链表其实是一个循环链表,这么做就不用判断表头表尾,相当于哨兵)
排序
可以利用稳定排序算法对多个字段排序。比如对订单按照时间先后排序,相同时间的订单按照金额大小再排序。可以先按照金额进行排序,再按照时间进行稳定排序。
O(n^2)
- 插入排序。工程上针对小规模数据较常用,相比冒泡排序,插入有着较少的交换次数;相比选择排序,插入是稳定的。
- 冒泡排序。比较并交换相邻两个元素。
- 选择排序。
O(n*logn)
归并排序
优点是不管什么情况下,时间复杂度都稳定在O(n*logn)。缺点是空间复杂度为O(n),需要辅助空间,工程中较少使用。
*常见题目: 现在有10个接口访问日志文件,每个日志文件大小约300MB,每个文件里的日志都是按照时间戳从小到大排序的。希望将这10个较小的日志文件,合并为1个日志文件,合并之后的日志仍然按照时间戳从小到大排列。机器内存只有1GB。
快速排序
工程上较为常用。分区点的选择是关键,如果每次都能选择到中间节点,时间复杂度是O(n*logn)。如果每次分区点都是头尾节点,那么时间复杂度就会退化为O(n^2)。工程中可以使用三数取中法或者随机法选择分区点。
常见题目:求第K大的数
堆排序
O(n)
排序能做到线性时间是因为这些排序不是基于比较的。
桶排序
桶排序对排序数据的要求非常严格,要求数据能平均地分配到各个桶中。
桶排序比较适合用在外部排序中。
常见题目: 有10GB的订单数据,希望按订单金额进行排序,但是内存有限,只有几百MB,没办法一次性把10GB的数据都加载到内存中。
根据年龄给100万用户排序。
计数排序
计数排序属于桶排序,这里把它单独拎出来是因为它是特殊的桶排序。
相比于桶排序,计数排序中桶的粒度更小,小到只有一个单位,就省去了桶内排序。
计数排序适用于数据范围不大的情况。
基数排序
基数排序要求数据可以分割出“位”来比较,且“位”的数据范围不能太大。对每一“位”使用桶排序,从低位一直排序到高位。
常见题目:对10万个手机号码从小到大排序
二分查找
二分查找对数据有以下要求:
- 数据需要是有序的
- 数据存储在数组中
- 数据集太小(顺序遍历即可)或太大(数组占用连续空间)都不合适二分查找
二分查找时间复杂度是O(logn),对数级有时比常数级还要高效,因为常数级中的常数可能会很大。
能用二分查找解决的问题,一般也能用动态数据结构散列表和二叉树解决,他们有一个区别是:二分查找由于依赖数组,因此它的占用内存会小一点,不需要存储指针等额外数据。
如果用链表来实现二分查找,时间复杂度会是O(n)。
二分查找适合查找近似的数值,它有4种变体问题:
- 查找第一个值等于给定值的元素
- 查找最后一个值等于给定值的元素
- 查找第一个大于等于给定值的元素
- 查找最后一个小于等于给定值的元素
常见题目: 有12万条IP区间与归属地的对应关系,如何快速定位出一个IP地址的归属地。
1 | [202.102.133.0, 202.102.133.255] 山东东营市 |
循环数据的二分查找
跳表
跳表可以理解为链表版的“二分”查找。时间复杂度为O(logn),空间复杂度为O(n)。跳表出现得比红黑树晚,在类库中有现成的红黑树代码,但没有跳表。
常见题目:为什么redis使用跳表而不是红黑树。(1.跳表实现比较简单;2.在区间查找上,跳表效率比红黑树高)
散列表
散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展。散列表有三个核心问题:散列函数设计、散列冲突解决、扩容策略。
冲突解决方法:开放寻址法和链表法。
当数据量比较小、装载因子小的时候,适合采用开放寻址法。
基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表
ThreadLocalMap通过线性探测的开放寻址解决冲突。
常见题目:
- Word单词拼写检查
- 假设有10万条URL访问日志,如何按照访问次数给URL排序
- 有两个字符串数组,每个数组大约有10万条字符串,如何快速找出两个数组中相同的字符串
- LRU缓存
- 配合跳表组成Redis有序集合
哈希算法
哈希算法有以下特点:
- 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法);
- 对输入数据非常敏感,哪怕原始数据只修改了一个Bit,最后得到的哈希值也大不相同;
- 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;
- 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。
常用的哈希算法有MD5、SHA、DES、AES等。根据鸽巢原理,哈希算法一定是会有冲突的。
哈希算法有以下几点应用:
安全加密
唯一标识
数据校验
散列函数
负载均衡。同一客户端请求路由到同一机器。
数据分片。
统计1T的搜索词日志,计算每个搜索词被搜索次数。
判断某张图片是否在图库中,图库有1亿张图片。
分布式存储。
对数据提取哈希,决定存储在哪台机器上。若新增机器,所有的数据都需要重新计算哈希。为了解决这个问题,可以使用一致性哈希算法,该算法不需要搬移全部数据,只需要将某几个小区间的数据搬移到新机器中。
二叉树
满二叉树
除叶子节点外,每个节点都有左右两个子节点。
完全二叉树
有一种二叉树,叶子节点都在最底下两层,最后一层叶子节都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
完全二叉树比较适合用数组来存储,由于不需要存储左右子节点,比较节省空间。单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
二叉查找树
二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉查找树相比散列表的优势:
- 散列表是无序的,二叉查找树中序遍历可输出有序。
- 散列表需要扩容。
- 在冲突较严重的情况下,散列表性能不稳定。
- 散列表的构造比二叉树复杂,要考虑散列函数、扩容、缩容、冲突解决等,而二叉树只需要考虑平衡性。
- 由于装载因子的存在,散列表较为消耗空间。
平衡二叉树
平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于1。
平衡二叉查找树
结合平衡树和查找树的二叉树就是平衡查找二叉树了。 平衡二叉查找树是为了解决普通二叉查找树在数据更新的过程中,复杂度退化的问题而产生的。
最先被发明的平衡二叉查找树是AVL树,它严格符合平衡二叉查找树的定义。
在工程中,红黑树要比AVL树更加常用。红黑树不是严格的平衡树。它的根节点到各个叶子节点的最长路径,有可能会比最短路径大一倍。
红黑树做到了近似平衡,并不是严格的平衡,所以在维护平衡的成本上,要比AVL树要低。
常见题目:
- 前中后序遍历;
- 层序遍历(用队列);
- 按层输出(用队列,且使用两个辅助变量);
堆
堆是一种特殊的数,满足以下两个条件的树可以称之为堆:
- 堆是一个完全二叉树;
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
既然堆是完全二叉树,那么堆就适合使用数组存储。从下标为1开始,假设一个节点下标为n,那么左右两个子节点的下标分别为2n和2n+1.
堆排序可分为两个阶段:建堆和排序。堆排序的时间复杂度是O(n*logn)
建堆:可以采用从下至上的插入数据的方法,假设刚开始数组里只有一个元素,每次往里插入一个元素,然后堆化。
排序:可以采用从上至下的删除数据的方法,将堆顶数据与最末尾的数据交换,然后重新堆化。
在工程中,堆排序没有快速排序快,有以下原因:
- 堆排序是跳着访问元素的,对CPU缓存不友好;
- 由于堆排序需要建堆,建堆后的数据可能比之前更加无序。所以堆排序需要的交换次数会大于快速排序
优先级队列是堆的一个应用方向。Java中PriorityQueue就是基于堆实现的。
常见题目:
topK
维护一个大小为K的堆,每次新来数据就堆化数组,每次插入时间复杂度为O(logn),取topK时间复杂度为O(1)(需要区别Partition方法的求第K大数)。
数据流的中位数
不仅仅可以求中位数,只要改变两个堆中的数据比例,就可以求类似“接口99响应时间”等问题了。
合并有序小文件
有100个小文件,每个文件的大小是100MB,每个文件中存储的都是有序的字符串。希望将这些100个小文件合并成一个有序的大文件。
字符串匹配
单模式匹配
BF算法
BF算法中的BF是Brute Force,暴力匹配。时间复杂度是O(n*m),其中n是主串长度,m是模式串长度。
BF算法简单简单朴素,效率低,但在实际开发中却较为常用。因为实际使用场景下,一般的字符串都比较小,选用BF效率影响不大。且BF算法简单不易出错。
RK算法
RK算法的全称叫Rabin-Karp算法,是由它的两位发明者Rabin和Karp的名字来命名的。
RK算法的思路是这样的:我们通过哈希算法对主串中的n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。哈希算法的选择很重要,理想情况下,时间复杂度是O(n),n为主串长度。
BM算法
BM(Boyer-Moore)算法是一种非常高效的字符串匹配算法,在工程非常常用。有实验统计,它的性能是著名的KMP算法的3到4倍。我们可以在BF算法的基础上理解BM算法。
在BF算法中,模式串在匹配失败后会往后移动一位。BM算法就是找到规律,让模式串每次多往后移动几位,减少不必要的字符比较。
BM算法的时间复杂度分析起来是非常复杂,这篇论文“Tight bounds on the complexity of the Boyer-Moore string matching algorithm”证明了在最坏情况下,BM算法的比较次数上限是3n。
常见题目:文本编辑器的查找功能。
KMP
KMP算法是根据三位作者(D.E.Knuth,J.H.Morris和V.R.Pratt)的名字来命名的。KMP算法的核心思想,跟BM算法非常相近。在模式串与主串匹配的过程中,当遇到不可匹配的字符的时候,我们希望找到一些规律,可以将模式串往后多滑动几位。
KMP算法的时间复杂度就是O(m+n) 。
多模式匹配
Trie树
Trie树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构, 用来解决在一组字符串集合中快速查找某个字符串的问题。Trie树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。构建Tire树的时间复杂度是O(m),m表示所有字符串的长度和。
将多个模式串构建成Tire树。Tire树构建完成后,就可以用主串来走这个Tire树了。时间复杂度为O(n),n为要查询的主串长度。
Tire的存储是比较浪费空间的,我们需要在效率和空间中找到平衡。
实际上,字符串的匹配问题,笼统上讲,其实就是数据的查找问题。对于支持动态数据高效操作的数据结构,比如散列表、红黑树、跳表等等。这些数据结构也可以实现在一组字符串中查找字符串的功能。我们拿散列表和红黑树,跟Trie树比较一下。Tire树对多个模式串有一定的要求,若不满足以下要求,Tire树的空间消耗会比较大:
字符串中包含的字符集尽量要小;
字符串的前缀重合尽量要多。
Trie树更加适合查找前缀匹配的字符串问题。散列表或者红黑树更加适合精确匹配查找问题。
常见题目:
- 手机英文输入自动提示单词(ACM题目:POJ1451 T9 http://poj.org/problem?id=1451);
- 搜索引擎的搜索关键词提示功能
AC自动机
AC自动机算法,全称是Aho-Corasick算法。其实,Trie树跟AC自动机之间的关系,就像单串匹配中朴素的串匹配BF算法,跟KMP算法之间的关系一样。AC自动机实际上就是在Trie树之上,加了类似KMP的next数组,只不过此处的next数组是构建在树上罢了。AC自动机适合大量文本中多模式串的精确匹配查找,时间复杂度可以到O(n*len)。len是模式串的平均长度。
常见题目:论坛中的敏感词过滤。
动态数据结构:散列表、二叉树、跳表
贪心算法
分治算法
子问题之间没有相关性
常见题目:求逆序对数量(借助归并排序)
回溯算法
回溯是一种在一组可能的解中,找到符合要求解的算法。剪枝操作可以增加回溯的效率。
常见问题:深度优先遍历、01背包、8皇后、旅行商问题、全排列等。
位运算
二进制1的个数(n&(n-1))
二分查找
杨氏矩阵是否存在某树(leetcode 74)
单峰值数组寻找峰值
多峰值数组寻找峰值
双指针
字符串翻转(单词,旋转)
堆
数据流中位数
DFS
二维矩阵中找字符串,不能重复(leetcode 79)
全排列
二叉树
二叉树深度(leetcode 104后序遍历)
判断是否平衡二叉树(leetcode 110后序遍历)
判断是否为另一棵树的子结构(leetcode 572)
判断是否镜像树(leetcode 101)
二叉树的最大直径(与深度有关)
合并二叉树
求两个节点的最低公共祖先(leetcode 236)
前中后序遍历
中序遍历的下一个节点(优先考虑右子树最左节点,若不存在,则考虑父节点)
链表
- 判断是否单链表回文
- 奇偶链表反转
- 两个链表的第一个交叉点(交换遍历)
Bit Map
- 使用10MB内存,找出40亿个整数的中位数
- 40亿个非负整数中找到出现两次的数
- 40亿个非负整数中找到没出现的数
数组
连续子数组的最大和(DP)
下一个排列
- 范围[1,n]共n+1个数,找唯一一个重复的数(使用环检测)
- 范围[0,n-1]共n个数,找重复的数(下标位置互换)
- 滑动窗口中的最大值
- 数组中找第K大的数(使用快排中的partition,O(n),特例:找出出现次数超过一半的数,计数法?)
- 数组中找出TopK(1.使用partition,会修改数组;2.使用堆O(n*logk),不会修改数组,适合大数据)
- 某数字在排序数组中出现的次数(两次二分查找找头尾,O(logn))
- 数组中只出现一次的数/数组中出现两次的数(使用异或位运算)