04树与图

⚠️面试题 04.01. 节点间通路

难度中等65

节点间通路。给定有向图,设计一个算法,找出两个节点之间是否存在一条路径。

  • 法一:

递归, 从target反向寻找路径,

  • 注意不要直接return findWhetherExistsPath(n,graph,start,arr[0]);

    • 因为可能有多个节点都能到达target , 而 能从starttarget 的却只有一个

注意点 :

  • 这个方法无法解决自环的问题:

    比如:

    • findWhetherExistsPath(6, new int[][]{{1,2},{2,3},{3,4},{4,5},{4,1}},5,1) 自环

    但是题目给的数据没有涉及到自环的,

解决自环问题:

  • 加一个visited数组即可
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target) {
if(start==target) return true;
boolean res=false;
for(int[] arr:graph){
if(arr[1]==target){
res= findWhetherExistsPath(n,graph,start,arr[0]);
}
if(res) return res;
}
return res;
}
}

解决自环问题:

  • 法二: 明天再写

DFS

面试题 04.02. 最小高度树

难度简单135

给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉搜索树

image-20220816171128457

因为题目给的是一个升序数组,需要构建二叉搜索树

当构造二叉搜索树的左右平衡时深度是最小。

如果想要左右平衡可以让根节点是排序数组的中点,这样左边有x个右边也约有x个,大致保持一致。

构造树一般采用的是前序遍历,

因为先构造中间节点,然后递归构造左子树和右子树。先要找到数组中中间值和对应的下标,

中间值构造根节点,下标用来下一步分割数组。

中间值所在的下标左区间 构造左子树,中间值所在的下标右区间 构造右子树。

步骤:

  • 递归截止条件:

  • left>right

  • 遍历数组 : left -> right , 找到数组的中间值以及对应的下标,

    • mid= left+ (right-left)/2
  • 然后创建根节点 new TreeNode(nums[mid])

  • 接着递归左右子树:

    • root.left=process(left,mid,nums)
    • root.left=process(mid+1,right,nums)
  • 最后返回根节点 return root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return process(0,nums.length,nums); // 左闭右开
}
//[-10,-3,0,5,9]
TreeNode process(int left,int right,int[]nums){
if (left>=right) return null;
int mid=left+(right-left)/2;
TreeNode root=new TreeNode(nums[mid]);
root.left=process(left,mid,nums); //左闭右开:[left, mid)
root.right=process(mid+1,right,nums); //左闭右开:[mid+1, right)
return root;
}
}

面试题 04.03. 特定深度节点链表

难度中等78

给定一棵二叉树,设计一个算法,创建含有某一深度上所有节点的链表(比如,若一棵树的深度为 D,则会创建出 D 个链表)。返回一个包含所有深度的链表的数组。

层序遍历+ 链表构造

边遍历, 边new 链表节点

image-20220816233118771image-20220816233118771

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public ListNode[] listOfDepth(TreeNode tree) { // 边遍历 , 边造链表
if(tree==null) return new ListNode[0];
Queue<TreeNode> queue=new LinkedList<>();
List<ListNode> list=new ArrayList<>();
ListNode head=new ListNode(Integer.MAX_VALUE);
queue.add(tree);
while(queue.size()!=0){
int size=queue.size();
ListNode cur=head;
for (int i = 0; i < size; i++) {
TreeNode tmp=queue.poll();
cur.next=new ListNode(tmp.val);
cur=cur.next;
if(tmp.left!=null)
queue.add(tmp.left);
if(tmp.right!=null)
queue.add(tmp.right);
}
list.add(head.next); // 每次循环cur 的指向都变为head, 因此可以直接add(head.next)
}
ListNode []res=new ListNode[list.size()];
for (int i = 0; i < res.length; i++) {
res[i]=list.get(i);
}
return res;
}
}

面试题 04.04. 检查平衡性

难度简单91

实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个节点,其两棵子树的高度差不超过 1。

平衡二叉树的条件:

  1. 左右子树的都是平衡二叉树
  2. 左右子树最大深度相差不超过1

递归求解即可

  • 注意所有的左右子树必须都满足是平衡二叉树

image-20220816234444257image-20220816234444257

