返回顶部

【算法题】LeetCode刷题(四)

数据结构和算法是编程路上永远无法避开的两个核心知识点,本系列【算法题】旨在记录刷题过程中的一些心得体会,将会挑出LeetCode等最具代表性的题目进行解析,题解基本都来自于LeetCode官网(https://leetcode-cn.com/),本文是第四篇。

1.组合总和(原第39题)

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

示例:

输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

(1)知识点

回溯法

(2)解题方法

方法转自:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/

方法:回溯+剪枝

回溯法本质就是一棵树,通过列举所有可能的情况,然后排出不符合要求的情况,见下图:

这张图展示了回溯法的解题过程,其中,因为不能出现重复数组,所以所有分支比上一个分支小的分支都得剪掉(这样就达到了去重的目的)。剩下的事情就是遍历这棵树了(深度优先)

(3)伪代码

函数头:List<List> combinationSum(int[] candidates, int target)

方法:回溯+剪枝

  • 对candidates进行排序
  • 调用dfs

定义dfs(int[] candidates, int len, int target, int begin, Deque path, List<List> res)

  • 如果target=0,将path(必须new一个)添加到res里面
  • 否则,执行下列循环(i:begin->len-1)
    • 如果target-candidates[i]<0,直接跳出循环
    • 否则将candidates[i]加到path后面
    • 继续调用自己将更新后的变量传入dfs
    • 递归完后需要将path最后一位去掉(回溯的本质)

(4)代码示例

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    Arrays.sort(candidates);
    List<List<Integer>> res = new ArrayList<>();
    backtrack(candidates, candidates.length, target, 0, new ArrayList(), res);
    return res;
}

private void backtrack(int[] candidates, int len, int target, int begin, List path, List<List<Integer>> res){
    if(target == 0) {
        res.add(new ArrayList(path));
    }

    int tmp = 0;
    for(int i = begin; i < len; ++i){
        tmp = candidates[i];
        if(target - tmp < 0) break;
        path.add(tmp);
        backtrack(candidates, len, target - tmp, i, path, res);
        path.remove(path.size() - 1);
    }
}


2.旋转图像(原第48题)

给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例:

给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]

(1)知识点

矩阵变换

(2)解题方法

方法转自:https://leetcode-cn.com/problems/rotate-image/solution/xuan-zhuan-tu-xiang-by-leetcode/

方法:转置+翻转

最直接的想法是先转置矩阵,然后翻转每一行。这个简单的方法已经能达到最优的时间复杂度O(N^2)。原解答还有另外两种旋转的方法,虽然理解不难,但是容易写错,浪费时间,所以还是这个最厉害了。

  • 时间复杂度:O(N^2)。
  • 空间复杂度:O(1)O(1) 由于旋转操作是 就地 完成的。

(3)伪代码

函数头:void rotate(int[][] matrix)

方法:转置+翻转

  • 第一次第一重循环:转置矩阵(i:0->n-1)
    • 第一次第二重循环:(j->n-1)
      • 交换第i行j列和j行i列的值
  • 第一次第一重循环:(横向)翻转矩阵(i:0->n-1)
    • 第一次第二重循环:(j->n-1)
      • 交换j列n-j-1列的值

(4)代码示例

public void rotate(int[][] matrix) {
    int len = matrix.length;
    //转置
    for(int i = 0; i < len; ++i){
        for(int j = i + 1; j < len; ++j){
            int temp = matrix[i][j];
            matrix[i][j] = matrix[j][i];
            matrix[j][i] = temp;
        }
    }
    //翻转    
    for(int i = 0; i < len / 2; ++i){
        for(int j = 0; j < len; ++j){
            int temp = matrix[j][i];
            matrix[j][i] = matrix[j][len-1-i];
            matrix[j][len-1-i] = temp;
        }
    }
}


3.字母异位词分组(原第49题)

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

示例:

输入: ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]

(1)知识点

哈希表

(2)解题方法

方法转自:https://leetcode-cn.com/problems/group-anagrams/solution/zi-mu-yi-wei-ci-fen-zu-by-leetcode/

方法一:按排序数组分类

当且仅当它们的排序字符串相等时,两个字符串是字母异位词。

  • 时间复杂度:O(NKlogK),其中 N 是 strs 的长度,而 K 是 strs 中字符串的最大长度。当我们遍历每个字符串时,外部循环具有的复杂度为 O(N)。然后,我们在 O(KlogK) 的时间内对每个字符串排序。
  • 空间复杂度:O(NK),排序存储在 ans 中的全部信息内容。

方法二:按照字母计数分类(非常精妙)

当且仅当它们的字符计数(每个字符的出现次数)相同时,两个字符串是字母异位词。

  • 时间复杂度:O(NK),其中 N 是 strs 的长度,而 K 是 strs 中字符串的最大长度。计算每个字符串的字符串大小是线性的,我们统计每个字符串。
  • 空间复杂度:O(NK),排序存储在 ans 中的全部信息内容。

(3)伪代码

函数头:List<List> groupAnagrams(String[] strs)

