算法题之数组

  • Post author:
  • Post category:其他




小总结

数组类题目,涉及到数组中两个数的关系(如求和,他们之间数的和等),可以考虑双指针法,但双指针的话不要局限在两层for循环这个死板的的认知上。还有诸如移动窗口以及left、right指针向中间靠拢这些变体。



1、旋转矩阵



题目

在这里插入图片描述



分析



解法1

本题的重点在于能否发现点在旋转前后的关系,假如旋转点是(i,j),可以想象,旋转后的行就是旋转前的列,而旋转后列同旋转前的行关于中心对称,即是(j,n-i-1);

在这里插入图片描述
在这里插入图片描述

第二点如上面两副图所示,整个矩阵的旋转可以理解为起点都在左上角区域,然后依次顺时针移动。如上左图,区域1转到了区域2,2转到3,3转回1。(还要注意奇偶的差别)

因此整个变换可分解为按区域的三次互换:

swap(matrix[i][j], matrix[j][n-i-1]);swap(matrix[i][j], matrix[n-i-1][n-j-1]);swap(matrix[i][j], matrix[n-j-1][i]);



解法2

此题还可以通过翻转整个数组,再按正对角线交换两边的数来解答,如下图:

在这里插入图片描述



扩展

这里不讨论下逆时针旋转90度是失职的,第一种解法不必说,观察即可,对于第二种方法。

第一步的交换不变,第二步交换要按逆对角线来做。亲测正确。



复杂度

我参考的那位仁兄给的如下:

时间复杂度为O(n^2);

空间复杂度为O(1);

注:这里的n是指边长



2、三数之和



题目

在这里插入图片描述



分析

这题的解法就是排序后使用三指针,这里主要说下对于避免重复的处理方式,就是在指针前进时,如果遇到和上一个指向的数相同的数就跳过,这样既可避免重复。

//下面两个查重只需要留一个,这里好好品味下缘由
if(left>start+1&&nums[left]==nums[left-1]){
   ++left;
   continue;
}
if(right<n-1&&nums[right]==nums[right+1]){
   --right;
   continue;
}

在第一个指针位置确定后,left和right就分别指向它右边区域的两端,并向中央逼近。但这里在查重的时候并不需要将left和right都进行检查,因为一旦遇到和等于0的组合后,会同时执行“++left” 和“–right”。而只要这两者中有一个没有重复,那么整体就不会重复。



复杂度

时间复杂度:O(n^2)

空间复杂度:O(1)



3、下一个排列



题目

在这里插入图片描述



分析

这题咋一看好像很简单,从后往前找到顺序的两数互换位置就行了,但实际上,这只是第一步……

我们应该再往后找比两数中大的最小的数,再来还要将后面的数顺序颠倒。

以“123465” 为例:首先找到顺序两数4和6,然后按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列.

解题步骤如下:

①从后向前查找第一个相邻升序的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序。

②在 [j,end) 从后向前查找第一个满足 A[i] < A[k] 的 k。

③将 A[i] 与 A[k] 交换。

④可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序。

⑤如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4。

代码如下:

    void nextPermutation(vector<int>& nums) {
        int n=nums.size();
        int i=n-2;
        for(;i>=0;--i){
            if(nums[i]<nums[i+1]) break;
        }

        if(i<0){
            reverse(nums.begin(),nums.end());
        }
        else{
            for(int j=n-1;j>i;--j){
                if(nums[j]>nums[i]){
                    swap(nums[i],nums[j]);
                    break;
                }
            }
            reverse(nums.begin()+i+1,nums.end());
        }
    }

注意如果出现 i<0 的情况,则说明nums是降序或者全是相等的数,这时统一将数组整个翻转即可。



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



4、盛最多水的容器和数对和



题目

在这里插入图片描述
在这里插入图片描述



分析

这两题依旧是使用了双指针,但和之前的不同,这里是要维护一个left和right指针分别从左右开始往中间遍历,同时数对和这题需要进行排序。如第11题:

        while(left<right){
            area=max(area,min(height[left],height[right])*(right-left));
            if(height[left]>height[right]) --right;
            else if(height[left]<height[right]) ++left;
            else{
                ++left;
                --right;
            }
        }

这里其实用到了贪心思想,即总是移动矮的那个指针。



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



5、删除排序数组中的重复项



题目

在这里插入图片描述



分析

