高频面试算法
来源《程序员代码面试指南》左程云著
排序问题
小和问题
在一个数组中, 每一个数左边比当前数小的数累加起来, 叫做这个数组的小和。 求一个数组
的小和。
1 | 例子: |
题解:
利用归并排序,在merge的时候计算有几个数比它小。因为分区两边都将是有序的,当左边分组元素小于右边分组元素,将会有 (r - p2 + 1) * arr[p1] 个小数。r为最右边边界,p2为右边分组元素下标,arr[p1] 为左边分组元素,相加则为产生的小和。
因为整体将趋于有序,而且每次merge之后元素都将归入左边,所有之后不会重复计算小和。
1 | public static int smallSum(int[] arr) { |
逆序对问题
在一个数组中, 左边的数如果比右边的数大, 则折两个数构成一个逆序对, 请打印所有逆序
对。
题解:
与上题解题思路一致。将计算小和改为判断 arr[p1] > arr[p2] 如果是,则打印 arr[p1], arr[p2]即可。
相邻两数的最大差值
给定一个数组, 求如果排序之后, 相邻两数的最大差值。
要求时间复杂度O(N),且要求不能用非基于比较的排序。
题解:
使用桶排序的概念,在此基础上进行优化。
- 遍历给定的数组,找到数组中的最大值、最小值(最大值等于最小值时说明,数组中数据都一样,返回 0 ),用于计算桶id。
- 准备n+1 个桶,对于每个桶中的元素设定三个标志位。是否有数据 hasNum、最大值 maxs、最小值 mins。
- 再次遍历标志位。第0 个桶一定非空,且为最小值。从第一个桶开始遍历,找到每个非空桶(hasNum[i] 为 true),非空桶的最小值与它前边最近的非空桶的最大值 作差,与全局变量res作比较。遍历结束之后将得到最大的差值。
1 | public static int maxGap(int[] nums) { |
栈和队列
数组实现固定大小的队列和栈
1 | package com.jelly.algorithm.array; |
实现返回栈中最小元素
实现一个特殊的栈, 在实现栈的基本功能的基础上, 再实现返
回栈中最小元素的操作。
要求
- pop、 push、 getMin操作的时间复杂度都是O(1)。
- 设计的栈类型可以使用现成的栈结构。
1 | package com.jelly.algorithm.array; |
如何仅用队列结构实现栈结构?
见下题
如何仅用栈结构实现队列结构?
1 | package com.jelly.algorithm.stack; |
猫狗队列
1 | public class Pet { |
实现一种狗猫队列的结构,
要求
- 用户可以调用add方法将cat类或dog类的实例放入队列中;
- 用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出;
- 用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出;
- 用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出;
- 用户可以调用isEmpty方法, 检查队列中是
否还有dog或cat的实例; - 用户可以调用isDogEmpty方法,检查队列中是否有dog类的实例;
- 用户可以调用isCatEmpty方法, 检查队列中是否有cat类的实例。
1 | package com.jelly.algorithm.queue; |
矩阵
转圈打印矩阵
给定一个整型矩阵matrix, 请按照转圈的方式打印它。
1 | 例如: |
要求
额外空间复杂度为O(1)。
1 | package com.jelly.algorithm.matrix; |
旋转正方形矩阵
给定一个整型正方形矩阵matrix,请把该矩阵调整成
顺时针旋转90度的样子。
要求
额外空间复杂度为O(1)。
1 | package com.jelly.algorithm.matrix; |
反转单向和双向链表
分别实现反转单向链表和反转双向链表的函数。
要求
如果链表长度为N, 时间复杂度要求为O(N), 额外空间复杂度要求为O(1)
1 | public static class Node { |
“之” 字形打印矩阵
给定一个矩阵matrix, 按照“之” 字形的方式打印这
个矩阵,
例如: 1 2 3 4 5 6 7 8 9 10 11 12
“之” 字形打印的结果为: 1, 2, 5, 9, 6, 3, 4, 7, 10, 11,8, 12
要求
额外空间复杂度为O(1)
1 | package com.jelly.algorithm.matrix; |
在行列都排好序的矩阵中找数
给定一个有N*M的整型矩阵matrix和一个整数K,
matrix的每一行和每一 列都是排好序的。 实现一个函数, 判断K
是否在matrix中。
例如: 0 1 2 5 2 3 4 7 4
4 4 8 5 7 7 9 如果K为7, 返回true; 如果K为6, 返回false。
要求
时间复杂度为O(N+M), 额外空间复杂度为O(1)。
1 | package com.jelly.algorithm.matrix; |
链表
打印两个有序链表的公共节点
给定两个有序链表的头指针head1和head2, 打印两
链表的公共节点
1 | package com.jelly.algorithm.list.s; |
判断一个链表是否为回文结构
给定一个链表的头节点head,请判断该链表是否为回文结构。
例如:
1 | 1->2->1, 返回true。 |
进阶: 如果链表长度为N, 时间复杂度达到O(N), 额外空间复杂度达到O(1)。
1 | package com.jelly.algorithm.list.s; |
将单向链表按某值划分成左边小、 中间相等、 右边大的形式
给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整 数pivot。
实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot的节点,中间部分都是值等于pivot的节点, 右部分都是值大于 pivot的节点。
除这个要求外,对调整后的节点顺序没有更多的要求。
1 | 例如: |
总之, 满 足左部分都是小于3的节点, 中间部分都是等于3的节点(本例中这个部
分为空) , 右部分都是大于3的节点即可。 对某部分内部的节点顺序不做要求。
进阶: 在原问题的要求之上再增加如下两个要求。
在左、 中、 右三个部分的内部也做顺序要求, 要求每部分里的节点从左 到右的
顺序与原链表中节点的先后次序一致。 例如: 链表9->0->4->5->1, pivot=3。
调整后的链表是0->1->9->4->5。 在满足原问题要求的同时, 左部分节点从左到
右为0、 1。 在原链表中也 是先出现0, 后出现1; 中间部分在本例中为空, 不再
讨论; 右部分节点 从左到右为9、 4、 5。 在原链表中也是先出现9, 然后出现4,
最后出现5。
如果链表长度为N, 时间复杂度请达到O(N), 额外空间复杂度请达到O(1)
1 | public static ListNode listPartition2(ListNode head, int pivot) { |
复制含有随机指针节点的链表
一种特殊的链表节点类描述如下:
1 | public class Node { |
Node类中的value是节点值,next指针和正常单链表中next指针的意义一样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点, 也可能指向null。
给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表中所有结构的复制, 并返回复制的新链表的头节点。
进阶:不使用额外的数据结构,只用有限几个变量, 且在时间复杂度为O(N)内完成原问题要实现的函数
1 | package com.jelly.algorithm.list.m; |
两个单链表相交的一系列问题
单链表可能有环,也可能无环。给定两个单链表的头节点 head1和head2,这两个链表可能相交,也可能不相交。
请实现一个函数,
- 如果两个链表相交, 请返回相交的第一个节点;
- 如果不相交, 返回null 即可。
要求: 如果链表1 的长度为N, 链表2的长度为M, 时间复杂度请达到 O(N+M), 额外空间复杂度请达到O(1)
可能情况
1 | package com.jelly.algorithm.list.m; |
二叉树
在二叉树中找到一个节点的后继节点
现在有一种新的二叉树节点类型如下:
1 | public class Node { |
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。
假设有一棵Node类型的节点组成的二叉树, 树中每个节点的parent指针都正确地指向 自己的父节点, 头节点的parent指向null。 只给一个在二叉树中的某个节点 node, 请实现返回node的后继节点的函数。 在二
叉树的中序遍历的序列中,node的下一个节点叫作node的后继节点.
中序遍历 左、根、右
1 | public static Node getSuccessorNode(Node node) { |
判断一棵二叉树是否是平衡二叉树
1 | // 检验是否是平衡树 |
判断一棵树是否是搜索二叉树、
1 | // 校验是否是平衡二叉搜索树.中序遍历,递增 |
判断一棵树是否是完全二叉树
1 | public static boolean isCBT(Node head) { |
已知一棵完全二叉树, 求其节点的个数
要求: 时间复杂度低于O(N), N为这棵树的节点个数
1 | public static int nodeNum(Node head) { |
折纸问题
请把一段纸条竖着放在桌子上, 然后从纸条的下边向
上方对折1次, 压出折痕后展开。 此时 折痕是凹下去的, 即折痕
突起的方向指向纸条的背面。 如果从纸条的下边向上方连续对折
2 次, 压出折痕后展开, 此时有三条折痕, 从上到下依次是下折
痕、 下折痕和上折痕。
给定一 个输入参数N, 代表纸条都从下边向上方连续对折N次,
请从上到下打印所有折痕的方向。 例如: N=1时, 打印: down
N=2时, 打印: down down up
类似二叉树:
1 | 右、中、左的遍历顺序。 |
1 | public static void printAllFolds(int N) { |
布隆过滤器
- bit数组大小:m= - n * ln(p) / (ln2)^2
- n: 样本量
- p: 0.0001 预期失误率
- 哈希函数个数:k = ln2 * m / n = 0.7 * m/n
- 真实失误率:(1-e^(-n*k/m) )^k
URL筛选
不安全网页的黑名单包含100亿个黑名单网页,每个网页的URL最多占用64B,现在要实现一个网页过滤系统,根据网页的URL判断该网页是否在黑名单上。
要求
- 允许一定的判断失误率
- 空间内存限制在30GB以内
如果是直接用哈希函数处理,肯定会长处空间限制,所以这里要用到的是布隆过滤器。
布隆过滤器实际上是一个位图bitMap,我们现在假设有一个长度为m的bit类型的数组,以及 k 个互相独立的优秀的哈希函数,且这k个哈希函数的输出域都大于或等于m。
我们将网页的URL作为k个哈希函数的输入对象进行哈希处理,分别得到 k 个值,这k个值中可能有相同的,但是值之间互相独立不关联的。我们将这k个值对m模运算,得到的 k 个值都在 0~m之间。最后将这 k 个值对应的bitMap的值置为1,当我们将所有的URL都处理完毕后,bitMap上的很多位都被置为了1。
当我们要查询一个URL是否在黑名单内时,我们先将这个URL进行哈希处理 ,因为有K个函数所以得到 k 个值。接着检查这 k 个值对应的bitMap 位是否为1,如果有一个不为 1,那么说明这个URL不在这个这里面,是安全的网站,如果对应的 bitMap 位的值都是 1 那么说明这个URL可能在这里面。说明可能是因为当URL的数量很多时,可能会出现不同URL哈希处理后得到相同的值,所以说是有可能在这里面。
一致性哈希
所有机器经过hash后,按照顺序排序。分担所有hash的数据。
新机器加入:新机器按照其hash分布到所有的hash环上,该点到下一点的数据拷贝到新机器上即可。
旧机器删除:删除该机器,该点管控数据,拷贝给他相邻节点。
问题
机器必须达到一定数量,否则数据可能分布严重不均。
解决办法
每个物理机增加一定数量的虚拟节点,均分整个hash环,根据虚拟节点的数量将数据划分给特定的宿主机存储。
岛问题
一个矩阵中只有0和1两种值,每个位置都可以和自己的上、下、左、右 四个位置相连,如果有一片1连在一起,这个部分叫做一个岛,求一个 矩阵中有多少个岛?
举例:
0 0 1 0 1 0 1 1 1 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0
这个矩阵中有三个岛
1 | public static int countIslands(int[][] m) { |
并查集
一种结构,主要提供两种功能
- 两个集合是否属于一个集合
- 合并两个集合
1 | public static class UnionFindSet { |
前缀树
arr2中有哪些字符,是arr1中出现的?请打印
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请 打印
arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印 arr2中出现次数最大的前缀。
1 | public static class TrieNode { |
贪心算法
切金条
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如 长度为20的 金条,不管切成长度多大的两半,都要花费20个铜 板。一群人想整分整块金 条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为 10+20+30=60. 金条要分成10,20,30三个部分。 如果, 先把长 度60的金条分成10和50,花费60 再把长度50的金条分成20和30, 花费50 一共花费110铜板。 但是如果, 先把长度60的金条分成30和30,花费60 再把长度30 金条分成10和20,花费30 一共花费90铜板。 输入一个数组,返回分割的最小代价。
1 | public static int lessMoney(int[] arr) { |
做项目(IPO)
输入:
- 参数1,正数数组costs
- 参数2,正数数组profits
- 参数3, 正数k
- 参数4,正数m
costs[i]表示i号项目的花费 profits[i]表示i号项目在扣除花 费之后还能挣到的钱(利润) k表示你不能并行、只能串行的最多 做k个项目 m表示你初始的资金 说明:你每做完一个项目,马上获得的收益,可以支持你去做下 一个 项目。 输出: 你最后获得的最大钱数。
构建两个堆:
按照花费构建小根堆,当花费小于当前所有资金时,进入按照收益构建的大根堆,每次大根堆中给的第一个。
1 | public static class Node { |
一个数据流中,随时可以取得中位数
最低字典序
给定一个字符串类型的数组strs,找到一种拼接方式,使得把所有字符串拼起来之后形成的字符串具有最低的字典序。
注意:
不能直接按照字典序比较,如 ba、b 直接按照字典序比较 ba > b 拼接字符串 bba,但是bab更小。
1 | public static class MyComparator implements Comparator<String> { |
宣讲会
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目 的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数 组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会 议室进行 的宣讲的场次最多。返回这个最多的宣讲场次。
按照结束时间排序
1 | public static class Program { |
–
递归&动态规划
暴力递归:
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个 子问题的解
动态规划
- 从暴力递归中来
- 将每一个子问题的解记录下来,避免重复计算
- 把暴力递归的过程,抽象成了状态表达
- 并且存在化简状态表达,使其更加简洁的可能
求n!的结果
n的阶乘依赖于 n-1 的阶乘, n-1 的阶乘依赖于 n-2 的阶乘……依赖 1 的阶乘。
于是,反过来,可以从 1 的阶乘计算到 n的阶乘
1 | public static long getFactorial1(int n) { |
汉诺塔问题
三个柱子。from、to、help
抽象问题:
- 将from 上的 n 个盘子中的 n-1 个移动到 help上
- 将from 剩余的第 n 个盘子移动到to上
- 将help 上的n-1 个盘子移动到to上
1 | public static void hanoi(int n) { |
打印全部子序列
打印一个字符串的全部子序列,包括空字符串
1 | public static void printAllSubsquence(String str) { |
打印全排列
打印一个字符串的全部排列
1 | package com.jelly.algorithm.string; |
生牛问题
母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只母牛,假设不会死。求N年后,母牛的数量。
因为题中牛不会死,三年后可以生母牛。那么可以得到递推公式:
1 | f(n) = f(n-1) + f(n-3) |
1 | public static int cowNumber1(int n) { |
生牛问题(母牛只能活10年)
如果每只母牛只能活10年(先生后死),求N年后,母牛的数量。
1 | f(n) = f(n-1) + f(n-3) - (f(n-10) - f(n-11)) |
递归逆序栈
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能 使用递归函数。如何实现?
1 | package com.jelly.algorithm.stack; |
最小路径
无后效性问题都可以改成动态规划
无后效性,当前选择对后边选择决策无关。当前可变参数确定,最后返回值是确定的。
给你一个二维数组,二维数组中的每个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途经过的数字要累加起来。返回最小的路径和
1 | public static int minPath1(int[][] matrix) { |
背包问题
给你一个数组arr,和一个整数aim。如果可以任意选择arr中的数字,能不能累加得到aim,返回true或者false
分析
每个位置 i 有 要和不要 两种选择;叶节点会看自己这里的结果是不是 aim,从而向父结点返回 true 或 false,父结点比较子节点的结果,有一个为 true 就一直返回 true,否则返回 false。
如上图所示:数组 arr = {3, 2, 5} ,aim = 7:
f(0, 0):代表0位置处状态值为0的点;
f(2, 5):代表2位置处状态值为5的点。
只要有叶节点的值等于 aim 的值,则会返回 true。
动态规划版图解:
1 | public static boolean money1(int[] arr, int aim) { |
商品价值
给定两个数组w和v,两个数组长度相等,w[i]表示第i件商品的 重量,v[i]表示第i件商品的价值。 再给定一个整数bag,要求 你挑选商品的重量加起来一定不能超 过bag,返回满足这个条件 下,你能获得的最大价值。
1 | public static int maxValue1(int[] c, int[] p, int bag) { |