高效制胜--算法总结

264 阅读6分钟

求和问题

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

示例 1: 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

思路:两数之和,数组未排序,考虑哈希图,边找边存,一次遍历即可完成,空间换时间。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length;i++) {
            if(map.containsKey(target - nums[i])) {
                res[0] = map.get(target - nums[i]);
                res[1] = i;
                return res;
            }
            map.put(nums[i], i);
        }
        return res;   
    }
}

167. 两数之和 II - 输入有序数组

给定一个已按照 升序排列  的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。

思路:排序过的数组,使用双指针,两头往里找,直到二者相加等于target。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int i = 0;
        int j = numbers.length - 1;
        int[] res = new int[2];
        while(i < j) {
            if(numbers[i] + numbers[j] < target) {
                i++;
            } else if(numbers[i] + numbers[j] > target) {
                j--;
            } else {
                res[0] = i + 1;
                res[1] = j + 1;
                return res;
            }
        }
        return res;
    }
}

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

思路:先排序,第一个指针循环从0到n-2,如果相同注意剪枝,后两个指针参考两数之和算法,当找到一答案时,如果后两个指针值相同注意剪枝。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        int len = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        if(len == 0) {
            return res;
        }
        if(nums[0] == 0 && len == 1) {
            return res;
        }
        Arrays.sort(nums);
        for(int i = 0; i < len; i++) {
            // 剪枝
            if(i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = len - 1;
            while(j < k) {
                if(nums[i] + nums[j] + nums[k] == 0) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[i]);
                    list.add(nums[j]);
                    list.add(nums[k]);
                    res.add(list);
                    j++;
                    k--;
                    // 剪枝
                    while (j < k && nums[j] == nums[j - 1]) j++;
                    while (j < k && nums[k] == nums[k + 1]) k--;
                } else if(nums[i] + nums[j] + nums[k] > 0) {
                    k--;
                } else {
                    j++;
                }
            }
        }
        return res;
    }
}

18. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

思路:第一个指针遍历0到n-3,注意剪枝,后面同三数求和思路

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {  
        int len = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || len < 4) {
            return res;
        }
        Arrays.sort(nums);
        for(int i = 0; i < len - 3; i++) {
            if(i > 0 && nums[i] == nums[i - 1] ) {
                continue;
            }
            for(int j = i + 1; j < len - 2; j++) {
                if(j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                int l = j + 1, r = len - 1;
                while(l < r) {
                    if(nums[i] + nums[j] + nums[l] + nums[r] == target) {
                        List<Integer> list = new ArrayList<Integer>();
                        list.add(nums[i]);
                        list.add(nums[j]);
                        list.add(nums[l]);
                        list.add(nums[r]);
                        res.add(list);
                        l++;
                        r--;
                        while(l < r && nums[l] == nums[l - 1]) l++;
                        while(l < r && nums[r] == nums[r + 1]) r--;
                    } else if(nums[i] + nums[j] + nums[l] + nums[r] > target) {
                        r--;
                    } else {
                        l++;
                    }
                }
            }
        }
        return res;
    }
}

动态规划

509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你 n ,请计算 F(n) 。

思路:递归 && dp->不用数组,节省内存。

