1. 构建乘积数组
题目:https://leetcode-cn.com/problems/gou-jian-cheng-ji-shu-zu-lcof/
思路:通过 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1],我们发现 B[i] 就是 A[i] 左边所有元素的积 乘 A[i] 右边所有元素的积。
- 从左往右遍历累乘,结果保存在数组 ret 中,此时 ret[i] 表示,A[i] 左边所有元素的乘积
- 然后从右往左遍历累乘,获取A[i] 右边所有元素的乘积
- 两边遍历之后得到的 ret,就是最终结果
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
if(a.empty()) return {};
int n=a.size();
vector<int> res(n,0);
res[0]=1;
for(int i=1; i<n; i++){
res[i] = res[i-1]*a[i-1];
}
int tmp=1;
for(int j=a.size()-2; j>=0; j--){
tmp *= a[j+1];
res[j] *= tmp;
}
return res;
}
};
2. 数组中数字出现的次数
题目:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/
思路:
异或的性质:
- 交换律:p⊕q=q⊕p
- 结合律:p⊕(q⊕r)=(p⊕q)⊕r
- 恒等率:p⊕0=p
- 归零率:p⊕p=0
如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?
答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。
两个只出现了一次的数字为 a 和 b,那么所有数字异或的结果就等于 a 和 b 异或的结果,我们记为 x。如果我们把 x 写成二进制的形式 ,其中 ∈{0,1},我们考虑一下 和 的含义是什么?它意味着如果我们把 a 和 b 写成二进制的形式, 和 的关系 表示 和 不等, 表示 和 相等。假如我们任选一个不为 0 的,按照第 i 位给原来的序列分组,如果该位为 0 就分到第一组,否则就分到第二组,这样就能满足条件
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
int i=0;
for(int j:nums){ //1. 全员异或
i ^=j;
}
int div=1;
while((div & i)==0){ //2. 找一个不为0的位置
div <<= 1;
}
int a=0,b=0;
for(int k: nums){ //3. 按照第i位给原来的序列分组
if(div & k){
a ^= k;
}
else{
b ^= k;
}
}
return vector<int>{a,b};
}
};
3. 数组中数字出现的次数Ⅱ
题目:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/
思路:考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3 的倍数。因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字。
实现:
- 使用 与运算 ,可获取二进制数字 num 的最右一位 : = num & 1;
- 配合 无符号右移操作 ,可获取 num 所有位的值(即 ~ ): num=num>>1
- 建立一个长度为 32 的数组 counts ,通过以上方法可记录所有数字的各二进制位的 1 的出现次数。
- 将 counts 各元素对 3 求余,则结果为 “只出现一次的数字” 的各二进制位
- 利用 左移操作 和 或 运算 ,可将 counts 数组中各二进位的值恢复到数字 res 上(循环区间是i∈[0,31] )
class Solution {
public:
int singleNumber(vector<int>& nums) {
vector<int> counts(32);
for(auto num : nums){
for(int i=0; i<32; i++){
counts[i] += num & 1;
num >>= 1;
}
}
int res=0, m=3;
for(int i=0; i<32; i++){
res <<= 1;
res |= counts[31-i]%m; //这两步顺序不能变,否则最后一步会多左移一次
}
return res;
}
};
4. 在排序数组中查找数字
题目:https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/
思路:查找左右边界即可。注意一下技巧。
class Solution {
public:
int search(vector<int>& nums, int target) {
return helper(nums,target)-helper(nums,target-1);
}
int helper(vector<int>& nums, int target){
int i=0,j=nums.size()-1;
while(i<=j){
int mid=(i+j)>>1;
if(nums[mid]<=target) i=mid+1; //=,i=mid+1,寻找右边界
else j=mid-1;
}
return i;
}
};
5. 数组中的逆序对
题目:https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/
思路:
- 利用「归并排序」计算逆序对,是非常经典的做法;
- 关键在于「合并两个有序数组」的步骤,利用数组的部分有序性,一下子计算出一个数之前或者之后元素的逆序的个数;
- 前面「分」的时候什么都不做,「合」的过程中计算「逆序对」的个数;
- 「排序」的工作是必要的,正是因为「排序」才能在下一轮利用顺序关系加快逆序数的计算,也能避免重复计算;
- 在代码实现上,只需要在「归并排序」代码的基础上,加上「逆序对」个数的计算,计算公式需要自己在草稿纸上推导。
思想是「分治算法」,所有的「逆序对」来源于 3 个部分:
- 左边区间的逆序对;
- 右边区间的逆序对;
- 横跨两个区间的逆序对。
class Solution {
public:
int reversePairs(vector<int>& nums) {
if(nums.size() < 2)
{
return 0;
}
vector<int> tmp(nums.size(),0);
return mergeSort(nums, 0, nums.size() - 1, tmp);
}
int mergeSort(vector<int>& nums, int left, int right, vector<int>& tmp)
{
if(left >= right)
{
return 0; //mergeSort递归的终结条件
}
int mid = left + (right-left)/2;
int leftPairs = mergeSort(nums, left, mid, tmp);
int rightPairs = mergeSort(nums, mid + 1, right, tmp);
int curPairs = mergeAndCount(nums, left, mid, right, tmp);
return curPairs + leftPairs + rightPairs;
}
int mergeAndCount(vector<int>& nums, int left, int mid, int right, vector<int>& tmp)
{
//copy nums[left,right] to tmp[i]
for(int i = left; i <= right; i++)
{
tmp[i] = nums[i];
}
int i = left;
int j = mid + 1;
int k=left;
int count = 0;
while(i<=mid && j<=right){
if(tmp[i] <= tmp[j]){
nums[k++] = tmp[i++];
}
else{
nums[k++] = tmp[j++];
count += (mid - i + 1);
}
}
while(i<=mid) nums[k++] = tmp[i++];
while(j<=right) nums[k++] = tmp[j++];
return count;
}
};