返回顶部

【算法题】LeetCode刷题(二)

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

1.盛水最多的容器(原第11题)

给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。

示例:

输入:[1,8,6,2,5,4,8,3,7]
输出:49

(1)知识点

双指针法

(2)解题方法

方法转自:https://leetcode-cn.com/problems/container-with-most-water/solution/sheng-zui-duo-shui-de-rong-qi-by-leetcode-solution/

方法:双指针

本题是一道经典的面试题,最优的做法是使用「双指针」。如果读者第一次看到这题,不一定能想出双指针的做法。
双指针法就是将两个指针(x和y)一头一尾放置,每次计算面积A=min(x,y)t,其中t是x和y之间的距离,每次将较小的那个指针往中间移动,继续计算A,得出最大值。用反证法很容易证明为什么要移动最小的那个而不是最大的,这很简单,假设x<y,那么A0=min(x,y)t=xt,移动y后A1=min(x,y')(t-1),如果y'>x,那么A1=x(t-1)<A0,如果y'<=x,那么A1=y'(t-1)<x*(t-1)<A0。

  • 时间复杂度:O(N),双指针总计最多遍历整个数组一次。
  • 空间复杂度:O(1),只需要额外的常数级别的空间。

(3)伪代码

函数头:int maxArea(int[] height)

方法:双指针

  • 定义max作为返回值
  • 定义i=0,j=len-1
  • 第一重循环(当i<j)
    • 更新max值
    • 比较i和j位置的高度大小,将高度小的坐标移动(++i或--j)

(4)代码示例

public int maxArea(int[] height) {
    int max = 0;
    int first = 0;
    int second = height.length - 1;
    while(first < second){
        max = Math.max(max, (second - first) * Math.min(height[first], height[second]));
        if(height[first] < height[second]) {
            first++;
        } else {
            second--;
        }
    }
    return max;
}


2.三数之和(原第15题)

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]

(1)知识点

排序+双指针:这里的排序虽然不是考察算法的重点,但是是为后续查找做铺垫的。

(2)解题方法

方法转自:https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/

方法:排序+双指针

由于题目的要求是不能出现重复的结果,那么如果当待查找的数据中包含很多重复数据的时候,就必须对最后的结果进行去重操作了,解决这个问题的最好的办法——排序,排好序的数组遍历的时候,遇到重复的跳过,那么就能保证不输出重复的结果了。
另外,其实这个题目的思路很简单,固定一个动另外两个,时间复杂度是O(N^3),常规方法是无法避免的,排序也只能避免重复数据。那么要想降低复杂度,就需要针对这个题想一个策略。
试想一个这样的排列:[-2,-1,0,1,2,3,4],第一步肯定是固定a=-2,b=-1,那么第三重循环的c呢,如果c从0开始,那么复杂度O(N^3)没得跑,那么我们可以让c从4开始往左移动,这就是我们超好用的双指针法。好处在哪呢,我们继续看,c=4不符合要求,左移然后c=3,诶,符合了,这样就没必要继续走了,输出[-2,-1,3]即可,而且当b=0时,由于记录了上一次循环的位置,c直接从3开始,就省了跑c=4这个无用的计算了。(为什么b=0,c直接从3开始?你想,b是越来越大的,c=3刚好满足前一个b的需求,那c=4必不可能满足需求了。)
所以,这里优化后的时间复杂度就变成了O(N2),可能看代码还会以为是O(N3),但实际上b和c是一起走的,他们两个加起来走的长度只有一次循环的长度,当b的位置=c的位置的时候这重循环就结束了,b根本不需要跑到底。
划重点:当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O(N^2) 减少至 O(N)。

  • 时间复杂度:O(N^2),其中 N 是数组 nums 的长度。
  • 空间复杂度:O(log⁡N)。我们忽略存储答案的空间,额外的排序的空间复杂度为 O(log⁡N)。然而我们修改了输入的数组 nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了 nums 的副本并进行排序,空间复杂度为 O(N)。

(3)伪代码

函数头:List<List> threeSum(int[] nums)

方法:排序+双指针

  • 将数组进行排序(Array.sort(nums))
  • 第一重循环:a=0->len-1
    • 判断如果a不为0且nums[a] == nums[a-1],则跳过下一重循环
    • 声明c=len-1用于当做双指针的右侧指针
    • 第二重循环:b=a+1->len-1
      • 判断如果b不为a+1且nums[a] == nums[a-1],则跳过后面步骤
      • 第三重循环(当c>b且三个位置数相加大于0)
        • c--
      • 如果c==b,跳出第二重循环
      • 判断三者和是否等于0

(4)代码示例

