贪心算法

1,011 阅读5分钟

image.png - 贪心算法的局限性 image.png - 具备贪心思想的问题 image.png

1. leetcode455 分发饼干

1.1 解题思路

image.png image.png

1.2 代码实现🔴

class Solution {
    public int findContentChildren(int[] g, int[] s) { // 饼干是s 小孩胃口值是g
        Arrays.sort(g);
        Arrays.sort(s);
        int i = 0;
        int j = 0;
        while (i < s.length && j < g.length) {
            if (s[i] >= g[j]) {
                j++;
            }
            i++;
        }
        return j;
    }
}

2. leetcode322 零钱兑换

2.1 解题思路

本题贪心 + 回溯不能提升性能,最优解为动态规划,后续再写。 image.png image.png

3. leetcode45 跳跃游戏II

3.1 解题思路

image.png

3.2 代码实现1:DFS(会超时)

class Solution {
    private int minSteps = Integer.MAX_VALUE;

    public int jump(int[] nums) {
        dfs(nums, 0, new ArrayList<>());
        return minSteps == Integer.MAX_VALUE ? 0 : minSteps;
    }

    private void dfs(int[] nums, int jumpedIndex, List<Integer> path) {
        if (jumpedIndex == nums.length - 1) {
            minSteps = Math.min(minSteps, path.size());
            return;
        }
        for (int i = 1; i <= nums[jumpedIndex]; i++) {
            if (jumpedIndex + i >= nums.length)
                continue;
            path.add(i);
            // jumpedIndex + i,表示跳到下一步所在的位置
            dfs(nums, jumpedIndex + i, path);
            path.remove(path.size() - 1);
        }
    }
}

3.2 代码实现2:BFS(会超时)

class Solution {
    public int jump(int[] nums) {
        if (nums.length == 1)
            return 0;
        Queue<Integer> queue = new ArrayDeque<>();
        queue.offer(0);
        int level = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int jumpedIndex = queue.poll();
                if (jumpedIndex == nums.length - 1)
                    return level;
                for (int j = 1; j <= nums[jumpedIndex]; j++) {
                    queue.offer(jumpedIndex + j);
                }
            }
            level++;
        }
        return 0;
    }
}

3.3 代码实现3:贪心

image.png

class Solution {
    // 贪心策略:每步都选择能跳到的最远距离
    public int jump(int[] nums) {
        if (nums.length == 1)
            return 0;
        int steps = 0;
        int start = 0, end = 0;
        while (end < nums.length - 1) { // 走到最后一个位置的时候就不用走了
            int maxPos = 0;
            // 每次从 [start, end] 中都选择能跳到的最远距离
            for (int i = start; i <= end; i++) {
                maxPos = Math.max(maxPos,i + nums[i]);
            }
            start = end + 1;
            end = maxPos;
            steps++;
        }
        return steps;
    }
}

3.4 代码实现4:🔴贪心(只遍历一次)

image.png

class Solution {
    // 贪心策略:每步都选择能跳到的最远距离
    public int jump(int[] nums) {
        if (nums.length == 1)
            return 0;
        int steps = 0;
        int maxPos = 0, end = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            // 仅遍历一次,计算每一个位置并求得最大位置
            maxPos = Math.max(maxPos, i + nums[i]);
            if (i == end) {
                steps++;
                end = maxPos;
            }
        }
        return steps;
    }
}

4. leetcode55 跳跃游戏

4.1 解题思路

image.png image.png

4.2 代码实现🔴

class Solution {
    public boolean canJump(int[] nums) {
        int maxPos = 0;
        for (int i = 0; i < nums.length; i++) {
            if (maxPos < i)
                return false;
            maxPos = Math.max(maxPos, i + nums[i]);
        }
        return true;
    }
}

4. leetcode1578 避免重复字母的最小删除成本

4.1 解题思路

image.png image.png

4.2 代码实现🔴

class Solution {
    public int minCost(String s, int[] cost) {
        int res = 0;
        int len = s.length();
        int i = 0;
        while (i < len) {
            char c = s.charAt(i);
            int maxCost = 0;
            int sumCost = 0;
            while (i < len && s.charAt(i) == c) {
                maxCost = Math.max(maxCost, cost[i]);
                sumCost += cost[i];
                i++;
            }
            res += sumCost - maxCost;
        }
        return res;
    }
}