这里用到了双指针的一种变体,一开始两个指针指向初始位置,如果left和right的值相等,就++right,直到不等为止。然后先将right的值赋值给left+1,然后++right。如下:

        while(right<n){
            if(nums[left]==nums[right]) ++right;
            else{
                nums[++left]=nums[right++];
            }
        }

注意:因为一开始left和right指向同一位置,所以第一次循环中肯定是会++right。



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



6、合并区间



题目

在这里插入图片描述



分析

这题其实就是按“看起来”的方法做就可以了,即:

1、首先按区间的第一个数字排序,关于这一点,STL的sort也可做到。
2、比较排序后区间进行合并。

这里合并的方式可以按如下进行,先将第一个区间放进输出的容器out中,然后循环比较当前区间和out中的尾部区间,并更新out的尾部或将当前区间放进out中,如下:

        vector<vector<int>> out;
        out.push_back(intervals[0]);
        for(int i=0;i<n;++i){
            if(intervals[i][1]<out.back()[1]) continue;
            else if(intervals[i][0]<=out.back()[1]){
                out.back()[1]=intervals[i][1];
            }
            else{
                out.push_back(intervals[i]);
            }
        }

注意这里back()函数的使用。



复杂度

时间复杂度:O(N)

空间复杂度:O(N)



7、移动零



题目

在这里插入图片描述



分析

方法一:

维护一个count记录遇到的0的个数,然后将数组遍历一遍,把不等于0的数 i 移动到第 i-count 位,如下:

        int count=0;
        for(int i=0;i<n;++i){
            if(nums[i]==0) ++count;
            else if(count){
                nums[i-count]=nums[i];
            }
        }

然后把后count个数全部置0。

方法二:

运用快排的思想,以0为参考数,将不等于0的数置换到左边,而等于0的数放在右边,如下:

        int j=0;
        for(int i=0;i<n;++i){
            if(nums[i]!=0){
                swap(nums[i],nums[j++]);
            }
        }

注:这里的 j 永远指向第一个 0(如果有的话)。



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



8、x的平方根&两数相除



题目

在这里插入图片描述
在这里插入图片描述



分析


x的平方根


这题使用二分法查找,如下:

        long left=0,right=x/2+1;
        while(left<right){
            long mid=left+((right-left+1)>>1);
            long square=mid*mid;
            if(square>x) right=mid-1;
            else left=mid;
        }

这里 right 的取值基于大于4的数的一半的平方一定大于它本身。同时令 right=x/2+1 以应对 1 的情况。


两数相除


这里先对被除数进行处理,主要是处理几种特殊情况,然后把正负的问题搞定,如下:

    int divide(int dividend, int divisor) {
        if(dividend==0) return 0;
        if(divisor==1) return dividend;
        if(divisor==-1){
            return dividend<=INT_MIN?INT_MAX:-dividend;
        }
        int sym=1;
        long a=dividend,b=divisor;
        if((a>0&&b<0)||(a<0&&b>0)) sym=-1;
        if(a<0) a=-a;
        if(b<0) b=-b;

        int ret=div(a,b);
        if(sym==1) return ret;
        return -ret;
    }

这里为防止越界,需要把 int 换成 long。

div函数进行递归,让b指数增长,直到大于a,这时得到刚好小于a的值,tb,然后让 a=a-tb,进入下一次递归,直到a<b,同时维护一个数count,用来记录用了多少个b,如下:

    int div(long a,long b){
        if(a<b) return 0;
        int count=1;
        long tb=b;
        while((tb+tb)<=a){
            tb+=tb;
            count+=count;
        }
        return count+div(a-tb,b);
    }



复杂度

时间复杂度:O(lgN)

空间复杂度:O(1)



9、和为K的子数组



题目

在这里插入图片描述



分析

方法一:暴力双指针

先用一个指针left固定左边界,然后用一个right指针遍历右边。

时间复杂度:O(N^2)

方法二:前缀和+哈希优化

所谓前缀和就是用数组把前 i 个数的和记录下来。

这里的暴力做法和法一一样,left固定左边界,right向右遍历,记录两个位置的前缀和之差等于 k 的节点。

哈希优化就是用map记录下前缀和及它出现的次数,然后用一个 sum 值记录当前的前缀和,然后在map中寻找是否存在 sum-k 的前缀和及它的个数是多少,代码如下:

    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int,int> um;
        um[0]=1;
        int sum=0;
        int count=0;
        for(int a:nums){
            sum+=a;
            if(um.count(sum-k)) count+=um[sum-k];
            ++um[sum];
        }

        return count;
    }



复杂度

时间复杂度:O(N)

空间复杂度:O(N)



