剑指Offer算法学习
剑指Offer
Day03
剑指Offer0.5替换空格
请实现一个函数,把字符串 s
中的每个空格替换成”%20”。
字符串为不可变对象
示例 1:
1 | 输入:s = "We are happy." |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
运用到String的toCharArray()
可以将字符串转换成字符数组
字符之间的拼接用StringBuffer,append()
方法可以添加字符串或者字符
1 | public String replaceSpace(String s) { |
剑指Offer58- ||.左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串”abcdefg”和数字2,该函数将返回左旋转两位得到的结果”cdefgab”。
示例 1:
1 | 输入: s = "abcdefg", k = 2 |
示例 2:
1 | 输入: s = "lrloseumgh", k = 6 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
String中的subString()
切片函数。返回字符串的子字符串,参数为索引。
StingBuffer和StringBuffer区别:
StringBuilder 的方法不是线程安全的(不能同步访问)。
StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
方法一:字符串切片
1 | public String reverseLeftWords(String s, int n) { |
方法二:列表遍历拼接
1 | public String reverseLeftWords(String s, int n) { |
方法二:字符串遍历拼接(只能用String时)
1 | public String reverseLeftWords(String s, int n) { |
Day04
剑指Offer03.数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
1 | 输入: |
知识点
可以用哈希表来做。直接使用是否contain方法查看是否包含,有包含则重复出现
HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。
HashSet主要针对单一元素。add()
重复元素不会被添加。
使用原地条件主要条件**在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组元素的 **索引和 值 是 一对多 的关系。
该方法不用占用空间。空间复杂度为O(1)。
方法一:哈希表/set
1 | public static int findRepeatNumber1(int[] nums) { |
方法二:原地交换
题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内
。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。
因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = i)。因而,就能通过索引映射对应的值,起到与字典等价的作用。
1 | public static int findRepeatNumber(int[] nums) { |
剑指Offer53-|.在排序数组中查找数字|
统计一个数字在排序数组中出现的次数。
示例 1:
1 | 输入: nums = [5,7,7,8,8,10], target = 8 |
示例 2:
1 | 输入: nums = [5,7,7,8,8,10], target = 6 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
排序数组中的搜索问题,首先想到 二分法 解决。
通过二分法找到target个数数组的左边界left和右边界right。应用了两次二分法可以得到target的数量为right-left-1
。
1 | public int search(int[] nums, int target) { |
剑指 Offer 53 - II. 0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例 1:
1 | 输入: [0,1,3] |
示例 2:
1 | 输入: [0,1,2,3,4,5,6,7,9] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
主要点是缺失的数字等于“右子数组的首位元素” 对应的索引。因此考虑使用二分法查找 “右子数组的首位元素” 。
即是找到右边界
排序数组使用双指针和二分法是高频选择
二分法时,循环的最后j始终在i的左边。
1 | public int missingNumber(int[] nums) { |
Day05
剑指 Offer 04. 二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
1 | [ |
给定 target = 5,返回 true。
给定 target = 20,返回 false。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
若使用暴力法遍历矩阵 matrix
,则时间复杂度为 O(NM) 。暴力法未利用矩阵 “从上到下递增、从左到右递增” 的特点,
显然不是最优解法。
我们将矩阵逆时针旋转 45° ,并将其转化为图形式,发现其类似于 二叉搜索树 ,即对于每个元素,其左分支元素更小、右分支元素更大。因此,通过从 “根节点” 开始搜索,遇到比 target
大的元素就向左,反之向右,即可找到目标值 target
。
若行索引或列索引越界,则代表矩阵中无目标值,返回 false
“根节点” 对应的是矩阵的 “左下角” 和 “右上角” 元素,本文称之为 标志数 ,以 matrix
中的 左下角元素 为标志数 flag
,则有:
若flag > target
,则 target
一定在 flag
所在 行的上方 ,即flag
所在行可被消去。
若 flag < target
,则 target
一定在 flag
所在 列的右方 ,即flag
所在列可被消去。
方法一:暴力解法
1 | public static boolean findNumberIn2DArray(int[][] matrix, int target) { |
方法二:利用标志数求解
1 | public static boolean findNumberIn2DArray(int[][] matrix, int target) { |
时间复杂度O(N+M):其中,NN 和 MM 分别为矩阵行数和列数,此算法最多循环 M+N次。
空间复杂度O(1)。
二分法:
仔细看题(解题的关键在认真读题),分析清楚题目要找的答案需要满足什么性质。采用两边夹的方式,每一轮把待搜索区间分成两个部分,排除掉一定不是答案的区间,最后左右指针重合的地方就是我们要找的元素。
即是要分清搜索区间是在哪。在于我们能够根据题意:得到某种单调性,和可以逐步缩小搜索规模的条件,进而准确地设计可以使得搜索区间缩小的条件。只看到 while (left < right) 里的 < ,不能说明右边界不能取到。真正看区间表示应该看左右边界到底如何设置,如果我们分析出下一轮搜索的范围是 [left..mid] 而设置 right = mid + 1,才表示搜索区间为「左闭右开」区间 。这是因为 [left..right) = [left..mid + 1) = [left..mid]。
while (left <= right) 表示在区间里只剩下一个元素的时候,我们还需要继续查找,因此循环可以继续的条件是 left <= right,这一行代码对应了二分查找算法的思路 1:在循环体中查找元素。
剑指 Offer 11. 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
示例 1:
1 | 输入:[3,4,5,1,2] |
示例 2:
1 | 输入:[2,2,2,0,1] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
递增排序的数组的一个旋转的特性:该数组的最小值一定是数组旋转过去后的第一个数。
排序数组的查找问题首先考虑的是二分法解决。其可将 遍历法 的 线性级别 时间复杂度降低至 对数级别 。
当numbers[mid] = numbers[j]时,只需要j-1即可,把j舍弃掉一个并不影响结果。
1 | //利用二分法来解决 |
时间复杂度O(log2*N*)。
空间复杂度O(1)。
剑指 Offer 50. 第一次只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例 1:
1 | 输入:s = "abaccdeff" |
示例 2:
1 | 输入:s = "" |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
考察哈希表的使用。在字符串长度较大、重复字符很多时,“有序哈希表” 解法理论上效率更高。
方法一:哈希表
1 | public static char firstUniqChar0(String s) { |
利用boolean类型
1 | class Solution { |
时间复杂度O(N):N 为字符串 s
的长度;需遍历 s
两轮,使用 O*(N) ;HashMap 查找操作的复杂度为O*(1) ;
空间复杂度O(1):由于题目指出 s
只包含小写字母,因此最多有 26 个不同字符,HashMap 存储需占用 O*(26)=O(1) 的额外空间。
方法二:有序哈希表
知识点
在哈希表的基础上,有序哈希表中的键值对是 按照插入顺序排序 的。因此可以遍历有序哈希表来找到第一次出现的字符。
即第二次循环采用遍历有序哈希表的方式
哈希表是 去重 的,即哈希表中键值对数量 ≤ 字符串 s
的长度
1 | class Solution { |
时间复杂度和空间复杂度和方法一相同
Day06
剑指 Offer 32-| 从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回:
1 | [3,9,20,15,7] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
广度优先搜索(BFS)通常借助队列的先入先出的特性来实现
1 | public static int[] levelOrder(TreeNode root) { |
时间复杂度O(N):N 为二叉树的节点数量,即 BFS 需循环 N 次。
空间复杂度O(N):最差情况下,即当树为平衡二叉树时,最多有 N*/2 个树节点同时在 queue
中,使用 O*(N) 大小的额外空间。
剑指 Offer 32-|| 从上到下打印二叉树 ||
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
1 | [ |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
主要重点是加入队列长度为循环条件,即是每层打印。
一次while即是二叉树的一层
1 | public static List<List<Integer>> levelOrder0(TreeNode root) { |
时间复杂度O(N):N 为二叉树的节点数量,即 BFS 需循环 N 次。
空间复杂度O(N):最差情况下,即当树为平衡二叉树时,最多有 N*/2 个树节点同时在 queue
中,使用 O*(N) 大小的额外空间。
剑指 Offer 32-||| 从上到下打印二叉树 |||
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
1 | [ |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
利用双端队列添加数据
方法一:双端队列
1 | public static List<List<Integer>> levelOrder(TreeNode root) { |
方法二:双端队列(奇偶层逻辑分离)
知识点
定死奇偶顺序,想比于方法一消除了冗余判断
循环打印奇偶层
1 | class Solution { |
时间复杂度等同上一个方法一样
方法三:倒序
1 | public List<List<Integer>> levelOrder(TreeNode root) { |
Day07
剑指 Offer 26. 树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
1 | 4 |
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例 1:
1 | 输入:A = [1,2,3], B = [3,1] |
示例 2:
1 | 输入:A = [3,4,5,1,2], B = [4,1] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
recur(A, B)
函数:
终止条件:
当节点 B 为空:说明树 B 已匹配完成(越过叶子节点),因此返回true ;
当节点 A 为空:说明已经越过树 A 叶子节点,即匹配失败,返回 false ;
当节点 A 和 B 的值不同:说明匹配失败,返回 false ;
返回值:
判断 A 和 B 的左子节点是否相等,即 recur(A.left, B.left)
;
判断 A 和 B 的右子节点是否相等,即 recur(A.right, B.right)
;
isSubStructure(A, B) 函数:
特例处理: 当 树 A 为空 或 树 B 为空 时,直接返回 false ;
返回值: 若树 B 是树 A 的子结构,则必满足以下三种情况之一,因此用或 ||
连接;
以 节点 A 为根节点的子树 包含树 B ,对应 recur(A, B);
树 B 是 树 A 左子树 的子结构,对应 isSubStructure(A.left, B);
树 B 是 树 A 右子树 的子结构,对应 isSubStructure(A.right, B);
1 | //这个方法就是判断B是否为A的子结构 |
剑指 Offer 27. 二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
1 | 输入:root = [4,2,7,1,3,6,9] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
二叉树镜像定义: 对于二叉树中任意节点 root ,设其左 / 右子节点分别为left,right ;则在二叉树的镜像中的对应 root 节点,其左 / 右子节点分别为 right,left 。
- 根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。
方法一:递归自上而下
1 | public TreeNode mirrorTree(TreeNode root) { |
时间复杂度 O(N) : 其中 NN 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N) 大小的栈空间。
方法二:自己写的
知识点
整体思路是将子树的左节点拼接在新树的右子结点,将右子结点拼接在新树的左子结点。
1 | public static TreeNode mirrorTree0(TreeNode root) { |
剑指 Offer 28.对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
示例 1:
1 | 输入:root = [1,2,2,3,4,4,3] |
示例 2:
1 | 输入:root = [1,2,2,null,3,null,3] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
对称二叉树定义: 对于树中 任意两个对称节点 LL 和 RR ,一定有:
L.val = R.val :即此两对称节点值相等。
L.left.val = R.right.val:即 L 的 左子节点 和 R的 右子节点 对称;
L.right.val = R.left.val :即 L 的 右子节点 和 R 的 左子节点 对称。
1 | public boolean isSymmetric(TreeNode root) { |
时间复杂度:O(N)
空间复杂度:O(N)
Day08
剑指 Offer 10-|.斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
1 | F(0) = 0, F(1) = 1 |
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
1 | 输入:n = 2 |
示例 2:
1 | 输入:n = 5 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处
知识点
答案需要取模 1e9+7(1000000007)意思是每次计算都要取模1000000007
由于 F*(n) 只和F*(n−1) 与 F*(n−2) 有关,因此可以使用「滚动数组思想」把空间复杂度优化成 O*(1)
方法一:递推动态规划
1 | public int fib(int n) { |
- 时间复杂度:O*(n)。
- 空间复杂度:O(1)。
方法二:递归动态规划
1 | int[] cache = new int[110]; |
- 时间复杂度:O*(*n)。
- 空间复杂度:O(n)。
剑指 Offer 10-||.青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
1 | 输入:n = 2 |
示例 2:
1 | 输入:n = 7 |
示例 3:
1 | 输入:n = 0 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/qing-wa-tiao-tai-jie-wen-ti-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
同斐波那契数列的转移方程相同。唯一不同的是起始数字不同
青蛙跳台阶问题: f(0)=1 , f(1)=1 , f(2)=2 ;
斐波那契数列问题: f(0)=0 , f(1)=1 , f(2)=1 。
1 | int[] cache = new int[110]; |
时间复杂度:O*(*n)。
空间复杂度:O(n)。
剑指 Offer 63.股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 1:
1 | 输入: [7,1,5,3,6,4] |
示例 2:
1 | 输入: [7,6,4,3,1] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/gu-piao-de-zui-da-li-run-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
状态定义: 设动态规划列表 dp ,dp[i] 代表以 prices[i] 为结尾的子数组的最大利润(以下简称为 前 i 日的最大利润 )。
转移方程: 由于题目限定 “买卖该股票一次” ,因此前 i 日最大利润 dp[i] 等于前 i−1 日最大利润 dp[i−1] 和第 i 日卖出的最大利润中的最大值。
前i日最大利润=max(前(i−1)日最大利润,第i日价格−前i日最低价格)
dp[i]=max(dp[i−1],prices[i]−min(prices[0:i]))
初始状态:dp[0]=0 ,即首日利润为 0 ;
返回值: dp[n−1] ,其中 n 为 dp 列表长度。
用一个变量代替dp数组降低空间复杂度
1 | public int maxProfit(int[] prices) { |
- 时间复杂度:O*(*n)。
- 空间复杂度:O(n)。
用到数组的动态规划
1 | public static int maxProfit(int[] prices) { |
Day09
剑指Offer 42.连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
1 | 输入: nums = [-2,1,-3,4,-1,2,1,-5,4] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
动态规划求解,原数组i下标数组下存放的是以i为结尾的连续数组最大和
转移方程:若nums[i-1]<=0,说明nums[i-1]对nums[i]产生负影响,即nums[i-1]+nums[i]还不如nums[i]本身大
又因为题目要求连续数组,此时可以直接舍弃前面的数,重新开始计数。nums[i] 值不变就是表明重新开始。
1 | public int maxSubArray(int[] nums) { |
时间复杂度 O(N) : 线性遍历数组 nums 即可获得结果,使用 O(N) 时间。
空间复杂度 O(1) : 使用常数大小的额外空间。
前缀和
1 | public int maxSubArray(int[] nums) { |
剑指 Offer 47.礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
1 | 输入: |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
根据题目说明,易得某单元格只可能从上边单元格或左边单元格到达。
设 f(i, j) 为从棋盘左上角走至单元格 (i ,j) 的礼物最大累计价值,易得到以下递推关系:f(i,j) 等于 f(i,j-1) 和 f(i-1,j)中的较大值加上当前单元格礼物价值 grid(i,j) 。
因此用动态规划来做:转移方程 f(i,j)=max[f(i,j−1),f(i−1,j)]+grid(i,j)
通过增加一行和一列的空间来简化代码并解决边界问题,因为i-1和j-1在i和j都为0时越界。
1 | public int maxValue(int[][] grid) { |
Daya10
剑指 Offer 46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
1 | 示例 1: |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
方法一:字符串遍历
1 | public static int translateNum1(int num) { |
此题的动态规划计算是 对称的 ,即 从左向右 遍历(从第 dp[2] 计算至 dp[n] )和 从右向左 遍历(从第 dp[n - 2]dp[n−2] 计算至 dp[0])所得方案数一致。
1 | public int translateNum2(int num) { |
1 | public int translateNum3(int num) { |
剑指 Offer 48. 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
哈希表用来记录元素和其索引下标
左指针 start
更新左指针根据上轮左指针start
和当前重复元素的下标的最大值,保证了区间 [ start+1, map.get[temp] ]内无重复元素
1 | start = Math.max(start,map.get(tmp)); |
最终结果max
:取上一轮max和滑动窗口i-start
中的最大值
1 | Math.max(max,i-start); |
需要注意的是左指针必须是从-1开始
1 | public int lengthOfLongestSubstring(String s) { |
复杂度分析:
时间复杂度 O(N): 其中 N 为字符串长度,动态规划需遍历计算 dp列表。
空间复杂度 O(1): 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dic 最多使用 O(128) = O(1)大小的额外空间。
Day11
剑指 Offer 18. 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
1 | 输入: head = [4,5,1,9], val = 5 |
示例 2:
1 | 输入: head = [4,5,1,9], val = 1 |
知识点
关键在与要设置一个前置指针用于删除结点。
1 | public static ListNode deleteNode(ListNode head, int val) { |
时间复杂度 O(N): N为链表长度,删除操作平均循环N/2次,最差N次。
空间复杂度 O(1): 占用常数个空间。
剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
1 | 给定一个链表: 1->2->3->4->5, 和 k = 2. |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
使用双指针。先初始化双指针都指向head。构建一个双指针的距离为k。然后共同移动双指针每轮都走一步。直到前指针走到链表尾结点时跳出(跳出后, latter
与尾节点距离为 k-1,即 latter
指向倒数第 k 个节点)。
1 | class Solution { |
普通方法:先计算长度然后得到目标结点
1 | public ListNode getKthFromEnd(ListNode head, int k) { |
Day12
剑指 Offer 25. 合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
1 | 输入:1->2->4, 1->3->4 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
因为两个链表是递增的,所以使用双指针L1和L2来遍历两个链表,根据两个指针所指向结点的大小关系来确定结点添加顺序
算法流程
首先初始化伪头节点,循环合并:当其中一个指针遍历到最后为空时,跳出循环。判断两个指针指向的节点值,将小的值添加到伪头节点后。当其中一个指针指向空时,则判断是哪个指针指向不为空,将该指针添加到结果链表后。
1 | public static ListNode mergeTwoLists1(ListNode l1, ListNode l2) { |
时间复杂度 O(M+N) : M, N分别为链表 L1, L2的长度,合并操作需遍历两链表。
空间复杂度 O(1) : 节点引用 dum, cur 使用常数大小的额外空间。
剑指 Offer 52. 两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
1 | 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 |
示例 2:
1 | 输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 |
示例 3:
1 | 输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 |
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
本题与主站 160 题相同:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
我们使用两个指针 node1,node2 分别指向两个链表 headA,headB 的头结点,然后同时分别逐结点遍历,当 node1 到达链表 headA 的末尾时,重新定位到链表 headB 的头结点;当 node2 到达链表 headB 的末尾时,重新定位到链表 headA 的头结点。
这样,当它们相遇时,所指向的结点就是第一个公共结点。
1 | public static ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
- 时间复杂度:O(M+N)。M和N为A,B链表的长度。
- 空间复杂度:O(1)。
Day13
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。
示例:
1 | 输入:nums = [1,2,3,4] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
双指针,在数组的头和尾分别定义一个指针,指针left从左往右寻找偶数,指针从右往左寻找奇数,将找到的偶数和奇数交换位置。
最终保证指针left左边都是奇数,指针right右边都是偶数。
指针left遇到奇数则执行left++跳过,直到找到偶数
指针right遇到偶数则执行right++跳过,直到找到奇数
1 | public static int[] exchange0(int[] nums) { |
快慢指针
定义快慢双指针 fast 和 low ,fast 在前, low 在后 .
fast的作用是向前搜索奇数位置,low 的作用是指向下一个奇数应当存放的位置
fast 向前移动,当它搜索到奇数时,将它和 nums[low] 交换,此时 low向前移动一个位置 .
重复上述操作,直到 fast指向数组末尾
1 | public static int[] exchange(int[] nums) { |
时间复杂度为O(N)
空间复杂度为O(1)
剑指 Offer 57. 和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
1 | 输入:nums = [2,7,11,15], target = 9 |
示例 2:
1 | 输入:nums = [10,26,30,31,47,60], target = 40 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/he-wei-sde-liang-ge-shu-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
使用双指针完成分别在数组的头尾定义一个指针。因为是有序递增数组。
1 | public static int[] twoSum1(int[] nums, int target) { |
时间复杂度O(N):遍历数组的长度
空间复杂度O(1)。
剑指 Offer 58 - I. 翻转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串”I am a student. “,则输出”student. a am I”。
示例 1:
1 | 输入: "the sky is blue" |
示例 2:
1 | 输入: " hello world! " |
示例 3:
1 | 输入: "a good example" |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/fan-zhuan-dan-ci-shun-xu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
双指针实现
- 倒序遍历字符串 ss ,记录单词左右索引边界 ii , jj ;
- 每确定一个单词的边界,则将其添加至单词列表 resres ;
- 最终,将单词列表拼接为字符串,并返回即可。
1 | class Solution { |
使用库函数实现,先除去单词列表前后空格并分割字符。
1 | public static String reverseWords0(String s) { |
时间复杂度O(N)
空间复杂度O(N):使用了StringBuffer占用线性大小空间。
Day14
剑指 Offer 12. 矩阵中的路径
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
示例 1:
1 | 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" |
示例 2:
1 | 输入:board = [["a","b"],["c","d"]], word = "abcd" |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
深度优先搜索(DFS)+ 剪枝 解决
深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝
递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
终止条件:
返回 false : (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。
返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
递推工作:
标记当前矩阵元素: 将 board[i][j]修改为 空字符 “”,代表此元素已访问过,防止之后搜索时重复访问。 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。 还原当前矩阵元素: 将 board [i][j] 元素还原至初始值,即 word[k] 。
返回值: 返回布尔量 res ,代表是否搜索到目标字符串。
时间复杂度 O(3^KMN) 最差情况下,需要遍历矩阵中长度为 K 字符串的所有方案,时间复杂度为 O(3^K);矩阵中共有 MN 个起点,时间复杂度为 O(MN) 。
方案数计算: 设字符串长度为 KK ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K) 。
空间复杂度 O(K): 搜索过程中的递归深度不超过 K ,因此系统因函数调用累计使用的栈空间占用 O(K) (因为函数返回后,系统调用的栈空间会释放)。最坏情况下 K = MN ,递归深度为 MN ,此时系统栈使用 O(MN) 的额外空间。
1 | public static boolean exist0(char[][] board, String word) { |
剑指 Offer 13. 机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
1 | 输入:m = 2, n = 3, k = 1 |
示例 2:
1 | 输入:m = 3, n = 1, k = 0 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 si, sj 。
终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 0 ,代表不计入可达解。
递推工作:
标记当前单元格 :将索引 (i, j) 存入 Setvisited
中,代表此单元格已被访问过。
搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数
,代表从本单元格递归搜索的可达解总数。
也可以直接往上下左右四个方向都进行递归。方便理解
1 | int m, n, k; |
时间复杂度 O(MN) : 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN) 。
空间复杂度 O(MN) : 最差情况下,Set visited 内存储矩阵所有单元格的索引,使用 O(MN)的额外空间。
Day15
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
1 | 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 |
示例 2:
1 | 输入:root = [1,2,3], targetSum = 5 |
示例 3:
1 | 输入:root = [1,2], targetSum = 0 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
算法流程:pathSum(root, sum)
函数:
- 初始化: 结果列表 res ,路径列表 path 。
- 返回值: 返回 res 即可。
recur(root, tar) 函数:
递推参数: 当前节点 root ,当前目标值 tar 。
终止条件: 若节点 root 为空,则直接返回。
递推工作:
路径更新: 将当前节点值 root.val 加入路径 path ;
目标值更新: tar = tar - root.val(即目标值 tar 从 sum 减至 00 );
路径记录: 当 ① root 为叶节点 且 ② 路径和等于目标值 ,则将此路径 path 加入 res 。
先序遍历: 递归左 / 右子节点。
路径恢复: 向上回溯前,需要将当前节点从路径 path 中删除,即执行 path.pop()
1 | 值得注意的是,记录路径时若直接执行 res.append(path) ,则是将 path 对象加入了 res ;后续 path 改变时, res 中的 path 对象也会随之改变。 |
1 | LinkedList<List<Integer>> list = new LinkedList<>(); |
时间复杂度 O(N): N 为二叉树的节点数,先序遍历需要遍历所有节点。
空间复杂度 O(N) : 最差情况下,即树退化为链表时,path 存储所有树节点,使用 O(N) 额外空间。
剑指 Offer 54. 二叉搜索树的第k大节点
给定一棵二叉搜索树,请找出其中第 k 大的节点的值。
示例 1:
1 | 输入: root = [3,1,4,null,2], k = 1 |
示例 2:
1 | 输入: root = [5,3,6,2,4,null,null,1], k = 3 |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
二叉搜索树的中序遍历为 递增序列 。
根据以上性质,易得二叉搜索树的 中序遍历倒序 为 递减序列 。
因此,求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k个节点”。
递归解析:
终止条件: 当节点 root为空(越过叶节点),则直接返回;
递归右子树: 即 dfs(root.right);
三项工作:
提前返回: 若 k = 0 ,代表已找到目标节点,无需继续遍历,因此直接返回;
统计序号: 执行 k = k - 1 (即从 k 减至 0 );
记录结果: 若 k = 0 ,代表当前节点为第 kk 大的节点,因此记录 res = root.val ;
递归左子树: 即 dfs(root.left) ;
1 | //逆中序遍历 |
时间复杂度 O(N): 当树退化为链表时(全部为右子节点),无论 kk 的值大小,递归深度都为 NN ,占用 O(N) 时间。
空间复杂度 O(N): 当树退化为链表时(全部为右子节点),系统使用 O(N) 大小的栈空间。
剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
算法流程:
dfs(cur): 递归法中序遍历;
- 终止条件: 当节点 cur 为空,代表越过叶节点,直接返回;
- 递归左子树,即 dfs(cur.left) ;
- 构建链表:
- 当 pre 为空时: 代表正在访问链表头节点,记为 head ;
- 当 pre 不为空时: 修改双向节点引用,即 pre.right = cur , cur.left = pre ;
- 保存 cur : 更新 pre = cur ,即节点 cur 是后继节点的 pre ;
4. 递归右子树,即 dfs(cur.right) ;
1 | //关键点:设前驱节点 pre 和当前节点 cur |
时间复杂度 O(N) : N 为二叉树的节点数,中序遍历需要访问所有节点。
空间复杂度 O(N): 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N) 栈空间。
Day16
剑指 Offer 61. 扑克牌中的顺子
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
示例 1:
1 | 输入: [1,2,3,4,5] |
示例 2:
1 | 输入: [0,0,1,2,5] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
根据题意,此 5 张牌是顺子的 充分条件 如下:
除大小王外,所有牌 无重复 ;
设此 5 张牌中最大的牌为 max ,最小的牌为 min (大小王除外),则需满足:max - min < 5
方法一: 集合 Set + 遍历
1 | public boolean isStraight(int[] nums) { |
时间复杂度 O(N) = O(5) = O(1): 其中 N 为 nums 长度,本题中 N≡5 ;遍历数组使用 O(N) 时间。
空间复杂度 O(N) = O(5) = O(1) : 用于判重的辅助 Set 使用 O(N) 额外空间。
方法二:排序 + 遍历
1 | //判断是否是同花顺 |
时间复杂度 O(N log N) = O(5 log 5) = O(1): 其中 NN 为 nums 长度,本题中 5N≡5 ;数组排序使用 O(NlogN) 时间。
空间复杂度 O(1) : 变量 jokerr 使用O(1) 大小的额外空间。
剑指 Offer 45. 把数组排成最小的数
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
1 | 输入: [10,2] |
示例 2:
1 | 输入: [3,30,34,5,9] |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y ,则规定 排序判断规则 为:
若拼接字符串 x + y > y + x ,则 x “大于” y ;
反之,若 x + y < y + x ,则 x “小于” y ;
初始化: 字符串列表 strs,保存各数字的字符串格式;
列表排序: 应用以上 “排序判断规则” ,对 strs 执行排序;
返回值: 拼接 strs 中的所有字符串,并返回。
1 | public String minNumber(int[] nums) { |
使用内置函数
1 | public String minNumber0(int[] nums) { |
时间复杂度 O(NlogN) : N 为最终返回值的字符数量( strs 列表的长度≤N );使用快排或内置函数的平均时间复杂度为 O(NlogN) ,最差为 O(N^2)
空间复杂度 O(N): 字符串列表 strs 占用线性大小的额外空间。
Day17
剑指 Offer 41. 数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:
1 | 输入: |
示例 2:
1 | 输入: |
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
知识点
题目要求获取一个数据流排序后的中位数,那么可以使用两个优先队列(堆)实现。
该题使用一个大顶堆,一个小顶堆完成。
- 大顶堆的每个节点的值大于等于左右孩子节点的值,堆顶为最大值。
- 小顶堆的每个节点的值小于等于左右孩子节点的值,堆顶为最小值。
因此我们使用 大顶堆(maxHeap) 来存储数据流中较小一半的值,用 小顶堆(minHeap) 来存储数据流中较大一半的值。
即“大顶堆的堆顶”与“小顶堆的堆顶”就是排序数据流的两个中位数。
如图所示,大顶堆(maxHeap)置于下方,小顶堆(minHeap)倒置于上方,两个堆组合起来像一个沙漏的形状。
根据堆的性质,可以判断两个堆的值从下往上递增,即:
maxHeap堆底 <= maxHeap堆顶 <= minHeap堆顶 <= minHeap堆底。
题目要求获取数据流排序后的中位数,而根据数据流的奇偶性以及堆的性质,将获取中位数的情况分为两类:
数据流为奇数时,保证两个堆的长度相差1,那么长度较大的堆的堆顶就是中位数;
数据流为偶数时,保证两个堆的长度相等,两个堆的堆顶相加除二就是中位数。
那么我们要保证每次插入元素后,两堆维持相对长度。让minHeap为长度较大的堆,每次插入元素时进行判断:
当两堆总长度为偶数时,即两堆长度相等,将新元素插入到minHeap,插入后minHeap比maxHeap长度大1;
当两堆总长度为奇数时,即两堆长度不等,将新元素插入到maxHeap,插入后两堆长度相等;
还要保证插入元素后,两堆仍是保证从下往上递增的顺序性。如上面的偶数情况,将新元素x直接插入到minHeap,是有可能破坏两堆的顺序性的,例如:(minHeap是存储“较大一半”的值)
若x的值恰好为“较大一半”,直接将插入到“较大一半”的minHeap中,是不会破坏顺序的;
若x的值为“较小一半”,而此时却插入到“较大一半”的minHeap中,是会破坏顺序的。
结论
当两堆总大小为偶数时,即两堆大小相等,先将新元素插入maxHeap,重新排序后将新的最值拿出并插入到minHeap;
当两堆总大小为奇数时,即两堆大小不等,先将新元素插入minHeap,重新排序后将新的最值拿出并插入到maxHeap;
1 | class MedianFinder { |
空间复杂度:O(N),即为数据流中的元素数量。
时间复杂度:
- 获取中位数:O(1)。
- 添加元素:O(logN),堆添加元素的时间复杂度为O(logN)。