算法基础 -- 位图、前缀和、二分、比较器

本文介绍了位运算在计算机科学中的应用,包括如何获取一个数的二进制表示、32位有符号整数的范围解析、前缀和的计算以及如何通过位运算调整随机概率。同时,讨论了如何从一个等概率区间获取器设计另一个等概率区间获取器,并给出了二分查找的多种实现。此外,还探讨了位图的实现及其在存储和查找数字中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

获取一个数的二进制数标识

public void print(int num) {
   	for (int i = 31; i >= 0; i--) {
        System.out.println((num & (1 << i)) == 0 ? "0" :"1");
    }
    System.out.println();
}

32位二进制无符号数的范围 0 ~ 232
32位二进制有符号数的范围 -231 ~ 231-1 解释为什么是这个范围,因为正数包含了0,所以最终到不了231这个数,否则会超出;而负数不包含0,可以到达231

对于32位有符号数,第32位表示符号,
正数的二进制表示为:0…………后面跟其所代表的的数的二进制表示;
负数的二进制表示为:1…………后面的二进制数取反+1来表示 所以一个数的负数其实可以取反+1来表示 int c = 9; d = (~c + 1) 即d = -9
举例:
-5的二进制表示为11111111111111111111111111111011 他所代表的含义为 最前面的1代表符号为负,后面31位取反+1 得到 1000000……101 也就是-5
原因:
为什么负数要取反+1:所有算数+ - * / %等都需要使用二进制来进行;方便底层计算的时候,对于这些运算符都有一套定制的计算方法,而不用判断操作数的正负性,提高系统性能

前缀和

计算一个数组中的 L 到 R 范围的和,可以使用前缀和
前缀和就是在一个数组中,按照下标位置,记录0~N的范围和;
当想要得到L到R的范围和时,就是用 0 ~ R的和减去0 ~ L-1 的和(如果L为0,则直接返回0~R的和即可)

@Test
// 前缀和
public int prefixSum(int L, int R) {
    int[] nums = new int[] {9,2,5,4,1,5,7,12,3};
    int N = nums.length;
    // 用来记录前缀和
    int[] preSum = new int[N];
    preSum[0] = nums[0];
    for (int i = 1; i < N; i++) {
        preSum[i] = preSum[i - 1] + nums[i];
    }
    
    // 求 L 到 R 的和
    return L == 0 ? preSum[R] : preSum[R] - preSum[L - 1];
}

修改随机的概率为原来的平方

任意的x,x在 [0 ~ 1)之间,[0~x)范围上的数出现的概率由原来的x调整为x的平方

@Test
// 任意的x,x在 [0 ~ 1)之间,[0~x)范围上的数出现的概率由原来的x调整为x的平方
// 例:在[0 ~ 1),x < 0.3 的概率为 0.3; 现在要改为概率为:0.3 * 0.3
public void XToXPow2() {
    int testTimes = 10000000;
    double x = 0.3;
    int count = 0;
    for (int i = 0; i < testTimes; i++) {
        // 只有两次随机结果都是小于x的,才能count++,也就是变为两次结果概率乘积
        double XToXPow2 = Math.max(Math.random(), Math.random());
        if (XToXPow2 < x) {
            count++;
        }
    }
    System.out.println((double) count / (double) testTimes);
    System.out.println((double) Math.pow((double) x, 2));
}

给定一个区间等概率获取器,获得另一个区间的等概率获取器

给定任意一个函数,规定在某个区间上的任意一个数都是等概率返回的(例如:[a, b]上的数都是等概率返回的),然后让你求另一个区间上的数,也是等概率返回的(例如:[c, d]上的数也是等概率返回的)。
可以设计算法:

  1. 首先根据给定区间[a, b],然后定义一个0,1随机生成器:a~b平分,[a,a1]、x、[a2,19],然后如果随机到x,则重做,直到得到其他两个区间之一,三目判断返回0,1;
  2. 判断,几个二进制数可以表示 d - c 的所有数,然后执行几次上述的0,1获得器,位移相加;如果得到了 [0,d-c] 上的数,就返回 + d,否则重做,就可以得到 [c,d] 上的数等概率返回了

例子:

// 题目要求, 给定 1 ~ 5 等概率返回的函数,不可再使用java的随机函数,设计一个 1 ~ 7 等概率返回的函数