class Solution { 
    public int fib(int n) { 
        if(n == 0) return 0; 
        if(n == 1) return 1; 
        return fib(n - 1) + fib(n - 2); 
    }
}
class Solution {
    public int fib(int n) {
        if (n < 2) {
            return n;
        }
        int p = 0, q = 0, r = 1;
        for (int i = 2; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
}

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

思路:动态规划

class Solution {
    public int climbStairs(int n) {
        int p = 0, q = 0, r = 1;
        for (int i = 1; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    } 
}

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

思路:dp

class Solution {
    public int maxSubArray(int[] nums) {
        int pre = 0;
        int max = nums[0];
        for(int x : nums) {
            pre = Math.max(pre + x, x);
            max = Math.max(max, pre);
        }
        return max;
    }
}

416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

思路:转化为是否可以选出若干个数达到数组之和的一半。数组长度小于2,就一个数不能完成。数组之和为奇数,不能平均分为两份。数组的最大值大于数组之和的一半,也不能平均分。

创建boolean dp[前i个数][是否能达到j值] 初始化:前多少个数,只要我不选,j都可以为0;选择第i个数,就可以达到nums[i]值。(官解只初始化第0个数达到nums[0]值)

状态转移方程:如果当前值nums[i]比要达到的j值小,可以选这个数,也可以不选。 如果选:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num]; 不选:dp[i][j]=dp[i−1][j] 如果大于j值,则只能不选:dp[i][j]=dp[i−1][j]

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        // 长度小于2返回false
        if(n < 2) {
            return false;
        }
        int max = nums[0];
        int sum = 0;
        for(int x : nums) {
            sum += x;
            max = Math.max(max, x);
        }
        int target = sum / 2;
        //总数无法整除2返回false
        if(sum % 2 != 0) {
            return false;
        }
        //最大值大于目标值返回false
        if(target < max) {
            return false;
        }
        boolean[][] dp = new boolean[n][target + 1];
        //只要不选,前n个数之和可以为0
        for(int i = 0; i < n; i++) {
            dp[i][0] = true;
            dp[i][nums[i]] = true;
        }
        //选择第一个数,可以达成第一个数的值
        // dp[0][nums[0]] = true;
        for(int i = 1; i < n; i++) {
            int num = nums[i];
            for(int j = 1; j <= target; j++) {
                // j比当前值大,可以选也可以不选
                if(j >= num) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n - 1][target];
    }
}

注意到:每行dp只与上一行dp有关,可以降低空间复杂度,此时状态转移方程 dp[j] = dp[j] || dp[j - nums[i]];则后半段代码可以优化为

        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            //由于如果j < target,只能不选,此时dp[j] = dp[j] 因此忽略
            for (int j = target; j >= num; --j) {
                dp[j] |= dp[j - num];
            }
        }
        return dp[target];

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

思路:数组dp[i]表示最少多少个数能到达i值,初始化给每个值设置为总金额+1,此时就算只用1元也无法实现,从而排除使用Math.min中初始dp[i]的问题,dp[0]设置为0,啥都不用也能达到0元。同时排查各个面值时,如果面值大于当前到达值,则会超出该值,不使用该面值。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        for(int i = 0; i <= amount; i++) {
            if(i != 0) {
                dp[i] = amount + 1;
            }
            for(int j = 0; j < coins.length; j++) {
                if(i - coins[j] >= 0) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            } 
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

堆栈问题

20. 有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。

思路:遍历进栈,如果为左括号,将([{改为)]},入栈。否则如果栈为空,或者出栈元素不等于当前元素则返回false,最后栈非空返回true。

class Solution {
    public boolean isValid(String s) {
        Deque<Character> dq = new LinkedList<>();
        for(char c : s.toCharArray()) {
            if(c == '(') {
                dq.push(')');
            } else if(c == '[') {
                dq.push(']');
            } else if(c == '{') {
                dq.push('}');
            } else if(dq.isEmpty() || dq.pop() != c) {
                return false;
            }
        }
        return dq.isEmpty();
    }
}

496. 下一个更大元素 I

给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

思路:问题重新描述:nums1是nums2子集,nums1每个元素对应nums2相同元素对应的位置后面是否有大于该元素的值。 使用单调栈,对nums2操作,得到大于每个元素的值放入图中。 遍历nums2,如果栈为空或者当前元素小于栈顶元素,入栈。 如果大于栈顶元素,该元素即大于当前元素,出栈并且更新图,直到栈为空或者栈顶元素大于当前元素。

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        Map<Integer, Integer> map = new HashMap<>();
        Deque<Integer> dq = new LinkedList<>();
        for(int i = 0; i < len2; i++) {
            // 有更大的则更新图
            while(!dq.isEmpty() && nums2[i] > dq.peek()) {
                map.put(dq.pop(), nums2[i]);
            }
            dq.push(nums2[i]);
        }
        int[] res = new int[len1];
        for (int i = 0; i < len1; i++) {
            res[i] = map.getOrDefault(nums1[i], -1);
        }
        return res;
    }
}

456. 132 模式

给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。

如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。

思路:考虑从末尾开始遍历,最后一个数入栈(单调栈),同时维护max_val作为最大的可以作为2的值。 首先判断当前元素是否可以作为1,如果大于max_val说明可以作为1,满足要求返回true

