leetcode算法学习-数组

141 阅读4分钟

1.定义

顺序存储的顺序表。数组都属于物理结构。

1. 读取元素
直接下标访问,如arr[0]

2. 更新元素
直接下标更新,如arr[0] = 4

3. 插入元素

  1. 尾部插入:在数组尾部空闲的位置插入 。
  2. 中间插入:先插入元素,再把插入位置+后面的元素全部后移。
  3. 超范围插入:先创建新的数组,然后把老的全部插入,再加上新的元素。

4. 删除元素
判断是否越界,没有则把删除的位置元素删除,并且把后面所有元素往前挪动。

2.刷题

26. 删除有序数组中的重复项

给你一个有序数组 nums ,请你 [原地] 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入: nums = [1,1,2]
输出: 2, nums = [1,2]
解释: 函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入: nums = [0,0,1,1,1,2,2,3,3,4]
输出: 5, nums = [0,1,2,3,4]
解释: 函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

答题

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if(nums.length === 0) {
        return 0
    }
    let fast = 1 //由于都是从1开始才能与前一格开始比较,所以从1开始循环
    let slow = 1
    while(fast < nums.length ) {
        if(nums[fast] !== nums[fast -1]) { 
            nums[slow] = nums[fast] //先赋值 
            slow++ //slow指针再移动索引
        }
        fast++  //每次都移动一格
    }
    // nums.length = slow  这里应该把nums.length 改为 slow的长度,不如实际原来的尾部数据还是会被保留
    return slow
};

80. 删除有序数组中的重复项 II

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 最多出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

输入: nums = [1,1,1,2,2,3]
输出: 5, nums = [1,1,2,2,3]
解释: 函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 不需要考虑数组中超出新长度后面的元素。

示例 2:

输入: nums = [0,0,1,1,1,1,2,3,3]
输出: 7, nums = [0,0,1,1,2,3,3]
解释: 函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 不需要考虑数组中超出新长度后面的元素。

答题

/**
 * @param {number[]} nums
 * @return {number}
 */
// 输入:nums = [1,1,1,2,2,3]
// 输出:5, nums = [1,1,2,2,3]
//快慢指针
var removeDuplicates = function(nums) {
    let len = nums.length
    if (len <= 2) {//只有两个数字的话,一样不一样都满足,直接返回
        return 2
    }
    let slow = 2
    let fast = 2
    while(fast < len) { //
        if(nums[slow - 2] != nums[fast]) {//从第0个和第2个开始比较,满足则把第2给第1个,然后 slow往前移动,fast是否满足都正常移动一位。
        //不管第0个和第1个是否一样,都是按隔两个数字一直判断下去。
       //     console.log(`当前[${slow - 2}]:${nums[slow - 2]} 与 [${fast}]:${nums[fast]} 不一样,覆盖`)
            nums[slow] = nums[fast] // 把最新的不一样值赋值给 当前的slow索引下标值
            slow++ //慢指针往前移动一位
        }else { 
       //     console.log(`当前[${slow - 2}]:${nums[slow - 2]} 与 [${fast}]:${nums[fast]} 一样,不覆盖`)
        } 
      //  console.log("结果nums",nums)
        fast++//快指针正常移动
    }
    return slow 
};
//测试代码
removeDuplicates( [1,1,1,2,2,3])
当前[0]:1 与 [2]:1 一样,不覆盖
结果nums [ 1, 1, 1, 2, 2, 3 ]
当前[0]:1 与 [3]:2 不一样,覆盖
结果nums [ 1, 1, 2, 2, 2, 3 ]
当前[1]:1 与 [4]:2 不一样,覆盖
结果nums [ 1, 1, 2, 2, 2, 3 ]
当前[2]:2 与 [5]:3 不一样,覆盖
结果nums [ 1, 1, 2, 2, 3, 3 ]

最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入: nums = [1]
输出: 1

示例 3:

输入: nums = [5,4,-1,7,8]
输出: 23

答题