5. leetcode402 移掉 K 位数字

5.1 解题思路

image.png image.png

5.2 代码实现1:贪心,时间复杂度O(k * n)

class Solution {
    public String removeKdigits(String num, int k) {
        StringBuilder sb = new StringBuilder(num);
        for (int i = 0; i < k; i++) {
            boolean hasDeleted = false;
            for (int j = 1; j < sb.length(); j++) {
                // 如果前面的数字大,则删除
                if (sb.charAt(j) < sb.charAt(j - 1)) {
                    sb.deleteCharAt(j - 1);
                    hasDeleted = true;
                    break;
                }
            }
            // 说明序列是递增的,那么删除最后一个字符
            if (!hasDeleted)
                sb.deleteCharAt(sb.length() - 1);
        }

        // 删除前面是0的字符
        int len = sb.length();
        while (len != 0) {
            if (sb.charAt(0) > '0')
                break;
            sb.deleteCharAt(0);
            len = sb.length();
        }
        return sb.length() == 0 ? "0" : sb.toString();
    }
}

5.3 代码实现2:🔴贪心 + 单调栈 (时间复杂度O(k + n),空间复杂度O(n))

image.png

class Solution {
    public String removeKdigits(String num, int k) {
        Deque<Character> deque = new ArrayDeque<>();
        for (int i = 0; i < num.length(); i++) {
            char c = num.charAt(i);
            while (!deque.isEmpty() && k > 0 && deque.peek() > c) {
                deque.pop();
                k--;
            }
            deque.push(c);
        }
        for (int i = 0; i < k; i++) {
            deque.pop();
        }

        StringBuilder sb = new StringBuilder();
        boolean isFirst = true;
        while (!deque.isEmpty()) {
            char c = deque.pollLast();
            if (c == '0' && isFirst)
                continue;
            sb.append(c);
            isFirst = false;
        }
        return sb.length() == 0 ? "0" : sb.toString();
    }
}

6 leetcode409 最长回文串

6.1 解题思路

1. 可以将出现次数为偶数个的字符分居两侧构成回文串
2. 只能在回文串的中间添加一个出现次数为奇数个的字符

6.2 代码实现🔴

class Solution {
    public int longestPalindrome(String s) {
        int[] counter = new int[128];
        int ans = 0;
        for (char c : s.toCharArray()) {
            counter[c]++;
        }
        for (int v : counter) {
            ans += v / 2 * 2;
            // 中间只能有一个出现奇数次的字符
            if (v % 2 == 1 && ans % 2 == 0) {
                ans++;
            }
        }
        return ans;
    }
}

7. leetcode680 验证回文字符串Ⅱ

7.1 解题思路

1. 贪心策略:只有在开头和结尾两个字符不相等的时候,才去尝试删除开头或者结尾任一个字符

7.2 代码实现:🔴贪心

class Solution {
    // 贪心策略: 只有在开头和结尾两个字符不相等的时候,才去尝试删除开头或者结尾任一个字符
    // 时间复杂度O(n)
    // 空间复杂度O(1)
    public boolean validPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) == s.charAt(right)) {
                left++;
                right--;
            } else {
                // 要么删除left指向的字符,要么删除right指向的字符
                // 然后再判断剩余的字符是否是回文
                return validPalindrome(s, left + 1, right) ||
                        validPalindrome(s, left, right - 1);
            }
        }
        return true;
    }

    private boolean validPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left) == s.charAt(right)) {
                left++;
                right--;
            } else {
                return false;
            }
        }
        return true;
    }
}

8. leetcode316 去除重复字母

8.1 解题思路

1. 记录字符在字符串s中的最后索引,用于判断当前字符后面是否还有将要弹出的字符
2. 维护一个单调栈
3. 使用一个boolean数组记录是否已经存在于栈中
image.png

8.2 代码实现:🔴贪心 + 单调栈

class Solution {
    public String removeDuplicateLetters(String s) {
        // 1. 计算字符在字符串s中的最后索引
        int[] lastIndex = new int[26];
        for (int i = 0; i < s.length(); i++) {
            lastIndex[s.charAt(i) - 'a'] = i;
        }

        // 2. 维护单调栈
        Deque<Character> stack = new ArrayDeque<>();
        // 用于记录字符是否已经存在于栈中
        boolean[] exits = new boolean[26];

        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (exits[c - 'a'])
                continue;

            // 条件:
            // (1). 当前字符小于栈顶字符
            // (2). 栈顶字符在当前字符的后面还会出现
            while (!stack.isEmpty() && stack.peek() > c
                    && lastIndex[stack.peek() - 'a'] > i) {
                char top = stack.pop();
                exits[top - 'a'] = false;
            }

            stack.push(c);
            exits[c - 'a'] = true;
        }

