常用的位操作
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()][]);
}