0
点赞
收藏
分享

微信扫一扫

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)


目录

  • ​​第16题. 3Sum Closest​​
  • ​​题目描述(中等难度)​​
  • ​​解法一 暴力解法​​
  • ​​解法二​​
  • ​​总​​
  • ​​第17题. Letter Combinations of a Phone Number​​
  • ​​题目描述(中等难度)​​
  • ​​解法一 定义相乘​​
  • ​​解法二 队列迭代​​
  • ​​解法三 递归​​
  • ​​总​​
  • ​​第18题: 4Sum​​
  • ​​题目描述(中等难度)​​
  • ​​总​​
  • ​​第19题: Remove Nth Node From End of List​​

  • ​​题目描述(中等难度)​​
  • ​​解法一​​
  • ​​解法二 遍历一次链表​​
  • ​​解法三​​
  • ​​总​​
  • ​​第20题 : Valid Parentheses​​
  • ​​题目描述(简单难度)​​
  • ​​总​​
  • ​​喜欢 请点个 + 关注​​

第16题. 3Sum Closest

题目描述(中等难度)

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_结点

和​​上一道题​​很类似,只不过这个是给一个目标值,找三个数,使得他们的和最接近目标值。

解法一 暴力解法

遍历所有的情况,然后求出三个数的和,和目标值进行比较,选取差值最小的即可。本以为时间复杂度太大了,神奇的是,竟然 AC 了。

public int threeSumClosest(int[] nums, int target) {
int sub = Integer.MAX_VALUE; //保存和 target 的差值
int sum = 0; //保存当前最接近 target 的三个数的和
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++)
for (int k = j + 1; k < nums.length; k++) {
if (Math.abs((nums[i] + nums[j] + nums[k] - target)) < sub) {
sum = nums[i] + nums[j] + nums[k];
sub = Math.abs(sum - target);
}
}
}
return sum;
}

时间复杂度:O(n³),三层循环。

空间复杂度:O(1),常数个。

解法二

受到​​上一题​​的启发,没有看的,推荐大家可以看一下。我们完全可以先将数组排序,然后先固定一个数字,然后利用头尾两个指针进行遍历,降低一个 O(n)的时间复杂度。

如果 sum 大于 target 就减小右指针,反之,就增加左指针。

public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int sub=Integer.MAX_VALUE;
int sum=0;
for(int i=0;i<nums.length;i++){
int lo=i+1,hi=nums.length-1;
while(lo<hi){
if(Math.abs((nums[lo]+nums[hi]+nums[i]-target))<sub){
sum=nums[lo]+nums[hi]+nums[i];
sub=Math.abs(sum-target);
}
if(nums[lo]+nums[hi]+nums[i]>target){
hi--;
}else{
lo++;
}
}
}
return sum;
}

时间复杂度:如果是快速排序的 O(logn)O(log_n)O(logn) 再加上 O(n²),所以就是 O(n²)。

空间复杂度:O(1)。

和上一道题非常非常的相似了,先对数组排序,然后利用两头的指针,可以说是十分的优雅了。

第17题. Letter Combinations of a Phone Number

题目描述(中等难度)

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_算法_02

给一串数字,每个数可以代表数字键下的几个字母,返回这些数字下的字母的所有组成可能。

解法一 定义相乘

自己想了用迭代,用递归,都理不清楚,灵机一动,想出了这个算法。

把字符串 “23” 看成 [“a”,“b”,c] * [“d”,“e”,“f”] ,而相乘就用两个 for 循环实现即可,看代码应该就明白了。

public List<String> letterCombinations(String digits) {
List<String> ans = new ArrayList<String>();
for (int i = 0; i < digits.length(); i++) {
ans = mul(ans, getList(digits.charAt(i) - '0'));
}
return ans;

}

public List<String> getList(int digit) {
String digitLetter[] = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
List<String> ans = new ArrayList<String>();
for (int i = 0; i < digitLetter[digit].length(); i++) {
ans.add(digitLetter[digit].charAt(i) + "");
}
return ans;
}
//定义成两个 List 相乘
public List<String> mul(List<String> l1, List<String> l2) {
if (l1.size() != 0 && l2.size() == 0) {
return l1;
}
if (l1.size() == 0 && l2.size() != 0) {
return l2;
}
List<String> ans = new ArrayList<String>();
for (int i = 0; i < l1.size(); i++)
for (int j = 0; j < l2.size(); j++) {
ans.add(l1.get(i) + l2.get(j));
}
return ans;
}

解法二 队列迭代

参考​​这里​​,果然有人用迭代写了出来。主要用到了队列。

