初级算法-数组

主要记录算法和数据结构学习笔记,新的一年更上一层楼!

初级算法-数组

  • 一、二分查找
  • 二、移除元素
  • 三、有序数组的平方
  • 四、长度最小的子数组
  • 五、螺旋矩阵II
  • 六、删除有序数组中的重复项
  • 七、买卖股票的最佳时机Ⅱ

数组是存放在 连续内存空间上的相同类型数据的集合。
数组可以方便的通过 下标索引的方式获取到下标下对应的数据。
数组的元素是 不能删的,只能覆盖

-数组下标都是从0开始
-数组内存空间的地址是连续的,删除或增添元素时,要移动其他元素的地址。
-数组的行标可以省略,也可用值指针说明

1.顺序存储 - 高下标优先
2.随机访问 - 低下标优先

一、二分查找

1.题目:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例1

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例2

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示
1.你可以假设 nums 中的所有元素是不重复的。
2.n 将在 [1, 10000]之间。
3.nums 的每个元素都将在 [-9999, 9999]之间。

2.解题思路
此题前提是升序的数组,数组中无重复元素

区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)

(1)target在左闭右闭
1.while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
2.if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    // right是数组最后一个数的下标,num[right]在查找范围内,是左闭右闭区间
    let mid, left = 0, right = nums.length - 1;
    // 当left=right时,由于nums[right]在查找范围内,所以要包括此情况
    while (left <= right) {
        // 位运算 + 防止大数溢出
        mid = left + ((right - left) >> 1);
        // 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid-1;如果右边界更新为mid,那中间数还在下次查找范围内
        if (nums[mid] > target) {
            right = mid - 1;  // 去左面闭区间寻找
        } else if (nums[mid] < target) {
            left = mid + 1;   // 去右面闭区间寻找
        } else {
            return mid;
        }
    }
    return -1;
};
// 运行时间:68ms
// 内存消耗:43.8MB
// 时间复杂度:O(log(n))

(2)target在左闭右开
1.while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
2。if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    // right是数组最后一个数的下标+1,nums[right]不在查找范围内,是左闭右开区间
    let mid, left = 0, right = nums.length;    
    // 当left=right时,由于nums[right]不在查找范围,所以不必包括此情况
    while (left < right) {
        // 位运算 + 防止大数溢出
        mid = left + ((right - left) >> 1);
        // 如果中间值大于目标值,中间值不应在下次查找的范围内,但中间值的前一个值应在;
        // 由于right本来就不在查找范围内,所以将右边界更新为中间值,如果更新右边界为mid-1则将中间值的前一个值也踢出了下次寻找范围
        if (nums[mid] > target) {
            right = mid;  // 去左区间寻找
        } else if (nums[mid] < target) {
            left = mid + 1;   // 去右区间寻找
        } else {
            return mid;
        }
    }
    return -1;
};
// 运行时间:56ms
// 内存消耗:43.9MB
// 时间复杂度:O(log(n))

二、移除元素

1.题目:给你一个数组 nums 和一个值 val,你需要 原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:

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

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

示例1

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例2

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

提示
1.0 <= nums.length <= 100
2.0 <= nums[i] <= 50
3.0 <= val <= 100
2.解题思路
数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
暴力解法: 双层for循环,时间复杂度和空间复杂度:O(n²)和O(1)

双指针法
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。画出图很好理解。

//时间复杂度:O(n)
//空间复杂度:O(1)
var removeElement = (nums, val) => {
    let k = 0;
    for(let i = 0;i < nums.length;i++){
        if(nums[i] != val){
            nums[k++] = nums[i]
        }
    }
    return k;
};
// 运行时间:60ms
// 内存消耗:41.1MB

三、有序数组的平方

1.题目:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例1

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例2

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示
1.0 <= nums.length <= 10000
2.-10000 <= nums[i] <= 10000
3.nums已按 非递减顺序 排序
进阶:设计时间复杂度为O(n)的算法解决本问题

2.解题思路
(1)暴力解法
每个数平方之后,排序。

class Solution {
public:
    vector sortedSquares(vector& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};
// 运行时间:36ms
// 内存消耗:25MB
// O(n+logn)

(2)双指针法
数组其实是有序的, 只不过负数平方之后可能成为最大数了。那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。此时可以考虑双指针法了,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];

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var sortedSquares = function(nums) {
    let n = nums.length;
    let res = new Array(n).fill(0);
    let i = 0, j = n - 1, k = n - 1;
    while (i <= j) {
        let left = nums[i] * nums[i],
            right = nums[j] * nums[j];
        if (left < right) {
            res[k--] = right;
            j--;
        } else {
            res[k--] = left;
            i++;
        }
    }
    return res;
};
// 运行时间:96ms
// 内存消耗:47MB
// 时间复杂度:O(n)        

四、长度最小的子数组

1.题目:给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例1

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组

示例2

输入:target = 4, nums = [1,4,4]
输出:1

示例3

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示
1.1 <= target <= 10^9
2.1 <= nums.length <= 10^5
3.1 <= nums[i] <= 10^5
进阶:设计时间复杂度为O(nlog(n))的算法解决本问题

2.解题思路
(1)暴力解法
两个for循环,然后不断的寻找符合条件的子序列

