leetcode-SecondWeek
本文为记录自己学习算法的过程
[28]实现 strStr()
实现strStr()
函数。
给你两个字符串haystack
和needle
,请你在haystack
字符串中找出needle
字符串出现的第一个位置(下标从0
开始)。如果不存在,则返回-1
。
说明:
当needle
是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当needle
是空字符串时我们应当返回0
。这与 C 语言的strstr()
以及Java
的indexOf()
定义相符。
示例 1:
1 | 输入:haystack = "hello", needle = "ll" |
示例 2:
1 | 输入:haystack = "aaaaa", needle = "bba" |
示例 3:
1 | 输入:haystack = "", needle = "" |
提示:
- $1 <= haystack.length, needle.length <= 10^4$
haystack
和needle
仅由小写英文字符组成
题解:
1 | class Solution { |
思路:
这题主要考察的就是字符串的匹配,其实在数据结构这门课程里面学习过KMP算法
,但是学习不精看到可以想到要使用这个算法,但是不知道怎么写,所以使用的是另一种自己单纯想到的算法,下面就来描述下自己的想法。
首先根据题意判断如果needle
字符串为空串就返回0
。定义一个index
记录匹配haystack
字符串开始的位置,初始值设置成-1
,因为找不到也是返回-1
,并且字符串下标是从0
开始的,所以不会冲突。直接遍历haystack
字符串,找到出现了某个字符与needle
字符串第一个字符匹配的情况,此时就开始同时遍历needle
和haystack
,判断每个字符是否都相等,如果都相等就将index
记录成当前在haystack
字符串匹配的第i
个位置并且跳出循环表示已经找到,否则继续找。
LeetCode题解
方法一:暴力匹配
思路及算法
我们可以让字符串needle
与字符串haystack
的所有长度为m
的子串均匹配一次。
为了减少不必要的匹配,我们每次匹配失败即立刻停止当前子串的匹配,对下一个子串继续匹配。如果当前子串匹配成功,我们返回当前子串的开始位置即可。如果所有子串都匹配失败,则返回−1
。
1 | class Solution { |
方法二:Knuth-Morris-Pratt(KMP) 算法*
思路及算法
KMP算法见我的理解之KMP算法
如何解决本题
记字符串haystack
的长度为n
,字符串needle
的长度为m
。
我们记字符串str=needle+#+haystack
,即将字符串needle
和haystack
进行拼接,并用不存在于两串中的特殊字符#
将两串隔开,然后我们对字符串str
求前缀函数。
因为特殊字符#
的存在,字符串str
中haystack
部分的前缀函数所对应的真前缀必定落在字符串needle
部分,真后缀必定落在字符串haystack
部分。当haystack
部分的前缀函数值为m
时,我们就找到了一次字符串needle
在字符串haystack
中的出现(因为此时真前缀恰为字符串needle
)。
实现时,我们可以进行一定的优化,包括:
我们无需显式地创建字符串
str
。- 为了节约空间,我们只需要顺次遍历字符串
needle
、特殊字符#
和字符串haystack
即可。
- 为了节约空间,我们只需要顺次遍历字符串
也无需显式地保存所有前缀函数的结果,而只需要保存字符串
needle
部分的前缀函数即可。- 特殊字符
#
的前缀函数必定为0
,且易知π
(
i
)≤
m
(真前缀不可能包含特殊字符#
)。 - 这样我们计算
π(i)
时,j=π(π(π(…)−1)−1)
的所有的取值中仅有π(i−1)
的下标可能大于等于m
。我们只需要保存前一个位置的前缀函数,其它的j
的取值将全部为字符串needle
部分的前缀函数。
- 特殊字符
我们也无需特别处理特殊字符
#
,只需要注意处理字符串haystack
的第一个位置对应的前缀函数时,直接设定j
的初值为0
即可。
这样我们可以将代码实现分为两部分:
- 第一部分是求
needle
部分的前缀函数,我们需要保留这部分的前缀函数值。 - 第二部分是求
haystack
部分的前缀函数,我们无需保留这部分的前缀函数值,只需要用一个变量记录上一个位置的前缀函数值即可。当某个位置的前缀函数值等于m
时,说明我们就找到了一次字符串needle
在字符串haystack
中的出现(因为此时真前缀恰为字符串needle
,真后缀为以当前位置为结束位置的字符串haystack
的子串),我们计算出起始位置,将其返回即可。
1 | class Solution { |
[35]搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为O(log n)
的算法。
示例 1:
1 | 输入: nums = [1,3,5,6], target = 5 |
示例 2:
1 | 输入: nums = [1,3,5,6], target = 2 |
示例 3:
1 | 输入: nums = [1,3,5,6], target = 7 |
提示:
- $1 <= nums.length <= 10^4$
- $-10^4= 1 <= nums[i] <= 10^4$
- $nums 为 无重复元素 的 升序 排列数组$
- $10^4 <= target <= 10^4$
题解:
1 | class Solution { |
思路:
这题其实主要考察的就是二分查找,所以拿到手看到查找就想到直接遍历一遍就可以了,但是看到要求是O(log n)
的时间复杂度,就知道了这题目考察的应该就是二分查找了,所以还是比较容易上手的。
二分查找解释:首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
在这题中我们就是完全按照二分查找的思路进行就可以了,找到就直接返回就可以了。不过在找不到的时候,我们需要进行分析下返回的是什么值。在这题中,如果找不到我们需要返回这个元素应该插入的位置。由于遍历的时候low
指针只有大于等于high
指针的时候才进行返回,所以这个元素当找不到的时候,插入的位置就是high
指针指向的后一个位置,所以我们需要返回的是high+1
。
LeetCode题解类似就不做介绍了
[53]最大子数组和
给你一个整数数组nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
示例 1:
1 | 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] |
示例 2:
1 | 输入:nums = [1] |
示例 3:
1 | 输入:nums = [5,4,-1,7,8] |
提示:
$1 <= nums.length <= 10^5$
$-10^4 <= nums[i] <= 10^4$
题解:
1 | class Solution { |
思路:
这题目考察的就是动态规划,不过一般的方法也可以求出来,但是就不是O(n)
的时间复杂度了。所以在这题还是需要使用动态规划。
这题目由于不需要返回最大子数组是说明,所以利用这个特点,思路就是用dp
记录局部最优值,用max
记录全局最优值。每遍历一个新元素时,判断(已遍历的连续子数组的和)加上(当前元素值),与(当前元素值)对比谁更大。
①如果已遍历的连续子数组的和 + 当前元素值 >= 当前元素值说明(已遍历的连续子数组的和)是大于等于0
的,令dp
= 已遍历的连续子数组的和 + 当前元素值。
②如果已遍历的连续子数组的和 + 当前元素值 < 当前元素值说明(已遍历的连续子数组的和)是小于0
的,加上这部分只会拖累当前元素,故应该直接抛弃掉这部分,令dp
= 当前元素值。
③对比dp
和max
,如果dp
更大,则更新到max
中。
这样就可以完成搜寻到最大子数组和了。
LeetCode题解
方法一:动态规划
思路和算法
思路和上述一致
1 | class Solution { |
方法二:分治
思路和算法
这个分治方法类似于「线段树求解最长公共上升子序列问题」的 pushUp 操作。 也许读者还没有接触过线段树,没有关系,方法二的内容假设你没有任何线段树的基础。当然,如果读者有兴趣的话,推荐阅读线段树区间合并法解决多次询问的「区间最长连续上升序列问题」和「区间最大子段和问题」,还是非常有趣的。
1 | class Solution { |
[58]最后一个单词的长度
给你一个字符串s
,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。
单词是指仅由字母组成、不包含任何空格字符的最大子字符串。
示例 1:
1 | 输入:s = "Hello World" |
示例 2:
1 | 输入:s = " fly me to the moon " |
示例 3:
1 | 输入:s = "luffy is still joyboy" |
提示:
- $1 <= s.length <= 10^4$
s
仅有英文字母和空格' '
组成s
中至少存在一个单词
题解:
1 | class Solution { |
思路:
这题目就是单纯的找最后一个单词,所以在思考的时候的想法还是比较简单的,一开始想的是直接正则表达式,但是不知道C++怎么写(Java曾经使用过)。所以就是想到最基本的方法,不过也还比较简单。
主要就是遍历字符串(从后面开始遍历,因为记录最后一个单词的长度),首先定义一个变量start
记录字符串最后一个单词出现的最后一个字母,然后向前走,找到这个单词结束的位置并记录下来退出循环。如果没有找到这个最后一个单词前面的空格
那就说明s
字符串就是一个单词,所以我们这里有个判断end
结指向的位置是否是第一个并且这个位置是字母还是空格,是字母就直接返回start+1
(这里可以自己假设字符串“str ”
和“ str”
),否则返回start-end
;
LeetCode题解类似
[66]加一
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
1 | 输入:digits = [1,2,3] |
示例 2:
1 | 输入:digits = [4,3,2,1] |
示例 3:
1 | 输入:digits = [0] |
提示:
1 <= digits.length <= 100
0 <= digits[i] <= 9
题解:
1 | class Solution { |
思路:
这题就是单纯的考察进位操作,所以还是比较的简单的。
主要的思路就是将最低位数字加1,然后从最低位数字开始遍历,如果需要进位,就让后面的数字进行加1(这里利用的是数组的特点,可以保存int
型数据,大于10
也没有关系),对于最高位数字就需要额外判断。如果最高位需要进位就在新的数组里面先放数字1
再将digits
数组里面的数字放进新的需要返回的数组,否则就直接将digits
数组放进新的数组里面。
LeetCode题解
方法一:找出最长的后缀 99
思路
当我们对数组digits
加一时,我们只需要关注digits
的末尾出现了多少个9
即可。我们可以考虑如下的三种情况:
如果
digits
的末尾没有9
,例如[1,2,3]
,那么我们直接将末尾的数加一,得到[1,2,4]
并返回;如果
digits
的末尾有若干个9
,例如[1,2,3,9,9]
,那么我们只需要找出从末尾开始的第一个不为9
的元素,即3
,将该元素加一,得到[1,2,4,9,9]
。随后将末尾的9
全部置零,得到[1,2,4,0,0]
并返回。如果
digits
的所有元素都是9
,例如[9,9,9,9,9]
,那么答案为[1,0,0,0,0,0]
。我们只需要构造一个长度比digits
多1
的新数组,将首元素置为1
,其余元素置为0
即可。
算法
我们只需要对数组digits
进行一次逆序遍历,找出第一个不为9
的元素,将其加一并将后续所有元素置零即可。如果digits
中所有的元素均为9
,那么对应着「思路」部分的第三种情况,我们需要返回一个新的数组。
代码
1 | class Solution { |
[67]二进制求和
给你两个二进制字符串,返回它们的和(用二进制表示)。
输入为非空字符串且只包含数字1
和0
。
示例 1:
1 | 输入: a = "11", b = "1" |
示例 2:
1 | 输入: a = "1010", b = "1011" |
提示:
- 每个字符串仅由字符
'0'
或'1'
组成。 - $1 <= a.length, b.length <= 10^4$
- 字符串如果不是
"0"
,就都不含前导零。
题解:
1 | class Solution { |
思路:
这题目就是直接进行字符的相加就可以,一开始不过都会想到进行转化成数字,但是想想这样子的操作太麻烦了,所以想到的就是直接进行字符的相加。
思路就是从末尾开始相加,对每个字符进行相加,并且加个carry
进位的char
类型的符号,一开始设定字符为0
表示不用进位,当需要进位的时候就将它设置为1
。由于可能两串字符串的长度可能不同,所以最后还需要进行遍历没有遍历完的字符串,进行最后的操作。在这里需要注意的是为了每个位置对应的相加操作只进行一次,所以我们加了个标志位flag
,防止由于carry
的改变导致再次进入后面的if
判断进行再次的相加操作。
LeetCode题解
方法一:模拟
思路和算法
我们可以借鉴「列竖式」的方法,末尾对齐,逐位相加。在十进制的计算中「逢十进一」,二进制中我们需要「逢二进一」。
具体的,我们可以取n=max{∣a∣,∣b∣}
,循环n
次,从最低位开始遍历。我们使用一个变量carry
表示上一个位置的进位,初始值为0
。记当前位置对其的两个位为 $a_i$ 和 $b_i$,则每一位的答案为 $(carry+a_i +b_i )mod2$,下一位的进位为⌊$\frac{carry+a_i+a_j}{2}$ ⌋。重复上述步骤,直到数字a
和b
的每一位计算完毕。最后如果carry
的最高位不为0
,则将最高位添加到计算结果的末尾。
注意,为了让各个位置对齐,你可以先反转这个代表二进制数字的字符串,然后低下标对应低位,高下标对应高位。当然你也可以直接把a
和b
中短的那一个补0
直到和长的那个一样长,然后从高位向低位遍历,对应位置的答案按照顺序存入答案字符串内,最终将答案串反转。这里的代码给出第一种的实现。
1 | class Solution { |
方法二:位运算
思路和算法
我们可以设计这样的算法来计算:
把
a
和b
转换成整型数字x
和y
,在接下来的过程中,x
保存结果,y
保存进位。当进位不为
0
时计算当前
x
和y
的无进位相加结果:answer = x ^ y
计算当前
x
和y
的进位:carry = (x & y) << 1
完成本次循环,更新
x = answer
,y = carry
返回
x
的二进制形式
为什么这个方法是可行的呢?在第一轮计算中,answer
的最后一位是x
和y
相加之后的结果,carry
的倒数第二位是x
和y
最后一位相加的进位。接着每一轮中,由于carry
是由x
和y
按位与并且左移得到的,那么最后会补零,所以在下面计算的过程中后面的数位不受影响,而每一轮都可以得到一个低 ii 位的答案和它向低i + 1
位的进位,也就模拟了加法的过程。
1 | class Solution: |
[69]x的平方根
给你一个非负整数x
,计算并返回x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如pow(x, 0.5)
或者x ** 0.5
。
示例 1:
1 | 输入:x = 4 |
示例 2:
1 | 输入:x = 8 |
提示:
- $0 <= x <= 2^{31} - 1$
题解:
方法一:投机取巧
1 | class Solution { |
方法二:二分查找
1 | class Solution { |
思路:
这题一开始拿到手还以为就是直接开根号出来进行直接取整就可以,后来看了下评论区发现那样子做其实并不好算是投机取巧了,所以就换了种方法写,也并不难,就相当于是二分法的变形吧。
思路主要就是每次折中进行查找,判断解会出现在哪个区间,就去那个区间进行查找,需要注意的是因为我们是进行向下取整的,所以我们需要在判断的时候判断mid <= x / mid && (mid + 1) > x / (mid + 1)
,因为此时找到的是mid
,不然解就出现在mid+1-high
区间里面。
LeetCode题解
方法一:袖珍计算器算法
袖珍计算器算法」是一种用指数函数 $ \exp $ 和对数函数 $ \ln $ 代替平方根函数的方法。我们通过有限的可以使用的数学函数,得到我们想要计算的结果。
我们将 $\sqrt{x}$ 写成幂的形式 $ x^{1/2} $ ,再使用自然对数 $ e $ 进行换底,即可得到:
这样我们就可以得到 $\sqrt{x}$ 的值了。
注意:由于计算机无法存储浮点数的精确值(浮点数的存储方法可以参考IEEE 754,这里不再赘述),而指数函数和对数函数的参数和返回值均为浮点数,因此运算过程中会存在误差。例如当 $x=2147395600$ 时,$e^{\frac{1}{2} \ln x}$的计算结果与正确值 4634046340 相差 $10^{-11}$,这样在对结果取整数部分时,会得到 $46339$ 这个错误的结果。
因此在得到结果的整数部分 $\textit{ans}$ 后,我们应当找出 $\textit{ans}$ 与 $\textit{ans} + 1$ 中哪一个是真正的答案。
1 | class Solution { |
方法二:牛顿迭代
牛顿迭代法是一种可以用来快速求解函数零点的方法。
为了叙述方便,我们用$C$表示待求出平方根的那个整数。显然,$C$的平方根就是函数
的零点。
牛顿迭代法的本质是借助泰勒级数,从初始值开始快速向零点逼近。我们任取一个 $x0$ 作为初始值,在每一步的迭代中,我们找到函数图像上的点 $(x_i, f(x_i))$,过该点作一条斜率为该点导数$ f’(x_i)$ 的直线,与横轴的交点记为 $x{i+1}$ 。$x_{i+1}$ 相于 $x_i$ 而言距离零点更近。在经过多次迭代后,我们就可以得到一个距离零点非常接近的交点。下图给出了从 $x_0$ 开始迭代两次,得到 $x_1$ 和 $x_2$ 的过程。
算法
我们选择 $x_0 = C$ 作为初始值。在每一步迭代中,我们通过当前的交点 $x_i$,找到函数图像上的点 $(x_i, x_i^2 - C)$,作一条斜率为 $f’(x_i) = 2x_i$ 的直线,直线的方程为:
与横轴的交点为方程 $2xix - (x_i^2 + C) = 0$ 的解,即为新的迭代结果 $x{i+1}$x :
在进行 $k$ 次迭代后,$x_k$ 的值与真实的零点 $\sqrt{C}$ 足够接近,即可作为答案。
细节
为什么选择 $x_0 = C$ 作为初始值?
- 因为 $y = x^2 - C$ 有两个零点 $-\sqrt{C}$ 和 $\sqrt{C}$ 。如果我们取的初始值较小,可能会迭代到 $-\sqrt{C}$ 这个零点,而我们希望找到的是 $\sqrt{C}$ 这个零点。因此选择 $x0 = C$ 作为初始值,每次迭代均有 $x{i+1} < x_i$,零点 $\sqrt{C}$ 在其左侧,所以我们一定会迭代到这个零点。
迭代到何时才算结束?
- 每一次迭代后,我们都会距离零点更进一步,所以当相邻两次迭代得到的交点非常接近时,我们就可以断定,此时的结果已经足够我们得到答案了。一般来说,可以判断相邻两次迭代的结果的差值是否小于一个极小的非负数 $\epsilon$,其中 $\epsilon$ 一般可以取 $10^{-6}$ 或 $10^{-7}$ 。
如何通过迭代得到的近似零点得出最终的答案?
由于 $y = f(x)$ 在$[\sqrt{C}, +\infty]$ 上是凸函数(convex function)且恒大于等于零,那么只要我们选取的初始值 $x_0$ 大于等于 $\sqrt{C}$ ,每次迭代得到的结果 $x_i$ 都会恒大于等于 $\sqrt{C}$ 。因此只要 $\epsilon$ 选择地足够小,最终的结果 $x_k$ 只会稍稍大于真正的零点 $\sqrt{C} $。在题目给出的 32 位整数范围内,不会出现下面的情况:
真正的零点为 $n - 1/2\epsilon$,其中 $n$ 是一个正整数,而我们迭代得到的结果为 $n + 1/2\epsilon$。在对结果保留整数部分后得到 $n$,但正确的结果为 $n - 1$。
1 | class Solution { |
总结:
在这一周主要就还是对字符串进行操作,在这周学到了新的算法,也重新对数学知识有了一定的了解,例如开跟号的题目,一般人都不会想到去列方程来求解一道编程题。在这几次的练习中,主要进行的是字符串的查找和匹配,在我看来,这一周最重要的就是KMP算法了,这其中的知识还是挺难的。
- 感谢你赐予我前进的力量