C语言【数据结构】算法复杂度【练习题】(含OJ题(C++)、选择题等),以及要注意的知识点

目录

一.OJ题

1. 面试题 17.04. 消失的数字

2. 剑指 Offer 56 - I. 数组中数字出现的次数

3.​​​​​​ 189. 轮转数组

4. 27. 移除元素

二.选择题

三.注意(部分较为简单的选择题的总结)


前言:算法复杂度:C语言【数据结构】算法的时间复杂度和空间复杂度O()_糖果雨滴a的博客-CSDN博客

一.OJ题

1. 面试题 17.04. 消失的数字

(1)描述

数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

示例 1:

输入:[3,0,1]
输出:2

示例 2:

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

(2)思路

①方法一:采用异或(^)的方法,两个相同的数异或后为0,因此让数组中的元素依次异或一遍,并且让1到n的所有数也依次异或一遍(1到n的所有数和数组中所有数全部异或在一起),最后的结果就是缺少的那个

方法一代码:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = 0;
        //两个相同的数异或(^)为0,最后结果就是缺少的那个
        for(int i = 0; i < nums.size(); ++i)
        {
            n ^= nums[i];
            n ^= i + 1;
        }

        return n;
    }
};

②方法二:直接进行+-操作,1到n的数全部相加减去数组中的所有元素,即得到结果

方法二代码:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = 0;
        for(int i = 0; i < nums.size(); ++i)
        {
            n -= nums[i];
            n += i + 1;
        }

        return n;
    }
};

也可以利用高斯求和公式:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int n = nums.size();
        int total = n * (n + 1) / 2;
        int arrSum = 0;
        for (int i = 0; i < n; i++) {
            arrSum += nums[i];
        }
        return total - arrSum;
    }
};

2. 剑指 Offer 56 - I. 数组中数字出现的次数

(1)描述

        一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]


示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

(2)思路

        这道题是在一个整形数组中有两个不同的数字,其它数字都出现了两次。上一道消失的数字,可以通过让1到n的所有数以及数组中的所有数全部异或在一起,而得到最好结果。

        这道也是用异或的方法来做,只不过我们想要得到这两个不同的数字,那么我们就要分成两组,每一组都有一个不同的数组,而那些重复的数无论在哪一组都不会影响(因为两个相同的数异或结果为0)

        一想到分组,那么我们应该会想到按位与(&),奇偶数分组时就是通过&1,用来判断二进制最后一位是1还是0,来进行分组。这道题分组也是采用按位与(&)的方式

        首先,我们把全部数都异或一遍,就可以得到两个不同的数异或的结果。又因为这两个数是不同的,那么一定有一个二进制位为1(异或^,相同为1,不同为0),我们得到了这个全部异或的结果时,我们并不知道哪一个二进制位为1,因此我们应该设立一个变量,通过这个变量找到那个为1的二进制位。

        找二进制为1的,只需要从后往前开始找,找到第一个二进制位为1的就可以了,因为我们只需要一个二进制位为1的就可以进行分组了(重点是分组)。

        前面准备工作都做完后,就要开始分组了。进行分组之前,我们为了要得到这两个不同的数字,就可以先创建两个变量。通过一个范围for循环,用数组里的数依次按位与(&)刚才那个二进制位为1的变量,如果按位与(&)结果为1,就分为一组,然后得到这一组的不同的数字,另一组就是按位与(&)结果为0,得到这一组的另一个不同的数字。

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int ret = 0;
        //全部异或一遍,可以得到两个不重复的数的异或结果
        for(int num : nums)
        {
            ret ^= num;
        }

        //从右往左,找到一个不同的位(第一个不同的即可)
        //因为前面这两个不重复的数异或过,所以异或的结果&1在哪个位是1就说明两个数对应的位不同
        //得到这个不同的位的位置,让这个位为1其它位全为0,然后就可以根据这个位进行分组异或了
        int n = 1;
        while((ret & n) == 0)
        {
            n <<= 1;
        }

        //分组异或
        int a = 0, b = 0;
        for(int num : nums)
        {
            if(num & n)
            {
                a ^= num;
            }
            else
            {
                b ^= num;
            }
        }

        return vector<int>{ a, b };
    }
};

3.​​​​​​ 189. 轮转数组

(1)描述

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]


示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

(2)思路

方法1:让每一个元素都轮转k % n次,依次轮转。这个是最容易想出来的,但是时间复杂度是最高的。两个循环,每个元素进行轮转前,先保存会被覆盖掉的最后一位,然后等这个元素轮转了k % n次后,再将最后一个元素放到第一个元素的位置。(这里k % n是为了提高效率,因为k = n时,轮转了n次,又回到了原来位置,相当于没有轮转过)

        我们提交代码到leetcode的时候,会提示超出时间限制,是因为时间复杂度太大了,为O(N * K)

方法1代码:

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        // k > n时,每次k = n都会轮转到原来位置,如果轮转k次,效率很低
        // 而轮转k % n就可以通过使轮转每一次都是有效的
        for(int i = 0; i < k % n; ++i)
        {
            // 保存最后一位
            int tmp = nums[n - 1];
            // 从第一个到倒数第二个依次往后串
            for(int j = n - 2; j >= 0; --j)
            {
                nums[j + 1] = nums[j];
            }
            // 让第一位变成最后一位
            nums[0] = tmp;
        }
    }
};