方法一:按排序数组分类

  • 定义一个HashMap<String, List> res,用于存储每个排好序的字符串的所有源字符串
  • 第一重循环:i:0->strs.len
    • 将strs[i].toArray(),然后sort,然后组合成串s
    • map添加这个s(key)和strs[i](value)(如果不存在这个key需要新增一个key和value(ArrayList)
  • 注意最后返回的方法:return new ArrayList(res.values())

方法二:按照字母计数分类

  • 定义一个HashMap<String, List>res目的和上个方法相同
  • 定义一个数组int[] arr = new int[26],用于存放每个字母出现的频率
  • 第一重循环:i:0->strs.len
    • 数组归零:Arrays.fill(arr, 0)
    • 重点步骤:遍历strs[i]的所有字符(char c : strs[i].toCharArray()),arr[c-'a']++,这一步骤就是计算当前的单词每一个字母的数量,堪称神迹
    • 将这个strs[i]存到map里面,这个地方需要将arr转化成字符串,这里采用一个方法,由于arr里面存储方式是这样的"abbcc"->[1,2,2,0,...,0],我们将它变成这样->"#1#2#2#0...#0",这样能保证键的唯一性
    • 将上一步生成的字符串作为键,同样看它是否存在,接下来的步骤和前一个方法相同

(4)代码示例

//排序数组
public List<List<String>> groupAnagrams(String[] strs) {
    HashMap<String, List> map = new HashMap<>();
    char [] ch;
    String key;
    for(String str : strs){
        ch = str.toCharArray();
        Arrays.sort(ch);
        key = String.valueOf(ch);
        if(!map.containsKey(key)) map.put(key, new ArrayList());
        map.get(key).add(str);
    }
    return new ArrayList(map.values());
}
//字符计数
public List<List<String>> groupAnagrams(String[] strs) {
    HashMap<String, List> map = new HashMap<>();
    StringBuilder key = new StringBuilder();
    String key1;
    for(String str : strs){
        int [] arr = new int[26];
        key.delete(0, key.length());
        for(int i = 0; i < str.length(); ++i){
            ++arr[str.charAt(i) - 'a'];
        }
        for(int i = 0; i < 26; ++i){
            key.append(arr[i]);
            key.append("#");
        }
        key1 = key.toString();
        if(!map.containsKey(key1)) map.put(key1, new ArrayList());
        map.get(key1).add(str);
    }
    return new ArrayList(map.values());
}


4.Pow(x, n)(原第50题)

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

示例:

输入: 2.00000, 10
输出: 1024.00000

说明:

-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1] 。

(1)知识点

快速幂

(2)解题方法

方法转自:https://leetcode-cn.com/problems/powx-n/solution/powx-n-by-leetcode-solution/

方法一:快速幂 + 递归

快速幂听起来复杂,其实很常见,在计算x^64时,如果不做任何处理,肯定就是循环将x乘64次,那么快速幂就会这样操作:
x->x2->x4->x8->x16->x32->x64
将每一项平方得到后一项,这样做6次就能得到结果,再来看x^53
x->x3->x6->x13->x26->x^53
这也能快速计算出结果,无非就是平方后再乘个x,或者不乘,那么问题就在于什么是乘呢?其实倒过来看就行了:
53变26只需要除以二再取整就可以,那么以此类推就能最终推到1那个地方,所以这种逆序的计算可以用递归来实现。

  • 时间复杂度:O(log⁡n),即为递归的层数。
  • 空间复杂度:O(log⁡n),即为递归的层数。这是由于递归的函数调用会使用栈空间。

方法二:快速幂+迭代

有递归的地方一般就可以有迭代,迭代使用循环代替了递归的过程,在本题中,快速幂的理念不变,只是操作的方法变化了一些,原答案讲解的很详细,但是我觉得要写出代码来不需要想那么多

  • 时间复杂度:O(log⁡n),即为对 n 进行二进制拆分的时间复杂度。
  • 空间复杂度:O(1)。

(3)伪代码

函数头:double myPow(double x, int n)

方法一:快速幂 + 递归

  • 如果n>=0,调用quickMul(x, n),否则调用1/quickMul(x,-n)

quickMul(double x, long n):

  • 如果n=0,返回1.0——递归终止条件
  • 递归y=quickMul(x, n / 2)
  • 如果n%2=0,说明不需要额外乘以x,直接返回yy,否则返回yy*x

方法二:快速幂+迭代

  • 如果n>=0,调用quickMul(x, n),否则调用1/quickMul(x,-n)

quickMul(double x, long n):

  • 定义结果res=1.0
  • 定义初始贡献值y=x
  • 第一重循环(n>0):
    • 如果n%2!=0,res*y
    • y*=y
    • n/=2

(4)代码示例

public double myPow(double x, int n) {
    long ln = n;
    if(n < 0){
        return 1 / pow(x, -ln);
    }
    return pow(x, ln);
}

private double pow(double x, long n){
    if(n == 0) return 1;            
    double y = pow(x, n / 2);
    return (n % 2 == 0) ? y * y: y * y * x;
}


5.最大子序和(原第53题)

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

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

(1)知识点

动态规划

(2)解题方法

方法转自:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/

方法一:动态规划

动态规划在于找出状态转移方程,我们假设f(i)是以i结尾的数组的最大自序和,那么结果显然就是maxf(i),f(n)肯定是有f(n-1)得到的,那么很简单,f(n)=max(f(n-1)+a[n],a[n])

  • 时间复杂度:O(n),其中 n 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
  • 空间复杂度:O(1)。我们只需要常数空间存放若干变量。

方法二:分治法(暂不考虑,比动态规划复杂)

(3)伪代码

函数头:int maxSubArray(vector& nums)

方法一:动态规划

  • 定义记录前一个状态的结果pre和最大值maxRes
  • 第一重循环:(i:0->len-1)
    • pre=max(pre+x,x)
    • maxRes=max(maxAns,pre)

方法二:分治法(暂不考虑,比动态规划复杂)

(4)代码示例

public int maxSubArray(int[] nums) {
    int len = nums.length;
    if(len == 0) return 0;
    int tmp = nums[0];
    int max = tmp;
    for(int i = 1; i < len; ++i){
        tmp = Math.max(nums[i], tmp+ nums[i]);
        max = Math.max(tmp, max);
    }
    return max;
}


posted @ 2020-08-05 23:09  藤原豆腐店の張さん  阅读(281)  评论(0编辑  收藏  举报