        // 3. 将栈中字符拼接成结果
        StringBuilder res = new StringBuilder();
        while (!stack.isEmpty()) {
            res.append(stack.pollLast());
        }
        return res.toString();
    }
}

9. leetcode1047 删除字符串中的所有相邻重复项

9.1 解题思路

1. 使用栈 image.png 2. 使用快慢指针,0-slow表示不需要删除的字符,fast指针用来遍历整个字符串
image.png image.png

9.2 代码实现1:🔴使用栈

class Solution {
    public String removeDuplicates(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        for (char c : s.toCharArray()) {
            if (!stack.isEmpty() && stack.peek() == c) {
                stack.pop();
            } else {
                stack.push(c);
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()){
            sb.append(stack.pollLast());
        }
        return sb.toString();
    }
}

9.3 代码实现2:🔴使用快慢指针(空间复杂度降为O(1))

class Solution {
    public String removeDuplicates(String s) {
        char[] chars = s.toCharArray();
        int slow = -1;
        int fast = 0;
        while (fast < s.length()) {
            if (slow >= 0 && chars[fast] == chars[slow]) {
                slow--;
            } else {
                slow++;
                chars[slow] = chars[fast];
            }
            fast++;
        }
        return new String(chars, 0, slow + 1);
    }
}

10. leetcode1209 删除字符串中的所有相邻重复项II

10.1 解题思路

1. 使用栈 image.png image.png 2. 使用快慢指针 image.png

10.2 代码实现1:🔴使用栈(时间复杂度:O(N),空间复杂度:O(N))

class Solution {
    class Pair {
        char ch;
        int count;

        public Pair(char ch, int count) {
            this.ch = ch;
            this.count = count;
        }
    }

    public String removeDuplicates(String s, int k) {
        Deque<Pair> stack = new ArrayDeque<>();
        for (int i = 0; i < s.length(); i++) {
            if (stack.isEmpty() || s.charAt(i) != stack.peek().ch) {
                stack.push(new Pair(s.charAt(i), 1));
            } else {
                stack.peek().count++;
                if (stack.peek().count == k) {
                    stack.pop();
                }
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()) {
            Pair p = stack.pollLast();
            for (int i = 0; i < p.count; i++) {
                sb.append(p.ch);
            }
        }
        return sb.toString();
    }
}

10.3 代码实现2:🔴使用快慢指针

class Solution {
    public String removeDuplicates(String s, int k) {
        char[] chars = s.toCharArray();
        Deque<Integer> count = new ArrayDeque<>();
        int slow = 0;
        for (int fast = 0; fast < chars.length; slow++, fast++) {
            if (slow != fast) {
                chars[slow] = chars[fast];
            }
            if (slow == 0 || chars[slow] != chars[slow - 1]) {
                count.push(1);
            } else {
                int incremented = count.pop() + 1;
                if (incremented == k) {
                    slow -= k;
                } else {
                    count.push(incremented);
                }
            }
        }
        return new String(chars, 0, slow);
    }
}

11. leetcode976 三角形的最大周长

11.1 解题思路

不失一般性,我们假设三角形的边长a,b,c满足a≤b≤c,那么这三条边组成面积不为零的三角形的充分必要条件为a+b>c

11.2 代码实现🔴

class Solution {
    public int largestPerimeter(int[] nums) {
        Arrays.sort(nums);
        for (int i = nums.length - 1; i >= 2; i--) {
            if (nums[i - 2] + nums[i - 1] > nums[i]) {
                return nums[i - 2] + nums[i - 1] + nums[i];
            }
        }
        return 0;
    }
}

12. leetcode674 最长连续递增序列

12.1 解题思路

image.png image.png

12.2 代码实现(贪心 + 双指针)🔴

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int res = 1;
        for (int start = 0, end = 1; end < nums.length; end++) {
            if (nums[end] <= nums[end - 1]) {
                start = end;
                continue;
            }
            res = Math.max(res, end - start + 1);
        }
        return res;
    }
}

