复杂度、数组vector容器详解(知识点+相关LeetCode题目)

本文介绍了算法的时间复杂度和空间复杂度,以及它们在数组操作中的应用。讨论了如何计算时间复杂度,并通过C++STL中的vector展示了数组操作。此外,还分析了几道LeetCode的数组相关题目,如二分查找、元素移除、有序数组的平方和最小长度子数组问题,探讨了不同算法的实现和时间复杂度分析。

目录

前言

一、复杂度

1.时间复杂度O()

1.1 单独计算时间复杂度

1.2 多次计算时间复杂度

1.3 时间复杂度大小

2.空间复杂度

二、数组

1.数组概念,操作的时间复杂度

2.C++STL实现数组操作

2.1创建数组:

2.2获得数组个数:

2.3访问修改元素:

2.4 存入新的元素i:

三、数组力扣leetCode题目分析

704 二分查找

27.移除元素

977.有序数组的平方

209.长度最小的子数组

59.螺旋矩阵


前言

主要阐述了数据结构中数组和复杂度的相关知识,也包括了数组相关的经典力扣题目

一、复杂度

1.时间复杂度O()

时间复杂度是算法的执行效率,也代表了算法执行时间与算法的输入值(变量)之间的关系。

1.1 单独计算时间复杂度

单个for循环,for循环即为O(n)

for (int i = 0; i < n; i++) {
		//执行操作
	}

常量即为O(1)

也有些特殊的,比如While循环中,以i的值作为判断条件。这里需要自己分析,i可以乘2的几次方即为O(logN)--log的底默认为2--

while (i < n) {
		i = i * 2;
	}

1.2 多次计算时间复杂度

一般是单独计算时间复杂度后相乘或相加,最简单的一个函数中存在两条for循环,时间复杂度即为O(m+n)

for (i = 0; i < m; i++) {
		//执行操作
}
for (int i = 0; i < n; i++) {
		//执行操作
}

如果是循环语句嵌套循环语句,那么就是各自的时间复杂度相乘,如下时间复杂度就是O(nlog(n))

for (i = 0; i < n; i++) {
	while (j < n) {
	  j = j * 2;
	}
}

1.3 时间复杂度大小

 在判断时间复杂度的好坏时,可以参照以下图,时间复杂度其实就是算法的运行时间,所以我们不难得出以下关系(运行时间),可用于性能优化

O(1) <O(logN) <O(N) <O(NlogN) <O(N^2) <O(2^N) <O(N!) 

2.空间复杂度

算法的空间复杂度就是算法的存储空间与输入值之间的关系

常量占空间,即为O(1),单纯的运行语句则不占,比如用输入值修改原先常量的值。

For循环n次,(存了n个数据,数据则来源于输入的totallist[i]),则空间复杂度为O(n)

for(int i =0;i<totallist.length();i++){
    a.push_back(totallist[i]);
}

时间空间复杂度只能2选1,优先care时间复杂度。因为空间复杂度可以通过硬件解决(一般硬件足够存储),但是时间复杂度需要考虑优化算法

二、数组

1.数组概念,操作的时间复杂度

数组:在连续的内存空间中,存储一组相同类型的元素

数组的操作时间复杂度--加深对数组的理解,满足题目或者实际的要求

数组元素访问,内存地址是随着索引而递增的,所以可以直接访问,即为O(1)

数组元素搜索,需要遍历一整个数组,即为O(n)

数组元素插入、删除,最极端情况是将整个数组移动(元素依次移动)然后再进行操作,所以时间复杂度为O(n)

2.C++STL实现数组vector操作

在这里介绍的是vector容器实现数组的常用基础操作。

vector 容器是STL中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。

注:数组中不含有split方法,一般采用反向遍历替代

2.1创建数组:

vector<int> s;

vector<double> values(20);//指定元素个数

vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};//指定初始值

2.2获得数组个数:

s.size();

2.3访问修改元素:

s[i] = 1;

2.4 存入新的元素i:

s.push_back(i);

2.5 合并数组

s.insert(s.end(),primes.begin(),primes.end())

2.6 排序数组

先定义排序数组--静态成员函数

static bool cmp(const int &a, const int &b){
      return a < b;//a小才为真,所以是升序,越来越大
 }

后执行排序操作--(默认升序)

sort(s.begin(),s.end(),cmp);