// 1 ~ 5等概率返回
public int getNum15() {
    return (int) (Math.random() * 5) + 1;
}

// 设计一个 返回 0 1 等概率的函数
public int getNum01() {
    int ans = 0;
    do {
        ans = getNum15();
    } while (ans == 3);
    return ans < 3 ? 0 : 1;
}

// 给定一个 1 ~ 5 的等概率随机函数,设计一个 0 ~ 7 的等概率返回函数
public int getNum07() {
    return (getNum01() << 2) + (getNum01() << 1) + (getNum01() << 0);
}

// 0 ~ 6等概率返回一个
public int getNum06() {
    int ans = 0;
    do {
        ans = getNum07();
    } while (ans == 7);
    return ans;
}

// 最终的到 1 ~ 7 等概率返回的函数
public int getNum17() {
    return getNum06() + 1;
}

变种:给定一个0,1不等概率获取器,定义一个0,1等概率获取

因为 getNum01EqualP() 会被执行两次,获得 1 1,0 0 的概率是不等的,而获得 1 0,0 1 的概率都是p(1 - p),所以只有两次都是 p(1 - p),才是等概率的,才会返回

// 0 1 不等概率获取器 例子
public int getNum01NotEqualP() {
    return Math.random() > 0.8 ? 1 : 0;
}

// 改为的 0 ,1 等概率获取
public int getNum01EqualP() {
    int ans = 0;
    do {
        ans = getNum01NotEqualP();
    } while (ans == getNum01NotEqualP());
    return ans;
}

二分法

// 普通二分查找
public static boolean binarySearch(int[] nums, int target) {
    if (nums == null || nums.length == 0) return false;
    // 确保有序
    Arrays.sort(nums);
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return true;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    return false;
}


// 二分查找 找到最左位置
public static int binarySearchLeft(int[] nums, int target) {
    if (nums == null || nums.length == 0) return -1;
    // 确保有序
    Arrays.sort(nums);

    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        }
    }

    if (left >= nums.length || nums[left] != target) return -1;
    return left;
}

// 二分查找 找到最右位置
public static int binarySearchRight(int[] nums, int target) {
    if (nums == null || nums.length == 0) return -1;
    // 确保有序
    Arrays.sort(nums);

    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] <= target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        }
    }

    if (right < 0 || nums[right] != target) return -1;
    return right;
}

// 局部最小值, 任意一个谷底值位置
public static int localMin(int[] nums) {
    if (nums == null || nums.length == 0) return -1;
    int N = nums.length;
    if (nums.length == 1) return 0;
    if (nums[0] < nums[1]) return 0;
    if (nums[N - 1] < nums[N - 2]) return N - 1;

    int left = 0;
    int right = N - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid + 1] > nums[mid]) right = mid;
        else left = mid + 1;
    }
    return right;
}

位图的实现

使用比哈希表节省极大空间的情况下,存储数字,判断是否存在

public class BitMap {
    // 可以存储的最大数
    private int max;
    private long[] bits;

    public BitMap(int max) {
        // +64 保证0~63存在0位置
        // 根据最大数,定义数组的最大值
        this.bits = new long[(max + 64) >> 6];
    }

    // 存入数字
    public void add(int num) {
        // 让bits的第 num >> 6索引上的 第num & 63位,置为1,代表nums存在(或1为1)
        // 1l << (num & 63) 代表第num & 63位上为1,其余位置全0的64位二进制表示
        bits[num >> 6] |= (1l << (num & 63));
    }

    // 删除数组
    public void delete(int num) {
        // bits的第 num >> 6索引上的 第num & 63位,置为0,代表nums不存在(与0为0)
        // ~(1l << (num & 63)) 代表第num & 63位上为0,其余位置全1的64位二进制表示
        bits[num >> 6] &= ~(1l << (num & 63));
    }

    // 判断数组是否存在
    public boolean contains(int num) {
        // bits的第 num >> 6索引上的 第num & 63位,和1与,如果为1,返回1,说明存在;如果为0,返回0,说明不存在
        // 所以结果不为0,代表存在;否则不存在
        return (bits[num >> 6] & (1l << (num & 63))) != 0;
    }
}

位运算实现+、-、*、/

加法:对应LeetCode 剑指 Offer 65. 不用加减乘除做加法

在这里插入图片描述

