LeetCode 三天打渔两天晒网 剩下一天睡大觉

学习要注意到细处,不是粗枝大叶的,这样可以逐步学习、摸索,找到客观规律

两数之和

难度 : Easy
题目链接: 两数之和

import java.util.HashMap;

/**
 * @author linliangxuan
 * @date 2021/10/15 11:15
 */
class Solution {
    public static int[] twoSum(int[] nums, int target) {
        // 值为key,下标为value
        HashMap<Integer,Integer> keyNumberIndexValuesCache = new HashMap<>(nums.length); 
        for (int i = 0; i < nums.length; i++) {
            //  将原本为两个目标值切换为一个目标值,只需要每次从 map  中寻找目标值即可
            int num = target - nums[i];
            if (keyNumberIndexValuesCache .containsKey(num)) {
                System.out.println(keyNumberIndexValuesCache );
                return new int[]{keyNumberIndexValuesCache .get(num), i};
            }
            //  每次遍历过的值都存储到 map  中,这样之后就能从 map  中寻找需要的目标值
            keyNumberIndexValuesCache .put(nums[i], i);
        }
        System.out.println(keyNumberIndexValuesCache );
        return null;
    }
    public static void main(String[] args) {
        int[] aa =  new int []{2,7,11,15};
        twoSum(aa,26);
    }
}

整数反转

难度 : Easy
原题连接: 整数反转

哈哈哈。暴力算法

class Solution {
    
    public  int reverse(int x) {

        if (x == 0) {
            return 0;
        } else if (x > 0) {
            String source = String.valueOf(x);
            final char[] chars = source.toCharArray();
            StringBuffer sb = new StringBuffer();
            for (int i = chars.length - 1; i >= 0; i--) {
                sb.append(chars[i]);
            }
            Long longResult = Long.valueOf(sb.toString());
            if (longResult > Integer.MAX_VALUE) {
                return 0;
            } else {
                return longResult.intValue();
            }

        } else {
            String source = String.valueOf(x);
            final char[] chars = source.substring(1).toCharArray();
            StringBuffer nonSymbolSource = new StringBuffer();
            for (int i = chars.length - 1; i >= 0; i--) {
                nonSymbolSource.append(chars[i]);
            }
            Long longResult = Long.valueOf(nonSymbolSource.toString());
            if (longResult > Integer.MAX_VALUE) {
                return 0;
            } else {
                return 0 - longResult.intValue();
            }
        }
    }
}

回文数

难度 : Easy
原题链接:回文数

暴力算法

class Solution {
    public boolean isPalindrome(int x) {
 		    if (x < 0) { //  排除小于 0 的数
            return false;
        }
        String str = String.valueOf(x);
        int n = str.length();
        for (int i = 0; i < n; i++) {
            if (str.charAt(i) != str.charAt(n - 1 - i)) { //  通过字符串前后对应字符比较,对比数字是否相等就行
                return false;
            }
        }
        return true;
    }
}

整数转罗马数字 & 罗马数字转整数

难度 : Easy
原题链接: 整数转罗马数字 & 罗马数字转整数

最长公共前缀

难度: Easy
原题链接:最长公共前缀

二分算法(常碰到)-- 是一种在有序数组中查找某一特定元素的搜索算法

在计算机科学中,二分搜索( binary search ),也称折半搜索( half-interval search )、对数搜索( logarithmicsearch ),是一种在 有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

二分的模型

给定一个递增(递减)数列,检索是否存在等于 m 的元素
给定一个单调函数 f(x) ,检索 f(x) <= m 的最大 x 值
给定一个单调函数 f(x) ,检索 f(x) >= m 的最小 x 值
二分的关键词是, 单调(有序)

leetcode-1011 1011. 在 D 天内送达包裹的能力

斐波那契数列 (常碰到)

什么是斐波那契数列?

斐波那契数列( Fibonacci sequence ),又称黄金分割数列、因数学家列昂纳多 · 斐波那契( Leonardoda Fibonacci )以兔子繁殖为例子而引入,故又称为 “ 兔子数列 ” ,指的是这样一个数列: 1 、 1 、 2 、 3 、 5 、 8 、
13 、 21 、 34 、 …… 在数学上,斐波那契数列以如下被以递推的方法定义: F(1)=1 , F(2)=1, F(n)=F(n-1)+F(n-2) ( n>=3 , n ∈ N* )在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用
​ ---- 以上来自维基百科

