代码随想录:数组

二分查找

使用basic二分查找的特征:

  • 数组是单调的
  • 没有重复元素(有重复元素可能会有多个结果)

left  middle  right

使用二分查找关键的是如何定义边界条件和终止条件,不同的定义写法也不一样。下面给出了两种定义

左闭右闭 [left, right]  使得循环终止条件为 while(left <= right)

左闭右开 [left, rught) 使得循环终止条件为 while(left < right)

//左闭右闭
int search1(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
    while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
        int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
        if (nums[middle] > target) {
            right = middle - 1; // target 在左区间,所以[left, middle - 1]
        }
        else if (nums[middle] < target) {
            left = middle + 1; // target 在右区间,所以[middle + 1, right]
        }
        else { // nums[middle] == target
            return middle; // 数组中找到目标值,直接返回下标
        }
    }
    // 未找到目标值
    return -1;
}

//左闭右开
int search2(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
    while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <<
        int middle = left + ((right - left) << 1);
        if (nums[middle] > target) {
            right = middle; // target 在左区间,在[left, middle)中
        }
        else if (nums[middle] < target) {
            left = middle + 1; // target 在右区间,在[middle + 1, right)中
        }
        else { // nums[middle] == target
            return middle; // 数组中找到目标值,直接返回下标
        }
    }
    // 未找到目标值
    return -1;
}

例子:使用二分法寻找target在数组中的左右边界

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题。

思路:使用两个二分法来分别查找左右边界;

//在升序数组中找某个target的左右边界
class findBoundInArraySolution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一
        if (leftBorder == -2 || rightBorder == -2) return { -1, -1 };
        // 情况三
        if (rightBorder - leftBorder > 1) return { leftBorder + 1, rightBorder - 1 };
        // 情况二
        return { -1, -1 };
    }
private:
    int getRightBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            }
            else if(nums[middle] == target){ // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
            else {
                left = middle + 1;
            }
        }
        return rightBorder;
    }
    int getLeftBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                //leftBorder = right;
            }
            else if (nums[middle] == target) {
                right = middle - 1;
                leftBorder = right;
            }
            else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }
};


int main() {
    //test_arr();
    //int nums[10] = {-1,0,3,5,9,12};
    vector<int>nums = { -1, 0, 3, 5,5, 9, 12 };
    vector<int> ret2;
    findBoundInArraySolution s;
    ret2 = s.searchRange(nums, 5);
    
    cout << ret2[0]<<":"<<ret2[1] << endl;

    return 0;
}

 

思路:使用一个二分法查找target,然后左右滑查找相同元素定边界

def searchRange(nums: List[int], target: int) -> List[int]:
    def binarySearch(nums:List[int], target:int) -> int:
        left, right = 0, len(nums)-1
        while left<=right: # 不变量:左闭右闭区间
            middle = left + (right-left) // 2
            if nums[middle] > target:
                right = middle - 1
            elif nums[middle] < target:
                left = middle + 1
            else:
                return middle
        return -1
    index = binarySearch(nums, target)
    if index == -1:return [-1, -1] # nums 中不存在 target,直接返回 {-1, -1}
    # nums 中存在 targe,则左右滑动指针,来找到符合题意的区间
    left, right = index, index
    # 向左滑动,找左边界
    while left -1 >=0 and nums[left - 1] == target: left -=1
    # 向右滑动,找右边界
    while right+1 < len(nums) and nums[right + 1] == target: right +=1
    return [left, right]


nums = [ -1, 0, 3, 5,5, 9, 12]

ret = searchRange(nums,5)
print(ret)

 

删除数组中的元素

不管是一维数组还是二维数组,在内存中都是处于连续地址,所以不能删除某一个元素的那个空间,只能覆盖

如果删除中间的某一个元素,在一些题目中,那么可能要把其他元素往前移

有两个思路,一个是暴力破解$O(n^2)$,干就完了;另一个是使用双指针,快慢指针$O(n)$云云

暴力破解,每找到一个要删的元素就把后面的所有元素往前移

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
                size--; // 此时数组的大小-1
            }
        }
        return size;

    }
};

 

快慢指针,每找到一个要留下的元素就把它存在新头的后面

 

class Solution:
    """双指针法
    时间复杂度:O(n)
    空间复杂度:O(1)
    """

    @classmethod
    def removeElement(cls, nums: List[int], val: int) -> int:
        fast = slow = 0

        while fast < len(nums):

            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1

            # 当 fast 指针遇到要删除的元素时停止赋值
            # slow 指针停止移动, fast 指针继续前进
            fast += 1

        return slow

 

