9.常用的数学运算技巧

常用的位操作

1.字符异或后会得到对应的大小写字符

2.判断两个数是否为异号

3.不使用零时变量交换两个数

'd'^' '=='D'

 int x=1,y=-1;//0被认为是正数
 System.out.println((x^y)<0);//true

 int x=1,y=0;
 x^=y;
 y^=x;
 x^=y;
//现在x是0,y是1

191--计算1的位数

前置技巧n&(n-1)可以消除数字n二进制中的最后一个1

  public int hammingWeight(int n) {
        int res=0;
        while(n!=0){
            n=n&(n-1);
            res++;
        }
        return res;
    }

231--判断一个数是不是2的幂次

如果一个数是2的幂次,那么它的二进制一定只有一个1

  public boolean isPowerOfTwo(int n) {
        if(n<=0)return false;
        return (n&(n-1))==0;//消除了1后直接等于0了
    }

136--只出现一次的数字

前置知识:一个数和它本身做异或运算结果为 0,即 a ^ a = 0;一个数和 0 做异或运算的结果为它本身,即 a ^ 0 = a, 并且异或运算满足交换律和结合律

public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }

172--阶乘后的零的数量

如果两个数的末尾有零,那么一定有2和5作为因子存在,所以计算出有n!可以分解出多少2和5就行了,因为每个偶数都可以分解出2,所以2肯定要比5多,所以只要看有多少个5就行了,继续分析,每个含有5的因子每隔5个出现一次,并且每隔25个数字出现的是两个5,最终5的个数就是n/5+n/25+n/125+...

public int trailingZeroes(int n) {
        int count = 0;
        while (n > 0) {
            count += n / 5;
            n = n / 5;
        }
        return count;
    }

793--阶乘函数后k个零

  

由于一定存在 zeta(5a-1) < zeta(5a) = zeta(5a+1) = zeta(5a+2) = zeta(5a+3) = zeta(5a+4) < zeta(5a+5),即如果存在某个 x 使得 zeta(x) = K,那么一定存在连续 5 个数的阶乘末尾零的个数都为 K;如果不存在这样的 x,那么阶乘末尾零的个数为 K 的数字只有 0 个。

k = zeta(x) = int(x/5) + int(x/25) + ... <= x/5 + x/25 + ... = 4x/5 ,故有 x >= 5K/4 >= K 。x <=10*K+1是个很宽泛的的上界,事实上这一题x <= 5*K+1 也是过

 public int preimageSizeFZF(long K) {
        long lo = K, hi = 10*K + 1;
        while (lo < hi) {
            long mi = lo + (hi - lo) / 2;
            long zmi = zeta(mi);
            if (zmi == K) return 5;
            else if (zmi < K) lo = mi + 1;
            else hi = mi;
        }
        return 0;
    }

    public long zeta(long x) {
        if (x == 0) return 0;
        return x/5 + zeta(x/5);
    }

448--找到数组中消失的数字

 public List<Integer> findDisappearedNumbers(int[] nums) {
        int n=nums.length;
        for (int num : nums) {
            int x=(num-1)%n;//让自己充当哈希表的索引x,由于同一个值可能被增加过多次,所以需要通过取模来还原原来的值
            nums[x]+=n;
        }
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if(nums[i]<=n)list.add(i+1);
        }
        return list;
    }

645--错误的集合

public int[] findErrorNums(int[] nums) {
        int n=nums.length;
        HashSet<Integer> set = new HashSet<>();
        int wrongSum=0;
        int sum=(1+n)*n/2;
        int lostNum=0;
        for (int num : nums) {
            set.add(num);
            wrongSum+=num;
        }
        for (int i =1; i <=n ; i++) {
            if(!set.contains(i)){
                lostNum=i;
                break;
            }
        }
        return new int[]{wrongSum-sum+lostNum,lostNum};
    }

382--链表随机节点

ListNode node;
    public likou382(ListNode head) {
        node=head;
    }

    public int getRandom() {
        Random random = new Random();
        int res=0;
        int i=0;
        ListNode p=node;
        while (p!=null){
            //生成一个在[0,i)之间的整数,那么等于0的概率就是1/i
            if(random.nextInt(++i)==0){
                res=p.val;
            }
            p=p.next;
        }
        return res;
    }

