【C++】vector 在OJ中的使用

vector 在OJ中的使用

OJ 1.只出现一次的数字i

复习一下异或(^)

异或运算的基本性质

恒等律:任何数与0异或的结果是其本身,即 X ⊕ 0 = X。

归零律:任何数与自身异或的结果是0,即 X ⊕ X = 0。

交换律:异或运算满足交换律,即 A ⊕ B = B ⊕ A。

结合律:异或运算满足结合律,即 (A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)。

cpp 复制代码
class Solution 
{ 
public:  
    int singleNumber(vector<int>& nums) 
    {     
        int value = 0;     
        for(auto e : nums)     
        {       
            value ^= e;    
        }          
        return value;  
    } 
}; 

OJ 2.杨辉三角OJ

cpp 复制代码
// 涉及resize / operator[] 
// 核心思想:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+ [j] 
class Solution 
{ 
public:   
    vector<vector<int>> generate(int numRows) 
    {     
        vector<vector<int>> vv;
        
        //创建numRows个元素,每个元素的内容是空的vector<int>对象
        //vector<int>()是匿名对象
        vv.resize(numRows,vector<int>());
        
        //第i行创建i+1个数据,并赋值为1     
        for(size_t i = 0; i < numRows; ++i)    
        {       
            vv[i].resize(i+1, 1);    
        }     
        for(size_t i = 2; i < vv.size(); ++i)    
        {       
            for(int j = 1; j < vv[i].size()-1; ++j)      
            {         
                vv[i][j] = vv[i-1][j] + vv[i-1][j-1];      
            }    
        }     
        return vv;  
    } 
};

在generate函数中,vv 是一个vector<vector< int>>,通过 vv.resize(numRows, vector()) 将其大小(行数)设置为 numRows。之后的所有操作(包括两个 for 循环)都只修改了各行 vector 的内容或长度,并没有改变 vv 本身的大小。因此,在整个函数执行期间,vv.size() 始终等于 numRows。

总结:通过上面的练习我们发现vector常用的接口更多是插入和遍历。遍历更喜欢用数组operator[i]的形式访问,因为这样便捷。

OJ 3.删除排序数组中的重复项

cpp 复制代码
class Solution {
public:
    int removeDuplicates(vector<int>& nums) 
    {
        int n = nums.size();
        if (n == 0)
            return 0;
        //slow表示下一次要插入的位置,fast遍历
        int fast = 1, slow = 1;
        while (fast < n)
        {
            if (nums[fast] != nums[fast - 1])
            {
                nums[slow] = nums[fast];
                slow++;
            }
            ++fast;
        }
        //此时slow刚好是个数
        return slow;
    }
};

OJ 4.只出现一次的数ii

为什么可以按位统计?

整数在计算机中用二进制表示(比如 32 位)。对于每个二进制位(第 0 位、第 1 位......第 31 位),我们可以独立地统计所有数字在该位上为 1 的总次数。

因为出现三次的数字,在同一个位上,要么是 0(贡献 0),要么是 1(贡献 3 次 1),所以它们在该位上的总计数一定是 0 或 3(即 3 的倍数)。而只出现一次的数字,在该位上要么是 0(贡献 0),要么是 1(贡献 1)。因此,所有数字在该位上的 1 的个数,除以 3 的余数,就是目标数字在该位上的值。

举例说明(以 4 位二进制简化为例)

假设数字只有 4 位(实际是 32 位,道理相同)。

数组:[2, 2, 2, 3]

二进制表示(4 位):

2 = 0010

3 = 0011

第 0 位(最右边)

2:第 0 位是 0

2:0

2:0

3:第 0 位是 1

总共有 1 个 1。

1 % 3 = 1 → 目标数字的第 0 位是 1。

第 1 位

2:第 1 位是 1

2:1

2:1

3:第 1 位是 1

总共有 4 个 1。

4 % 3 = 1 → 目标数字的第 1 位是 1。

第 2 位

2:第 2 位是 0

2:0

2:0

3:第 2 位是 0

总共有 0 个 1。

0 % 3 = 0 → 目标数字的第 2 位是 0。

第 3 位(最高位)