public List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> lists = new ArrayList<>();
    List<Integer> list;
    int len = nums.length;
    for(int i = 0; i < len; ++i){
        if(i != 0 && nums[i] == nums[i-1]) continue;
        int j = i + 1;
        int k = len - 1;
        for(; j < len; ++j){
            if(j != i + 1 && nums[j] == nums[j-1]) continue;
            while(k > j && nums[i] + nums[j] + nums[k] > 0){
                k--;
            }
            if(j == k) break;
            if( nums[i] + nums[j] + nums[k] == 0){
                list = new ArrayList<>();
                list.add(nums[i]);
                list.add(nums[j]);
                list.add(nums[k]);
                lists.add(list);
            }
        }
    }
    return lists;
}


3.电话号码的字母组合(原第17题)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

(1)知识点

回溯法:回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。

(2)解题方法

方法转自:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/solution/dian-hua-hao-ma-de-zi-mu-zu-he-by-leetcode/

方法:回溯法

这个题首先他就是一个排列组合的问题,而且你会发现这是没法用几重循环做的,因为每多按一次按键就会多一重循环,所以只能用递归的算法来解决这个问题。
如下图所示,所有的情况都可以用这个树状图表示,那么通过对树的先序遍历的理解,这个题也只需要按照这个思路进行求解即可。

  • 时间复杂度: O(3^N * 4^M),其中 N 是输入数字中对应 3 个字母的数目(比方说 2,3,4,5,6,8), M 是输入数字中对应 4 个字母的数目(比方说 7,9),N+M 是输入数字的总数。
  • 空间复杂度:O(3^N * 4^M),这是因为需要保存 3^N * 4^M个结果。

(3)伪代码

函数头:List letterCombinations(String digits)

方法:回溯法

声明一个全局的List list用于返回最后的结果(这是因为要用到递归,不用全局变量就太难受了)
声明一个全局的HashMap<String, String> map用于存储所有可能的数字对应的字符串,如"2"对应着"abc","3"对应着"def",以此类推
定义一个递归函数(backtrack),参数为已经组合的字符串(combStr)和剩余的数字串(restDigits):

  • 如果剩余的数字串长度为0,则map添加这个combStr,否则执行下列步骤:
  • 取得待求映射的数字字符(digit)
  • 将映射的结果得出:letters=map.get(digit)
  • 第一重循环:(从letters的第一个字母到最后一个字母(letter))
    • 组合combStr和letter
    • 递归:将组合后的字符串和restDigits.subString(1)传入backtrack函数(subString(1)表示从第二个字符到最后的字符的子串)
      定义包裹函数(就是最后返回的那个函数):
  • 将""和digits传入backtrack

(4)代码示例

List<String> res = new ArrayList<>();

Map<String, String> map = new HashMap<String, String>(){{
    put("2", "abc");
    put("3", "def");
    put("4", "ghi");
    put("5", "jkl");
    put("6", "mno");
    put("7", "pqrs");
    put("8", "tuv");
    put("9", "wxyz");
}};

public List<String> letterCombinations(String digits) {
    if(digits.length() != 0){
        backtrack("", digits);
    }
    return res;
}

private void backtrack(String combStr, String restDigits){
    int len = restDigits.length();
    if(len == 0) {
        res.add(combStr);
        return;
    }
    String digit = restDigits.substring(0, 1);
    String letters = map.get(digit);
    for(int i = 0; i < letters.length(); ++i){
        backtrack(combStr + letters.substring(i, i + 1), restDigits.substring(1));
    }
}


4.合并两个有序链表(原第21题)

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

(1)知识点

链表

(2)解题方法

方法转自:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/

方法一:递归

方法其实很简答,递归必然是理解起来最简单的方法,具体操作为:
取两个头结点进行比较(a,b)如果a是较小的那个,将a.next和b继续递归,将返回的节点作为a的next节点。

  • 时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。
  • 空间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。

方法二:迭代

迭代法的原理和递归是一样的,但是实现形式不同,没有用递归函数,而是采用循环的方式,不断的检测两个链表的头结点,从而构建出新的链表。

  • 时间复杂度:O(n+m) ,其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。
  • 空间复杂度:O(1) 。我们只需要常数的空间存放若干变量。

(3)伪代码

函数头:ListNode mergeTwoLists(ListNode l1, ListNode l2)

方法一:递归

  • 如果l1为空,返回l2
  • 如果l2为空,返回l1
  • 如果l1.val > l2.val,l2.next=mergeTwoLists(l1, l2.next),返回l2
  • 否则,l1.next=mergeTwoLists(l1.next, l2),返回l1