判断是否可以作为3,与栈顶元素比较,如果大于说明当前元素作为3,栈顶元素可以作为2,弹出栈顶元素并且更新栈顶可以作为最大的值max_val。此过程为建立单调栈过程

最后如果该元素大于max_val说明可能可以作为2的备选,入栈,如果不大于max_val入栈也没啥用,并不能影响结果。

栈中就是个32模式,所以只需要找到一个比2(max_val)小的数。

class Solution {
    public boolean find132pattern(int[] nums) {
        int max_val = Integer.MIN_VALUE;
        int len = nums.length;
        Deque<Integer> dq = new LinkedList<>();
        dq.push(nums[len - 1]);
        for(int i = len - 2; i >= 0; i--) {
            if(nums[i] < max_val) {
                return true;
            }
            while(!dq.isEmpty() && nums[i] > dq.peek()){
                max_val = dq.pop();
            }
            if(nums[i] > max_val) {
                dq.push(nums[i]);
            }
        }
        return false;
    }
}

数学问题

119. 杨辉三角 II

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex **行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

1627433557.png

输入: rowIndex = 3
输出: [1,3,3,1]

思路:硬解

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<List<Integer>> triangle = new ArrayList<>();
        for(int i = 0; i <= rowIndex; i ++) {
            List<Integer> list = new ArrayList<>();
            for(int j = 0; j <= i; j++) {
                if(j == 0 || j == i) {
                    list.add(1);
                } else {
                    list.add(triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j));
                }
            }
            triangle.add(list);
        }
        return triangle.get(rowIndex);
    }
}

注意到第n行仅用到n-1行数据,可以用滚动数组,优化空间复杂度

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<Integer> triangle = new ArrayList<>();
        for(int i = 0; i <= rowIndex; i ++) {
            List<Integer> list = new ArrayList<>();
            for(int j = 0; j <= i; j++) {
                if(j == 0 || j == i) {
                    list.add(1);
                } else {
                    list.add(triangle.get(j - 1) + triangle.get(j));
                }
            }
            triangle = list;
        }
        return triangle;
    }
}

进一步优化,如果里层遍历从右到左,可以只用一个数组

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<Integer> triangle = new ArrayList<>();
        triangle.add(1);
        for(int i = 1; i <= rowIndex; i ++) {
            triangle.add(0);
            for(int j = i; j > 0; j--) {
                triangle.set(j, triangle.get(j - 1) + triangle.get(j));
            }
        }
        return triangle;
    }
}

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

思路:数学/动态规划

class Solution {
    public int numSquares(int n) { 
        int[] dp = new int[n + 1];
        for(int i = 1; i <= n; i++) {
            int min = Integer.MAX_VALUE;
            for(int j = 1; j*j <= i;j++) {
                min = Math.min(dp[i - j * j], min);
            }
            dp[i] = min + 1;
        }
        return dp[n];
    }
}

112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。

叶子节点 是指没有子节点的节点。

思路:递归即可

class Solution {
    public boolean hasPathSum(TreeNode root, int sum) {
        if(root == null) {
            return false;
        }
        if(root.left == null && root.right == null) {
            return sum == root.val;
        }
        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
    } 
}

230. 二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

思路:递归/迭代

    public int kthSmallest(TreeNode root, int k) {
        Deque<TreeNode> dq = new LinkedList<>();
        while(true) {
            while(root != null) {
                dq.push(root);
                root = root.left;
            }
            root = dq.pop();
            if(--k == 0) {
                return root.val;
            }
            root = root.right;
        }
    }
class Solution {
    int count = 0;
    int target;
    int res;
    public void dfs(TreeNode root) {
        if(root == null) {
            return;
        }
        dfs(root.left);
        count++;
        if(count == target){
            res = root.val;
            return; 
        }
        dfs(root.right);
    }
    public int kthSmallest(TreeNode root, int k) {
        target = k;
        dfs(root);
        return res;
    }
}

720. 词典中最长的单词

给出一个字符串数组words组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中字典序最小的单词。

若无答案,则返回空字符串。

思路:前缀树