public List<String> letterCombinations(String digits) {
LinkedList<String> ans = new LinkedList<String>();
if(digits.isEmpty()) return ans;
String[] mapping = new String[] {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
ans.add("");
for(int i =0; i<digits.length();i++){
int x = Character.getNumericValue(digits.charAt(i));
while(ans.peek().length()==i){ //查看队首元素
String t = ans.remove(); //队首元素出队
for(char s : mapping[x].toCharArray())
ans.add(t+s);
}
}
return ans;
}

假如是 “23” ,那么

第 1 次 for 循环结束后变为 a, b, c;

第 2 次 for 循环的第 1 次 while 循环 a 出队,分别加上 d e f 然后入队,就变成 b c ad ae af

第 2 次 for 循环的第 2 次 while 循环 b 出队,分别加上 d e f 然后入队,就变成 c ad ae af bd be bf

第 2 次 for 循环的第 3 次 while 循环 c 出队,分别加上 d e f 然后入队,就变成 ad ae af bd be bf cd ce cf

这样的话队列的元素长度再也没有等于 1 的了就出了 while 循环。

解法三 递归

参考​​这里​​

private static final String[] KEYS = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };

public List<String> letterCombinations(String digits) {
if(digits.equals("")) {
return new ArrayList<String>();
}
List<String> ret = new LinkedList<String>();
combination("", digits, 0, ret);
return ret;
}

private void combination(String prefix, String digits, int offset, List<String> ret) {
//offset 代表在加哪个数字
if (offset == digits.length()) {
ret.add(prefix);
return;
}
String letters = KEYS[(digits.charAt(offset) - '0')];
for (int i = 0; i < letters.length(); i++) {
combination(prefix + letters.charAt(i), digits, offset + 1, ret);
}
}

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_leetcode_03

从 a 开始 ,然后递归到 d ,然后 g ,就把 adg 加入,然后再加入 adh,再加入 adi … 从左到右,递归到底之后就将其加入。

这种题的时间复杂度和空间复杂度自己理的不太清楚就没有写了。

第18题: 4Sum

题目描述(中等难度)

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_结点_04

和​​3Sum​​类似,只不过是找四个数,使得和为 target,并且不能有重复的序列。

如果之前没有做过​​3Sum​​可以先看看,自己在上边的基础上加了一个循环而已。

public List<List<Integer>> fourSum(int[] num, int target) {
Arrays.sort(num);
List<List<Integer>> res = new LinkedList<>();
//多加了层循环
for (int j = 0; j < num.length - 3; j++) {
//防止重复的
if (j == 0 || (j > 0 && num[j] != num[j - 1]))
for (int i = j + 1; i < num.length - 2; i++) {
//防止重复的,不再是 i == 0 ,因为 i 从 j + 1 开始
if (i == j + 1 || num[i] != num[i - 1]) {
int lo = i + 1, hi = num.length - 1, sum = target - num[j] - num[i];
while (lo < hi) {
if (num[lo] + num[hi] == sum) {
res.add(Arrays.asList(num[j], num[i], num[lo], num[hi]));
while (lo < hi && num[lo] == num[lo + 1])
lo++;
while (lo < hi && num[hi] == num[hi - 1])
hi--;
lo++;
hi--;
} else if (num[lo] + num[hi] < sum)
lo++;
else
hi--;
}
}
}
}
return res;
}

时间复杂度:O(n³)。

空间复杂度:O(N),最坏情况,即 N 是指 n 个元素的排列组合个数,即 N=Cn4N=C^4_nN=Cn4,用来保存结果。

完全是按照 3Sum 的思路写的,比较好理解。


第19题: Remove Nth Node From End of List

题目描述(中等难度)

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_结点_05

给定一个链表,将倒数第 n 个结点删除。

解法一

删除一个结点,无非是遍历链表找到那个结点前边的结点,然后改变下指向就好了。但由于它是链表,它的长度我们并不知道,我们得先遍历一遍得到它的长度,之后用长度减去 n 就是要删除的结点的位置,然后遍历到结点的前一个位置就好了。

public ListNode removeNthFromEnd(ListNode head, int n) {
int len = 0;
ListNode h = head;
while (h != null) {
h = h.next;
len++;
}
//长度等于 1 ,再删除一个结点就为 null 了
if (len == 1) {
return null;
}

int rm_node_index = len - n;

//如果删除的是头结点
if (rm_node_index == 0) {
return head.next;
}

//找到被删除结点的前一个结点
h = head;
for (int i = 0; i < rm_node_index - 1; i++) {
h = h.next;
}

//改变指向
h.next = h.next.next;
return head;
}

时间复杂度:假设链表长度是 L ,那么就第一个循环是 L 次,第二个循环是 L - n 次,总共 2L - n 次,所以时间复杂度就是 O(L)。

空间复杂度:O(1)。