398--随机数索引

private int[] nums;
    public likou398(int[] nums) {
        this.nums = nums;
    }

    public int pick(int target) {
        Random r = new Random();
        int n = 0;
        int index = 0;
        for(int i = 0;i < nums.length;i++)
            if(nums[i] == target){
                //计算同一个target的个数
                n++;
                //我们以同一个数字的频数1/n的概率选出其中一个索引
                if(r.nextInt(n) == 0) index = i;
            }
        return index;
    }

292--Nim游戏

游戏规则是这样的:你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。

假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 n,返回你是否能赢(true 或 false)。

比如现在有 4 颗石子,算法应该返回 false。因为无论你拿 1 颗 2 颗还是 3 颗,对方都能一次性拿完,拿走最后一颗石子,所以你一定会输。

如果我能赢,那么最后轮到我取石子的时候必须要剩下 1~3 颗石子,这样我才能一把拿完。

如何营造这样的一个局面呢?显然,如果对手拿的时候只剩 4 颗石子,那么无论他怎么拿,总会剩下 1~3 颗石子,我就能赢。

如何逼迫对手面对 4 颗石子呢?要想办法,让我选择的时候还有 5~7 颗石子,这样的话我就有把握让对方不得不面对 4 颗石子。

如何营造 5~7 颗石子的局面呢?让对手面对 8 颗石子,无论他怎么拿,都会给我剩下 5~7 颗,我就能赢。

这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输

    public boolean canWinNim(int n) {
        return n%4!=0;
    }

319--灯泡开关游戏

一盏灯如果要被打开就必须按下奇数次,比如对于第16盏灯进行16轮,它会被按下五次,因为其因子为1*16 2*8 4*4,其中因为在第四轮的时候,两个因子重合了,因此第16栈灯在16回合后是亮着的,我们对16进行开方计算,就能获得最大的双乘因子4*4,此外还有3*3,2*2,1*1也就是1,4,9盏灯会是亮的,故可以开方解决

 public int bulbSwitch(int n) {
        return (int)Math.sqrt(n);
    }

likou560--和为k的子数组

 public int subarraySum(int[] nums, int k) {
        int ans=0;
        int n=nums.length;
        //PreSum(i)代表从0到i-1的和
        int[] preSum=new int[n+1];
        //base
        preSum[0]=0;
        for (int i = 1; i <= n; i++) {
            preSum[i]=preSum[i-1]+nums[i-1];
        }
        //至此我们获得了所有的前缀和,两个前缀和相减就可以获得所有的子集和
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <i ; j++) {
                if(preSum[i]-preSum[j]==k){
                    ans++;
                }
            }
        }
        return ans;
    }

可以再进一步进行优化 计算preSum[i]=k+preSum[j]的量就行,其道理和两数之和比较相似,使用哈希表来替代for循环

  public int subarraySum(int[] nums, int k) {
        int n = nums.length;
        // map:前缀和 -> 该前缀和出现的次数
        HashMap<Integer, Integer> preSum = new HashMap<>();
        // base case
        preSum.put(0, 1);

        int ans = 0, sum0_i = 0;
        for (int i = 0; i < n; i++) {
            sum0_i += nums[i];
            // 这是我们想找的前缀和 nums[0..j]
            int sum0_j = sum0_i - k;
            // 如果前面有这个前缀和,则直接更新答案
            if (preSum.containsKey(sum0_j))
                ans += preSum.get(sum0_j);
            // 把前缀和 nums[0..i] 加入并记录出现次数
            preSum.put(sum0_i, preSum.getOrDefault(sum0_i, 0) + 1);
        }
        return ans;
    }

1109--航班预订统计

 暴力解法:

  public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] ans = new int[n];
        for (int i = 0; i < bookings.length; i++) {
            for (int j = bookings[i][0]-1; j <=bookings[i][1]-1 ; j++) {
                ans[j]+=bookings[i][2];
            }
        }
        return ans;
    }

