目录
引言
在整理自己LeetCode刷题的代码时,突然想着不如借此机会,开个博客记录一下LeetCode刷题的思路、代码与心得,聊作复习与分享之用。其中部分题解题思路简单、直接、可笑之处,还请各位多多担待。如果部分简单的思路能给各位提供参考之处,也是不甚荣幸。
本篇博客主要记录当时分析题目时的思路和最后的代码,并不一定是最好的解法。还请大家多多指教。
题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com
LeetCode 1-10题
1. 两数之和
题目描述
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
难度:简单
输入输出示例
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
思路分析
不论是在线OJ还是LeetCode这类的刷题平台,第一题往往都是两数之和、相加这种问题,用于熟悉平台、热身练手。
本题采用嵌套循环的 双指针 方法可以很轻松地解决,而题目给出假设每种输入只会对应一个答案,所以无需考虑多答案情况。
代码
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
int[] result = new int[2];
for (int i = 0; i < len; i++){
for (int j = i + 1; j < len; j++){
if (target == nums[i] + nums[j]){
result[0] = i;
result[1] = j;
}
}
}
return result;
}
2. 两数相加
题目描述
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
难度:中等
输入输出示例
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
思路分析
一道类似于大数相加的问题,本题的难点在于相加的两数有可能非常大,以至于超过整型的表示范围,所以将链表表示的数转换为两个整型相加是不可取的。
对于这类题目,可以一步步实现加法的运算与进位,使用flag表示进位,自后向前进行加法运算(题目所给的链表恰好是反向的链表)。
代码
public class ListNode {// 题目给出的ListNode链表
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int tNum = l1.val + l2.val;
int flag = 0;// 进位标志
if (tNum > 9){
flag = 1;
tNum = tNum % 10;
}else {
flag = 0;
}
ListNode listNode = new ListNode(tNum);// 这里有不妥之处,应该创建空链表然后add比较好
ListNode list = listNode;
while (l1.next != null || l2.next != null || flag == 1){// 考虑到最高位有进位的情况,需要加上flag == 1
int num1=0, num2=0;
if (l1.next != null){
l1 = l1.next;
num1 = l1.val;
}
if (l2.next != null){
l2 = l2.next;
num2 = l2.val;
}
int num = num1 + num2 +flag;
if (num > 9){
flag = 1;// 再次进位
num = num % 10;
}else {
flag = 0;// 进位重置
}
ListNode temp = new ListNode(num);
list.next = temp;
list = list.next;
}
return listNode;
}
3. 无重复字符的最长子串
题目描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
难度:中等
输入输出示例
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
思路分析
题目说明非常简洁,要实现的功能也非常简单,然后我们果断使用 穷举法,代码洋洋洒洒一气呵成,然后…
Time Limit Error
没错,穷举法又一次成为了反面典型。
既然穷举法不行的话,我们换种思路,注意到题目中的 最长子串 ,既然是最长子串,需要输出的只是长度,我们是不是可以用一个 队列 来记录非重复子串,然后用一个max记录最长的长度。
这种方法正是 滑动窗口 方法,使用一个队列作为窗口,进入这个窗口的子串满足题目要求(即无重复字符)时,则继续进入新的字符;当不满足要求(即出现重复字符)时,则移动这个队列,把左边的元素移出,右面的元素加入,类似于整体窗口右移,从而使max能够记录下最长的长度。
代码
public int lengthOfLongestSubstring(String s) {
if (0 == s.length()){// 以前写C的时候留下的习惯,java大可不必
return 0;
}
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int max = 0, left = 0;
for (int i =0; i<s.length(); i++){
if (map.containsKey(s.charAt(i))){// 出现重复字符
left = Math.max(left, map.get(s.charAt(i))+1);
}
map.put(s.charAt(i),i);// 滑动
max = Math.max(max, i - left + 1);
}
return max;
}
4. 寻找两个正序数组的中位数
题目描述
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
难度:困难
输入输出示例
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
思路分析
如果按照顺序刷题的话,这是我们刷题路上的第一道困难题。
我们总结一下题意,长度为m和n的两个正序数组,不会同时为空(意思是可能会存在其中一个为空的情况),找中位数。
如果没有题目对时间复杂度 O(log(m + n))的要求,我们大可直接将两个数组合并排序,然后找出中位数。这种思路虽然不行,但是给了我们一个启发。考虑单个有序数组,当长度为奇数时,中位数正是1/2长度处的元素;当长度为偶数时,中位数是长度1/2处两元素的平均数。
回过头来我们看这个题目,总长度为m + n的两个数组,取k = (m + n)/2,这个问题就可以转换为我们要找两个数组中第k小的元素。我们该如何缩小这个查找的范围呢?我们再设想一种情况。
当两个数组完全一致时,将k这个指标分配到两个数组中去,第k小的数字正是两个数组k/2小的数字。而实际情况下,两个数组很少会出现完全一致的情况,那么我们可以通过比较两个数组k/2处元素的大小,将较小的数组前k/2个元素全部抛弃。通过上面的分析我们明白,两个数组完全一致的情况下,k/2才会相等;换个思路来想,k/2处较小的数组,其前k/2的元素中不可能存在中位数,所以我们可以通过这种思路来不停地缩小范围。
而同理,当我们不断缩小范围后,剩余的两个数组同样满足前面我们所说的k/2规则,只不过此处的k经过舍去元素后,发生了改变。随着范围不断缩小,当k缩小到1时,即需要找到当前数组最小的元素,只需要比较当前两个元素,找出其中的最小者,即可找到中位数。
其中一个特殊情况是,若其中一个数组较小,被完全舍去后,剩余的k便可以直接在另一个数组中,找出第k小的数作为中位数。
分析到这里,我们很明显可以发现递归的方法了。
代码
public int toFind(int[] nums1, int begin1, int end1, int nums2[], int begin2, int end2, int k){
int len1 = end1 - begin1 + 1;
int len2 = end2 - begin2 + 1;
// 一份题解里写道,若len2比len1小就交换,保持len1最小,可以省掉越界时判断是哪个越界
if(len1 > len2){
return toFind(nums2, begin2, end2, nums1, begin1, end1, k);
}
// 越界
if (len1 == 0){
return nums2[begin2 + k - 1];
}
// k == 1意味着找到了,返回被标记的两者中最小的那个
if (k == 1){
return nums1[begin1] < nums2[begin2] ? nums1[begin1] : nums2[begin2];
}
// 标记后移
int tag_1 = begin1 + Math.min(k/2, len1) - 1;
int tag_2 = begin2 + Math.min(k/2, len2) - 1;
if (nums1[tag_1] > nums2[tag_2]){
return toFind(nums1, begin1, end1, nums2, tag_2 +1, end2, k - (tag_2 - begin2 +1));
}else {
return toFind(nums1, tag_1 + 1, end1, nums2, begin2, end2, k - (tag_1 - begin1 +1));
}
}
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 找到第k小的数,依次排除,将奇数个看作偶数个处理
int k_left = (nums1.length + nums2.length + 1)/2;
int k_right = (nums1.length + nums2.length + 2)/2;
return (double)(toFind(nums1, 0,nums1.length - 1, nums2, 0, nums2.length - 1, k_left)
+ toFind(nums1, 0,nums1.length - 1, nums2, 0, nums2.length - 1, k_right))/2;
}
5. 最长回文子串
题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
难度:中等
输入输出示例
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
输入: "cbbd"
输出: "bb"
思路分析
又是一个最大子串的问题,所涉及的是回文串的判断。单独的回文串判断并不难,本体的难点主要体现在 最长 。我们对这个关键字“最长”分析来看,那么最理想的情况下,一个字符串本身应该就是这个最长回文子串;次理想情况下,缺少一个字符后的字符串可以作为最长的回文子串。
分析到这一步我们可以发现,我们既然是在找 最长 的话,完全可以从最理想的情况下开始判断,判断他是否是回文串,然后去次理想情况下依次判断,依次类推,所找到的第一个回文子串一定是最长回文子串。
代码
public Boolean isPalindrome(String str){
// 判断是否是回文子串,这里使用双指针方法判断
for (int i =0; i < str.length()/2; i++){
if (str.charAt(i) != str.charAt(str.length()-1-i)){
return false;
}
}
return true;
}
public String longestPalindrome(String s) {
int flag = 0;
String result = "";
for (int i = s.length(); i >0; i--){
for (int j = 0; j <s.length()- i + 1; j++){
if (isPalindrome(s.substring(j,j+i))){
result = s.substring(j,j+i);
flag = 1;// 跳出最外层循环的标志位,也可以通过符号标记tag标记外层循环然后break tag直接跳出
break;
}
}
if (1 == flag){
break;
}
}
return result;
}
6. Z 字形变换
题目描述
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “LEETCODEISHIRING” 行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“LCIRETOESIIGEDHN”。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
难度:中等
输入输出示例
输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN"
输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"
解释:
L D R
E O E I I
E C I H N
T S G
思路分析
一道找规律的题目,总结公式直接循环判断即可,对于numRows=1的情况直接输出防止TL。
对于每一行输出第几个字符,在纸上画一画很容易就总结出规律,这类题目比动态规划那种题目简单明了多了。
代码
public String convert(String s, int numRows) {
int length = s.length();
String result = "";
if (1 == numRows){
return s;// 一行直接返回
}else {
for (int a = 0; a < numRows; a++) {
// 找到的规律直接套进去
if (0 == a) {
for (int i = 0; i < length; i = i + (numRows - 1) * 2) {
result += s.charAt(i);
}
} else if (numRows - 1 == a) {
for (int i = numRows - 1; i < length; i = i + (numRows - 1) * 2) {
result += s.charAt(i);
}
} else {
for (int i = a, j = i + (numRows - a - 1) * 2; i < length; i = i + (numRows - 1) * 2, j = i + (numRows - a - 1) * 2) {
result += s.charAt(i);
if (j < length) {
result += s.charAt(j);
}
}
}
}
}
return result;
}
7. 整数反转
题目描述
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
难度:简单
输入输出示例
输入: 123
输出: 321
输入: -123
输出: -321
输入: 120
输出: 21
思路分析
最简单的思路是,首先转换为String,然后翻转转换回来。
其中需要考虑两种特殊情况,负数与溢出,使用tag捕获负数情况,try-catch来捕获溢出情况。
代码
public int reverse(int x) {
int result = 0;
char t;
int tag = 0;// 用来标记负数情况
String temp = String.valueOf(x);// 转换为String
if (x < 0){
temp = temp.substring(1,temp.length());// 滤掉负号,防止干扰翻转
tag = 1;// 标记负数
}
char toChange[] = temp.toCharArray();// 转换为字符数组
for (int i = 0; i<toChange.length/2; i++){
// 翻转
t = toChange[i];
toChange[i] = toChange[toChange.length - i - 1];
toChange[toChange.length - i - 1] = t;
}
temp = String.copyValueOf(toChange);
if (1 == tag){
//加回来负号
temp = "-"+temp;
}
try {// try-catch捕获溢出异常
result = Integer.valueOf(temp);
}catch (Exception e){
return 0;// 溢出输出0
}
return result;
}
8. 字符串转换整数 (atoi)
题目描述
请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。接下来的转化规则如下:
- 如果第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字字符组合起来,形成一个有符号整数。
- 假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成一个整数。
- 该字符串在有效的整数部分之后也可能会存在多余的字符,那么这些字符可以被忽略,它们对函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换,即无法进行有效转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0 。
提示:
- 本题中的空白字符只包括空格字符 ’ ’ 。
- 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [ − 2 31 -2^{31} −231, 2 31 − 1 2^{31} − 1 231−1]。如果数值超过这个范围,请返回 INT_MAX ( 2 31 − 1 2^{31} − 1 231−1) 或 INT_MIN ( − 2 31 -2^{31} −231) 。
难度:中等
输入输出示例
输入: "42"
输出: 42
输入: " -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42
输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
因此无法执行有效的转换。
输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。
因此返回 INT_MIN 。
思路分析
题目描述得非常繁杂,提取出的题意为:丢掉开头的空格后,如果首字母是正负号或数字则将完整的提取出来,忽略后面的字符,否则输出0,溢出则输出最大值/最小值。
说到这里,我们的思路就非常清晰了,去段首空格,然后正则匹配即可。
题目的难点在于特殊条件有点多,+、-、空、+-、-+这些特殊情况都要过滤掉,仍然使用try-catch捕获溢出。(这个题的代码写得有点low)
代码
public int myAtoi(String str) {
int result = 0;
str = str.trim();// 去空格
if (str.equals("") || str.equals("-") || str.equals("+")){
// 特殊情况直接返回
return result;
}
if (str.charAt(0) == '+' && str.charAt(1) != '-')
// 正号+数字的情况
{
str = str.substring(1,str.length());
}
// 这里当时写得太low了,可以有更好的处理办法的,而不是if一串条件
if (str.charAt(0) == '0' || str.charAt(0) == '1' || str.charAt(0) == '2' || str.charAt(0) == '3' || str.charAt(0) == '4' || str.charAt(0) == '5' || str.charAt(0) == '6' || str.charAt(0) == '7' || str.charAt(0) == '8' || str.charAt(0) == '9'){
// 正则
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher(str);
m.find();
try {// 捕获溢出
result = Integer.parseInt(m.group());
}catch (Exception e){
result = (int)Math.pow(2,31);
}
}else if (str.charAt(0) == '-' && (str.charAt(1) == '0' || str.charAt(1) == '1' || str.charAt(1) == '2' || str.charAt(1) == '3' || str.charAt(1) == '4' || str.charAt(1) == '5' || str.charAt(1) == '6' || str.charAt(1) == '7' || str.charAt(1) == '8' || str.charAt(1) == '9')){
// 负数情况
str = str.substring(1,str.length());
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher(str);
m.find();
try {
result = Integer.parseInt('-' + m.group());
}catch (Exception e){
result = -(int)Math.pow(2,31)-1;
}
}
return result;
}
9. 回文数
题目描述
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
难度:简单
输入输出示例
输入: 121
输出: true
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
思路分析
判断是否是回文数,可以用 双指针 方法或者 堆栈 方法实现,这里用双指针方法,由两端到中间进行比较。这个没太多好说的。
代码
public boolean isPalindrome(int x) {
String temp = String.valueOf(x);
char[] chr = temp.toCharArray();
for (int i = 0; i < chr.length/2; i++){
char t = chr[i];
chr[i] = chr[chr.length - i - 1];
chr[chr.length - i - 1] = t;
}
String toBack = String.copyValueOf(chr);
if (temp.equals(toBack)){
return true;
}else {
return false;
}
}
10. 正则表达式匹配
题目描述
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
- s 可能为空,且只包含从 a-z 的小写字母。
- p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
难度:困难
输入输出示例
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
思路分析
我认为这道题是前10题中难度最大的一道题,要求实现正则表达式的匹配。规则其实很简单,".“可以匹配任意字符,”*“表示前面字符可以匹配0-n个。而复杂点就在于”.*"这种组合,可以匹配空气,也可以匹配任意个任意数,如何去决断它是这道题的难点。
这种题我们首先去尝试的思路肯定是暴力法了,对于p和s,从末尾到头部进行一一匹配,遇到".“直接匹配当前那个元素,遇到”*“可以从后向前用一个while匹配一或多个字符,然后…成功卡在了倒数第三个测试用例的”.*"上。(其实想想这种题也不可能让你轻松用暴力法直接做出来吧)
所以我们怎么做呢?先分析一下,在匹配的过程中会遇到哪些情况呢?
- 第一种,遇到字符,这种情况两个字符正好匹配即可;
- 第二种,遇到".","."可以与任意字符匹配上,这种和第一种情况其实是一样的;
- 第三种,遇到"*",这种情况则衍生出不同的组合,"*“可以与字符、”."进行搭配,表示出现0次或n次。
那么到了这里,我们可以考虑使用 动态规划 来解决此问题了。用 dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配
- 对于第一种情况,匹配到字符,dp[i][j] = dp[i - 1][j - 1];
- 对于第二种情况,匹配到".",和第一种情况一样,dp[i][j] = dp[i - 1][j - 1];
- 对于第三种情况就更复杂了,我们分两种情况来分析:
(1) 如果p.charAt(j-1) != s.charAt(i),那么意味着p中"*“前的字符是与s中当前字符匹配不上的,那么”*“所代表的只能是前面的字符出现0次;
(2) 而如果p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == ‘.’,意味着p中”*"前的字符是与s中当前字符可以匹配的。
那么根据s中匹配情况的不同,可以有以下三种情况:
- dp[i][j] = dp[i-1][j] (多个字符匹配的情况);
- dp[i][j] = dp[i][j-1] (单个字符匹配的情况);
- dp[i][j] = dp[i][j-2] (没有匹配的情况)。
根据递推式,我们可以开始尝试写代码了。
代码
public boolean isMatch(String s,String p){
if (s == null || p == null) {// 特殊情况判断
return false;
}
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;//dp[i][j] -> s 的前 i 个是否能被 p 的前 j 个匹配
for (int i = 0; i < p.length(); i++) {
if (p.charAt(i) == '*' && dp[0][i - 1]) {
dp[0][i + 1] = true; // 初始化
}
}
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j < p.length(); j++) {
if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)){//任意元素或是元素匹配
dp[i + 1][j + 1] = dp[i][j];
}
if (p.charAt(j) == '*') {
if (p.charAt(j - 1) != s.charAt(i) && p.charAt(j - 1) != '.') {//前一个元素不匹配且不为任意元素
dp[i + 1][j + 1] = dp[i + 1][j - 1];
} else {
dp[i + 1][j + 1] = (dp[i + 1][j] || dp[i][j + 1] || dp[i + 1][j - 1]);
/*
dp[i][j] = dp[i-1][j] // 多个字符匹配的情况
or dp[i][j] = dp[i][j-1] // 单个字符匹配的情况
or dp[i][j] = dp[i][j-2] // 没有匹配的情况
*/
}
}
}
}
return dp[s.length()][p.length()];
}
后记
还没想好写什么…