1
2
3
4
5
6
7
8
9
class Solution {
public boolean isBalanced(TreeNode root) {
return root==null||(isBalanced(root.left)&&isBalanced(root.right)&&Math.abs(calDepth(root.left)-calDepth(root.right))<=1);
}
public int calDepth(TreeNode root){
if(root==null) return 0;
return Math.max(calDepth(root.left),calDepth(root.right))+1;
}
}

面试题 04.05. 合法二叉搜索树

难度中等85

实现一个函数,检查一棵二叉树是否为二叉搜索树。

  • 中序遍历二叉树, 如果是二叉搜索树, 那么得到的序列 一定是有序的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2ms
class Solution {
List<Integer> list=new ArrayList<>();
public boolean isValidBST(TreeNode root) {
inOrder(root); // 中序遍历二叉树, 如果是二叉搜索树, 那么得到的序列 一定是有序的
for (int i = 0; i < list.size()-1; i++) {
if(list.get(i)>=list.get(i+1)){
return false;
}
}
return true;
}
public void inOrder(TreeNode root){
if(root==null) return ;
inOrder(root.left);
list.add(root.val);
inOrder(root.right);
}
}
  • 递归解法: 0ms

写法一

注意灵活运用 二叉搜索树的特性以及 Integer 的特性 (包装类)

根据二叉搜索树的特性, 在递归调用左右子树的时候, 可以更改上下界

  • 递归左子树时, 上下界为: (low, val)
  • 右子树 : (val, high)

函数调用时的入口为: dfs(root,Integer.MIN_VALUE,Integer.MAX_VALUE)

  • 但是有一个这样的样例 :image-20220817143818261image-20220817143818261

所以还是定义为null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public boolean isValidBST(TreeNode root) {
return dfs(root,null,null);
}

public boolean dfs(TreeNode root,Integer low,Integer high){
// 性质, 当前节点的值, 一定小于自己的最近的祖先的值, 并且小于根节点的值
if(root==null) return true;
int val=root.val;
if(low!=null&&val<=low) {
return false;
}
if(high!=null&&val>=high){
return false;
}
return dfs(root.left,low,val)&&dfs(root.right,val,high);
}
}

写法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, null, null);
}

public boolean helper(TreeNode node, Integer lower, Integer upper) {
if (node == null) {
return true;
}

int val = node.val;
if (lower != null && val <= lower) {
return false;
}
if (upper != null && val >= upper) {
return false;
}

if (!helper(node.right, val, upper)) {
return false;
}
if (!helper(node.left, lower, val)) {
return false;
}
return true;
}
}

面试题 04.06. 后继者

难度中等203

设计一个算法,找出二叉搜索树中指定节点的“下一个”节点(也即中序后继)。

如果指定节点没有对应的“下一个”节点,则返回null

由于本题的要求是寻找目标节点的中序遍历(左根右)的下一个节点,

因此对应的当前的节点的val与目标节点的val 有两种对应情况:

  • root.val<= p.val , 此时 p的下一个节点是在右子树
  • root.val > p.val , 此时 p的下一个节点是在左子树

一直递归, 知道递归到叶子节点 ,返回

1
2
3
4
5
6
7
8
9
10
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
if(root==null) return null;
if(root.val<=p.val){
return inorderSuccessor(root.right,p);
}
TreeNode ans= inorderSuccessor(root.left,p);
return ans==null?root:ans;
}
}

面试题 04.08. 首个共同祖先

难度中等81

设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。

image-20220817160512405image-20220817160512405

题目对应的有三种情况:

  1. 两个节点分别位于根节点的左右子树中 :

    • left!=null && right!=null

    此时直接return root 即可

  2. 两个节点都位于左子树

    • right=null

    • 此时直接返回left

      • 因为最终的返回结果是第一次的递归 , 当后面的所有的递归都结束, 回到第一层时 , 对应的root 也会逐层传递

        image-20220817161123975image-20220817161123975例如下面的 7 , 4 他们的最近公共祖先 2 就会被逐级传递 最终, 回到第一层 ,然后返回 2

  3. 两个节点都位于右子树:

    • 与左子树同理