13. leetcode738 单调递增的数字

13.1 解题思路

1. 先遍历找到第一个非递增的序列 image.png
2. 回退check直至其为递增序列 image.png
3. 将最后改变高位之后的数字全部换成9
image.png

13.2 代码实现🔴

class Solution {
    public int monotoneIncreasingDigits(int n) {
        char[] strN = String.valueOf(n).toCharArray();
        int i = 1;
        // 1. 找到第一个递减的位
        while (i < strN.length && strN[i - 1] <= strN[i])
            i++;
        if (i < strN.length) {
            // 2. 不断将前一个数字 -1,直到前一个数字小于等于后一个数字
            while (i > 0 && strN[i - 1] > strN[i]) {
                strN[i - 1] -= 1;
                i--;
            }
            // 3. 将 i 后面的数字都设置为9
            i++;
            while (i < strN.length) {
                strN[i++] = '9';
            }
            return Integer.parseInt(new String(strN));
        } else {
            return n;
        }
    }
}

14. leetcode134 加油站

14.1 解题思路

总结:如果x到不了y + 1(但能到y),那么从x到y的任一点出发都不可能到达y+1。因为从其中任一点出发的话,相当于从0开始加油,而如果从x出发到该点则不一定是从0开始加油,可能还有剩余的油。既然不从0开始都到不了y+1,那么从0开始就更不可能到达y+1了。
image.png image.png

14.2 代码实现🔴

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int totalGas = 0;
        int currGas = 0;
        int startStation = 0;
        for (int i = 0; i < gas.length; i++) {
            totalGas += gas[i] - cost[i];
            currGas += gas[i] - cost[i];
            if (currGas < 0) {
                startStation = i + 1;
                currGas = 0;
            }
        }
        return totalGas >= 0 ? startStation : -1;
    }
}

15. leetcode767 重构字符串

15.1 解题思路

image.png image.png

15.2 代码实现🔴

class Solution {
    public String reorganizeString(String s) {
        // 1. 统计每个字符出现的次数
        char[] chars = s.toCharArray();
        int n = chars.length;
        int[] count = new int[26];
        for (char c : chars) {
            count[c - 'a']++;
            if (count[c - 'a'] > (n + 1) / 2)
                return "";
        }
        // 2. 拿到出现次数最多的字符
        int maxCountIndex = 0;
        for (int i = 0; i < 26; i++) {
            if (count[i] > count[maxCountIndex])
                maxCountIndex = i;
        }
        // 3. 先将出现次数最多的字符放在偶数索引上
        char[] res = new char[n];
        int index = 0;
        while (count[maxCountIndex] > 0) {
            res[index] = (char) (maxCountIndex + 'a');
            index += 2;
            count[maxCountIndex]--;
        }
        // 4. 将其他的字符放在其他的位置
        for (int i = 0; i < 26; i++) {
            while (count[i] > 0) {
                if (index >= n) {
                    index = 1;
                }
                res[index] = (char) (i + 'a');
                index += 2;
                count[i]--;
            }
        }
        return new String(res);
    }
}

16. leetcode621 任务调度器

16.1 解题思路

1. 完成所有任务所需要的最短时间 = 待命的任务数 + 所有的任务数
image.png 2. 次数最多的任务数只出现了一个的情况 image.png 3. 次数最多的任务数出现了多个的情况 image.png 4. 出现次数最多的任务数大于冷却时间
image.png

16.2 代码实现🔴

class Solution {
    public int leastInterval(char[] tasks, int n) {
        int[] counter = new int[26];
        int maxAppearCount = 0; // 出现次数最多的任务 最大出现次数
        int maxTaskCount = 0; // 出现次数最多的任务 最大任务数量
        for (char task : tasks) {
            counter[task - 'A']++;
            if (counter[task - 'A'] == maxAppearCount) {
                maxTaskCount++;
            } else if (counter[task - 'A'] > maxAppearCount) {
                maxAppearCount = counter[task - 'A'];
                maxTaskCount = 1;
            }
        }

        int partCount = maxAppearCount - 1; // 空余部分个数
        int partLength = n - (maxTaskCount - 1); // 空余部分的长度
        int emptySlots = partCount * partLength; // 空闲槽位数
        int availableTasks = tasks.length - maxAppearCount * maxTaskCount; // 剩余可使用任务数
        int idles = Math.max(0, emptySlots - availableTasks); // 待命任务数
        return tasks.length + idles; // 完成所有任务所需要的最短时间 = 待命的任务数 + 所有的任务数
    }
}