10、毒蘑菇(ZJ)



题目

在这里插入图片描述



分析

这里用到了贪心的思想,无视所有的有毒蘑菇,而一旦遇到食用蘑菇就将其加到体力上,代码如下:

bool mushroom(vector<int> &nums,int k){
    int n=nums.size();
    int life=k;
    for(int i=0;i<n;++i){
        if(nums[i]>0) life+=nums[i];
        --life;
        if(life==0) return false;
    }
    return true;
}



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



11、1~n整数中1出现的次数



题目

在这里插入图片描述



分析

参考:

link


将 1 ~ n 的个位、十位、百位、…的 1 出现次数相加,即为 1 出现的总次数。

在这里插入图片描述

计算方法:

1.当cur=0时:此位1的出现次数只由高位high决定,计算公式为:high*digit
2.当cur=1时:此位1的出现次数由高位high和低位low决定,计算公式为:high*digit+low+1
3.其他情况,此位1的出现次数只由高位high决定计算公式为:(high+1)*digit

注:情况2中“+1”是因为还有low位全为0时的这种情况。情况3中的“+1”是因为还有他自身为1这一种情况。



复杂度

时间复杂度:O(1)

空间复杂度:O(1)



12、跳跃游戏I&II



题目

在这里插入图片描述

在这里插入图片描述



分析

这两题本质上都用了贪心思想。


跳跃游戏:


这里的思路是在遍历时记录能跳的最远的位置,如果当前位置大于最远位置就返回false,否则返回true,代码如下:

    bool canJump(vector<int>& nums) {
        int n=nums.size();
        //count记录能到达的最远距离
        int count=0;
        for(int i=0;i<n;++i){
            if(count<i) return false;
            count=max(count,i+nums[i]);
        }
        return true;
    }



复杂度

时间复杂度:O(N)

空间复杂度:O(1)


跳跃游戏II:


这题和上题的思路是一样的,即尽量往远了跳。但是这题的目光要更长远一些,我们这一步要往下一步能跳的更远的位置跳,代码如下:

    int jump(vector<int>& nums) {
        int n=nums.size();
        if(n<2) return 0;
        int i=0,step=0;
        while(i<n){
            //记录这一步加下一步一起能跳的最远的值
            int maxstep=0;
            int pos=0;
            ++step;
            
            for(int j=1;j<=nums[i];++j){
                if(i+j>=n-1) return step; 
                if(i+j+nums[i+j]>maxstep){
                    maxstep=i+j+nums[i+j];
                    pos=i+j;
                }
            }
            i=pos;
        }

        return step;
    }



复杂度

时间复杂度:O(M*N) 其中M为能跳的步数的平均值

空间复杂度:O(1)



13、螺旋矩阵



题目

在这里插入图片描述



分析

参考:

link


这里的思路很朴素,但充满了闪光点,即维护四个数来分别记录矩阵的上下左右边界,并在每记录完一行或者一列后将这四个数刷新。

停止条件:上下或者左右边界交错。

代码如下:

    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int row=matrix.size();
        if(row==0) return {};
        int col=matrix[0].size();
        int a = 0, b = col - 1, c = 0, d = row - 1;
        vector<int> ret;
        while(true){
            for(int i=a;i<=b;++i) ret.push_back(matrix[c][i]);
            if(++c>d) break;
            for(int i=c;i<=d;++i) ret.push_back(matrix[i][b]);
            if(--b<a) break;
            for(int i=b;i>=a;--i) ret.push_back(matrix[d][i]);
            if(--d<c) break;
            for(int i=d;i>=c;--i) ret.push_back(matrix[i][a]);
            if(++a>b) break;
        }

        return ret;
    }



复杂度

时间复杂度:O(M

N) 其中M,N分别表示矩阵的长宽

空间复杂度:O(M

N)



14、数据流中的中位数



题目

在这里插入图片描述



分析

参考:

link


这里要准备一个大顶堆和一个小顶堆,每个堆中装数据流中一半的数,同时,大顶堆中装较小的数,小顶堆中装较大的数。然后中位数可以直接由两个堆的顶得出,如下:

在这里插入图片描述

这里的难点集中在把数加入到堆中这一步上,代码如下:

class MedianFinder {
public:
    /** initialize your data structure here. */
    MedianFinder() {
        size=0;
    }
    
