剑指offer之数组(二)

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 写成二进制的形式  x_k x_{k - 1} \cdots x_2 x_1 x_0,其中 x_{i}∈{0,1},我们考虑一下 x_{i}=0 和 x_{i}=1 的含义是什么?它意味着如果我们把 a 和 b 写成二进制的形式, a_i 和 b_i 的关系 x_{i}=1 表示 a_i 和 b_i 不等,x_{i}=0 表示 a_i 和 b_i 相等。假如我们任选一个不为 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 的最右一位 n_1:                                                 n_1= num & 1;
  • 配合 无符号右移操作 ,可获取 num 所有位的值(即 n_1~ n_{32}):                                 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/

思路:

  1. 利用「归并排序」计算逆序对,是非常经典的做法;
  2. 关键在于「合并两个有序数组」的步骤,利用数组的部分有序性,一下子计算出一个数之前或者之后元素的逆序的个数;
  3. 前面「分」的时候什么都不做,「合」的过程中计算「逆序对」的个数;
  4. 「排序」的工作是必要的,正是因为「排序」才能在下一轮利用顺序关系加快逆序数的计算,也能避免重复计算;
  5. 在代码实现上,只需要在「归并排序」代码的基础上,加上「逆序对」个数的计算,计算公式需要自己在草稿纸上推导。

思想是「分治算法」,所有的「逆序对」来源于 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;
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值