Algorithm——Array
数组
1.数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合
数组可以通过下标索引的方式获取下标下对应的数据
注意
数组下标都是从0开始的
数组内存空间的地址是连续的
数组的元素是不能删的,只能覆盖
2.!二分查找
704.给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
1 | 输入: nums = [-1,0,3,5,9,12], target = 9 |
1 | 输入: nums = [-1,0,3,5,9,12], target = 2 |
提示:
可以假设nums中的所有元素是不重复的
n将在[1,10000]之间
nums的每个元素都将在[-9999,9999]之间
思路
数组为有序数组
数组中无重复元素
- 存在重复元素,使用二分查找法返回的元素下标可能不是唯一的
满足上述两点可考虑二分法
二分法的区间定义就是不变量
- 在二分查找过程中,保持不变量,即在while寻找中每一次边界处理都要坚持根据区间的定义来操作
二分法区间定义
左闭右闭[left,right]
while(left<=right)要使用<=,因为left == right是有意义的
if [ num[middle] > target ) right要赋值为 middle - 1 ;因为当前nums[middle]一定不是target,那么下面查找的左区间结束下标位置就是middle - 1
时间复杂度:O(log n)
空间复杂度:O(1)
左闭右开[left,right)
while(left < right),因为left == right在区间中是没有意义的
if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
代码
左闭右闭区间
1 | /** |
左闭右开区间
1 | /** |
相关题目
-35.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
1 | 输入: nums = [1,3,5,6], target = 5 |
1 | 输入: nums = [1,3,5,6], target = 2 |
1 | 输入: nums = [1,3,5,6], target = 7 |
提示
1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums 为 无重复元素 的 升序 排列数组
-10^4 <= target <= 10^4
思路
存在以下四种情况
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中的位置
- 目标值在数组所有元素之后
代码
1 | /** |
-34.在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
1 | 输入:nums = [5,7,7,8,8,10], target = 8 |
1 | 输入:nums = [5,7,7,8,8,10], target = 6 |
1 | 输入:nums = [], target = 0 |
提示
0 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9
思路
寻找target在数组里的左右边界,有如下三种情况:
- 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
- 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
- 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
代码
1 | var searchRange = function(nums, target) { |
1 | /** |
-69.x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意: 不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
1 | 输入:x = 4 |
1 | 输入:x = 8 |
提示:
0 <= x <= 2^31 - 1
思路
整数x的平方根一定小于或等于x
除0之外的所有整数的平方根都大于或等于1
整数x的平方根一定是在1到x的范围内,取这个范围内的中间数字mid,并判断mid的平方是否小于或等于x
如果mid的平方小于x,那么接着判断(mid+1)的平方是否大于x,如果(mid+1)的平方大于x,那么mid就是x的平方根
如果mid的平方小于x并且(mid+1)的平方小于x,那么x的平方根比mid大,接下来搜索从mid+1到x的范围
如果mid的平方大于x,则x的平方根小于mid,接下来搜索1到mid-1的范围
然后在相应的范围内重复这个过程,总是取出位于范围中间的mid
代码
1 | /** |
-367.有效的完全平方数
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。
1 | 输入:num = 16 |
1 | 输入:num = 14 |
思路
num 是正整数,若正整数 x 满足
x*x = num
,则x一定满足1<= x <= num
;1 和 num 作为二分查找搜索区间的初始边界。
时间复杂度:O(log n),n为正整数num的最大值
空间复杂度:O(1)
代码
1 | /** |
3.移除元素
27.给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
1 | // nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 |
1 | 输入:nums = [3,2,2,3], val = 3 |
1 | 输入:nums = [0,1,2,2,3,0,4,2], val = 2 |
思路
要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖
!双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置
时间复杂度:O(n)
空间复杂度:O(1)
代码
1 | /** |
相关题目
-26.删除排序数组中的重复项
给你一个 升序排列 的数组 nums
,请你 原地 删除重复出现的元素,使每个元素 **只出现一次 **,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
更改数组
nums
,使nums
的前k
个元素包含唯一元素,并按照它们最初在nums
中出现的顺序排列。nums
的其余元素与nums
的大小不重要。返回
k
。
判题标准:
系统会用下面的代码来测试你的题解:
1 | int[] nums = [...]; // 输入数组 |
1 | 输入:nums = [1,1,2] |
1 | 输入:nums = [0,0,1,1,1,2,2,3,3,4] |
思路
数组
nums
有序的,只能在原地修改nums
数组,不能创建新的数组空间来存储删除重复出现的元素后的结果。需要一边遍历数组查找相同元素,一边在对比发现不同元素时修改数组元素,
考虑双指针法的快慢指针
定义
slow
和fast
作为指针初始化时指针
slow
指向数组的起始位置(nums[0]
)随着指针
fast
不断向后移动,将指针fast
指向的元素与指针slow
指向的元素进行比较如果
nums[fast] ≠ nums[slow]
,那么nums[slow + 1] = nums[fast]
如果
nums[fast] = nums[slow]
,那么指针fast
继续向后查找
时间复杂度:
O(n)
代码
1 | /** |
-283.移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
1 | 输入: nums = [0,1,0,3,12] |
1 | 输入: nums = [0] |
思路
相当于对整个数组移除元素0,然后slowIndex之后都是移除元素0的冗余元素,把这些元素都赋值为0就可以了。
代码
1 | /** |
-844.比较含退格的字符串
给定 s
和 t
两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true
。# 代表退格字符。
注意 :如果对空文本输入退格字符,文本继续为空。
1 | 输入:s = "ab#c", t = "ad#c" |
1 | 输入:s = "ab##", t = "c#d#" |
1 | 输入:s = "a#c", t = "b" |
思路
准备指针
i``j
分别指向S``T
的末位字符,再准备变量skipS
,skipT
来存放S,T字符串中的#数量从后向前遍历S,遇到以下情况
若当前字符为#,则
skipS
自增1若当前字符不是#,且
skipS
不为0,则skipS
自减1若当前字符不是#,且
skipS
为0,则代表当前字符不会被消除,我们可以用来和 T 中的当前字符作比较。
若对比过程中
S
,T
当前字符不匹配,则遍历结束,返回false
若遍历结束,且一一匹配,则返回
true
代码
1 | /** |
-977.有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序
思路
数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置
如果
A[i] * A[i] < A[j] * A[j]
那么result[k--] = A[j] * A[j]
如果
A[i] * A[i] >= A[j] * A[j]
那么result[k--] = A[i] * A[i]
时间复杂度为
O(n)
1 | 输入:nums = [-4,-1,0,3,10] |
1 | 输入:nums = [-7,-3,2,3,11] |
代码
1 | /** |
4.长度最小的子数组
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其和 ≥ target
的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
1 | 输入:target = 7, nums = [2,3,1,2,4,3] |
1 | 输入:target = 4, nums = [1,4,4] |
1 | 输入:target = 11, nums = [1,1,1,1,1,1,1,1] |
思路
窗口是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
!滑动窗口
就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置
确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
代码
1 | /** |
相关题目
-904.水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits
表示,其中 fruits[i]
是第 i
棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits
,返回你可以收集的水果的 最大 数目。
1 | 输入:fruits = [1,2,1] |
1 | 输入:fruits = [0,1,2,2] |
1 | 输入:fruits = [1,2,3,2,2] |
1 | 输入:fruits = [3,3,3,1,2,1,1,2,3,3,4] |
思路
用滑动窗口遍历fruits,当有新种类的水果进入窗口时
如果窗口中只有一种水果,将这种水果加入arr数组
如果有两种水果,更新窗口的左边界,更新arr中水果的种类
如果进来了一种新的类型的水果 更新前一种水果的位置
更新滑动窗口的最大值
时间复杂度O(n)
空间复杂度O(1)。
代码
1 | /** |
-76.最小覆盖字串
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
1 | 输入:s = "ADOBECODEBANC", t = "ABC" |
1 | 输入:s = "a", t = "a" |
1 | 输入: s = "a", t = "aa" |
思路
使用两个指针一个left,一个right,分别表示窗口的左边界和右边界。
当窗口内的所有字符不能覆盖t的时候,要扩大窗口,也就是right往右移。
当窗口内的所有字符可以覆盖t的时候,记录窗口的起始位置以及窗口的长度,然后缩小窗口(因为这里求的是能覆盖的最小子串),left往右移。如果缩小的窗口还能覆盖t,保存长度最小的窗口即可。
重复上面的操作,直到窗口的右边不能再移动为止。
代码
1 | /** |
5. 螺旋矩阵II
给你一个正整数 n
,生成一个包含 1
到 n^2
所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
1 | 输入:n = 3 |
1 | 输入:n = 1 |
思路
坚持循环不变量原则。
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
代码
1 | /** |
1 | /** |
相关题目
-54.螺旋矩阵
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100
1 | 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] |
1 | 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] |
思路
如果一条边从头遍历到底,则下一条边遍历的起点随之变化
选择不遍历到底,可以减小横向、竖向遍历之间的影响
一轮迭代结束时,4条边的两端同时收窄 1
一轮迭代所做的事情变得很清晰:遍历一个“圈”,遍历的范围收缩为内圈
一层层向里处理,按顺时针依次遍历:上、右、下、左。
不再形成“环”了,就会剩下:一行或一列,然后单独判断
四个边界
上边界 top : 0
下边界 bottom : matrix.length - 1
左边界 left : 0
右边界 right : matrix[0].length - 1
矩阵不一定是方阵
top < bottom && left < right 是循环的条件
无法构成“环”了,就退出循环,退出时可能是这 3 种情况之一:
top == bottom && left < right —— 剩一行
top < bottom && left == right —— 剩一列
top == bottom && left == right —— 剩一项(也算 一行/列)
- 处理剩下的单行或单列
因为是按顺时针推入结果数组的,所以
剩下的一行,从左至右 依次推入结果数组
剩下的一列,从上至下 依次推入结果数
- 处理剩下的单行或单列
代码
1 | /** |
内容来源链接:力扣