1
2
3
4
5
6
7
8
9
10
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null||root==p||root==q) return root;
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left==null) return right;
if(right==null) return left;
return root;
}
}

面试题 04.10. 检查子树

难度中等66

检查子树。你有两棵非常大的二叉树:T1,有几万个节点;T2,有几万个节点。设计一个算法,判断 T2 是否为 T1 的子树。

如果 T1 有这么一个节点 n,其子树与 T2 一模一样,则 T2 为 T1 的子树,也就是说,从节点 n 处把树砍断,得到的树与 T2 完全相同。

简单解法

  • 在较小、较简单的问题中,我们可以考虑对两棵树的遍历结果进行比较,该遍历结果通常用字符串表示。如果T2 是T1 的一棵子树,那么T2 的遍历结果应该是T1 的遍历结果的一个子串。

遍历方式的选择

中序遍历当然行不通。我们可以试试两棵二叉搜索树。二叉查找树的中序遍历结果总是
有序的。因此,即使两棵有着相同节点的二叉搜索树结构不同,其也总是有着相同的中序遍历
结果。

  • 如果只是

    简单的前序遍历

    , 不同的两个树依然有可能是 相同的结果

    • 但是如果吧null 值也存储, 那么两个不同的树的前序遍历的结果是必然不会相同的

前序遍历 , 将null值记为 n , 将序列存储在字符串中, 最后调用contains() 返回即可

空间:O(m+n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public boolean checkSubTree(TreeNode t1, TreeNode t2) {
StringBuilder s1=new StringBuilder("");
StringBuilder s2=new StringBuilder("");
dfs(t1,s1);
dfs(t2,s2);
return s1.toString().contains(s2.toString());
}

public void dfs(TreeNode root,StringBuilder s){ // 前序遍历
if(root==null){
s.append('n');
return;
}
s.append(root.val);
dfs(root.left,s);
dfs(root.right,s);
}

}

解法二:

先遍历大的树, 如果匹配到了与小树的根节点相同的值, 就调用treeMatch()

image-20220817163956815image-20220817163956815

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
TreeNode lessTree=null;
boolean res=false;
public boolean checkSubTree(TreeNode t1, TreeNode t2) {
lessTree=t2;
dfs(t1);
return res;
}
public void dfs(TreeNode root){ // 前序遍历 :根左右
if(root==null) return;
if(root.val==lessTree.val){
res=treeMatch(root,lessTree);
}
dfs(root.left);
dfs(root.right);
}
public boolean treeMatch(TreeNode t1,TreeNode t2){
if(t1==null&&t2==null) return true; //两个都为空
if(t1==null||t2==null) return false; // 一个为空 ,一个不为空
return t1.val==t2.val&&treeMatch(t1.left,t2.left)&&treeMatch(t1.right,t2.right);
}
}

面试题 04.12. 求和路径

难度中等118

给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。

一篇文章解决所有二叉树路径问题(问题分析+分类模板+题目剖析) - 求和路径 - 力扣(LeetCode)

法一:

DFS

  1. 可以从根节点开始 计算
  2. 可以从中间的节点开始计算

直接层序遍历, 暴力解法 ,

  • 每一个节点都开始计算一次

注意 : 一条路径, 可以出现多次目标值, (节点的val可以是负数) , 因此当 root.val == target时 ,不要返回, 继续计算!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
int ans=0;
public int pathSum(TreeNode root, int sum) {
if(root==null) return ans;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
while(queue.size()!=0){
int size=queue.size();
for (int i = 0; i < size; i++) {
TreeNode tmp=queue.poll();
dfs(tmp,sum); // 从当前节点开始 计算val target
if(tmp.left!=null) queue.add(tmp.left);
if(tmp.right!=null) queue.add(tmp.right);
}
}
return ans;
}
public void dfs(TreeNode root, int target){
if(root==null) return ;
if(root.val==target){
ans++;
// return ; 一条路径, 可以出现多次目标值, (节点的val可以是负数)
}
dfs(root.left,target-root.val);
dfs(root.right,target-root.val);
}
}
class Solution {
public int pathSum(TreeNode root, int sum) {
if (root == null) {
return 0;
}
int ret = rootSum(root, sum);
ret += pathSum(root.left, sum);
ret += pathSum(root.right, sum);
return ret;
}

public int rootSum(TreeNode root, int sum) { // 返回符合的次数
int ret = 0;
if (root == null) {
return 0;
}
int val = root.val;
if (val == sum) {
ret++;
}
ret += rootSum(root.left, sum - val);
ret += rootSum(root.right, sum - val);
return ret;
}
}