17. leetcode670 最大交换

17.1 解题思路

贪心策略:拿高位后面比高位大的值进行交换,而且越大越好

17.2 代码实现1:暴力

class Solution {
    public int maximumSwap(int num) {
        char[] chars = String.valueOf(num).toCharArray();
        int max = num;
        for (int i = 0; i < chars.length; i++) {
            for (int j = i + 1; j < chars.length; j++) {
                swap(chars, i, j);
                max = Math.max(max, Integer.parseInt(new String(chars)));
                swap(chars, i, j);
            }
        }
        return max;
    }

    private void swap(char[] chars, int i, int j) {
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
    }
}

17.3 代码实现2:🔴贪心

class Solution {
    public int maximumSwap(int num) {
        char[] chars = String.valueOf(num).toCharArray();

        // 记录每个数字出现的最后一次出现的下标
        int[] last = new int[10];
        for (int i = 0; i < chars.length; i++) {
            last[chars[i] - '0'] = i;
        }

        // 从高位向低位扫描,找到当前位置右边的最大的数字并交换
        for (int i = 0; i < chars.length; i++) {
            // 找最大,所以倒着找
            for (int d = 9; d > chars[i] - '0'; d--) {
                if (last[d] > i) {
                    char temp = chars[i];
                    chars[i] = chars[last[d]];
                    chars[last[d]] = temp;
                    // 只允许交换一次,因此直接返回
                    return Integer.parseInt(new String(chars));
                }
            }
        }
        return num;
    }
}

18. leetcode861 翻转矩阵后的得分

18.1 解题思路

image.png image.png

18.2 代码实现🔴

class Solution {
    public int matrixScore(int[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;
        // 使得每一行都从 1 开头
        for (int row = 0; row < rows; row++) {
            if (grid[row][0] == 0) {
                // 行翻转
                for (int col = 0; col < cols; col++) {
                    grid[row][col] ^= 1;
                }
            }
        }
        int res = 0;
        // 1 的数量越多,得到的数值越大
        for (int col = 0; col < cols; col++) {
            int count = 0;
            // 统计第 col 列有多少个1
            for (int row = 0; row < rows; row++) {
                count += grid[row][col];
            }
            int maxOneCount = Math.max(count, rows - count);
            res += maxOneCount * (1 << (cols - col - 1));
        }
        return res;
    }
}

19. leetcode1029 两地调度

19.1 解题思路

最优方案:选出price_A - price_B最小的N个人,让他们飞往A市,其余的飞往B市

19.2 代码实现:🔴贪心

class Solution {
    public int twoCitySchedCost(int[][] costs) {
        Arrays.sort(costs, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return (o1[0] - o1[1]) - (o2[0] - o2[1]);
            }
        });

        int n = costs.length / 2;
        int total = 0;
        for (int i = 0; i < n; i++) {
            total += costs[i][0] + costs[i + n][1];
        }
        return total;
    }
}

20. leetcode330 按要求补齐数组

20.1 解题思路

1. 贪心思想:每次加入nums的数都要在满足条件的前提下尽可能的大以覆盖更大的范围。 2. 对于正整数x,如果区间[1, x - 1]内的所有数字都已经被覆盖,且x在数组中,则区间[1, 2x - 1]内的数字也都被覆盖。 image.png image.png

20.2 代码实现🔴

class Solution {
    public int minPatches(int[] nums, int n) {
        int res = 0;
        // 贪心的保证 [1, x - 1] 这个区间中所有数字会被覆盖
        long x = 1;
        int index = 0;
        while (x <= n) {
            if (index < nums.length && nums[index] <= x) {
                // 因为根据贪心思想,我们总保证区间小于x的所有值会被覆盖掉
                // [1, x + x - 1] [1, x + nums[index] - 1]
                x += nums[index];
                index++;
            } else {
                res++; // 把 x 放入数组中
                // 对于正整数 x,如果区间 [1, x - 1] 内的所有数字都已经被覆盖。
                // 且 x 在数组中,则区间 [1, 2x - 1] 内的所有数字也都被覆盖
                x *= 2;
            }
        }
        return res;
    }
}