问题一:递推方程 f(n) = f(n - 1) + f(n - 2) ,已知 f(1) = 1 , f(2) = 1 ,求第 n 项 ( n <= 30 )

如果递归求这个问题,将会重复计算大量的状态,出现指数级的时间复杂度。当然可以使用递推的方式来解决这个方程。这里来一步一步理解记忆化搜素。

如何理解函数:

一个代码片段,将输入的数据通过计算转成输出的数据。函数同样可以表示成 y=f(x) , x 表示输入数据,也就是状态, y 表示函数的输出结果。所有 x 组成的集合叫状态空间。类比于数学上的函数,函数应当有以下特点:唯一性:同一个状态 x ,函数值是唯一的这就要求我们尽可能将函数设计成跟环境无关的,只跟状态相关。

理解递归

很多人理解递归局限在代码调用代码中,于是很难理解递归什么时候跳出来,变量的值如何变。

举个函数调用,上下文切换的例子:

假设说你要修电脑,先要用螺丝刀把机箱给打开,然后。。。
等等,螺丝刀呢?没有螺丝刀咋办,买呗,于是你就把电脑丢下,跑出去买了一个螺丝刀。
买回来之后,继续把机箱打开,发现内存条坏了,于是你就把电脑丢下,跑出去买了一个内存条(这里有个细节,机箱你是打开的,有两种做法,一种是打开着的机箱丢在地上,等买完内存条再回来继续,一种是把打开着的机箱装回去,等买完内存条再回来重新拆)。

根据这个例子,再想一下什么是递归。递归本身就是个多层函数调用,只不过函数的代码都一样。大家理解递归调用,其实可以理解成,当前函数调用了 其他 的函数。函数调用就是,计算父任务时,需要子任务一,于是代码中把父任务挂起,先执行子任务,等到子任务执行完之后,再把结果返回到父任务这里。

所以不管是递归,还是函数调用,都是有上下文的。每次调用,状态和环境变量都不一样。基于上下文的理解,大家可以尝试一下把前中后序遍历的代码从递归改成,用一个栈模拟递归,可以加深对上下文的理解。
在这里插入图片描述
记忆化搜索
看上图,是第 6 项斐波那契数列的调用图,可以看出,计算第 6 项需要计算第 4 项的结果,计算第 5 项也需要计算第 4项的结果,为了避免重复计算,我们需要将计算第 4 项的结果缓存起来,以便重复使用。

递归过程中将函数的调用缓存起来,叫做记忆化搜索。

记忆化搜索有个条件:需要遵循函数与环境无关的原则,也是说,一个函数参数必须唯一对应一个返回值,否则不能缓存。

int cache[N]; //n 如果有确切的范围,并且 n 比较小时,我们可以开个数组来存储函数值,存取函数值只有 o(1) 的复杂度
// map<int, int> cache; // 如果没有确切的范围,或者 n 比较大时,我们需要利用映射数据结构来存储函数值,比如红黑树、哈希表
memset(cache, -1, sizeof(cache)); //-1 表示 cache 还没计算过
int f(int n) {
	if (n <= 2) {
		return 1;
	}
	if (cache[n] != -1) {
		return cache[n];
	}
	return cache[n] = f(n - 1) + f(n - 2);
}

问题二:递推方程 f(n) = f(n - 1) + f(n - 2) ,已知 f(1) = 1 , f(2) = 1 ,求第 n 项 ( n <= 80 )

解析:
注意数据范围, n 从 30 加到 80 。这个问题跟上一个问题解法一样,只是注意坑点, f(n) 超过 int 的存储范围,因此需要使用 64 位整型, java 为 long ,而 python 本身支持大整数计算,直接忽略这个坑点

问题三:递推方程 f(n) = f(n - 1) + f(n - 2) ,已知 f(1) = 1 , f(2) = 1 ,求第 n 项 ( n <= 10000 ),结果对 10^9 + 7 取模 记忆化搜索

问题四:递推方程 f(n) = f(n - 1) + f(n - 2) ,已知 f(1) = 1 , f(2) = 1 ,求第 n 项 ( n <= 10^18 ),结果对 10^9 + 7 取模
解析:
n 的数据范围超级大,达到 10^18 ,已经无法用记忆化搜索或者递推来做,这就需要另一种做法