**法二 **

我们仔细思考一下,解法一中应该存在许多重复计算。我们定义节点的前缀和为:

  • 由根结点到当前结点的路径上所有节点的和。

我们利用先序遍历二叉树,记录下根节点 root 到当前节点 pp 的路径上除当前节点以外所有节点的前缀和,在已保存的路径前缀和中查找是否存在前缀和刚好等于当前节点到根节点的前缀和 curr减去 sum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int pathSum(TreeNode root, int sum) {
Map<Long, Integer> prefix = new HashMap<Long, Integer>();
prefix.put(0L, 1);
return dfs(root, prefix, 0, sum);
}

public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int sum) {
if (root == null) {
return 0;
}

int ret = 0;
curr += root.val;

ret = prefix.getOrDefault(curr - sum, 0);
prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);
ret += dfs(root.left, prefix, curr, sum);
ret += dfs(root.right, prefix, curr, sum);
prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);

return ret;
}
}

05位运算

面试题 05.01. 插入

难度简单58

给定两个整型数字 NM,以及表示比特位置的 iji <= j,且从 0 位开始计算)。

编写一种方法,使 M 对应的二进制数字插入 N 对应的二进制数字的第 i ~ j 位区域,不足之处用 0 补齐。具体插入过程如图所示。img

思路:

  • 分段append , 最终 StringBuilder存储的是二进制形式的 结果, 倒序改成十进制即可
1
2
3
for (int i1 = 0; i1 < chars.length; i1++) {
ans+=Math.pow(2,mic++)*(chars[i1]-'0');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public int insertBits(int N, int M, int i, int j) {
StringBuilder res=new StringBuilder();
int idx=0;
while(idx<i){ // 先复制 i 右边的数字
res.append(N&1);
N>>=1;
idx++;
}
while(idx<=j){ // 先复制[i~j]的数字
res.append(M&1);
M>>=1;
N>>=1;
idx++;
}
while(N!=0){ // 先复制左边部分的数字
res.append(N&1);
N>>=1;
idx++;
}
int ans=0;
int mic=0;
char[]chars=res.toString().toCharArray();
for (int i1 = 0; i1 < chars.length; i1++) {
ans+=Math.pow(2,mic++)*(chars[i1]-'0');
}
return ans;
}
}

面试题 05.02. 二进制数转字符串

难度中等40

二进制数转字符串。给定一个介于0和1之间的实数(如0.72),类型为double,打印它的二进制表达式。如果该数字无法精确地用32位以内的二进制表示,则打印“ERROR”。

小数转二进制:

文字描述该过程如下:

  • 将该数字乘以2,取出整数部分作为二进制表示的第1位
  • 然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位
  • 以此类推,直到小数部分为0
  • 特殊情况: 小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因

下面以 0.625 举例

  • 0.625 -> 1 .25 sb.append((int)num) ; num -=1;
  • 0.25 -> 0.5 sb.append((int)num) ;
  • 0.5 -> 1 sb.append((int)num) ;
  • return

不难发现, Num-=1 只有在 num >=1 的时候 才会执行,

因此可以将上面的表示翻译成下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public String printBin(double num) {
StringBuilder sb=new StringBuilder("");
int cnt=0;
while(num%1!=0){
if(cnt>=32) return "ERROR";
num*=2;
sb.append((int)num);
if(num>=1){
num-=1;
}
cnt++;
}
return "0."+sb.toString();
}
}

面试题 05.03. 翻转数位

难度简单82

给定一个32位整数 num,你可以将一个数位从0变为1。请编写一个程序,找出你能够获得的最长的一串1的长度。

遍历数字的二进制位,

当两段1的中间只有一个 0 的时候

  • 可以将每一个部分的1序列 与相邻的部分的1序列 看做是一个 , 此时的长度就是两段1 的长度 + 中间的那个0的长度(1)
    • previousLength+currentLength+1
    • 遍历维护maxLength最后返回即可

