java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 |
---|
- 思路分析
位运算分治算法
public class Solution {
// you need to treat n as an unsigned value
/** 位运算分治法,O(1) O(1)
* 位运算分治
* 就是两个两个算,然后四个四个算这样,为了方便理解,用十进制9876543210举例
* 首先两个两个分治,98,76,54,.... 可以通过 & M1 = 0x55555555; // 01010101010101010101010101010101 来进行
* 然后4个,4个分治,9876,5432,... & M2 = 0x33333333; // 00110011001100110011001100110011
* 然后8个,8个, 98765432,.... & M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
* 然后16个,16个, & M8 = 0x00ff00ff; // 00000000111111110000000011111111
* 然后32个 依次类推 &M16= 0x0000ffff; // 00000000000000001111111111111111
* 我们是int类型,一共32位,
*
* 现在假设两个两个分治,10,11,01,00,如何知道有几个1呢?
* 10 我们可以看到有1个1。 取出1和0,然后相加,变成01,也就是1,就是分治结果
* 11 有2个1,取出1和1,相加 变成10,也就是2,就是分治结果
* 举个例子========================================================================
* 例如整数987654321的二进制是0011 1010 1101 1110 0110 1000 1011 0001,其中1出现的次数为17
* 两两分组,计算每组高1位和低1位中的1的个数,相加来计算
* 0011 1010 1101 1110 0110 1000 1011 0001
* —— 00 11 10 10 11 01 11 10 01 10 10 00 10 11 00 01
* —— 0+0 1+1 1+0 1+0 1+1 0+1 1+1 1+0 0+1 1+0 1+0 0+0 1+0 1+1 0+0 0+1
* —— 00 10 01 01 10 01 10 01 01 01 01 00 01 10 00 01
* 0个 2个 1个 1个 可见成功统计出 0011 1010 有4个1,两个两个计数,结果0010 0101 => 00表示0个1,10表示2个1,01表示1个1,01表示1个1
* 四四分组,计算每组高2位和低2位中的1的个数,相加来计算
* 0010 0101 1001 1001 0101 0100 0110 0001
* —— 0010 0101 1001 1001 0101 0100 0110 0001
* —— 00+10 01+01 10+01 10+01 01+01 01+00 01+10 00+01
* —— 0010 0010 0011 0011 0010 0001 0011 0001
* 2个 2个 统计出0010 0101 有4个1,结果变为 0010 0010 => 0010 表示2个1,0010 表示2个1
* 八八分组,计算每组高4位和低4位中的1的个数,相加来计算
* 0010 0010 0011 0011 0010 0001 0011 0001
* —— 00100010 00110011 00100001 00110001
* —— 0010+0010 0011+0011 0010+0001 0011+0001
* —— 00000100 00000110 00000011 00000100
* 4个 统计出0010 0010 有4个1 结果变为 0000 0100 表示4个1
* 十六十六分组,计算每组高8位和低8位中的1的个数,相加来计算
* 00000100 00000110 00000011 00000100
* —— 0000010000000110 0000001100000100
* —— 00000100+00000110 00000011+00000100
* —— 0000000000001010 0000000000000111
* 三十二三十二分组,计算每组高16位和低16位中的1的个数,相加最后得到的二进制结果转换成十进制数就是该数中1的个数
* 0000000000001010 0000000000000111
* —— 0000000000001010 0000000000000111
* —— 0000000000001010+0000000000000111
* —— 0000000000000000 0000000000010001
* —— 17
*/
/**
* 如何让分治进行运算呢? 也就是 1011中两个两个分,10,11怎么运算
* 比如10 ,我们要让1和0运算 就让他们错位即可,比如>>>1 ,但是这样会有问题
* i = 10 ,i >>> 1 = 01
* i + (i>>>1) = 10 + 01 ,这样就参与了运算
* 但是11 , 我们要1和1运算
* i = 11, i>>1 = 01
* 11 + 01 这样是错误的
* 因此我们需要通过 & M1 = 0x55555555; // 01010101010101010101010101010101 来进行
* i1 = 1011 & M1 = 0001
* i2 >>> 1 = 0101 & M1 = 0101
* 此时 i1 & i2 = 0001 + 0101 = 0110 ================= 01 表示1个1,10 表示2个1 。一共3个1.
*
*/
/** 因此可以将代码写成这样,但是有很多多余操作可以优化
* i = 987654321;
* i = (i & M1) + ((i>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
* i = (i & M2) + ((i>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
* i = (i & M4) + ((i>>4)&M4); //四个运算四个
* i = (i & M8) + ((i>>8)&M8); //八个运算八个
* i = (i & M16) + ((i>>16)&M16); //16个运算16个
* return i;
*/
private static final int M1 = 0x55555555; // 01010101010101010101010101010101
private static final int M2 = 0x33333333; // 00110011001100110011001100110011
private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
private static final int M16= 0x0000ffff; // 00000000000000001111111111111111
public int hammingWeight1(int n) {
n = (n & M1) + ((n>>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
n = (n & M2) + ((n>>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
n = (n & M4) + ((n>>>4)&M4); //四个运算四个
n = (n & M8) + ((n>>>8)&M8); //八个运算八个
n = (n & M16) + ((n>>>16)&M16); //16个运算16个
return n;
}
/**
接下来是Integer.bitCount()JavaApi的源码中对上面代码的优化
首先,1个1个比较,两个两个分治,比如1011
(1011 >>> 1) & M1 = 0101
1011 - 0101 = 0110 , 01表示1个1,10表示2个1
我们发现 直接用i - 右移的结果就是得数,因此
n = (n & M1) + ((n>>>1)&M1);
可以优化为
n = n - ((n>>>1)&M1); 少了一次 & 运算
=========================================
两个两个比较时,没有什么好的优化方式,因此(n & M2) + ((n>>>2)&M2);不变
=========================================
4个4个比较时,和1个1个比较一样,可以直接优化掉一个&运算。>>> 4 然后相加 ,然后&M4
0010 0010 0011 0011 0010 0001 0011 0001 直接>>>4
0000 0010 0010 0011 0011 0010 0001 0011 然后相加
0010 0100 0101 0110 0101 0011 0100 0100 然后 & M4
0000 1111 0000 1111 0000 1111 0000 1111
0000 0100 0000 0110 0000 0011 0000 0100 结果正确
因此(n & M4) + ((n>>>4)&M4)
可以优化为
(n + (n>>>4)) & M4
=========================================
八个八个比较,其实此时就已经算出结果了,可以进行汇总了,所以我们需要让答案全部集中到一起
我们选择全部集中在最后6位中,因此直接位移>>>4相加,这样答案整体后向移动了
0000 0100 0000 0110 0000 0011 0000 0100 >>> 8
0000 0000 0000 0100 0000 0110 0000 0011 相加
0000 0100 0000 1010 0000 1001 0000 0111
没用了 没用了 前8位已经没用了
因此,(n & M8) + ((n>>>8)&M8)可以优化为n + (n >>> 8)
==========================================
然后同理,这次一次移动16个,进行分治结果整合
0000 0100 0000 1010 0000 1001 0000 0111 >>> 16
0000 0000 0000 0000 0000 0100 0000 1010 相加
0000 0100 0000 1010 0000 1101 0001 0001
没用 没用 没用 没用 没用 没用
因此 (n & M16) + ((n>>>16)&M16)可以优化为 n + (n >>> 16)
=========================================
最终,取最后6位即可,为什么取6位?如果取5位就是00011111 = 31,int 类型有32位,如果取5位不够当做结果
00111111 = 1 + 2 + 4 + 8 + 16 + 32 可以保存所有结果
因此通过n & 0x3f 即可取出结果(0x3f = 00000000 00000000 00000000 00111111 )
*/
public int hammingWeight(int n) {
n = n - ((n>>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
n = (n & M2) + ((n>>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
n = (n + (n>>>4)) & M4; //四个运算四个
n = n + (n >>> 8); //八个运算八个
n = n + (n >>> 16); //16个运算16个
return n & 0x3f;//0x3f = 00000000 00000000 00000000 00111111 也就是只保留后6位
}
}
刷题一定要坚持,总结套路,不单单要把题做出来,要举一反三,也要参考别人的思路,学习别人解题的优点,找出你觉得可以优化的点。
- 单链表解题思路:双指针、快慢指针、反转链表、预先指针
- 双指针:对于单链表而言,可以方便的让我们遍历结点,并做一些额外的事
- 快慢指针:常用于找链表中点,找循环链表的循环点,一般快指针每次移动两个结点,慢指针每次移动一个结点。
- 反转链表:通常有些题,将链表反转后会更好做,一般选用三指针迭代法,递归的空间复杂度有点高
- 预先指针:常用于找结点,比如找倒数第3个结点,那么定义两个指针,第一个指针先移动3个结点,然后两个指针一起遍历,当第一个指针遍历完成,第二个指针指向的结点就是要找的结点
- 数组解题思路:双指针、三指针,下标标记
- 双指针:多用于减少时间复杂度,快速遍历数组
- 三指针:多用于二分查找,分为中间指针,左和右指针
- 下标标记:常用于在数组范围内找东西,而不想使用额外的空间的情况,比如找数组长度为n,元素取值范围为[1,n]的数组中没有出现的数字,遍历每个元素,然后将对应下标位置的元素变为负数或者超出[1,n]范围的正数,最后没有发生变化的元素,就是缺少的值。
- 差分数组:对差分数组求前缀和即可得到原数组
- 用差值,作为下标,节省空间找东西。比如1900年到2000年,就可以定义100大小的数组,每个数组元素下标的查找为1900。
- 前缀和数组,对于数组 [1,2,2,4],其差分数组为 [1,1,0,2],差分数组的第 ii个数即为原数组的第 i-1 个元素和第 i个元素的差值,也就是说我们对差分数组求前缀和即可得到原数组
- 前缀和:假设有一个数组arr[1,2,3,4]。然后创建一个前缀和数组sum,记录从开头到每个元素区间的和。第一个元素是0。第二个元素,保存第一个和sum[1] = sum[0]+arr[0],第二个元素,保存第二个和sum[2] = sum[1]+arr[1]
- 位运算,异或。不使用额外空间找东西可用
- 任何数异或0 都为本身。a^0 = a
- 自己异或自己 = 0。a^a = 0
- 满足交换律:aba = baa = b(aa) = b^0 = b
- 栈解题思路:倒着入栈,双栈
- 倒着入栈:适用于出栈时想让输出是正序的情况。比如字符串’abc’,如果倒着入栈,那么栈中元素是(c,b,a)。栈是先进后出,此时出栈,结果为abc。
- 双栈:适用于实现队列的先入先出效果。一个栈负责输入,另一个栈负责输出。