专题 :大整数

在编程中,大整数的概念即是整数数据运算超过了 32 、 64 位,计算机里面的简单类型无法满足要求。符合这样条件的整数就被称作为大整数.

一个整数的 10 进制写法,是 n 位 0~9 构成的,我们可以用一个只包含 0-9 的整数数组来表示一个长整数,下面用一个代码例子,来实现一个大整数的类。

删除有序数组中的重复项 从排序链表中移除重复元素

https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/

难度: Easy
原题链接: https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/

解题思路:
解法: 双指针
首先注意数组是有序的,那么重复的元素一定会相邻。
要求删除重复元素,实际上就是将不重复的元素移到数组的左侧。
考虑用 2 个指针,一个在前记作 p,一个在后记作 q,算法流程如下:
1.比较 p 和 q 位置的元素是否相等。
如果相等,q 后移 1 位
如果不相等,将 q 位置的元素复制到 p+1 位置上,p 后移一位,q 后移 1 位
重复上述过程,直到 q 等于数组长度。
返回 p + 1,即为新数组长度。

小结

  1. 解决完一个问题之后,我们还要多问自己几个问题来加强认识。比如我们已经知道,怎么处理从数组中移除重复元素的时候,我们可以问下自己如果允许出现一次重复呢? 再问自己如果换成链表呢?有一个科学实验证明,重复一样的操作不如稍微改变一点的训练。因此我们做完任务后稍微改变一下再做一次,这样记得更牢,理解也更深;
  2. 遇到一个问题的时候,我们要善于将它们归纳关联到已知的知识上。比如问你链表中移除重复元素怎么处理,我们就要往我们已知的领域,即数组移除重复元素上靠;
  3. 善于总结自己的模版,同时牢记自己遇到过的坑。比如对于数组问题,我们需要注意越界问题;对于链表问题,加一个 dummy ,对我们的处理会更加方便

移除元素

https://leetcode-cn.com/problems/remove-element/%25E3%2580%2582/
https://leetcode-cn.com/problems/remove-element/%25E3%2580%2582/

其实在学习算法的过程中,我们总是在一直总结各种模板和小技巧。一旦发现这样便于解题的小技巧,就在自己的小本本上记录下来。以后碰上类似题目的时候,就可以直接用上了。

实现 strStr() 函数难度 : Easy

原题链接: https://leetcode-cn.com/problems/implement-strstr/

看到关于字符串匹配类型的问题,这里我要给著名的 KMP 算法 打 call 。如果不了解 KMP 算法的同学可以先看一下从头到尾彻底理解 KMP ( 2014 年 8 月 22 日版) 这篇文章。

https://blog.csdn.net/v_july_v/article/details/7041827

在计算机科学中, Knuth-Morris-Pratt 字符串查找算法(简称为 KMP 算法)可在一个 主文本字符串 S 内查找一个 词 W 的出现位置。此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪
里开始的发现,从而避免重新检查先前匹配的字符。这个算法是由高德纳和沃恩 · 普拉特在 1974 年构思,同年詹姆斯 ·H· 莫里斯也独立地设计出该算法,最终由三人于 1977 年联合发表。
KMP 的时间复杂度为 O(m+n)​ — 来自维基百科

用一门熟悉的语言去刷题,一定要把比较爽的一些 api 记下来,最好还要知道底层的逻辑。这样可以帮助我们节省时间,对算法复杂度的理解也更加游刃有余;可以像我一样,学习了一个算法就自己写一个小文档,然后记录下来,下次碰到可以用这个算法的时候就直接贴自己的模版,贴模版解题要方便得多。最好再加几个小例子帮助自己快速加深记忆,以后面试的时候捡起来也比较快

LRU Cache 最近最少使用算法 (设计上考量)

难度: 中等
LRU Cache 最近最少使用算法

什么是 LRU Cache

LRU Cache 算法是 Least Recently Used ,也就是最近最少使用算法。

对于一个操作系统来说,我们的缓存是有限的,所以有的时候我们必须要舍弃掉一些 object 来增加当前程序的运行效率。 LRU Cache 算法的概念是:当缓存空间满了的时候,将最近最少使用的数据从缓存空间中删除以增加可用的缓存空间来缓存新的数据。这个算法的核心是一个缓存列表,当我们在缓存中的数据被访问的时候,这个数据就会被提到列表的头部,这样的话列表尾部的数据就是不会经常被访问的数据。当缓存空间不足的时候,就会删除列表尾部的数据来提升程序运行效率。