class Solution {
    public String longestWord(String[] words) {
        Trie trie = new Trie();
        int index = 0;
        for(String word : words) {
            trie.insert(word, ++index);
        }
        trie.words = words;
        return trie.dfs();
    }
}
class Node {
    char c;
    Map<Character, Node> children = new HashMap<>();
    int end;
    public Node(char c) {
        this.c = c;
    }
}
class Trie {
    Node root;
    String[] words;
    public Trie() {
        root = new Node('0');
    }
    public void insert(String word, int index) {
        Node cur = root;
        for(char c : word.toCharArray()) {
            cur.children.putIfAbsent(c, new Node(c));
            cur = cur.children.get(c);
        }
        cur.end = index;
    }
    public String dfs() {
        String ans = "";
        Stack<Node> stack = new Stack();
        stack.push(root);
        while(!stack.isEmpty()) {
            Node node = stack.pop();
            // 该位置有单词或为根节点
            if(node.end > 0 || node == root) {
                // 如果有单词
                if(node != root) {
                    // 对应到该单词
                    String word = words[node.end - 1];
                    // 该单词长度大于最终答案长度 或者 相等且当前word字典序列小 
                    if(word.length() > ans.length() || 
                        word.length() == ans.length() && word.compareTo(ans) < 0) {
                            ans = word;
                        }
                }
                for(Node n : node.children.values()) {
                    stack.push(n);
                }
            }
        }
        return ans;
    }
}

补充:208. 实现 Trie (前缀树)

class Trie {

    private Trie[] children;
    private Boolean isEnd; 

    /** Initialize your data structure here. */
    public Trie() {
       children = new Trie[26];
       isEnd = false;
    }
    
    /** Inserts a word into the trie. */
    public void insert(String word) {
        Trie node = this;
        for(int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            int index = c - 'a';
            if(node.children[index] == null) {
                node.children[index] = new Trie();
            }
            node = node.children[index];     
        }
        node.isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        Trie node = this;
        for(int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            int index = c - 'a';
            if(node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        if(node.isEnd == false) {
            return false;
        }
        return true;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        Trie node = this;
        for(int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            int index = c - 'a';
            if(node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        return true;
    }
}

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

思路:滑动窗口,双指针

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Set<Character> set = new HashSet<>();
        int ans = 0;
        int j = 0;
        for(int i = 0; i < s.length(); i++) {
            while(j < s.length() && !set.contains(s.charAt(j))) {
                set.add(s.charAt(j++));
            }
            if(j - i > ans) {
                ans = j - i;
            }
            set.remove(s.charAt(i));
        }
        return ans;
    }
}

97. 交错字符串

给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。

两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

s = s1 + s2 + ... + sn t = t1 + t2 + ... + tm |n - m| <= 1 交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ... 提示:a + b 意味着字符串 a 和 b 连接。

思路:dp

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int n = s1.length(), m = s2.length(), t = s3.length();
        if (n + m != t) {
            return false;
        }
        boolean[][] f = new boolean[n + 1][m + 1];
        f[0][0] = true;
        for (int i = 0; i <= n; ++i) {
            for (int j = 0; j <= m; ++j) {
                int p = i + j - 1;
                if (i > 0) {
                    f[i][j] = f[i][j] || (f[i - 1][j] && s1.charAt(i - 1) == s3.charAt(p));
                }
                if (j > 0) {
                    f[i][j] = f[i][j] || (f[i][j - 1] && s2.charAt(j - 1) == s3.charAt(p));
                }
            }
        }
        return f[n][m];
    }
}

字符串搜索

28. 实现 strStr()

实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回  -1 。

思路:kmp,前缀和后缀相同位置直接跳着找就行。

class Solution {
    public int strStr(String haystack, String needle) {
        int len1 = haystack.length();
        int len2 = needle.length();
        int[] pi = new int[len2];
        if(len2 == 0) {
            return 0;
        }
        for(int i = 1, j = 0; i < len2; i++) {
            while(j > 0 && needle.charAt(j) != needle.charAt(i)) {
                j = pi[j - 1];
            }
            if(needle.charAt(i) == needle.charAt(j)) {
                j++;
            }
            pi[i] = j;
        }
        for(int i = 0, j = 0; i < len1; i++) {
            while(j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = pi[j - 1];
            }
            if(haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if(j == len2) {
                return i - len2 + 1;
            }
        }
        return -1;
    }
}

题目来源:力扣