开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
堆
堆是一种特殊的树。
- 堆必须是完全二叉树
- 堆中每一个节点的值都必须大于等于,或者小于等于子节点的值。
大顶堆:顶部数据为堆中最大值。
小顶堆:顶部数据为堆中最小值。
java 默认是小顶堆。大顶堆函数为:(x,y)-> y - x
PriorityQueue<Integer> queue = new PriorityQueue<>(4);
queue.add(5);
queue.add(7);
System.out.println(queue.peek());
System.out.println(queue.poll());
peek/poll 都是堆顶元素,大堆就是最大值,小堆就是最小值
堆的使用技巧
查找:找⼤⽤⼩,⼤的进;找⼩⽤⼤,⼩的进。
排序:升序⽤⼩,降序⽤⼤。
为什么查找是找大用小?
首先如果找最大,肯定是用大顶堆。
如果找第K大的值,此时用小堆。因为小堆堆顶是最小的元素,让大的进堆,最后输出堆顶的就是第K大元素。
比如:[8,6,3,5,9,7,2] 找第二大元素。
- 设置堆元素个数为2,其实不设置也OK,new PriorityQueue<>(2),2 表示的是初始元素,不够会扩容,并非限制元素。
- 先添加两个元素进堆。
- 从2开始遍历剩余数据
- 判断是否比堆顶数据大,大的进堆。
- 输出堆顶值。
滑动窗口
滑动窗口本质上也是双指针算法思想的运用。
窗口:左右指针中间的部分。窗口大小固定需要考虑越界,不固定需要考虑是否满足要求。左右指针初始化为0,右指针向前,左指针不变,直到不满要求位置。当不满足要求时,需要根据要求调整左指针位置。
滑动:右指针一直向前,直到最后。
滑动窗口可以与堆配合。比如求窗口内的最大、最小元素。
常见算法题
子数组最大平均数
给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。
请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。
任何误差小于 10-5 的答案都将被视为正确答案。
示例 1:
输入:nums = [1,12,-5,-6,50,3], k = 4
输出:12.75
解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75
示例 2:
输入:nums = [5], k = 1
输出:5.00000
提示:
n == nums.length1 <= k <= n <= 105-104 <= nums[i] <= 104
思路
长度已固定,且提示中不存在越界问题,直接写代码即可。
- 先从头获取k个长度子数组。取平均值
- 再从k位置继续往后遍历,每次删除掉 k 长度前的数据。
- 判断是否大于之前的值,大于则替换
- 返回结果
实现
public double findMaxAverage(int[] nums, int k) {
int sum = 0;
// 找到长度K平均数
for (int i = 0; i < k; i++) {
sum += nums[i];
}
double maxAvg = sum / k;
// 滑动窗口遍历 + 结果比对
for (int i = k; i < nums.length; i++) {
sum = sum + nums[i] - nums[i - k];
maxAvg = Math.max(maxAvg, sum / k);
}
return maxAvg
}
该方式 虽然实现了,但是性能并非最优解。
需求是求最大平均值没错,但是没有必要每次都求平均,求最大和也是一样的。所以此处多了 N个平均值操作。
最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104-109 <= nums[i] <= 109
思路
在发现不满足条件时仅移动左指针
- 初始化左指针为0
- 从1开始执行遍历。
- 发现不满足条件,i <= i -1,调整左指针
- 满足条件计算长度
- 返回结果
在发现不满足条件时进行计算,并移动左指针
- 初始化左指针为0
- 从1开始执行遍历。
- 发现不满足条件,i <= i -1,
- 计算长度并判断是否最大
- 调整左指针
- 如果right到最后(1,3,5,6)
- 计算长度并判断是否最大
- 发现不满足条件,i <= i -1,
- 返回结果
两种思路都可以,但是很显然,第一种更清晰。第二种还需要额外的判断是否到最后一位,虽然少一些长度计算,但是多了一次判断,总体来说性能并没有很大差别。(第一种,每次计算最大长度,第二种,每次做一下判断)
不用滑动窗口直接计算
- 初始化当前长度为1;
- 从1开始执行遍历
- 发现不满足条件
- 判断并赋值最大长度
- 长度 重新赋值为1
- 满足条件长度++
- 发现不满足条件
- 返回最大长度
实现
思路1:
public int findLengthOfLCIS(int[] nums) {
int result = 1;
int left = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] <= nums[i - 1]) {
left = i;
}
result = Math.max(result, i - left + 1);
}
return result;
}
思路2:
public static int findLengthOfLCIS(int[] nums) {
int result = 1;
int left = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] <= nums[i - 1]) {
result = Math.max(result, i - left);
left = i;
}
if (i == (nums.length - 1)) {
result = Math.max(result, i - left + 1);
}
}
return result;
}
public static int findLengthOfLCIS1(int[] nums) {
int result = 1;
int left = 0;
for (int i = 0; i < nums.length; i++) {
if (i == (nums.length - 1) || nums[i] > nums[i + 1]) {
result = Math.max(result, i - left + 1);
left = i + 1;
}
}
return result;
思路3:
/**
* 不使用 滑动窗口
*/
public static int findLengthOfLCIS2(int[] nums) {
int result = 1;
int count = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] <= nums[i - 1]) {
count = 1;
} else {
count++;
}
result = Math.max(result, count);
}
return result;
}
无重复字符最长字符串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104s由英文字母、数字、符号和空格组成
思路
- 初始化左指针为0
- 初始化返回结果值为0 (提示字符串长度允许为 0 )
- 初始化Map用于判断 是否存在重复值。
- 遍历字符串
- 判断如果存在重复值
- 调整左指针位置
- 添加Map集合
- 计算最大长度。
- 判断如果存在重复值
- 返回最大长度
实现
public int lengthOfLongestSubstring(String s) {
int result = 0;
int left = 0;
// 使用Map判断是否存在 Map记录下标位置
Map<Character, Integer> map = new HashMap();
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
// 2 这里要取最大值
left = Math.max(left, map.get(c) + 1);
}
map.put(c, right);
result = Math.max(result, right - left + 1);
}
return result;
}
代码中 2 位置为什么要取最大值
比如:"abccbde" 第一次c重复时,left = 4,接下来b也要重复,此时如果不取最大,left = 2,显然不行。当出现重复之后,重复值之前的均不再考虑了,left指针绝对不能再回去。
最多包含两个不同字符的最长子串
“abc” 该字符串包含三个字符,在条件限制为最多两个不同字符时,最长子串为 ab、bc均为2
“abbc” 同理, 该字符子串为 “abb” 、"bbc" 均为 3
思路
- 初始化左指针为0
- 初始化最大长度为0
- 初始化Map集合
- 遍历字符
- 添加元素到集合
- 判断如果大小超过2
- 集合删除左指针元素
- 左指针前移一位
- 直到集合元素为2
- 计算长度
- 返回结果
实现
private static int twoCharOfLongestSubstring(String s, int k) {
int max = 0;
int left = 0;
Map<Character, Integer> map = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
map.put(c, right);
while (map.size() > k) {
// 1 注意这里 left 必须增加删除元素的下标 不能想着简单的 left++
Integer index = map.remove(s.charAt(left));
// 2 这里还要记得 + 1
left = index + 1;
}
// 3 这里同样要记得 + 1
max = Math.max(max, right - left + 1);
}
return max;
}
1 位置 为什么不能 ++
比如:"abbabbcccc" 当c出现时,不满足两个元素条件,此时需要删除元素a(left指针),在删除a时候,要使用a的最新下标,也是3的位置,所以要赋值 a 的最新下标位置。
2 位置 为什么要 + 1
比如:"abc" 当 c 出现时,要删除 a,此时a元素下标位置为0。而我们后续计算长度要排除a,从b位置开始,所以要 + 1;
3 位置 为什么要 + 1
因为长度最小也是1。假如只有1个元素时,right 下标为 0 , left 下标为 0,right - left = 0, 但是结果必须为1。
长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 1091 <= nums.length <= 1051 <= nums[i] <= 105
思路
- 初始化left指针为 0,sum为0
- 初始化最小长度为0。
- 遍历数组
- sum += 数组值
- 如果sum >= 目标值
- 计算长度,并更新
- 如果长度为0 直接赋值,否则取最小值
- sum - left 指针位置值
- 更新left指针 + 1
- 遍历,直到 < 目标值
- 计算长度,并更新
- 返回结果
实现
public int minSubArrayLen(int target, int[] nums) {
int min = Integer.MAX_VALUE;
int left = 0;
int sum = 0;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
min = Math.min(min, right - left + 1);
sum -= nums[left];
left++;
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
寻找字符串的异位排列
异位排列:对于某个字符串 s 来说,对其内部字符打乱形成新的字符就是s的异位排列。比如 "abc" 异位排列有“acb”、“bac”、“bca”...... 包含"abc"
字符串 s1 = "ab",字符串s2="acebac",对于s1字符串来说,他的异位字符串为 “ba”, 本题在于判断 字符串s2中是否包含s1的异位字符串。
思路
首先,判断 s2 中 是否包含 s1 的异位排列,那么s1的长度即可作为一个滑动窗口的大小。由于异位字符顺序实在是太多了,如何合理的判断才是首要问题。此时,题目中明确表示 字符串的取值 均为 小写英文字母。
小写字母共26个,我们可以使用26位数组对应26个字母,通过判断数组是否一样来实现判断是否异位排列。
java中对于字符的操作有以下规则:
'a' - 'a' = 0;
'b' - 'a' = 1;
......
'z' - 'a' = 25
- 获取字符串长度并判断
- 初始化 两个 26位数组,分别用于存储两个字符的数据。
- 将s1长度的字符串加入两个数组中。判断是否一致,一致返回true
- 不一致,继续遍历s2利用滑动窗口特性。(此时左指针可以初始化为0,right初始化为s1长度)
- 增加一个right指针元素,删除一个left指针元素。
- 判断是否一致,一致返回true
- 返回false
实现
/**
* 判断s2 是否包含 s1 的排列
* 滑动窗口思想:
* 1. 初始化左指针 left = 0
* 2. 将s1长度作为窗口的大小
* 3. 初始化26位数组,保存26位字母,通过判断数组是否一致来确认是否包含s1的排列
*
* @param s1
* @param s2
*/
private static boolean checkInclusion(String s1, String s2) {
int s1Len = s1.length(), s2Len = s2.length();
if (s2Len < s1Len) {
return false;
}
int[] arr1 = new int[26];
int[] arr2 = new int[26];
// 先初始化 s1 数组 用于后续的比对。同时顺带将s2也初始化部分
for (int i = 0; i < s1Len; i++) {
arr1[s1.charAt(i) - 'a']++;
arr2[s2.charAt(i) - 'a']++;
}
// 直接比对
if (Arrays.equals(arr1, arr2)) {
return true;
}
// 窗口长度固定,没有其他的控制条件,直接初始化为0即可
int left = 0;
for (int right = s1Len; right < s2Len; right++) {
arr2[s2.charAt(right) - 'a']++;
// 直接 ++ 即可
arr2[s2.charAt(left++) - 'a']--;
if (Arrays.equals(arr1, arr2)) {
return true;
}
}
return false;
}
颜色分类
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
提示:
n == nums.length1 <= n <= 300nums[i]为0、1或2
进阶:
- 你能想出一个仅使用常数空间的一趟扫描算法吗?
思想
其实就是一个排序。我们这里重点关注,使用常数空间的 一次遍历算法。
首先我们可利用的点就是 值是固定的,只有 0/1/2 三种。我们期望的顺序就是,0 最前,1中间,2最后。
这里结合双指针思想,left 位置放 0 ,right 位置放 2。可以出现以下判断。
- 当前位置如果为2,那需要与right指针交换,交换完成后 right -- (交换完以后,需要继续判断当前位置值,所以当前位置不能变)
- 如果当前位置为0,那需要与left指针交换,交换完后,left++ (交换完以后,当前位置 ++ )
- 如果当前位置为1,当前位置 ++
为什么当前位置为0时,交换完需要 ++ ?
首先当前位置为0时,是于左侧指针交换,左侧指针肯定是 0 或者 1,如果是0那么表示0肯定要在这个位置,如果是1那么 index++ left不动(等后续有0时替换)。
实现
private static void changeColor(int[] color) {
int left = 0, right = color.length - 1, index = 0;
while (index <= right) {
if (color[index] == 2) {
swap(color, index, right--);
}
if (color[index] == 0) {
swap(color, index++, left++);
} else {
index++;
}
}
}
private static void swap(int[] color, int i, int j) {
int temp = color[i];
color[i] = color[j];
color[j] = temp;
}
// 还有一种交换方法 看起来比较高大上
private static void swap1(int[] color, int i, int j) {
// 这里有个前提 i != j
if (i == j) {
return;
}
color[i] = color[i] + color[j];
color[j] = color[i] - color[j];
color[i] = color[i] - color[j];
}