设计一个 LRU Cache

LRU cache 相当于要维护一个跟时间顺序相关的数据结构,那么能找到最早更新元素的数据结构有 queue 、 heap 和 LinkedList 这几种:

  1. queue :队列是先进先出的数据结构,我们确实知道哪一个元素是先进去的,哪一个元素是后进去的。
  2. heap :我们可以通过维护一个丢入时间 time 来确定哪一个元素是先进去的,哪一个元素是后进去的。
  3. LinkedList :链表有头部和尾部,我们自然而然可以利用头部来始终存放最新的元素(即最近使用的那个元素)

选择哪一种?

我们知道一旦某个中间进去的元素突然被调用后,我们就应该将它的位置更新到头部。频繁更新位置对于 queue 来说时间消耗太多,因此排除;
有时候我们要删除某一个很久不用的元素。如果用 heap 的话我们可能需要遍历所有的元素才能找到 time 为最早的那个节点,除非我们同时维护一个最小堆。但是每次更新元素位置还是需要 O(lgN) 的时间来调整,远不如LinkedList 快;
LinkedList 对于插入删除元素只需要 O(1) 的时间,但是我们还需要能够快速地访问到指定需要被更新的元素。这一点 LinkedList 要用 O(n) 遍历,但是我们可以通过一个字典来对应 key 和 node 的信息,这样就可以用 O(1) 的时间来找到对应元素了;
再进一步,由于要随时删除指定的 node ,我们还需要将该 node 前的 node 与其后方的 node 相连,所以会有一个找指定 node 前面一个 node 的需求。双向链表 Doubly LinkedList 就可以用 O(1) 时间来实现这个需求,所以我们确定使用 双向链表 Doubly LinkedList 来完成一个 LRU Cache 。

确定了要使用的数据结构之后,我们来捋一捋接下来的思路:

1 、 LRU Cache 整体设计:
LRU Cache 里面维护一个 cache 字典对应 key 和 node 的信息。一个 cap 表示最大容量,一个双向链表,其中head.next 是 most recently (最近使用)的 node , tail.prev 是 least recently (最近最少使用) 的 node (即 LRU 容量满了会被删除的那个 node )。

2 、对于 get 方法:
如果 key 在 cache 字典中,说明 node 在链表中
根据 key 从 cache 字典中拿到对应的 node ,从双向链表中删除这个 node ,再向双向链表中重新插入这个node (插入逻辑包含了更新到最新的位置)如果不在直接返回 -1

3 、对于 put 方法:
如果 key 在 cache 字典中,说明 node 在链表中
根据 key 从 cache 字典中拿到对应的 node ,从双向链表中删除这个 node ,再向双向链表中重新插入这个node (插入逻辑包含了更新到最新的位置)

如果 key 不在 cache 字典中,说明是一个新的 node
如果此时容量还没满的话:生成新 node ,插入双向链表中,同时放入 cache 中

如果此时容量满了的话:从双向链表中删除 tail.prev ,即 least recently 的 node
从 cache 中删除这个 node 的信息生成新 node ,插入双向链表中,放入 cache 中

有效的括号
https://leetcode-cn.com/problems/valid-parentheses/%25E3%2580%2582/
思路·1 :要知道在 “ 有效的括号 ” 中,不论是 () 还是 [] 还是 {} ,肯定都是成对出现的,不会有半边括号出现,所以我们只需要用 replace() 方法将字符串中的 () , [] , {} 全部替换成空字符串 ‘’ 即可。如果将全部成对出现的括号全部替换为空字符串的话,字符串的长度应该为 0 。如果不为 0 说明字符串不是一个 “ 有效的括号 ” 字符串。
思路 2 :时间复杂度 : O(N) 空间复杂度 : O(N)
其实关于这道题我们可以用 栈 这个数据结构来做。我们知道栈是后进先出的数据结构,这样的特性方便我们进行左右括号的对比,先来看下大体的思路:

  1. 遍历整个字符串,如果是左括号就入栈;
  2. 如果是右括号则查看当前栈顶元素是否与之相匹配,如果不匹配直接返回 false ;
  3. 遍历完成之后,如果栈内没有元素则说明全部匹配成功,返回 true ;如果栈内还有元素则说明不匹配,返回false ,其实直接 return stack == 0 就行。