方法二:迭代

  • 声明一个preHead(哨兵节点),用于指向最后要返回的节点
  • 声明一个prev=preHead,这个节点始终指向我们判断大小后的小节点前一个节点
  • 第一重循环(当l1且l2不为空)
    • 如果l1.val > l2.val,prev.next = l2,l2 = l2.next
    • 否则prev.next = l1,l1 = l1.next
    • prev = prev.next
  • 上面循环做完后,可能较长的那个链表还没跑完,同样的让prev.next指向那个没跑完的节点就可以了。
  • 最后返回preHead.next

(4)代码示例

//递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if(l1 == null) return l2;
    if(l2 == null) return l1;
    if(l1.val <= l2.val){
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    }else{
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}

//迭代
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if(l1 == null) return l2;
    if(l2 == null) return l1;
    ListNode dummy = new ListNode(-1);
    ListNode prev = dummy;
    while(l1 != null && l2 != null){
        if(l1.val <= l2.val){
            prev.next = l1;
            l1 = l1.next;
        }else{
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next;
    }
    if(l1 != null) prev.next = l1;
    if(l2 != null) prev.next = l2;
    return dummy.next;
}


5.括号生成(原第22题)

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

(1)知识点

递归:本题因为涉及到n的量级是指数级的,所以采用递归更为直观

(2)解题方法

方法转自:https://leetcode-cn.com/problems/generate-parentheses/solution/gua-hao-sheng-cheng-by-leetcode-solution/

方法一:暴力法

暴力法思路很简单,我就是生成2^2n个字符串,然后一个一个判断是否满足要求,但是注意,这两步并不是分开的,也就是说不需要递归两次,只需要递归到全部括号都用完了之后进行判断就可以了,而判断的方法也很简单,要想括号合理,只需要从左开始数,右括号永远不多于左括号就行。

方法二:回溯法

众所周知,回溯法精髓也在于递归,本质是一棵二叉树,每次遇到分支后执行递归后都需要把之前的操作抹除,这样就能进入到另一个路口了。而回溯法在解这个题的时候,相比暴力法的好处就是,我永远不生成不符合要求的字符串,就是边做边检查,保证自己的右括号始终不多于左括号。

方法三:按括号序列的长度递归(看起来有点复杂,复杂度和回溯法也差不多,这里就不展开了)

(3)伪代码

函数头:List generateParenthesis(int n)

方法一:暴力法

  • 声明一个字符串列表,用于返回Listres
  • 调用generate(new char[2 * n], 0, res)

定义一个生成括号数组的函数:generate(char[] curr, int pos, List res)

  • 如果pos == curr.length,判断是否符合要求(调用judge(char[] ch),如果满足,res.add(curr)——递归终止条件
  • 否则执行下列操作:
  • 添加一个左括号即curr[pos] = '('
  • 调用自己递归generate(curr, pos+1, res)
  • 添加一个右括号即curr[pos]=')'
  • 调用自己递归generate(curr, pos+1, res)

定义一个判断字符数组是否符合要求的函数judge(char[] ch)

  • 声明int banlance = 0
  • 第一层循环:c=0->ch.length - 1
    • 如果c=='(',balance++,否则balance--
    • 如果循环过程中banlance < 0,直接返回false
  • 返回balance == 0

方法二:回溯法

  • 声明一个字符串列表,用于返回Listres
  • 调用backtrack(res, new StringBuilder(), 0, 0, n)

定义一个回溯函数backtrack(List res, StringBuilder str, int left, int right, int max)

  • 如果str.length() == 2*max,res.add(str.toString())——递归终止条件
  • 否则执行下列操作:
  • 如果left < max,添加一个左括号,str.append("("),调用自己backtrack(res, str, left+1, right, max),删掉最后一个字符(回溯)str.deleteCharAt(str.length() - 1)
  • 如果right < left,添加一个右括号,str.append(")"),调用自己backtrack(res, str, left, right+1, max),删掉最后一个字符(回溯)str.deleteCharAt(str.length() - 1)

(4)代码示例

private List<String> list = new ArrayList<>();
public List<String> generateParenthesis(int n) {
    backtrack(new StringBuilder(), 0, 0, n);
    return list;
}

private void backtrack(StringBuilder combStr, int left, int right, int n){
    if(left == n && right == n){
        list.add(combStr.toString());
    }
    if(left < n){
        //加个左括号
        combStr.append("(");
        backtrack(combStr, left+1, right, n);
        combStr.deleteCharAt(combStr.length() - 1);
    }
    if(right < left){
        //加个右括号
        combStr.append(")");
        backtrack(combStr, left, right+1, n);
        combStr.deleteCharAt(combStr.length() - 1);
    }
}


posted @ 2020-08-02 18:46  藤原豆腐店の張さん  阅读(318)  评论(0编辑  收藏  举报