首先要明确:

  1. ^:表示二进制的无进位相加,0 1 1 0 1 + 1 0 1 1 1 = 1 1 0 1 0 (1 和 1相加进位消除为0, 1 和 0相加无进位为1)
  2. &:两二进制位相与,同1为1,有0则0

所以上面两个符号刚好可以配合,^进行无进位相加,然后&后向左移动一位就是进位信息(同1则需进位,向前一位进位,所以向左移动一位)
循环上述过程,无符号相加,然后再加上进位;因为加上进位之后还有可能需要进位,所以递归调用;

class Solution {
    public int add(int a, int b) {
        if(b == 0) {
            return a;
        }
        if(a == 0) {
            return b;
        }

        // 两数无符号相加
        int sum = a ^ b;
        // 保存进位
        int carry = (a & b) << 1; 
        return add(sum, carry);
    }
}
减法:就等于 a加上b的相反数

因为不能出现+、-、*、/,有因为b的相反数等于 ~b +1(b取反+1);所以可以用**add(a,add(~b,1))**表示

乘法

在这里插入图片描述

public static int mul(int a, int b) {
    int res = 0; // 存储结果
    // 其中一个乘数不为0,就循环相乘
    while (b != 0) {
        // b 的二进制末尾为1,将 a 加入结果一份
        if ((b & 1) == 1) {
            res = add(res, a);
        }
        // a 左移一位后面补0
        a <<= 1;
        // b 右移一位,用0补
        b >>>= 1;
    }
    return res;
}
除法:对应LeetCode 29. 两数相除

把除数不停的往左移,当除数离被除数最近的时候就用被除数减去除数(除数中一定包含了这个左移后的数,减去接着循环);然后将结果的移位位置上设置为1

 private static boolean isNeg(int num) {
   return num < 0;
}
// 除法
public static int div(int a, int b) {
    // 保证除数和被除数都是正数
    int x = isNeg(a) ? add(~a, 1) : a;
    int y = isNeg(b) ? add(~b, 1) : b;
    int res = 0;

    for (int i = 30; i >= 0; i = sub(i, 1)) {
        // 让被除数 x 右移, 找与 y 最接近的最大2的幂
        // 如果找到比 y 大的最接近的2的幂,记录i,将结果的第i为置为1,然后x减去左移i位后的y
        // 周而复始,知道计算结果所有位的情况
        if ((x >> i) >= y) {
            res |= (1 << i);
            x = sub(x, y << i);
        }
    }
    if (isNeg(a) && isNeg(b) || !isNeg(a) && !isNeg(b)) {
        return res;
    } else {
        return add(~res, 1);
    }
}

题目解法

class Solution {
    public static int add(int a, int b) {
		int sum = a;
		while (b != 0) {
			sum = a ^ b;
			b = (a & b) << 1;
			a = sum;
		}
		return sum;
	}

	public static int negNum(int n) {
		return add(~n, 1);
	}

	public static int minus(int a, int b) {
		return add(a, negNum(b));
	}

	public static int multi(int a, int b) {
		int res = 0;
		while (b != 0) {
			if ((b & 1) != 0) {
				res = add(res, a);
			}
			a <<= 1;
			b >>>= 1;
		}
		return res;
	}

	public static boolean isNeg(int n) {
		return n < 0;
	}

	public static int div(int a, int b) {
		int x = isNeg(a) ? negNum(a) : a;
		int y = isNeg(b) ? negNum(b) : b;
		int res = 0;
		for (int i = 30; i >= 0; i = minus(i, 1)) {
			if ((x >> i) >= y) {
				res |= (1 << i);
				x = minus(x, y << i);
			}
		}
		return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
	}

    // 左神新手班 第五节
	public static int divide(int a, int b) {
		if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
			return 1;
		} else if (b == Integer.MIN_VALUE) {
			return 0;
		} else if (a == Integer.MIN_VALUE) {
			if (b == negNum(1)) {
				return Integer.MAX_VALUE;
			} else {
				// a / b
				// (a + 1) / b = c
				// a - (b * c) = d
				// d / b = e
				// return c + e
				int c = div(add(a, 1), b);
				return add(c, div(minus(a, multi(c, b)), b));
			}
		} else {
			return div(a, b);
		}
	}
}

比较器

比较器规则
相关题目1, LeetCode 23. 合并K个升序链表
相关题目2, LeetCode 937. 重新排列日志文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值