我们看到如果长度等于 1 和删除头结点的时候需要单独判断,其实我们只需要在 head 前边加一个空节点,就可以避免单独判断。

public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
int length = 0;
ListNode first = head;
while (first != null) {
length++;
first = first.next;
}
length -= n;
first = dummy;
while (length > 0) {
length--;
first = first.next;
}
first.next = first.next.next;
return dummy.next;
}

解法二 遍历一次链表

上边我们遍历链表进行了两次,我们如何只遍历一次呢。

看了 ​​leetcode​​ 的讲解。

想象一下,两个人进行 100m 赛跑,假设他们的速度相同。开始的时候,第一个人就在第二个人前边 10m ,这样当第一个人跑到终点的时候,第二个人相距第一个人依旧是 10m ,也就是离终点 10m。

对比于链表,我们设定两个指针,先让第一个指针遍历 n 步,然后再让它俩同时开始遍历,这样的话,当第一个指针到头的时候,第二个指针就离第一个指针有 n 的距离,所以第二个指针的位置就刚好是倒数第 n 个结点。

public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
ListNode second = dummy;
//第一个指针先移动 n 步
for (int i = 1; i <= n + 1; i++) {
first = first.next;
}
//第一个指针到达终点停止遍历
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummy.next;
}

时间复杂度:

第一个指针从 0 到 n ,然后「第一个指针再从 n 到结束」和「第二个指针从 0 到倒数第 n 个结点的位置」同时进行。

而解法一无非是先从 0 到 结束,然后从 0 到倒数第 n 个结点的位置。

所以其实它们语句执行的次数其实是一样的,从 0 到倒数第 n 个结点的位置都被遍历了 2 次,所以总共也是 2L - n 次。只不过这个解法把解法一的两次循环合并了一下,使得第二个指针看起来是顺便遍历,想法很 nice。

所以本质上,它们其实是一样的,时间复杂度依旧是 O(n)。

空间复杂度:O(1)。

解法三

没看讲解前,和室友讨论下,如何只遍历一次链表。室友给出了一个我竟然无法反驳的观点,哈哈哈哈。

第一次遍历链表确定长度的时候,顺便把每个结点存到数组里,这样找结点的时候就不需要再遍历一次了,空间换时间???哈哈哈哈哈哈哈哈哈。

public ListNode removeNthFromEnd(ListNode head, int n) {
List<ListNode> l = new ArrayList<ListNode>();
ListNode h = head;
int len = 0;
while (h != null) {
l.add(h);
h = h.next;
len++;
}
if (len == 1) {
return null;
}
int remove = len - n;
if (remove == 0) {
return head.next;
}
//直接得到,不需要再遍历了
ListNode r = l.get(remove - 1);
r.next = r.next.next;
return head;
}

时间复杂度:O(L)。

空间复杂度:O(L)。

利用两个指针先固定间隔,然后同时遍历,真的是很妙!另外室友的想法也很棒,哈哈哈哈哈。

第20题 : Valid Parentheses

题目描述(简单难度)

LeetCode 16-20 题 详解 Java版 ( 万字 图文详解 LeetCode 算法题16-20 =====>>> <建议收藏>)_结点_06

括号匹配问题。

如果只有一种括号,我们完全可以用一个计数器 count ,遍历整个字符串,遇到左括号加 1 ,遇到右括号减 1,遍历结束后,如果 count 等于 0 ,则表示全部匹配。但如果有多种括号,像 ( [ ) ] 这种情况它依旧会得到 0,所以我们需要用其他的方法。

栈!

遍历整个字符串,遇到左括号就入栈,然后遇到和栈顶对应的右括号就出栈,遍历结束后,如果栈为空,就表示全部匹配。

public boolean isValid(String s) {
Stack<Character> brackets = new Stack<Character>();
for(int i = 0;i < s.length();i++){
char c = s.charAt(i);
switch(c){
case '(':
case '[':
case '{':
brackets.push(c);
break;
case ')':
if(!brackets.empty()){
if(brackets.peek()== '('){
brackets.pop();
}else{
return false;
}
}else{
return false;
}
break;
case ']':
if(!brackets.empty()){
if(brackets.peek()=='['){
brackets.pop();
}else{
return false;
}
}else{
return false;
}
break;
case '}':
if(!brackets.empty()){
if(brackets.peek()=='{'){
brackets.pop();
}else{
return false;
}
}else{
return false;
}

}
}

return brackets.empty();
}

时间复杂度:O(n)。

空间复杂度:O(n)。

如果学过数据结构,一定写过计算器,括号匹配问题一定遇到过的。

今天我们一起学习了LeetCode 题的算法分析,感谢大家阅读,觉得不错记得收藏哦!

喜欢 请点个 + 关注


举报

相关推荐

0 条评论