//这里特点是每次判断上一个(pre+当前值) 与 当前值 中的 取最大值,
//可以过滤掉有负数的转折情况,因为正数是前面所有的合计,当遇到有负数大于前面正数合值,则从新去下一个整数位起点
//pre已经是处理过最大值, 
// maxAns, pre 取最大值,保留在全局,一直记录最大值
var maxSubArray = (nums) => {
    if(!nums) {
        return 0
    }
    let pre = 0
    let maxVal = nums[0] 
    for (let i = 0; i < nums.length; i++) {
        const num = nums[i]; 
        pre = Math.max(num + pre,num)
        maxVal = Math.max(maxVal,pre)
    }
    return maxVal
}

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c , 使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

示例 1:

输入: nums = [-1,0,1,2,-1,-4]
输出: [[-1,-1,2],[-1,0,1]]

示例 2:

输入: nums = []
输出: []

示例 3:

输入: nums = [0]
输出: []

关键逻辑

  • 先将数组进行排序
  • 从左侧开始,选定一个值为 定值 ,右侧进行求解,获取与其相加为 00 的两个值
  • 类似于快排,定义首和尾
  • 首尾与 定值 相加
  • 等于 00,记录这三个值
  • 小于 00,首部右移
  • 大于 00,尾部左移
  • 定值右移,重复该步骤

image.png

答题

 /**
 * @param {number[]} nums
 * @return {number[][]}
 */
 // 方案1 暴力循环3次 
 //测试用例一般通不过,时间复杂度太高
 var threeSum = function(nums) {
    let ret = []
    nums.sort()
    for (let i = 0; i < nums.length; i++) { 
         for (let j = i+1; j < nums.length; j++) {
            nums[j];
            for (let k = j+1; k < nums.length; k++) {
                nums[k];
               if(i!== j && i !== k) {
                  if(nums[i] + nums[j] + nums[k] == 0) {
                    let newSub = []
                    newSub.push(nums[i])
                    newSub.push(nums[j])
                    newSub.push(nums[k])
                    ret.push(newSub)
                  }
               }
           }
       }
    }
    let set = new Set(ret.map( (obj)=> {
        return obj.join(',') 
    }))
    let last = Array.from(set).map( (o) => o.split(',').map(o=> parseInt(o))  )
    console.log(last)
    return last
};
 
 
// 方案2 排序+ 3个游标
var threeSum = function(nums) {
    let ans = [];
    const len = nums.length;
    if(nums == null || len < 3) return ans;//数组的长度大于3
    nums.sort((a, b) => a - b); // 排序,
    //这里需要指定排序方法,默认不传会按字符编码进行排序 如 [10,101,19,1001] 排序完为 [10,1001,101,19]
    for (let i = 0; i < len ; i++) {
        if(nums[i] > 0) break; // 由于数据已经是排好顺序的,所以当前与后的数都是大于0 ,直接不处理。
        if(i > 0 && nums[i] == nums[i-1]) continue; // 去重,减少多余处理
        let L = i+1;
        let R = len-1;
        while(L < R){// L 和 R 一直往中间移动,直到相遇
            const sum = nums[i] + nums[L] + nums[R];
            if(sum == 0){
                ans.push([nums[i],nums[L],nums[R]]);
                while (L<R && nums[L] == nums[L+1]) L++; // 去重,减少多余处理
                while (L<R && nums[R] == nums[R-1]) R--; // 去重,减少多余处理
                L++;
                
                R--;
            }
            else if (sum < 0) L++;//当值小于0 ,证明负数太大,要L 右移动,减少负数值
            else if (sum > 0) R--;//当值大于0 ,证明正数太大,要R 左移动,减少正数值
        }
    }        
    return ans;
}; 

11. 盛最多水的容器

给你 n 个非负整数 a1,a2,...,a``n,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明: 你不能倾斜容器。

示例 1:

输入: [1,8,6,2,5,4,8,3,7]
输出: 49 
解释: 图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

示例 2:

输入: height = [1,1]
输出: 1

示例 3:

输入: height = [4,3,2,1,4]
输出: 16