    void addNum(int num) {
        if(big.empty()) big.push(num);
        else if(size%2){
            if(num>big.top()){
                small.push(big.top());
                big.pop();
                big.push(num);
            }
            else small.push(num);
        }
        else{
            if(!small.empty()&&num<small.top()){
                big.push(small.top());
                small.pop();
                small.push(num);
            }
            else big.push(num);
        }
        ++size;
    }
    
    double findMedian() {
        if(size%2) return big.top();
        else return (big.top()+small.top())/2;
    }

private:
    priority_queue<double,vector<double>,greater<double>> big;
    priority_queue<double,vector<double>,less<double>> small;
    int size;
};

因为要将较大的数放在小顶堆把较小的数放在大顶堆中,所以每次add时都要视把数加到哪个堆,和另一个堆的堆顶进行比较。同时,还应该判断堆是否为空。



15、牛牛摘苹果(ZJ)



题目

牛牛有一个苹果园。又到了一年一度的收获季,牛牛现在要去采摘苹果买给市场的摊贩们。
牛牛的果园里面有n棵苹果树,第i棵苹果树上有ai个果子。
牛牛为了保证果子的新鲜程度,每天都会去苹果树上采摘果子。

牛牛特意安排一个计划表:
计划m天去采摘果子。
对于第i天,它会去所有果树上轮流采摘bi个果子。
如果对于第i天,某棵果树上没有bi个果子,那么它只会把当前果树上的果子采摘完。
牛牛想知道它每天能供应多少个苹果给市场的摊贩们。

输入:[10,20,10],[5,7,2]
输出:[15,17,2]



分析

这里用暴力法解并不难,但是如果出题人对时间复杂度提了要求,这题就难弄了。

先上代码,

vector<int> pick(vector<int> &a,vector<int> &b){
    vector<int> ret(b.size(),0);
    sort(a.begin(),a.end());
    
    //当前要摘的总苹果数
    int sum=0;
    //pos前的苹果树都已经被摘空
    int pos=0;
    for(int i=0;i<b.size();++i){
        sum+=b[i];
        while(pos<a.size()&&a[pos]<sum){
            //当前pos树上的苹果已经不够摘了,所以直接加上剩余的苹果数
            ret[i]+=a[pos]-(sum-b[i]);
            ++pos;
        }
        ret[i]+=(b.size()-pos)*b[i];
    }
    return ret;
}

这里的思路是假设所有的苹果都是在第一天摘的,我们维护一个sum指针来表示当前要摘的苹果总数,如例题中,第一天是5,第二天是12,第三天是14。

一开始先将苹果数组排序,同时把sum加上第i天要摘的苹果数,

然后用sum与苹果的数量进行比较,如果sum>=当前苹果树上苹果的数量,表示这个苹果这次将被摘空,这里直接加上苹果树上剩余的苹果数。同时这里为防止重复访问,维护一个pos指针,表示还有剩余苹果的树的位置(这意思一开始要排序的原因)。

否则,就计算当前树以及以后树的总数,直接加上总数乘以这次要摘的苹果数量,因为后面的苹果数肯定也是够的(另一个一开始要排序的原因)。



复杂度

时间复杂度:O(N)

空间复杂度:O(1)



16、存在重复元素III



题目

在这里插入图片描述



分析

这题主要考察对红黑树set的使用,

题目的意思是指,在数组 nums[i] 中,在任意有效区间 [i, i + k] 里是否存在两个数的绝对值小于等于 t,即

|nums[i] - nums[j] | <= t
等价于 nums[i] - nums[j] <= t 并且 nums[i]−nums[j]>=−t。
也即,nums[i]<=t+nums[j] 并且 nums[i] >= nums[j] - t。

于是我们可以把nums中的值存进set中,然后在set中找是否存在nums[i]使得nums[i]<=t+nums[j] 并且 nums[i] >= nums[j] – t。

这里set的查找操作需要用到如下函数

std::set::upper_bound 是 >
std::set::lower_bound 是 >=

代码如下:

    bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
        int n=nums.size();
        set<double> haved;
        for (int i=0;i<n;++i){
            auto cur=haved.lower_bound((double)nums[i]-(double)t);
            if(cur!=haved.end()&& *cur<=(double)nums[i]+(double)t) return true;

            haved.insert(nums[i]);
            if(haved.size()>k){
                haved.erase(nums[i-k]);
            }
        }

        return false;
    }



复杂度分析:

时间复杂度:O(N\log K),遍历数组使用 O(N),在遍历的同时向二叉搜索树中插入元素和移除元素的时间复杂度是O(logK)。

空间复杂度:O(1)。



版权声明:本文为weixin_42137276原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。