在遍历的过程中,追踪当前 1 序列的长度和上一段 1 序列的长度。当发现一个比特位为 0 时,更新 previousLength 的值。

使用 num&2 ==0? 来判断num的下一个比特位是否为0

  • ONLY(1&1)==1
  • 如果下一个比特位是 1,那么 previousLength 应被置为 currentLength 的值。
  • 如果下一个比特位是 0,我们则不能合并这两个 1 序列。因此,将 previousLength 的值置为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int reverseBits(int num) {
if(~num==0) return Integer.BYTES*8;/* 如果每一位都是1,那么这已经是最长的序列了 */
int currentLength=0,previousLength=0;
int maxLength=1;
// 我们总能够找到至少包含一个 1 的序列 , 因为至少可以吧一个0 变成 1 , 因此即使是0的maxLength也至少是1
while(num!=0){
if((num&1)==1){ // 当前位为1
currentLength++;
}else if((num&1)==0){ // 当前位为0
/*如果下一位仍然是 0 , 那么这两部分的 1 无法合并, previousLength 仍然为0 */
previousLength=(num&2)==0?0:currentLength;
currentLength=0;
}
maxLength=Math.max(previousLength+currentLength+1,maxLength);
num>>>=1; // 注意要用无符号右移 , 否则在处理负数的时候, 最高位会一直补 1 , num始终不会为0, 陷入死循环
}
return maxLength;
}
}

面试题 05.04. 下一个数

难度中等55

下一个数。给定一个正整数,找出与其二进制表达式中1的个数相同且大小最接近的那两个数(一个略大,一个略小)。

蛮力法

取较大的数字(getNext())

下面以 13948为例

image-20220819212250408image-20220819212250408

经过思考不难得出下面的结论:

  1. 若将某个 0 翻转成 1,就必须将某个 1 翻转为 0。
  2. 进行位翻转时,如果 0 变 1 的位处于 1 变 0 的位的左边,这个数字就会变大。
  3. 我们想让这个数变大,但又不致太大。因此,必须翻转最右边的 0,且它的右边必须还有个 1。

换句话说,我们要翻转最右边但非拖尾的 0。用上面的例子来说,拖尾 0 位于第 0 到第 1个位置。

因此,最右边但不是拖尾的 0 处在位置 7。我们把这个位置记作 p。

  1. 步骤(1):翻转最右边、非拖尾的 0

image-20220819213552962image-20220819213552962

  1. 步骤(2):将 p 右方的所有位清零
    • 由步骤(1)可知,c0 = 2,c1 = 5,p = 7

image-20220819213623891image-20220819213623891

  1. 步骤(3):回填 c1 - 1 个 1

image-20220819213648408image-20220819213648408

image-20220819213657712image-20220819213657712

取较小的数字(getPrev())

  • getPrev 的实现方法与 getNext 极为相似。
  1. 计算 c0 和 c1。注意 c1 是拖尾 1 的个数,而 c0 为紧邻拖尾 1 的左方一连串 0 的个数。
  2. 将最右边、非拖尾 1 变为 0,其位置为 p = c1 + c0。
  3. 将位 p 右边的所有位清零。
  4. 在紧邻位置 p 的右方,插入 c1 + 1 个 1。

注意,步骤(2)将位 p 清零,而步骤(3)将位 0 到位 p - 1 清零,我们可以将这两步合并