思路

  • 通过公式 min(leftHeigt,rightHeight) * x之间的距离 求出最大面积
  • 遍历方式:通过左右双指针的方式,移动条件是那一边较小就优先移动
  • 每次移动前都记录下来最大面积
  • 遍历结束时最大值得出来 image.png

答题

//最终转化的效果是 y坐标:[1,8,6,2,5,4,8,3,7],与x坐标 动态长度 之间的关系互相相乘 ,最终结果面积取最大值
// 核心公式 min(leftHeigt,rightHeight) * 距离
// 如:min(1,7)∗8=8  这里的*8 是对应x坐标的每一个格 *8
//    min(8,7)∗7=49
//    min(8,3)∗6=18

//方案1  暴力双循环 
var maxArea = function(height) {
    let max = 0
    for (let i = 0; i < height.length; i++) {
        for (let j = i+1; j < height.length; j++) {
            let aera = Math.min(height[i],height[j]) * (j-i) 
            max = Math.max(max,aera)   
        }
    }
    return max
}; 

//方案 while+双指针
var maxArea = function(height) { 
   let left = 0 //左边的游标
   let right = height.length - 1 //右边的游标
   let max = 0 //全局最大值
   while (left < right) { //两个游标相碰就结束
       let minHeight = Math.min(height[left],height[right]) //取最小高度
       let width = right - left //中间间隔多少格
       let area = minHeight * width 
       max = Math.max(max,area)
       if(height[left] < height[right]) {
           left++
       }else {
           right--
       }
   }
   return max
}; 


//方案 while+双指针 简化版
var maxArea = function(height) {
    let max = 0;
    for (let i = 0, j = height.length - 1; i < j;) {//双指针i,j循环height数组
      	//i,j较小的那个先向内移动 如果高的指针先移动,那肯定不如当前的面积大
        const minHeight = height[i] < height[j] ? height[i++] : height[j--];
        const area = (j - i + 1) * minHeight;//计算面积
        max = Math.max(max, area);//更新最大面积
    }
    return max;
}; 

219. 存在重复元素 II

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [1,2,3,1], k = 3
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1
输出:true

示例 3:

输入:nums = [1,2,3,1,2,3], k = 2
输出:false 

答题

注意,这里只要处理最小的,所有当nums = [1,0,1,1], k = 3 , 只需要处理 第index 0 到 2 的上的值都是一就满足 <= k ,不需要处理 =k的情况,所以通过map记录上一个值的下标,判断小于等于K 就可以立刻返回 ,每次都会更新最新的map的下标值

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {boolean}
 */
 //双重循环
 var containsNearbyDuplicate = function(nums, k) {
    for (let i = 0; i < nums.length; i++) { 
        for (let j = i+1; j < nums.length; j++) {
            if(nums[i] == nums[j]) {
                if(Math.abs(i-j) <= k) {
                    return true
                }
            }
        }
    }
    return false;
}; 
 
 //map 方式
var containsNearbyDuplicate = function(nums, k) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
        const num = nums[i];
        if (map.has(num)) {
           if(i - map.get(num) <= k) {
               return true
           }
        }
        map.set(num,i) //注意,这里即使重复,每次都覆盖最新的下标
    }
    return false;
}; 
//set + 滑动窗口
var containsNearbyDuplicate = function(nums, k) {
    const set = new Set();
    const length = nums.length;
    for (let i = 0; i < length; i++) {
        if (i > k) {
            set.delete(nums[i - k - 1]);
        }
        if (set.has(nums[i])) {
            return true;
        }
        set.add(nums[i])
    }
    return false;
};
 

349. 两个数组的交集

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]

示例 2:

输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [9,4]

说明:

  • 输出结果中的每个元素一定是唯一的。
  • 我们可以不考虑输出结果的顺序。

答题

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
 
var intersection = function(nums1, nums2) { 
    let set1 = new Set(nums1)
    let set2 = new Set(nums2)
    let ret = []
    //优化set1固定循环的集合,默认按最短的遍历
    if(set1.length > set2.length) {
        [set1,set2] = [set2,set1]
    }
    set1.forEach((val) => {
        if(set2.has(val)){
            ret.push(val)
        } 
    })
    return ret
};