有序数组的平方

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

直接平方+一次排序 $O(n + n\log n)$:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};

 

这个有序数组可能有负数,因为平方,最小的可能要跑到最大的位置,但是因为它有序,所以取绝对值最大的一定在两端,那么使用双指针就可以达到$O(n)$

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        int k = A.size() - 1;
        vector<int> result(A.size(), 0);
        for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素; 把循环前进步骤放到后面做也可以
            if (A[i] * A[i] < A[j] * A[j])  {
                result[k--] = A[j] * A[j];
                j--;
            }
            else {
                result[k--] = A[i] * A[i];
                i++;
            }
        }
        return result;
    }
};

这里的双指针是 i 和 j 

 

滑动窗口

用一道最经典的题目来说滑动窗口:

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

暴力破解显然也可以做但是复杂度$O(n^2)$

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。尽管有点像双指针,他就是一个窗口在滑,so...

使用滑动窗口有几个重要的问题要搞清楚

  1. 窗口内是什么?
  2. 窗口左端怎么移动?
  3. 窗口右端怎么移动?
  4. 结束条件

复杂度 $O(n)$解法:

窗口内是正标定的目标子序列

当子序列的和大于target时候,就把左端往前移(缩短子序列)

当子序列的和小于target时候,就把右端往前移(延长子序列)

结束条件:窗口右端从0到末尾 $O(n)$ 体现在这

class minLengthSumSolution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int result = INT32_MAX;
        int sum = 0; // 滑动窗口数值之和
        int i = 0; // 滑动窗口起始位置
        int subLength = 0; // 滑动窗口的长度
        for (int j = 0; j < nums.size(); j++) {
            sum += nums[j];
            // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
            while (sum >= s) {
                subLength = (j - i + 1); // 取子序列的长度
                result = result < subLength ? result : subLength;
                sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};   

生成螺旋矩阵的二维数组,和顺时针打印矩阵

给定一个正整数 n,生成一个包含 1 到 $n^2$ 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

示例:

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

主要考察过程的模拟

 

 当n一大,这道题有非常多的循环,如果不定好边界怎么处理,将非常麻烦

从上图可以看出,每一个颜色都是一条边,当到了拐角处,拐角处的位置让给新的边

那么就可以写代码了.

上述矩阵生成之后要按顺时针打印出来

class buildSpiralMatrixSolution {
public:
    vector<vector<int>> buildMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0));
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count = 1; // 用来给矩阵中每一个空格赋值
        int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度  拐角要让给新的边
        int i, j;
        while (loop--) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j = starty; j < starty + n - offset; j++) {
                res[startx][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = startx; i < startx + n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈里每一条边遍历的长度  外层的边长比内层大二
            offset += 2;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[mid][mid] = count;
        }


    
        return res;
    }
    
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        //https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/
        //顺时针打印矩阵
        //边界法
        if (matrix.size() == 0) return vector<int>();
        int l = 0; //最左边的列
        int r = matrix[0].size() - 1;  //最右边的列
        int u = 0;//最上面一行
        int d = matrix.size() - 1;//最底下一行
        vector<int> res;
        while (true) {
            //从左往右输出最上面一行
            for (int i = l; i <= r; i++) {
                res.push_back(matrix[u][i]);
            }
            //更新顶
            u += 1;
            if (u > d) break;
            //从上到下输出最右边一行
            for (int i = u; i <= d; i++) {
                res.push_back(matrix[i][r]);
            }
            //更新右
            r -= 1;
            if (l > r) break;
            //从右往左输出最底下一行
            for (int i = r; i >= l; i--) {
                res.push_back(matrix[d][i]);
            }
            //更新底
            d -= 1;
            if (u > d) break;
            //从下往上输出最左边一行
            for (int i = d; i >= u; i--) {
                res.push_back(matrix[i][l]);
            }
            //更新左
            l += 1;
            if (l > r) break;
        }
        return res;
    }
    

};

c/c++数组基础知识

  • 数组是存放在连续内存空间上的相同类型数据的集合。
  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的
  • 数组的元素是不能删的,只能覆盖。
  • 二分法是一种思想,有使用的固定条件
  • 双指针是一种使用一个循环做两个循环做的事情的替代方法
  • 滑动窗口注意事项要搞清楚
  • 行为模拟的化,抓住循环不变量
posted @ 2022-01-27 14:42  PiaYie  阅读(42)  评论(0编辑  收藏  举报