方法1优化版:这个方法在方法1上进行改进,使时间复杂度变为O(N)。

        这个是一次就让一个元素轮转到该到的位置(一次到位)。我们可以定义几个变量,分别是记录当前位置的pos,以及temp保存当前这个元素应该轮转到的位置的那个元素(因为那个元素会被覆盖,所以要保存),还有一个pre用来每次将这个元素赋值给nums[pos],然后再更新pre为这个被覆盖了的元素(被temp保存了)

        但是这时,会有个问题,当k与n的最大公约数不是1时,会出现循环,比如有4个元素,k = 2,pos从0开始,经过了两次轮转,最终又回到0(pos为其他值也会出现这种情况),导致无法对其他数进行修改(或是无法正确修改)。这时,我们可以定义一个新的变量i,如果出现这种情况,我们就让pos变为下一个位置,继续进行轮转。轮转n次,让每一个元素都被轮转到即可。

        最后,我们可以再优化一下,当k是n的倍数时,就直接返回,这时是不需要轮转的。

方法1优化版代码:

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int len = nums.size();
        int n = len;
        int i = 0;
        int pos = 0;
        int temp = nums[pos], pre = nums[pos];

        // k是n的倍数,不需要轮转,直接返回
        if(k % n == 0)
        {
            return;
        }

        while(n--)
        {
            pos = (pos + k) % len;
            temp = nums[pos];
            nums[pos] = pre;
            pre = temp;
            if(pos == i)
            {
                pos = ++i;
                temp = nums[pos];
                pre = nums[pos];
            }
        }
    }
};

方法2:我们可以额外开一个长度为n的数组,然后让原数组中的第i个元素变为新开的数组的第 ( i + k) % n   个元素,这就直接实现了轮转,最后可以使用assign,把这个新开的数组赋值给原数组

方法2代码:

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> v(n);
        for(int i = 0; i < n; ++i)
        {
            v[(i + k) % n] = nums[i];
        }

        nums.assign(v.begin(), v.end());
    }
};

方法3:轮转k次,相当于:后面的k个元素移至了数组前面,前面的元素就挪到了后面。

因此,我们可以采取三步翻转法来完成,我们先把前面n - k个元素(下标为0到n - k - 1)进行翻转,再把后面k个元素进行翻转,最后全部进行翻转即可。(全部进行翻转可以让后面的元素到前面,但是这时候元素是被翻转过的,顺序是反着的,这时我们再让前一部分和后一部分,分组进行翻转,就可以翻转到原来的顺序,并且保证是轮转之后的结果)

方法3代码:

class Solution {
public:
    void Reverse(vector<int>& a, int begin, int end)
    {
        while(begin < end)
        {
            swap(a[begin], a[end]);
            ++begin;
            --end;
        }
    }

    void rotate(vector<int>& nums, int k) {
        int n = nums.size();
        k %= n;

        Reverse(nums, 0, n - k - 1);
        Reverse(nums, n - k, n - 1);
        Reverse(nums, 0, n - 1);
    }
};

4. 27. 移除元素

(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,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

(2)思路

方法1:双指针法,一个指针cur一直往后走,如果这个指针所在数组元素 != val,就把这个元素放到另一个指针prev那,之后prev往后走,如果这个这种所在元素 == val,那么prev就不动。等到cur走到最后一个元素时,prev所在位置就是元素个数。

方法1代码:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int cur = 0, prev = 0;
        for(int i = 0; i < nums.size(); ++i)
        {
            if(nums[cur] != val)
            {
                nums[prev++] = nums[cur];
            }
            ++cur;
        }

        return prev;
    }
};

方法1优化:上一个如果第一个元素就是val,那么后面的元素需要全部挪一遍,在最坏情况下,两个指针都遍历了一遍数组。这个优化版可以让两个指针在一起最坏情况只遍历一遍。

       写法1:定义两个指针,left在数组最左边,right在数组最右边。如果left所在元素 == val,那么就让right所在位置元素赋值给left位置,right-- ,然后再次循环,如果又 == val,那么再次继续。如果 != val,就让left ++,最后当left > right时,循环结束,left就是元素的个数。

        写法2:类似与写法1,只是写法2是采用交换的方式。

方法1优化版代码:

①写法1:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = nums.size() - 1;
        while(left <= right)
        {
            if(nums[left] == val)
            {
                nums[left] = nums[right--];
            }
            else
            {
                ++left;
            }
        }

        return left;
    }
};

②写法2:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int i = 0, j = nums.size() - 1;
        for(i = 0; i <= j; ++i)
        {
            if(nums[i] == val)
            {
                swap(nums[i--], nums[j--]);
            }
        }

        return i;
    }
};

二.选择题

1. 分析以下函数的时间复杂度(D)

void fun(int n) 
{
  int i=l;
  while(i<=n)
    i=i*2;
}

A.O(n)

B.O(n^2)

C.O(nlogn)

D.O(logn)

解释:

此函数有一个循环,但是循环没有被执行n次,i每次都是2倍进行递增,所以循环只会被执行log2(n)次

2. 给定一个整数sum,从有N个有序元素的数组中寻找元素a,b,使得a+b的结果最接近sum,最快的平均时间复杂度是(A)

A.O(n)

B.O(n^2)

C.O(nlogn)

D.O(logn)

 解释:

        此题目中,数组元素有序,所以a,b两个数可以分别从开始和结尾处开始搜,根据首尾元素的和是否大于sum,决定搜索的移动,整个数组被搜索一遍,就可以得到结果,所以最好时间复杂度为n

3. 如果一个函数的栈空间中只定义了一个二维数组a[3][6],请问这个函数的空间复杂度为(C)

A.O(n)

B.O(n^2)

C.O( 1 )

D.O(m*n)

解释:

要注意这并不是一个完整的数组,这只是一个空间, 是常数空间,空间复杂度为1

三.注意(部分较为简单的选择题的总结)

1. 大O是一个渐进表示法不会去表示精确的次数,cpu的运算速度很快,估计精确的没有意义。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰果滴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值