差分可以看做是求前缀和的逆向过程,对于将区间[l,r]整体增加v的操作,可以对差分数组c的影响看成两部分

  • 对于c[l]+=v:由于差分是前缀和的逆向古城,这个操作对于将来的查询而言,带来的影响是对于所有的下标大于l的位置都增加了值v
  • 对于c[r+1]-=v:由于我们只期望对[l,r]产生影响,因此需要对下标大于r的位置进行减值操作,从而抵消影响
 public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] c=new int[n+1];
        //进入区间的时候加上value,出去区间的时候减去value
        for (int[] b : bookings) {
            int l=b[0]-1,r=b[1]-1,v=b[2];
            c[l]+=v;
            c[r+1]-=v;
        }
        int[] ans=new int[n];
        ans[0]=c[0];
        //给ans赋值
        for (int i = 1; i < n; i++) {
            ans[i]=ans[i-1]+c[i];
        }
        return ans;
    }

215--数组中的第k个最大的元素

二叉堆解法:

 public int findKthLargest(int[] nums, int k) {
        //该数据结构可以理解为一个筛子,小的元素会在上面,大的元素会在下面
        PriorityQueue<Integer> pq=new PriorityQueue<>();
        for (int num : nums) {
            pq.offer(num);
            //当超过k个元素的时候将元素取出,最后堆里就只剩下k个元素,而且都是最大的
            if(pq.size()>k){
                pq.poll();
            }
        }
        return pq.peek();
    }

快速排序算法:若要对 nums[lo..hi] 进行排序,我们先找一个分界点 p,通过交换元素使得 nums[lo..p-1] 都小于等于 nums[p],且 nums[p+1..hi] 都大于 nums[p],然后递归地去 nums[lo..p-1]nums[p+1..hi] 中寻找新的分界点,最后整个数组就被排序了。

    public int findKthLargest(int[] nums, int k) {
       sort(nums,0,nums.length-1);
       return nums[nums.length-k];
    }
    void sort(int[] nums, int lo, int hi) {
        if (lo >= hi) return;
        // 通过交换元素构建分界点索引 p
        int p = partition(nums, lo, hi);
        // 现在 nums[lo..p-1] 都小于 nums[p],
        // 且 nums[p+1..hi] 都大于 nums[p]
        sort(nums, lo, p - 1);
        sort(nums, p + 1, hi);
    }
    int partition(int[] nums, int lo, int hi) {
        if (lo == hi) return lo;
        // 将 nums[lo] 作为默认分界点 pivot
        int pivot = nums[lo];
        // j = hi + 1 因为 while 中会先执行 --
        int i = lo, j = hi + 1;
        while (true) {
            // 保证 nums[lo..i] 都小于 pivot
            while (nums[++i] < pivot) {
                if (i == hi) break;
            }
            // 保证 nums[j..hi] 都大于 pivot
            while (nums[--j] > pivot) {
                if (j == lo) break;
            }
            if (i >= j) break;
            // 如果走到这里,一定有:
            // nums[i] > pivot && nums[j] < pivot
            // 所以需要交换 nums[i] 和 nums[j],
            // 保证 nums[lo..i] < pivot < nums[j..hi]
            swap(nums, i, j);
        }
        // 将 pivot 值交换到正确的位置
        swap(nums, j, lo);
        // 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
        return j;
    }
    // 交换数组中的两个元素
    void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

快速选择算法:

    int findKthLargest(int[] nums, int k) {
        int lo = 0, hi = nums.length - 1;
        // 索引转化
        k = nums.length - k;
        while (lo <= hi) {
            // 在 nums[lo..hi] 中选一个分界点
            int p = partition(nums, lo, hi);
            if (p < k) {
                // 第 k 大的元素在 nums[p+1..hi] 中
                lo = p + 1;
            } else if (p > k) {
                // 第 k 大的元素在 nums[lo..p-1] 中
                hi = p - 1;
            } else {
                // 找到第 k 大元素
                return nums[p];
            }
        }
        return -1;
    }
    int partition(int[] nums, int lo, int hi) {
        if (lo == hi) return lo;
        // 将 nums[lo] 作为默认分界点 pivot
        int pivot = nums[lo];
        // j = hi + 1 因为 while 中会先执行 --
        int i = lo, j = hi + 1;
        while (true) {
            // 保证 nums[lo..i] 都小于 pivot
            while (nums[++i] < pivot) {
                if (i == hi) break;
            }
            // 保证 nums[j..hi] 都大于 pivot
            while (nums[--j] > pivot) {
                if (j == lo) break;
            }
            if (i >= j) break;
            // 如果走到这里,一定有:
            // nums[i] > pivot && nums[j] < pivot
            // 所以需要交换 nums[i] 和 nums[j],
            // 保证 nums[lo..i] < pivot < nums[j..hi]
            swap(nums, i, j);
        }
        // 将 pivot 值交换到正确的位置
        swap(nums, j, lo);
        // 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
        return j;
    }
    // 交换数组中的两个元素
    void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