class Solution {
public:
    int minSubArrayLen(int s, vector& nums) {
        int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
            sum = 0;
            for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) { // 一旦发现子序列和超过了s,更新result
                    subLength = j - i + 1; // 取子序列的长度
                    result = result < subLength ? result : subLength;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
                }
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 超出时间范围

(2)滑动窗口
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在本题中实现滑动窗口,主要确定如下三点:
窗口就是 满足其和 ≥ target 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于target了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。

var minSubArrayLen = function(target, nums) {
    let start, end
    start = end = 0
    let sum = 0
    let len = nums.length
    let ans = Infinity
    
    while(end < len){
        sum += nums[end];
        while (sum >= target) {
            ans = Math.min(ans, end - start + 1);
            sum -= nums[start];
            start++;
        }
        end++;
    }
    return ans === Infinity ? 0 : ans
};
// 运行时间:60ms
// 内存消耗:45.3MB
// 时间复杂度:O(n)      

五、螺旋矩阵II

1.题目
给你一个正整数 n ,生成一个包含 1 到 n² 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例1
初级算法-数组_第1张图片

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例2

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

提示
1 <= n <= 20
2.解题思路
求解本题要坚持 循环不变量 的原则。
1.填充上行从左到右
2.填充右列从上到下
3.填充下行从右到左
4.填充左列从下到上
每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
初级算法-数组_第2张图片
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。这也是坚持了每条边左闭右开的原则。

/**
 * @param {number} n
 * @return {number[][]}
 */
var generateMatrix = function(n) {
    let startX = startY = 0;   // 起始位置
    let loop = Math.floor(n/2);   // 旋转圈数
    let mid = Math.floor(n/2);    // 中间位置
    let offset = 1;    // 控制每一层填充元素个数
    let count = 1;     // 更新填充数字
    let res = new Array(n).fill(0).map(() => new Array(n).fill(0));

    while (loop--) {
        let row = startX, col = startY;
        // 上行从左到右(左闭右开)
        for (; col < startY + n - offset; col++) {
            res[row][col] = count++;
        }
        // 右列从上到下(左闭右开)
        for (; row < startX + n - offset; row++) {
            res[row][col] = count++;
        }
        // 下行从右到左(左闭右开)
        for (; col > startY; col--) {
            res[row][col] = count++;
        }
        // 左列做下到上(左闭右开)
        for (; row > startX; row--) {
            res[row][col] = count++;
        }

        // 更新起始位置
        startX++;
        startY++;

        // 更新offset
        offset += 2;
    }
    // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
    if (n % 2 === 1) {
        res[mid][mid] = count;
    }
    return res;
};
// 运行时间:64ms
// 内存消耗:41MB

六、删除有序数组中的重复项

1.题目:给你一个有序数组 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 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 3 * 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按升序排列

2.解题思路
(1)1.给定的数组nums是有序的,因此对于任意i 2.如果数组nums的长度为0,则数组不包含任何元素,因此返回0;
3.当数组nums的长度大于0时,数组中至少包含一个元素,在删除重复元素之后也至少剩下一个元素,因此nums[0]保持原状即可,从下标1开始删除重复元素
4.定义两个指针 fast 和 slow 分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标1;
5.将快指针 fast 依次遍历从1到n-1的每个位置,对于每个位置,如果nums[fast]=nums[fast-1],说明nums[fast]和之前的元素都不同,因此将nums[fast]的值复制到nums[slow],然后将slow的值加1,即指向下一个位置;
6.遍历结束之后,从nums[0]到nums[slow-1]的每个元素都不相同且包含原数组中的每个不同的元素,因此新的长度即为slow,返回 slow 即可。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0

        n = len(nums)
        fast = slow = 1 #删除重复元素之后也至少剩下一个元素
        while fast < n:
            if nums[fast] != nums[fast - 1]: #说明nums[fast]和之前的元素都不同
                nums[slow] = nums[fast]      #nums[fast] 的值复制到nums[slow]
                slow += 1
            fast += 1

        return slow #从nums[0]到nums[slow-1]的每个元素都不相同
#运行时间:40ms
#内存消耗:16.1MB
#时间复杂度:O(n),其中n是数组的长度,快指针和慢指针最多各移动n次。
#空间复杂度:O(1),只需要使用常数的额外空间。

(2)逆序实现
大多数解题思路都是考虑使用双指针从头到尾遍历,这样用for循环就会衍生出一个问题: 在遍历列表/数组/切片等的过程中, 此时该列表/数组/切片等的长度会发生变化. 然后有很多大佬直接改用while循环进行解答.
其实, 我们可以换位思考一下: 正向遍历有影响, 我可以反向遍历啊。
从nums的最后一个开始遍历, 然后与前一个进行对比,如果相等, 则删除该位置的数;不等,,则继续往前遍历.

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        for num_index in range(len(nums)-1, 0, -1):
            if nums[num_index] == nums[num_index-1]:
                nums.pop(num_index)
        return len(nums)
#运行时间:56ms
#内存消耗:16MB

七、买卖股票的最佳时机Ⅱ

1.题目:给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大利润
示例 1

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7

示例 2

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4

示例 3

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0

提示:

  • 1 <= prices.length <= 3 * 10^4
  • 0 <= prices[i] <= 10^4

2.解题思路

1 2


  1. 数组思路-双指针 ↩︎

  2. 数组思路-逆序 ↩︎

你可能感兴趣的:(数据结构与算法,算法,leetcode,数据结构)