贪心算法
1.1 最优除法
题目链接:最优除法
class Solution {
public String optimalDivision(int[] nums) {
int n = nums.length;
StringBuffer ret = new StringBuffer();
// 先处理只有一个数字和只有两个数字的情况,如果只有一个数字的情况:直接返回该数字作为结果
// 如果只有两个数字的情况:直接用 "/" 分隔两个数字
if (n == 1) return ret.append(nums[0]).toString();
if (n == 2) return ret.append(nums[0]).append("/").append(nums[1]).toString();
// 接下来就是处理多个数字的情况,先拼接第一个数字和第二个数字,第一个数字当作分子,第二个数字当作分母
ret.append(nums[0]).append("/(").append(nums[1]);
// 遍历剩余的数字,从第3个数字开始依次加入除法符号和数字
for (int i = 2; i < n; i++) {
ret.append("/").append(nums[i]); // 添加除法符号和当前数字
}
// 关闭括号
ret.append(")");
// 返回最终结果字符串
return ret.toString();
}
}
1.2 跳跃游戏 II
题目链接:跳跃游戏 II
class Solution {
public int jump(int[] nums) {
// left 和 right 表示当前层能覆盖的范围,ret 存储跳跃次数,maxPos 表示下一层的最远覆盖范围, n 记录数组的大小
int left = 0, right = 0, ret = 0, maxPos = 0, n = nums.length;
while (left <= right) {
// 如果下一层的最远覆盖范围已经到达或超过了最后一个位置,直接返回跳跃次数。
if (maxPos >= n - 1) return ret;
// 遍历当前 left 到 right 的层数,更新下一层的最远覆盖范围
for (int i = left; i <= right; i++)
maxPos = Math.max(maxPos, nums[i] + i);
// 接着更新下一层的范围:下一层的左端点是当前层的右端点的下一个位置,下一层的右端点是当前层能跳到的最远位置 maxPos,接着更新一下跳跃次数
left = right + 1;
right = maxPos;
ret++;
}
return -1;
}
}
1.3 跳跃游戏
题目链接:跳跃游戏
class Solution {
public boolean canJump(int[] nums) {
// left 和 right 表示当前层能覆盖的范围,ret 存储跳跃次数,maxPos 表示下一层的最远覆盖范围, n 记录数组的大小
int left = 0, right = 0, ret = 0, maxPos = 0, n = nums.length;
while (left <= right) { // 确保当前还在跳跃范围内
// 如果当前最远范围已经覆盖到最后一个位置,返回 true。
if (maxPos >= n - 1) return true;
// 遍历当前范围内所有可能的跳跃位置。
for (int i = left; i <= right; i++)
maxPos = Math.max(maxPos, nums[i] + i);
// 更新下一次跳跃的范围:
left = right + 1;
right = maxPos;
}
return false;
}
}
1.4 加油站
题目链接:加油站
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
// 遍历所有可能的起点,尝试找到一个能完成绕行的起点
for (int i = 0; i < n; i++) {
int rest = 0; // 记录当前油量余额
int step = 0; // 记录从当前起点行驶的步数
// 尝试从第 i 个加油站开始走 n 步,即绕行整个环
for (; step < n; step++) {
int index = (i + step) % n; // 当前加油站的下标
rest = rest + gas[index] - cost[index];
if (rest < 0) break; // 如果当前油量余额小于 0,则说明无法从当前起点完成绕行
}
if (step == n) return i;
// 优化:直接跳过已经尝试过的步数
i = i + step;
}
// 如果尝试了所有起点仍然无法绕行,则返回 -1
return -1;
}
}
1.5 单调递增的数字
题目链接:单调递增的数字
class Solution {
public int monotoneIncreasingDigits(int n) {
// 采用贪心的策略解决问题
// 先把 n 转为字符串,再由字符串转为字符数组
char[] s = Integer.toString(n).toCharArray();
int i = 0, m = s.length; // i 用来遍历字符数组 s,m 记录字符数组的长度
// 接着从左往右找到第一个递减的数字
while(i + 1 < m && s[i] <= s[i + 1]) i++;
// 如果没找到,说明到最后一个数字都还是递增的,那么直接返回这个数字即可,如果找到了就看看是否需要回退
if(i == m - 1) return n;
while (i - 1 >= 0 && s[i] == s[i - 1]) i--;
// 接着把 i 位置上的值减一,其他位置变成 9
s[i]--;
for(int j = i + 1; j < m; j++)
s[j] = '9';
return Integer.parseInt(new String(s));
}
}
1.6 环形的计算器
题目链接:环形的计算器
class Solution {
public int brokenCalc(int startValue, int target) {
// 反向思维 + 贪心算法,用 ret 记录最小操作数
int ret = 0;
while(target > startValue){
if(target % 2 == 1) target++;
else target /= 2;
ret++; // 判断完后要让操作次数加一
}
// 因为 target 小于 startValue 的时候是一直加一的,我们没必要再写一个循环,直接加上他们的差值即可
return ret + (startValue - target);
}
}
1.7 合并区间
题目链接:合并区间
class Solution {
public int[][] merge(int[][] intervals) {
// 1. 按照区间的左端点升序排序
Arrays.sort(intervals, (v1, v2) -> {
return v1[0] - v2[0];
});
// 初始化左右边界,设置 left 和 right 为第一个区间的左右端点
int left = intervals[0][0];
int right = intervals[0][1];
// ret 用于存储合并后的区间结果
List<int[]> ret = new ArrayList<>();
// 从第二个区间开始合并
for (int i = 1; i < intervals.length; i++) {
// a 为左端点,b 为右端点
int a = intervals[i][0];
int b = intervals[i][1];
if (a <= right) right = Math.max(right, b);
else {
ret.add(new int[]{left, right});
left = a; right = b;
}
}
// 别忘了处理最后一个区间,将其加入结果列表,最后把 ret 转为二维数组返回
ret.add(new int[]{left, right});
return ret.toArray(new int[0][]);
}
}
1.8 无重叠区间
题目链接:无重叠区间
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 按照区间的左端点升序排序
Arrays.sort(intervals, (v1, v2) -> {
return v1[0] - v2[0];
});
// ret 存储要移除区间的数量
int ret = 0;
// 初始化 left 和 right 为第一个区间的左右端点
int left = intervals[0][0]; // 左端点用不到,可删除
int right = intervals[0][1];
// 开始从第二个区间检查重叠并移除
for (int i = 1; i < intervals.length; i++) {
// 初始化 a 为左端点,b 为右端点
int a = intervals[i][0];
int b = intervals[i][1];
if (a < right) {
right = Math.min(right, b); // 贪心策略:保留右端点较小的区间,减小后续重叠可能性
ret++;
} else right = b;
}
return ret;
}
}
1.9 用最少数量的箭引爆气球
题目链接:用最少数量的箭引爆气球
class Solution {
public int findMinArrowShots(int[][] points) {
// 按照气球区间的左端点进行升序排序
Arrays.sort(points, (v1, v2) -> {
return Integer.compare(v1[0], v2[0]); // 直接相减会溢出,所以采用 compare 的方式来比较
});
// 初始化 right 为第一个区间的右端点,用 ret 存储最终结果,ret 初始化为 1,因为至少需要一支箭
int right = points[0][1], ret = 1;
// 接着从第二个区间开始遍历所有气球的区间
for (int i = 1; i < points.length; i++) {
// a 为左端点,b 为右端点
int a = points[i][0];
int b = points[i][1];
if (a <= right) right = Math.min(right, b);
else {
right = b;
ret++;
}
}
return ret;
}
}
1.10 整数替换
题目链接:整数替换
class Solution {
public int integerReplacement(int n) {
// 采用贪心策略解决这个问题, 用 ret 存储最终的结果
int ret = 0;
// 只要当 n 大于 1 的时候就一直执行循环
while(n > 1){
// 如果 n 是偶数,没得贪,只能除 2
if(n % 2 == 0){
n /= 2; ret++;
}else{
// 如果 n 是奇数,要以 3 为条件分三个情况讨论
if(n == 3){
ret += 2;
n = 1;
}else if(n % 4 == 1){
// 贪心策略,这个情况减一除二就可以了,但是因为 n 的数据范围太大,我们直接除 2,因为是奇数会自动把 1 消除
n /= 2;
ret += 2;
}else{
// 贪心策略,这个情况加一除二,同样的,n 的范围太大,所以我们先除 2 再加一
n /= 2; n += 1;
ret += 2;
}
}
}
return ret;
}
}
1.11 俄罗斯套娃信封问题
题目链接:俄罗斯套娃信封问题
class Solution {
public int maxEnvelopes(int[][] e) {
// 采用贪心的策略解决问题首先按照第一个维度(宽度)升序排序,如果宽度相同,则按照第二个维度(高度)降序排序
Arrays.sort(e, (v1, v2) -> {
if (v1[0] != v2[0]) return v1[0] - v2[0]; // 如果宽度不同,按宽度升序排列
else return v2[1] - v1[1]; // 如果宽度相同,按高度降序排列
});
// 用 ret 存储递增的高度列表,首先把第一个信封的高度加入到 ret 中
ArrayList<Integer> ret = new ArrayList<>();
ret.add(e[0][1]);
// 接着遍历所有的信封,尝试构建递增子序列
for (int i = 1; i < e.length; i++) {
int b = e[i][1]; // 当前信封的高度
// 如果当前信封的高度大于 ret 中最后一个高度,说明可以继续扩展递增子序列
if (b > ret.get(ret.size() - 1)) ret.add(b);
else {
// 如果当前信封的高度小于或等于 ret 中最后一个高度,我们就需要通过二分查找找到合适的位置来替换掉原有的元素
int left = 0, right = ret.size() - 1;
while (left < right) {
int mid = (left + right) / 2;
if (ret.get(mid) >= b) {
right = mid;
} else {
left = mid + 1;
}
}
ret.set(left, b);
}
}
return ret.size();
}
}
1.12 可被三整数的最大和
题目链接:可被三整数的最大和
class Solution {
public int maxSumDivThree(int[] nums) {
// 利用贪心的策略解决这个问题。因为数据过大,所以我们用 0x3f3f3f3f 表示正无穷大,sum 用于记录整个数组的和
int INF = 0x3f3f3f3f, sum = 0;
// 用 x1, x2 表示余数为一时的最小值和次小值,y1,y2 表示余数为 2 时的最小值和次小值
int x1 = INF, x2 = INF, y1 = INF, y2 = INF;
// 接着我们在遍历数组的同时更新 sum 和 x1,x2,y1,y2 的值
for(int i = 0; i < nums.length; i++){
// 首先更新 sum
sum += nums[i];
// 接着更新 x1,x2,y1,y2,接着分情况更新他们的值
// 当余数为 1 的时候更新 x1 和 x2 的值
if(nums[i] % 3 == 1){
if(nums[i] < x1){
x2 = x1;
x1 = nums[i];
}else if(nums[i] >= x1 && nums[i] < x2){
x2 = nums[i];
}
}
// 当余数为 2 的时候,更新 y1 和 y2 的值
if(nums[i] % 3 == 2){
if(nums[i] < y1){
y2 = y1;
y1 = nums[i];
}else if(nums[i] >= y1 && nums[i] < y2){
y2 = nums[i];
}
}
}
// 接着可以开始正式操作了
if(sum % 3 == 0) return sum;
else if(sum % 3 == 1) return Math.max(sum - x1, sum - y1 - y2);
else return Math.max(sum - y1, sum - x1 - x2);
}
}
1.13 距离相等的条形码
题目链接:距离相等的条形码
class Solution {
public int[] rearrangeBarcodes(int[] barcodes) {
// 采用贪心的策略解决这个问题
// 首先统计每个数字的出现次数,此处采用哈希表统计
Map<Integer, Integer> hash = new HashMap<>();
// 接着把 barcodes 中的元素全丢入哈希表中,用 maxValue 标记出现最多的那个数字,用 maxCount 标记出现最多的次数
int maxValue = 0, maxCount = 0;
for(int x : barcodes){
hash.put(x, hash.getOrDefault(x, 0) + 1);
// 把 x 丢入哈希表中看看 x 数字的出现次数有没有超过出现最多的那个数字
if(hash.get(x) > maxCount){
maxValue = x;
maxCount = hash.get(x);
}
}
// 统计好后先把出现次数最多的那个数字先填了
int index = 0; // index 用于标记正在填写的位置
int[] ret = new int[barcodes.length];
for(int i = 0; i < maxCount; i++){
ret[index] = maxValue;
index += 2;
}
// 把出现最多次数最多的数字填完后把这个数字移除哈希表
hash.remove(maxValue);
// 接着开始遍历哈希表中 keySet 剩下的数字
for(int x : hash.keySet()){
for (int i = 0; i < hash.get(x); i++){
// 如果 index 超出数组范围,从索引 1 开始填充奇数位置
if (index >= barcodes.length) index = 1;
ret[index] = x;
index += 2;
}
}
return ret;
}
}
1.14 重构字符串
题目链接:重构字符串
class Solution {
public String reorganizeString(String s) {
// 首先统计一下每个字符出现的次数,此处用数字模拟哈希表,maxChar 用来记录出现次数最多的字符,maxCount 记录其出现次数
int[] hash = new int[26];
char maxChar = ' ';
int maxCount = 0;
// 统计每个字符出现的次数,同时记录出现次数最多的字符
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
hash[ch - 'a']++;
// 如果当前字符的计数超过 maxCount,则更新 maxChar 和 maxCount
if (maxCount < hash[ch - 'a']) {
maxChar = ch;
maxCount = hash[ch - 'a'];
}
}
// 处理一下特殊情况,当 maxCount 大于 数组长度的一半,那么这个字符串是不能重构的,返回空字符串即可
int n = s.length();
if (maxCount > (n + 1) / 2) return "";
// ret 用于存储最终结果
char[] ret = new char[n];
int index = 0; // index 代表目前填充的位置
// 首先处理出现次数最多的字符
for (int i = 0; i < maxCount; i++) {
ret[index] = maxChar;
index += 2;
}
// 将 maxChar 的计数清零,因为它已经全部填入结果数组
hash[maxChar - 'a'] = 0;
// 接着处理剩下的字符
for (int i = 0; i < 26; i++) {
for (int j = 0; j < hash[i]; j++) {
if (index >= n) index = 1; // 如果偶数位置已满,从索引 1 开始填充奇数位置
ret[index] = (char)(i + 'a');
index += 2;
}
}
return new String(ret);
}
}