241--为运算表达式设计优先级 

 采用分治算法的逻辑,不聚焦于嵌套括号的情况(可以交给递归去做),我们只需要分析出括号不嵌套的情况就行了,对于1+2*3-4*5,只有四种情况,对于后续怎么做,那是递归的事情 

public List<Integer> diffWaysToCompute(String expression) {
        int n=expression.length();
        List<Integer> res=new ArrayList<>();
        for (int i = 0; i < n; i++) {
            char c=expression.charAt(i);
            if(c=='+'||c=='*'||c=='-'){
                //分
                //substring为左闭右开区间
                List<Integer> left=diffWaysToCompute(expression.substring(0,i));
                //一个参数为从i+1往后
                List<Integer> right=diffWaysToCompute(expression.substring(i+1));
                //治
                for (Integer a : left) {
                    for (Integer b : right) {
                        if(c=='+')res.add(a+b);
                        else if(c=='-')res.add(a-b);
                        else res.add(a*b);
                    }
                }
            }
        }
        //base条件 res为空说明此时c是一个数字,也就是不能再往下分了
        if(res.isEmpty()){
            res.add(Integer.parseInt(expression));
        }
        return res;
    }

 1288--删除被覆盖区间

再起点升序,终点逆序排列以后,只需要比较终点的位置,如果终点位置比上一个小,那么就可以确定为重复区间,否则就更新终点位置

public int removeCoveredIntervals(int[][] intervals) {
        int n=intervals.length;
        //起点升序,终点逆序
        Arrays.sort(intervals,(a,b)->{return a[0]==b[0]?b[1]-a[1]:a[0]-b[0];});
        int right=intervals[0][1];
        int res=n;
        for (int i = 1; i < n; i++) {
            int[] now=intervals[i];
            if(now[1]<=right){
                res--;
            }else if(now[0]<right||now[0]>=right){
                right=now[1];
            }
        }
        return res;
    }

56--合并区间

    public int[][] merge(int[][] intervals) {
        // 先按照区间起始位置排序
        Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]);
        // 遍历区间
        int[][] res = new int[intervals.length][2];
        int idx = -1;
        for (int[] interval: intervals) {
            // 如果结果数组是空的,或者当前区间的起始位置 > 结果数组中最后区间的终止位置,
            // 则不合并,直接将当前区间加入结果数组。
            if (idx == -1 || interval[0] > res[idx][1]) {
                res[++idx] = interval;
            } else {
                // 反之将当前区间合并至结果数组的最后区间
                res[idx][1] = Math.max(res[idx][1], interval[1]);
            }
        }
        return Arrays.copyOf(res, idx + 1);
    }

 986--区间列表的交集

 public int[][] intervalIntersection(int[][] A, int[][] B) {
    List<int[]> ans = new ArrayList();
    int i = 0, j = 0;

    while (i < A.length && j < B.length) {
      // Let's check if A[i] intersects B[j].
      // lo - 区间开始点
      // hi - 区间结束点
      int lo = Math.max(A[i][0], B[j][0]);
      int hi = Math.min(A[i][1], B[j][1]);
      if (lo <= hi)
        ans.add(new int[]{lo, hi});

      // 如果当前B的结束点大于i的结束点,把i往前移否则j往前移
      if (A[i][1] < B[j][1])
        i++;
      else
        j++;
    }

    return ans.toArray(new int[ans.size()][]);
  }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值