2.7 设置数组大小--防止数组还有额外的空间

s.resize(m);

2.8 删除所有大小为n的元素

并且返回指向最后一个元素下一个位置的迭代器--因为这样做不会减少大小和容量

auto thisnext = remove(s.begin(),s.end(),n);//返回一个迭代器

2.9 删除尾部元素

s.pop_back();

3.0 删除迭代器位置指向的元素

size减小,容量不变

s.erase(s.begin()+1);

3.1 交换数组指定位置的两个元素

swap(nums[start],nums[end]);

3.2 数组高性能添加元素至末尾

s.emplace_back(i)

3.3 数组获取尾部元素

int last = s.back();

3.4 普通数组以固定的值初始化

int rows[9][9];
memset(rows,0,sizeof(rows))

三、数组力扣leetCode题目分析

704 二分查找

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


示例 1:

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

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size()-1;//定义最右边的索引
        while(left <= right){
            //采用的是左闭右闭的规则
            int middle = (left + right)/2;
            if(nums[middle] < target){
                left = middle + 1;//缩小区间,right不变,left从完全大于或者等于的索引开始            
            }else if(nums[middle] > target){
                right = middle - 1;//因为闭区间,right已经不用再包括不是target的索引
            }else{
                return middle;//说明已经寻找到了
            }
        }

        return -1;
    }
};

题解如下:

这里使用的是左闭右闭的二分查找的方法,千万注意left和right对middle的关系


在升序数组nums中寻找目标值target,对于特定下标mid,比较nums[mid]和 target的大小

1.如果nums[mid]== target,则下标即为要寻找的下标,说明找到了目标

2.如果nums[mid]> target,则 target只可能在下标i的左侧,所以将right移动到mid的左侧(不包括mid),再在right和mid之间寻找目标

3.如果nums[mid]<target,则 target 只可能在下标i的右侧,所以将left移动到mid的右侧(不包括mid),再在right和mid之间寻找目标

时间复杂度分析:
二分查找的做法是,定义查找的范围[left, right),初始查找范围是整个数组。每次取查找范围的中点mid,比较nums[mid]和target的大小,如果相等则mid即为要寻找的下标,如果不相等则根据nums[mid]和target的大小关系将查找范围缩小—半。
由于每次查找都会将查找范围缩小一半,因此二分查找的时间复杂度是O(log n),其中n是数组的长度。二分查找的条件是查找范围不为空,即 left≤right。如果target在数组中,二分查找可以保证找到 target,返回target在数组中的下标。如果target不在数组中,则当left > right时结束查找,返回-1。

27.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        //双指针
        int fast=0;
        int slow =0;
        for(;fast < nums.size();++fast){
            if(nums[fast]!= val){
                //不等于的时候,赋值-覆盖slow对应的位置,一开始fast,slow一个位置,后面遇见一个不同,slow就会滞后一个位置
                nums[slow++] = nums[fast];//双指针同时移动
            }
        }

        return slow;//当fast移动完了,slow的值就是长度--因为还有个++
    }
};

题解如下:

由于数组的数据具有连续性,其实只可以通过覆盖元素的方式,达到一个“伪移除”的效果,在一些库中,虽然包括了删除的函数,比如Erase函数,但其实都是将删除元素后面的元素整体向前移动,所以时间复杂度O(n)

所以本题使用双指针的思路,双指针一快fast一慢slow,有序着一起移动,时刻赋值

除非遇到了需要移除的值,这时候slow停止移动,fast可以继续移动。

直到fast对应的不为移除值的时候,slow才可以移动,并且将fast对应的值赋值给slow

也就是说slow的索引下的数组将只保留非移除值,这样就达到了移除的效果

977.有序数组的平方

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

示例 1:

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

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        //双指针思路,从两边向中间,取平方最大值存入result数组最右边--取两边的原因在于,原数组是递增数组,所以从两头这样比较
        vector<int> result(nums.size());//双指针排序
        int k = nums.size()-1;
        int left,right;
        for(left=0,right = nums.size()-1;left <= right;){
            if(nums[left]*nums[left] < nums[right]*nums[right]){
                result[k--]=nums[right]*nums[right];
                right --;
            }else{
                //即使是等于也要先有一方指针移动一下,之后再换另一方移动即可
                result[k--]=nums[left]*nums[left];
                left ++;
            }
        }

        return result;
    }
};

