目录
一.OJ题
1. 面试题 17.04. 消失的数字
2. 剑指 Offer 56 - I. 数组中数字出现的次数
3. 189. 轮转数组
4. 27. 移除元素
二.选择题
三.注意(部分较为简单的选择题的总结)
一.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的运算速度很快,估计精确的没有意义。