合并两个有序链表

合并两个有序链表
思路 1 :时间复杂度 : O(N) 空间复杂度 : O(1)
首先两个链表都是有序的。这里说的有序一般都指的是升序,我们最后也是想返回一个升序的链表。
首先我们要充分利用两个链表 l1, l2 都是有序的这个条件。我们知道,返回结果 res 中的第一个节点的值,一定是从两个输入的链表 l1,l2 当中的某一个的第一个节点中取得。因此我们每次取出比较小的那一个赋值给结果链表中的当前节点 cur ,然后将结果链表当前节点 cur 往后挪动一位,链表中节点值较小的那一个也向后移动,直到某一条输入链表值被取完了,我们就停止这个操作。如果此时还有一条链表没有取完,我们需要将结果链表 res 的当前节点 cur 的 next 直接指向那一个链表就可以了,因为剩下的那个链表当前节点的值肯定比我们结果链表当前节点cur 的值要更大。

总结:相信大家也看到了,这里我们巧妙运用了一个 dummy 节点来做我们的操作,这个方法可以很有效地避免一些链表的边界问题。因此希望大家能够养成一个习惯,只要做到链表类的问题,就可以无脑使用一个 dummy 节点”

两数相加

原题连接:两数相加 https://leetcode-cn.com/problems/add-two-numbers/
思路 1 的话,我们先把相关信息全部存成我们适应处理的结构,然后才去处理,这样的话会浪费一定的空间,我们能做一些优化吗?不存下来可以吗?

思路 2 :时间复杂度 : O(N) 空间复杂度 : O(N)
因为我们一定得遍历完 l1 和 l2 的每一位才能得到最终结果,所以时间复杂度为 O(N) 没得商量。

虽然时间复杂度无法减小,但是我们可以考虑减小我们的空间复杂度啊,刚才我们是将 l1 和 l2 全部转回数字,然后用两个列表将它们的数字形式存了下来,这消耗了 O(N) 的空间。

实际上我们完全可以模拟真正的加法操作,即从个位数开始相加,如果有进位就记录一下,等到十位数相加的时候记得加上那个进位 1 就可以了,这是我们小学就学过的知识。

那么我们就先处理个位数的相加。然后我们发现处理十位数、百位数和后面的位数都和个位数相加的操作是一个样子的,只不过后面计算的结果乘上 10 再加上个位数相加的结果,这才是最终的结果。

于是我们就想到了用递归的方法,即一步一步将大问题转化为更小的问题,直到遇到基础情况(这里指的是个位数相加)返回即可。

class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
            ListNode l3 = null;
            if (l1 == null && l2 == null) {
                // l1  与 l2  都为 null  的情况
                return null;
            } else if (l1 == null && l2 != null) {
                // l1  为 null  但是 l2  不为 null  的情况
                return l2;
            } else if (l1 != null && l2 == null) {
                // l1  不为 null  但是 l2  为 null  的情况
                return l1;
            } else {
                      // l1 l2  都不为 null  的情况
                    if (l1.val + l2.val < 10) {
                        //  不需要进位的情况
                        l3 = new ListNode(l1.val + l2.val);
                        l3.next = addTwoNumbers(l1.next, l2.next);
                    }else {
                        //  需要进位的情况
                        l3 = new ListNode(l1.val + l2.val - 10);
                        l3.next = addTwoNumbers(l1.next, addTwoNumbers(l2.next, new ListNode(1)));
                    }
             }
            return l3;
        }
}

无重复字符的最长子串

难度 : Medium
原题连接: https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

最长回文子串

难度 : Medium
原题链接: 最长回文子串

题目详解:
回文的概念:指的是一个字符串 a 和其逆序 a[::-1] 是相等的;
题目中要求我们输出的是最长的那个回文子串,而不是子序列;
这样的子串可能会有多个,题目说了,任意返回其中一个即可。

思路 1 :时间复杂度 : O(N^2) 空间复杂度 : O(N^2)
dp[i][j] = True 代表 s[i:j+1] 是回文;
dp[i][j] = False 代表 s[i:j+1] 不是回文;
那么自然而然地,如果 s[i-1] == s[j+1] 的时候,如果 dp[i][j] = True 的话, dp[i-1 ][j+1] 也为 True 。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值