题解如下:

本题使用滑动窗口+双指针的思路,设置双指针分别在最左与最右,两者向中间移动,移动前判断对应的值平方谁大,大的移动,并且把平方值存入结果数组中,结果数组从后往前存储,以达到从小至大的效果,直到两者相遇,说明已经将平方值全部有序的存入了

补充:

为何不可以用快排?

快速排序的最好情况O(nlogn)

快速排序的实现方式,就是在当前区间中选择一个x,区间中所有比x小的数都需要放到x的左边,而比x大的数则放到右边。在理想的情况下,我们选取的分界点刚好就是这个区间的中位数。也就是说,在操作之后,正好将区间分成了满足数字个数相等的左右两个子区间(快排是按照值的大小划分,个数可能相等,可能不等)。此时就和归并排序基本一致了:

递归的第一层,n个数被划分为2个子区间,每个子区间的数字个数为n/2;

递归的第二层,n个数被划分为4个子区间,每个子区间的数字个数为n/4;

递归的第三层,n个数被划分为8个子区间,每个子区间的数字个数为n/8;

  …

递归的第logn层,n个数被划分为n个子区间,每个子区间的数字个数为1;

以上过程与归并排序基本一致,而区别就是,归并排序是从最后一层开始进行merge操作,自底向上;而快速排序则从第一层开始交换区间中数字的位置,是自顶向下的。但是,merge操作和快速排序的调换位置操作,时间复杂度是一样的,对于每一个区间,处理的时候,都需要遍历一次区间中的每一个元素。这也就意味着,快速排序和归并排序一样,每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。而且在最好的情况下,同样也是有logn层,所以快速排序最好的时间复杂度为O(nlogn)。

209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 

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

 

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        //滑动窗口思路,顺序遍历计算是否大于目标,大于了用start指针移动,看看能不能再细分,细分完之后再让end右移
        int end,start=0,sum,result;
        sum=0;
        result = INT_MAX;
        for(end = 0;end<nums.size();end++){
            sum += nums[end];
            while(sum >= target){
                int lenD = end-start+1;
                result = min(lenD,result);
                sum -= nums[start];//看看能不能再细分
                start++;//start和end分别作为起点指针和终点指针
            }
        }

        //还要考虑如果压根加起来都不够大的话     
        return result == INT_MAX ? 0 :result;
    }
};

题解如下:

分别设置start和end指针,分别代表了子数组的开始和结尾,end负责向前移动,start一开始不动,一旦发现两者之间形成的子数组的元素和大于目标值,就移动start指针,移动到不能移动位置(形成的子数组的元素和大于目标值),之后再次移动end,如此反复,当end移动到结尾,min函数得到的最小值result即为结果

59.螺旋矩阵

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

示例 1:


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

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        //按层模拟 -- 使用从左至右,尾元素交给下个遍历的思路 i、j、offset都是可变量
        vector<vector<int>> nums(n, vector<int>(n));//定义二维数组
        int startx=0,starty=0,i=0,j=0;//startx和y分别对应初始行、初始列
        int offset = 1;
        int count=1;
        int times = n/2;
        while(times){
            //n/2决定圈数
            //上边  对应列
            for(j=starty;j<n-offset;j++){
                nums[startx][j] = count++;
            }
            //右边  对应行
            for(i=startx;i<n-offset;i++){
                nums[i][j] = count++;
            }
            //下面 不到最后一个元素且start_可变
            for(;j>starty;j--){
                nums[i][j] = count++;
            }
            //左边 不到最后一个元素且start_可变
            for(;i>startx;i--){
                nums[i][j] = count++;
            }

            //整体缩小
            startx++;
            starty++;
            offset++;

            //次数清空 
            times--;

        }

        //跳出循环的时候,i+1和j+1其实对应了最中间的元素(i>0,j>0)
        if(n%2 == 1){
            nums[startx][starty]=count;
        }

        return nums;
    }
};

题解如下:

本题使用按层模拟的思路解决问题,如下图所示,只需要限定好每层遍历的规则和从外圈到内圈转化的规则,就可以实现环形逻辑。总体上的逻辑就是,为了第一层、第二层而增加的i和j分别作为第三层和第四层的遍历条件,如此循环。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花火の云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值