image-20220819222057969image-20220819222057969

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Solution {
public int[] findClosedNumbers(int num) {
int[] res = new int[2];
if (num <=0 || num>=Integer.MAX_VALUE) {
res[0] = -1;
res[1] = -1;
} else {
res[0] = getNextI(num);
res[1] = getPrevI(num);
}
return res;
}
public int getNextI(int n){
int c=n,c0=0,c1=0; // c1 1 的个数, c0 : 0的个数
while(c!=0&&(c&1)==0){
c0++;
c>>=1;
}
while(c!=0&&(c&1)==1){
c1++;
c>>=1;
}
// 错误:若n=111111...000, 那么就没有更大的数字
// 如果是n的二进制不存在可翻转的0,或者n就是0
if (c0 + c1 == 31 || c0 +c1 ==0) {
return -1;
}
int p=c0+c1;
n|=1<<p; // 这一步把 需要 由0 -> 1的位 变为了1 步骤1:翻转最右边,非拖尾0
// 接下来需要吧 原来的 1 都移动到 c 的最后面
n&=~((1<<p)-1);// 步骤2:将p右方的所有位清零,先获得(1<<p):第p位为1 , 别的都为0, (1<<p)-1 是 p后面的数字都是1 (p-1)个1 // 后面 p 为都为0 的数字
//然后需要补1, 原来剩余的1 的个数 -1 |
n|=((1<<(c1-1))-1); // 步骤3:在右方插入(c1-1)个1 比如 c1=3 那么需要在后面补2个1 , 也就是需要 (1<<2)-1 对应 c1-1
return n;
}
private int getPrevI(int n) {
int c=n,c1=0,c0=0;
while((c&1)==1){
c1++;
c>>=1;
}
if(c==0) return -1; // 序列 都是1
while((c&1)==0&&c!=0){
c0++;
c>>=1;
}
int p=c0+c1;
n&=((~0)<<(p+1)); //需要 由1 -> 0的位 变为了0 , 同时将 0~p 清零
//补1 c1+1 个 1 :(1<<(c1+1)-1) , 一共是 p 位,我们假设 c1=3 c0=2 p=5,则 一共需要补4个1,也就是最后只有一个0,因此是向左移1 位
// 也就是 p-c1-1 => c0-1 , 或者直接就是 , 后面留有c0-1 个0 ,因此左移c0-1位, 留出0的位置
n|=((1<<(c1+1))-1)<<(p-c1-1);
return n;
}
}

面试题 05.06. 整数转换

难度简单44

整数转换。编写一个函数,确定需要改变几个位才能将整数A转成整数B。

  • 本题的做法就是统计A, B的二进制位不同的个数

调整大小, 使得A 始终为较大的那个数字

遍历A, B 的二进制 , 如果遇到了不同的数位 就res++

当B ==0 之后

继续遍历A 的数位 , 如果A 的数位==1 res++

  • 最后返回res即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int convertInteger(int A, int B) {
if(A>B){
A^=B;
B^=A;
A^=B;
}
int res=0;
while(B!=0){
if((A&1)!=(B&1)) res++;
A>>>=1;
B>>>=1;
}
while(A!=0){
if((A&1)==1) res++;
A>>>=1;
}
return res;
}
}

面试题 05.07. 配对交换

难度简单69

配对交换。编写程序,交换某个整数的奇数位和偶数位,尽量使用较少的指令(也就是说,位0与位1交换,位2与位3交换,以此类推)。

先操作奇数位,然后再操作偶数位。

有办法将数字 n 的奇数位左移或右移 1 位吗?

  • 当然有。我们可以用 10101010(即 0xAA)作为掩码,提取奇数位,并将它们右移 1位,移到偶数位的位置。
  • 对于偶数位,可以施以同样的操作。最后,将两次操作的结果合并成一个值。
  • 这种做法共需 5 条指令
  • 16进制5是0101 a是1010,分别进行与运算的话刚好取了奇数位和偶数位,再进行移位(也就是乘二除二),相加。

奇数位右移,偶数位左移,取或得结果。

0xaaaaaaaa = 10101010101010101010101010101010 (偶数位为1,奇数位为0)

0x55555555 = 01010101010101010101010101010101 (偶数位为0,奇数位为1)

十六进制的一位, 会被分解成二进制的四位

a=10 => 8+2 => 1010

​ 5 = > 4+1=> 0101

image-20220819224637036

下面以29为例

29: 11101 => 101110

&0xaaaaaaaa =>01000 >>>1 => 00100

&0x55555555 =>10101 << 1 => 101010

00100 | 101010 = > 101110

1
2
3
4
5
6
class Solution {
public int exchangeBits(int x) {
return ( ((x&0xaaaaaaaa) >>>1)| ((x&0x55555555)<<1) );
// 0x55提取偶数为, 0xaa提取奇数位
}
}