所有数字第 3 位都是 0,总数为 0,余数 0 → 目标数字第 3 位是 0。

所以目标数字的二进制是 0011,即 3。

cpp 复制代码
class Solution
{
public:
	int singleNumber(vector<int>& nums)
	{
		int ans = 0;
		for (int i = 0; i < 32; i++)
		{
			int total = 0;
			for(int num:nums)
			{
				total += ((num >> i) & 1);
				
			}
			if (total % 3)
				ans |= (1 << i);
		}
		return ans;
	}

};

(1 << i) 生成了一个只在第 i 位为 1、其余位为 0 的掩码。

ans |= mask 将 ans 的第 i 位设置为 1(因为按位或运算:只要有一位为 1,结果就为 1)。

因此,1 << i 用来构造一个只有第 i 位为 1 的整数,便于将结果 ans 的对应位置 1。

OJ 5. 只出现一次的数iii

第一步:全部异或

cpp 复制代码
int xorsum = 0;
for (int num: nums) {
    xorsum ^= num;
}

异或(^)的性质:

a ^ a = 0(相同数字抵消)

a ^ 0 = a

所以把整个数组异或一遍,所有成对出现的数字都会抵消为0,剩下的就是两个只出现一次的数字的异或结果:

cpp 复制代码
xorsum = a ^ b

其中 a 和 b 就是我们要找的两个数。

2.第二步:找到 a 和 b 不同的某一位

因为 a != b,所以 a ^ b 的二进制中至少有一位是 1。这一位在 a 中是 0,在 b 中是 1(或者反过来)。

我们只要随便找出这样的一位,就可以把 a 和 b 区分开。

如何找出这一位?

常用技巧:++xorsum & (-xorsum) 可以取出 xorsum 最低位的 1(即从右往左第一个为 1 的位)。++

例如 xorsum = 6(二进制 110),

-6 在计算机中是补码:先取反得 ...111001,再加1得 ...111010。

6 & (-6) = 110 & 010 = 010(二进制),即数字 2,代表第1位(从0开始)是1。

代码中特殊处理了 xorsum == INT_MIN 的情况,++这是为了避免整数溢出(INT_MIN 的负值等于自身,直接取 xorsum 即可)<u>。++

3.第三步:根据这一位分组

现在我们有了一个掩码 lsb,它只有一位是 1(比如 2,即二进制 010)。

我们将数组中所有数字按照 该位是否为 1 分成两组:

第1组:num & lsb == 0(该位为0)

第2组:num & lsb != 0(该位为1)

为什么这样分组有效?

因为 a 和 b 在这一位上不同,所以它们一定会被分到不同的组。

而所有其他数字都出现了两次,并且它们在这一位上的值相同(因为它们是相同的数字),所以成对地落在同一个组里。

于是,每个组内,成对出现的数字异或后抵消,剩下的就是本组里那个单独的数字(即 a 或 b)。

4.第四步:分别异或各组

cpp 复制代码
int type1 = 0, type2 = 0;
for (int num: nums) {
    if (num & lsb) {
        type1 ^= num;
    } else {
        type2 ^= num;
    }
}

type1 会得到该位为 1 的那一组中只出现一次的数字(假设是 a)。

type2 会得到另一组的只出现一次的数字(即 b)。

最终返回 {type1, type2}。

5.用例子走一遍

数组 [1,2,1,3,2,5]

全部异或:1^ 2^ 1 ^ 3^ 2 ^ 5 = 6(二进制 110)。

最低位1:6 & (-6) = 2(二进制 010,代表第1位)。

分组:

第1组(该位为0):数字 1(001)、1(001)、5(101) → 异或:1 ^ 1 ^ 5 = 5

第2组(该位为1):数字 2(010)、3(011)、2(010) → 异或:2 ^ 3 ^ 2 = 3

结果:[5,3]

6.为什么要特殊处理 INT_MIN?

INT_MIN 是 -2147483648,二进制是 1000...000(32位中最高位为1,其余全0)。

它的负值 -INT_MIN 在补码中也是 1000...000,所以 INT_MIN & (-INT_MIN) 会得到 INT_MIN 自身,这没有问题,但为了避免某些编译器的未定义行为(比如溢出),官方题解加了一个判断,如果 xorsum 就是 INT_MIN,直接用它作为 lsb,否则用 xorsum & (-xorsum)。

