本文为记录自己学习算法的过程
实现 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 = "" |
提示:
haystack
和 needle
仅由小写英文字符组成1 | class Solution { |
这题主要考察的就是字符串的匹配,其实在数据结构这门课程里面学习过KMP算法
,但是学习不精看到可以想到要使用这个算法,但是不知道怎么写,所以使用的是另一种自己单纯想到的算法,下面就来描述下自己的想法。
首先根据题意判断如果 needle
字符串为空串就返回 0
。定义一个 index
记录匹配 haystack
字符串开始的位置,初始值设置成 -1
,因为找不到也是返回 -1
,并且字符串下标是从 0
开始的,所以不会冲突。直接遍历haystack
字符串,找到出现了某个字符与 needle
字符串第一个字符匹配的情况,此时就开始同时遍历 needle
和 haystack
,判断每个字符是否都相等,如果都相等就将 index
记录成当前在 haystack
字符串匹配的第 i
个位置并且跳出循环表示已经找到,否则继续找。
思路及算法
我们可以让字符串 needle
与字符串 haystack
的所有长度为 m
的子串均匹配一次。
为了减少不必要的匹配,我们每次匹配失败即立刻停止当前子串的匹配,对下一个子串继续匹配。如果当前子串匹配成功,我们返回当前子串的开始位置即可。如果所有子串都匹配失败,则返回 −1
。
1 | class Solution { |
思路及算法
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 { |
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 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 | class Solution { |
这题其实主要考察的就是二分查找,所以拿到手看到查找就想到直接遍历一遍就可以了,但是看到要求是O(log n)
的时间复杂度,就知道了这题目考察的应该就是二分查找了,所以还是比较容易上手的。
二分查找解释:首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
在这题中我们就是完全按照二分查找的思路进行就可以了,找到就直接返回就可以了。不过在找不到的时候,我们需要进行分析下返回的是什么值。在这题中,如果找不到我们需要返回这个元素应该插入的位置。由于遍历的时候 low
指针只有大于等于 high
指针的时候才进行返回,所以这个元素当找不到的时候,插入的位置就是 high
指针指向的后一个位置,所以我们需要返回的是 high+1
。
LeetCode题解类似就不做介绍了
给你一个整数数组 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
中。
这样就可以完成搜寻到最大子数组和了。
思路和上述一致
1 | class Solution { |
这个分治方法类似于「线段树求解最长公共上升子序列问题」的 pushUp 操作。 也许读者还没有接触过线段树,没有关系,方法二的内容假设你没有任何线段树的基础。当然,如果读者有兴趣的话,推荐阅读线段树区间合并法解决多次询问的「区间最长连续上升序列问题」和「区间最大子段和问题」,还是非常有趣的。
1 | class Solution { |
给你一个字符串 s
,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。
单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。
示例 1:
1 | 输入:s = "Hello World" |
示例 2:
1 | 输入:s = " fly me to the moon " |
示例 3:
1 | 输入:s = "luffy is still joyboy" |
提示:
s
仅有英文字母和空格 ' '
组成s
中至少存在一个单词1 | class Solution { |
这题目就是单纯的找最后一个单词,所以在思考的时候的想法还是比较简单的,一开始想的是直接正则表达式,但是不知道C++怎么写(Java曾经使用过)。所以就是想到最基本的方法,不过也还比较简单。
主要就是遍历字符串(从后面开始遍历,因为记录最后一个单词的长度),首先定义一个变量 start
记录字符串最后一个单词出现的最后一个字母,然后向前走,找到这个单词结束的位置并记录下来退出循环。如果没有找到这个最后一个单词前面的 空格
那就说明 s
字符串就是一个单词,所以我们这里有个判断 end
结指向的位置是否是第一个并且这个位置是字母还是空格,是字母就直接返回 start+1
(这里可以自己假设字符串 “str ”
和 “ str”
),否则返回 start-end
;
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 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
数组放进新的数组里面。
当我们对数组 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 { |
给你两个二进制字符串,返回它们的和(用二进制表示)。
输入为 非空 字符串且只包含数字 1
和 0
。
示例 1:
1 | 输入: a = "11", b = "1" |
示例 2:
1 | 输入: a = "1010", b = "1011" |
提示:
'0'
或 '1'
组成。"0"
,就都不含前导零。1 | class Solution { |
这题目就是直接进行字符的相加就可以,一开始不过都会想到进行转化成数字,但是想想这样子的操作太麻烦了,所以想到的就是直接进行字符的相加。
思路就是从末尾开始相加,对每个字符进行相加,并且加个carry
进位的 char
类型的符号,一开始设定字符为 0
表示不用进位,当需要进位的时候就将它设置为 1
。由于可能两串字符串的长度可能不同,所以最后还需要进行遍历没有遍历完的字符串,进行最后的操作。在这里需要注意的是为了每个位置对应的相加操作只进行一次,所以我们加了个标志位 flag
,防止由于 carry
的改变导致再次进入后面的 if
判断进行再次的相加操作。
我们可以借鉴「列竖式」的方法,末尾对齐,逐位相加。在十进制的计算中「逢十进一」,二进制中我们需要「逢二进一」。
具体的,我们可以取 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: |
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
示例 1:
1 | 输入:x = 4 |
示例 2:
1 | 输入:x = 8 |
提示:
1 | class Solution { |
1 | class Solution { |
这题一开始拿到手还以为就是直接开根号出来进行直接取整就可以,后来看了下评论区发现那样子做其实并不好算是投机取巧了,所以就换了种方法写,也并不难,就相当于是二分法的变形吧。
思路主要就是每次折中进行查找,判断解会出现在哪个区间,就去那个区间进行查找,需要注意的是因为我们是进行向下取整的,所以我们需要在判断的时候判断 mid <= x / mid && (mid + 1) > x / (mid + 1)
,因为此时找到的是 mid
,不然解就出现在 mid+1-high
区间里面。
袖珍计算器算法」是一种用指数函数 $ \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 = 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算法了,这其中的知识还是挺难的。