数组相关算法
- 1.数组理论
- 2.二分查找
- 704. [二分查找](https://leetcode.cn/problems/binary-search/)
- [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/)
- [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/)
- [69. x 的平方根 ](https://leetcode.cn/problems/sqrtx/)
- [367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/)
- 3.移除元素(双指)
- 4.有序数组的平方(双指)
- 5.长度最小子数组(滑窗)
- 6.螺旋矩阵Ⅱ
- 7.总结
1.数组理论
C++中的二维数组在空间上连续
Java中的二维数组在空间上并非连续
2.二分查找
二分查找结束后,如果没找到目标,left指向的元素比目标值大,right指向的元素比目标值小
704. 二分查找
推荐使用左闭右闭或者左闭右开
边界条件判断(左闭右闭时)
left <= right 还是 left < right
- 当left等于right时,比如
[1]
中,开始时left和right都是0,这时候应该是合法区间
所以应该选用left <= right
代码
public int search(int[] nums, int target) {
int n = nums.length;
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = (left + right) / 2;
if(target == nums[mid]){
return mid;
}else if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
35. 搜索插入位置
边界判断
-
如果要找的值为
11,
最后一步前,left和right都停留在了10上
那么left需要右移一格,所以插入的位置就是left(mid+1)
的位置 -
如果要找的值为
9,
最后一步前,left和right都停留在了10上
那么right需要左移一格,所以插入的位置就是left(mid)
的位置 -
所以只需要返回left的下标就行了
二分查找结束后,如果没找到目标,left指向的元素比目标值大,right指向的元素比目标值小
代码
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n-1;
while(left <= right){
int mid = (left + right) / 2;
if(target == nums[mid]){
return mid;
}else if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
思路
先用二分查找,找到任意一个目标元素的位置,然后向前/向后遍历,找边界
代码
class Solution {
public int[] searchRange(int[] nums, int target) {
int n = nums.length;
int left = 0;
int right = nums.length - 1;
int mid = 0;
while(left <= right){
mid = (left + right) / 2;
if(target == nums[mid]){
break;
}else if(target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
if(left <= right){
//说明在数组中找到了目标
left = mid;
right = mid;
//遍历找左边界
while(left >= 0 && nums[left] == target){
left--;
}
//遍历找右边界
while(right <= n-1 && nums[right] == target){
right++;
}
return new int[]{left + 1,right - 1};
}else{
return new int[]{-1,-1};
}
}
}
69. x 的平方根
思路
二分查找,如果正好找到某个值的平方等于x,那么直接返回
如果没找到的话,left的平方比x大,right的平方比x小,所以直接return right就行
注意这个吊毛long,不加的话int可能溢出
代码
class Solution {
public int mySqrt(int x) {
if(x == 1){
return 1;
}
int left = 1, right = x / 2;
while(left <= right){
int mid = (right + left) / 2;
if(x == (long)mid * mid){
return mid;
}else if(x < (long)mid * mid){
right = mid - 1;
}else{
left = mid + 1;
}
}
return right;
}
}
367. 有效的完全平方数
思路
和69题差不多,还更简单一些
代码
class Solution {
public boolean isPerfectSquare(int num) {
if(num == 1){
return true;
}
int left = 1, right = num / 2;
while(left <= right){
int mid = (left + right) / 2;
if(num == (long)mid * mid){
return true;
}else if(num < (long)mid * mid){
right = mid - 1;
}else{
left = mid + 1;
}
}
return false;
}
}
3.移除元素(双指)
27. 移除元素
暴力
时间复杂度为O(n²)
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
for(int i = 0; i < n ; i++) {
if(nums[i] == val){
for(int j = i; j < n - 1; j++){
nums[j] = nums[j+1];
}
i--;
n--;
}
}
return n;
}
}
双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向新数组中要插入位置的下标
这里的新数组其实还是原来的数组,所以依旧为原地工作
分为两种情况
- 快指针指向元素不是目标元素,将元素移动到新数组,慢指针加一
- 快指针指向元素是目标元素,无需管,直接下一个(因为不用加入到新数组中)
class Solution {
public int removeElement(int[] nums, int val) {
int slowIndex = 0;
for(int fastIndex = 0; fastIndex < nums.length; fastIndex++){
if(nums[fastIndex] != val){
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
26. 删除有序数组中的重复项
双指针法
class Solution {
public int removeDuplicates(int[] nums) {
//慢指针从1开始,因为第一个元素必定不会重复
int slowIndex = 1;
for(int fastIndex = 1; fastIndex < nums.length; fastIndex++){
if(nums[fastIndex] != nums[slowIndex - 1]){
//如果当前元素和新数组当前最后一个元素不相等,说明可以放入
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
283. 移动零
双指针
非0的都移入新数组,然后在后面填充0就OK
class Solution {
public void moveZeroes(int[] nums) {
int slowIndex = 0;
//将非0的元素移入新数组
for(int fastIndex = 0; fastIndex < nums.length; fastIndex++){
if(nums[fastIndex] != 0){
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
//将慢指针后边都变为0
while(slowIndex < nums.length){
nums[slowIndex] = 0;
slowIndex++;
}
}
}
844. 比较含退格的字符串
双指针
找到退格#后慢指针回退一次就行
class Solution {
public boolean backspaceCompare(String s, String t) {
s = getResult(s);
t = getResult(t);
return s.equals(t) ? true : false;
}
public String getResult(String s){
byte[] b = s.getBytes();
int slowIndex = 0;
for(int fastIndex = 0; fastIndex < b.length; fastIndex++){
//如果为#,并且#不为第一个元素
if(b[fastIndex] == '#' && slowIndex != 0){
slowIndex--;
}else{
b[slowIndex] = b[fastIndex];
slowIndex++;
}
}
//String.valueOf会有问题,使用new String把byte数组转String
String res = new String(b);
res = res.substring(0, slowIndex);
return res;
}
}
4.有序数组的平方(双指)
977. 有序数组的平方
思路
- 我们很容易想到这道题的暴力解法就是给数组每个元素平方后再进行排序,时间复杂度为
O(NlogN)
。 - 然而,由于数组是有序的,即使负数平方后可能会成为最大数,但数组最大值都在数组两端,不是最左边就是最右边,因此可以考虑双指针法.
代码
class Solution {
public int[] sortedSquares(int[] nums) {
int[] res = new int[nums.length];
int left = 0;
int right = nums.length - 1;
int index = nums.length - 1;
while(left <= right){
//两头取最大的的放入新数组末尾
if(nums[left] * nums[left] >= nums[right] * nums[right]){
res[index] = nums[left] * nums[left];
left++;
}else{
res[index] = nums[right] * nums[right];
right--;
}
index--;
}
return res;
}
}
5.长度最小子数组(滑窗)
209. 长度最小的子数组
滑动窗口(双指针)
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
需要注意的是,我们循环的变量应该是子序列的右边界,如果循环左边界将和暴力没什么区别
- 右边界向右移动,并不断累加区间的总和sum
- 如果区间总和sum大于目标数,左边界就可以向右移动了
- 当左区间移动到区间总和sum小于目标数时,右区间继续移动
- 以此往复,就能找到最小的区间长度
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int left = 0;
int minLen = Integer.MAX_VALUE;
int sum = 0; //子数组元素总和
//right表示子数组的结束下标
for(int right = 0; right < n; right++){
sum += nums[right];
//如果子数组的总和比目标数大,左指针右移
while(sum >= target){
minLen = Math.min(minLen,right - left + 1);
sum -= nums[left];
left++;
}
}
//如果长度还是最大值,说明没找到
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
904. 水果成篮
滑动窗口
class Solution {
public int totalFruit(int[] fruits) {
int n = fruits.length;
int typeA = fruits[0]; //水果类型A
int typeB = -1; //水果类型B
int typeALen = 1; //A的个数
int typeBLen = 0; //B的个数
//找到类型B
for(int i = 1; i < n; i++){
if(fruits[i] != typeA){
typeB = fruits[i];
break;
}
}
int left = 0;
int maxLen = 1;
for(int right = 1; right < n; right++){
if(fruits[right] == typeA){
//如果当前水果为A类型.A的个数加一
typeALen++;
maxLen = Math.max(maxLen,typeALen + typeBLen);
}else if(fruits[right] == typeB){
//如果当前水果为A类型.B的个数加一
typeBLen++;
maxLen = Math.max(maxLen,typeALen + typeBLen);
}else{
//如果当前水果不为AB,循环直到子序列中只有一种水果
while(typeALen > 0 && typeBLen > 0){
if(fruits[left] == typeA){
typeALen--;
}else{
typeBLen--;
}
left++;
}
//把新型水果加入
if(typeALen == 0){
typeA = fruits[right];
typeALen++;
}else{
typeB = fruits[right];
typeBLen++;
}
}
}
return maxLen;
}
}
使用HashMap+滑窗
class Solution {
public int totalFruit(int[] fruits) {
int n = fruits.length;
Map<Integer, Integer> map = new HashMap<>();
int left = 0, maxLen = 0;
for(int right = 0; right < n; right++){
if(map.containsKey(fruits[right])){
map.put(fruits[right],map.get(fruits[right])+1);
}else{
map.put(fruits[right],1);
}
while(map.size() > 2){
//最左边的水果个数减一
map.put(fruits[left],map.get(fruits[left])-1);
//如果个数为0,删除
if(map.get(fruits[left]) == 0){
map.remove(fruits[left]);
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
6.螺旋矩阵Ⅱ
59. 螺旋矩阵 II
关键在于边界条件的填写,四次的边都得是相同的填写规则,比如左闭右开
class Solution {
public int[][] generateMatrix(int n) {
int[][] arr = new int[n][n];
int number = 1;
//循环的次数
int sumTime = n / 2;
for(int i = 0; i < sumTime; i++){
int partLen = n - i * 2 - 1; //每次遍历的长度(边长-1)
//每次处理左闭右开
for(int j = 0; j < partLen; j++){
arr[i][i + j] = number++;
}
for(int j = 0; j < partLen; j++){
arr[i + j][i + partLen] = number++;
}
for(int j = 0; j < partLen; j++){
arr[i + partLen][i + partLen - j] = number++;
}
for(int j = 0; j < partLen; j++){
arr[i + partLen - j][i] = number++;
}
}
//奇数,处理中间的数
if(n % 2 != 0){
arr[sumTime][sumTime] = number;
}
return arr;
}
}
54. 螺旋矩阵
思路
- 先遍历顶层,然后顶层下标+1
- 遍历右边,右边下标+1
- 遍历下边,下边下标+1
- 遍历左边,左边下标+1
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
List<Integer> res = new ArrayList<>();
// 定义四个边界
int upper = 0, lower = m - 1;
int left = 0, right = n - 1;
// res.size() == m * n 则遍历完整个数组
while(res.size() < m * n){
if(upper <= lower){
// 在顶部从左向右遍历
for(int j = left; j <= right; j++){
res.add(matrix[upper][j]);
}
// 上边界下移
upper++;
}
if(left <= right){
// 在右侧从上向下遍历
for(int i = upper; i <= lower; i++){
res.add(matrix[i][right]);
}
// 右边界左移
right--;
}
if(upper <= lower){
// 在底部从右向左遍历
for(int j = right; j >= left; j--){
res.add(matrix[lower][j]);
}
// 下边界上移
lower--;
}
if(left <= right){
// 在左侧从下向上遍历
for(int i = lower; i >= upper; i--){
res.add(matrix[i][left]);
}
// 左边界右移
left++;
}
}
return res;
}
}
7.总结
二分查找
切记**二分查找结束后,如果没找到目标,left指向的元素比目标值大,right指向的元素比目标值小
**
- 暴力解法时间复杂度:O(n)
- 二分法时间复杂度:O(logn)
移除元素
直接暴力删时间复杂度为O(n^2)
转变思想:利用双指针,把删除目标元素转化为移动其余的元素到新数组
(其实还是原数组,比喻一下)
时间复杂度变O(n)
双指针法(快慢指针法):
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
- 暴力解法时间复杂度:O(n^2)
- 双指针时间复杂度:O(n)
滑动窗口
通过双指针,不断移动头指针和尾指针,调整子序列
- 暴力解法时间复杂度:O(n^2)
- 滑动窗口时间复杂度:O(n)
本文图片大都来自于链接