cpp 复制代码
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int xorsum = 0;
        for (int num: nums) {
            xorsum ^= num;
        }
        // 防止溢出
        int lsb = (xorsum == INT_MIN ? xorsum : xorsum & (-xorsum));
        int type1 = 0, type2 = 0;
        for (int num: nums) 
        {
        //lsb只有一位为1
        		//此时num在这位为1
            if (num & lsb) 
            {
            	//得到这个值
                type1 ^= num;
            }
            else 
            {
                type2 ^= num;
            }
        }
        return {type1, type2};
    }
};

原理:

观察 x 和 -x 的二进制关系:

设 x 的最低位的 1 在第 k 位(从 0 开始计数),那么 x 的第 0 到 k-1 位都是 0。

在 -x 中,第 0 到 k-1 位也是 0(因为取反加 1 的过程会使低位变为 0),第 k 位为 1,而更高位与 x 的对应位相反。

因此 x & (-x) 只会保留第 k 位的 1,更高位因为互补而相与为 0,更低位也都是 0。

OJ 6. 数组中出现次数超过一半的数字

数组中一定存在一个数字出现的次数超过数组长度的一半

cpp 复制代码
class Solution
{
public:
	  int MoreThanHalfNum_Solution(vector<int>& numbers) {
	  sort(numbers.begin(), numbers.end());
	  int count = 0;
	  int n = numbers.size() ;
	  for (size_t i=0;i<n;i++)
	  {
	    if(numbers[i]==numbers[n/2])	
	    {
	      count++;
	    }
	  }
	  if (count > n / 2)
	    return numbers[n / 2];
	  return 0;	
	}
}

多数元素的定义是:出现次数严格大于数组长度的一半(即 count > n/2)。当数组排序后,相同的元素会连续排列。由于该元素的个数超过了一半,无论它从哪个位置开始连续,其连续区间必然覆盖数组的中间位置(下标 n/2)。因此,排序后下标 n/2 处的元素一定是多数元素。

OJ 7. 电话号码字母组合

cpp 复制代码
class Solution {
public:
  void func(vector<string> &res, string str, string &digits, unordered_map<char, string> &m, int k)
  {
    if(str.size() == digits.size())
    {
      res.push_back(str);
      return;
    }
    string tmp = m[digits[k]];
    for(char w : tmp)
    {
      str += w;
      func(res, str, digits, m, k+1);
      str.pop_back();
    }
    return ;
  }
  vector<string> letterCombinations(string digits) {
    unordered_map<char, string> table
    {
      {'0', " "}, {'1',"*"}, {'2', "abc"},
      {'3',"def"}, {'4',"ghi"}, {'5',"jkl"},
      {'6',"mno"}, {'7',"pqrs"},{'8',"tuv"},
      {'9',"wxyz"}};  
    vector<string> res;
    if(digits == "") 
      return res;
    func(res, "", digits, table, 0);
    return res;
  }
};

动图可看

相关推荐
咚为2 小时前
深入浅出 Rust RefCell:打破静态检查的“紧箍咒”
开发语言·后端·rust
圣光SG2 小时前
面向对象编程(OOP)通用跨语言笔记
开发语言·笔记·oop
AI-小柒2 小时前
大模型API中转推荐:Dataeyes API 600+模型统一网关与负载均衡部署,claude编程、香蕉生图、视频大模型聚合平台
大数据·运维·开发语言·人工智能·算法·机器学习·负载均衡
E_ICEBLUE2 小时前
在 Python 中给 PDF 设置背景图或背景色
开发语言·python·pdf
ling__i2 小时前
接口测试常见问题
开发语言·lua
knighthood20012 小时前
VTK/PCL点云可视化:解决加载后需要手动缩放的问题
c++·pcl
daxi1502 小时前
C语言从入门到进阶——第18讲:内存函数
c语言·开发语言·算法
unityのkiven2 小时前
如何通过DirectShow用C++实现PTZ相机的控制?
开发语言·c++·数码相机
实心儿儿2 小时前
C++ —— C++11
开发语言·c++