找出第 N 个二进制字符串中的第 K 位 图解题解
这道题到底在问什么
- 输入
- n=3, k=1
- 输出
- "0"
- 输入
- n=4, k=11
- 输出
- "1"
- 输入
- n=2, k=3
- 输出
- "1"
最优解:一步一步想明白
- 3记牢:中点第 2^(n-1) 位恒为 "1"。k = 中点直接是 1;k 在左半就降到 S(n-1) 同一个 k;k 在右半镜像到第 2^n - k 位、翻转一次再降层。一路折到 S1,翻转奇偶定答案。
- 4七个格子代表 S3 的第 1 到第 7 位,现在全是问号,因为我们打算一位都不去算,只靠折叠定位。紫色指针停在第 5 位,这就是要找的目标。右边面板记三件事:当前在哪个串、要找第几位、到目前为止翻转了几次的奇偶。
- 5先标出正中间。S3 长度 7,正中间是第 4 位,它是构造时硬插进去的那个 "1",所以不用算就知道是 1,标成蓝色。这个中点把整串劈成对称的左右两半:左边第 1 到第 3 位,右边第 5 到第 7 位。
- 6比一比:目标第 5 位在中点第 4 位的右边,落在右半段。把左半段先压灰,我们只盯右半。右半段不是凭空来的,它是左半段 S2 先把每位取反、再整体反过来得到的,所以右半的每一位都能在左半找到对应。
- 7右半段第 k 位,正好等于第 2^n 减 k 位「取反」后的样子。这里 2 的 3 次方减 5 等于 3,也就是第 5 位的值,等于第 3 位取反。于是把目标镜像到第 3 位,并且记下一次翻转,翻转奇偶从 0 变成 1。
- 8左半段本身就是 S2,长度 3。问题干净利落地缩小成:在 S2 里找第 3 位,最后再把翻转应用回去。右边那三格已经折叠完成,压灰收起,我们的视线收回到第 1 到第 3 位。
- 9同样的套路再来一遍。S2 长度 3,正中间是第 2 位,又是插进去的那个 "1",标蓝。它把第 1 到第 3 位再次劈成左右:左边只剩第 1 位,右边只剩第 3 位。
- 10目标第 3 位在 S2 中点第 2 位的右边,又落在右半段。这一层的右半,是 S1 取反再反转得到的。把左边第 1 位先压灰,只看右边这一格。
- 11再镜像一次:2 的 2 次方减 3 等于 1,第 3 位等于第 1 位取反。把目标镜像到第 1 位,再记一次翻转,翻转奇偶从 1 翻回 0。注意奇偶现在是偶数,意味着两次取反最终会互相抵消。
- 12降到 S1 了,它就是单独一个 "0"。要找的第 1 位,值就是 0,标成绿色。这是递归的出口,再也不用往下折了。接下来只剩一件事:把途中记下的翻转应用回去。
- 13一路上我们记了两次翻转,两次都是取反。偶数次取反相当于没动,所以底部的 0 原样保留。如果是奇数次,才需要把它翻成 1。这里是偶数,答案保持 0。
- 14把答案填回第 5 位:是 0。整个过程我们只折叠了两次、看了两个中点再落到 S1 出口,从没真的拼出 S3 那七位。注意第 3、6、7 位始终是问号,因为根本没必要去算它们。
- 15换个目标,这次找 S3 的第 2 位,看看落在左半会怎样。翻转奇偶重新从 0 起算。之前揭示过的中点蓝格还在,因为同一个 S3 它们的值不变,正好省得重标。
- 16第 2 位在中点第 4 位的左边,落在左半段。左半段最省心:它就是 S2 一字不差的原样,位置不用镜像、也不用记翻转。把右半压灰,直接降层。
- 17问题缩小成在 S2 里找第 2 位,k 还是 2,翻转奇偶仍然是 0。左半段的好处就在这,什么都不用改,层数减一就行。
- 18S2 长度 3,中点正是第 2 位,而我们要找的就是第 2 位,直接撞上了中点。中点恒为 "1",标成绿色,答案当场揭晓,连降层都省了。
- 19这一路全走的左半段,没碰过任何右半,翻转次数是 0,所以中点的 1 原样就是答案。S3 第 2 位是 1。可见踩中点是最快的情形,一步到位。
- 20最后一个例子,找 S3 的第 1 位,也就是整串开头。第 1 位远在中点第 4 位左边,落在左半段,降到 S2、k 仍是 1。
- 21到了 S2,中点是第 2 位,第 1 位还在它左边,继续落左半,降到 S1,k 依旧是 1。每一层都往左,翻转次数始终为 0。
- 22降到 S1,要找第 1 位,值是 0,标绿。一路没经过右半,0 次翻转。
- 23每个 Sn 的开头都是 S1 的那个 "0",所以任何 Sn 的第 1 位都是 0,这跟样例里 n = 3、k = 1 的答案完全吻合。三个例子走完,折叠法的左、右、命中三种走向都见过了。
- 24把三次查询的结果放一起回看:第 1 位是 0、第 2 位是 1、第 5 位是 0,都标成绿色,中点第 4 位是蓝色的 1。注意第 3、6、7 位至今还是问号,我们一次都没去算它们,这正是折叠法的精髓:要哪一位就只折哪一位,长串永远不必拼出来。
⚠️ 容易写错的地方
✗ 错:老老实实把 Sn 整串拼出来再取第 k 位
✓ 对:只用位置算术沿中点折叠,从不拼串
Sn 长度是 2^n - 1,n 到 20 就是上百万位,拼整串是 O(2^n) 的时间和空间,既不是最优也没体现递归结构。其实第 k 位完全由它在中点左右的位置递推决定,O(n) 步就能定位
✗ 错:右半段折叠时忘了取反,直接照搬左半的值
✓ 对:右半 = reverse(invert),镜像后必须翻转一次
右半段是 S(n-1) 先取反再反转来的,所以第 k 位等于第 2^n - k 位「取反」。漏了这次取反,凡是落右半的查询都会答错
✗ 错:把中点也当成普通位置继续往下折
✓ 对:k 正好等于中点时直接返回 1,立即收手
中点第 2^(n-1) 位是构造时插入的固定 "1",是递归的天然出口之一(参考代码用 k 是 2 的幂来识别)。当成普通位置会多折几层甚至越界
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def findKthBit(self, n: int, k: int) -> str:
def dfs(n: int, k: int) -> int:
if k == 1:
return 0
if (k & (k - 1)) == 0:
return 1
m = 1 << n
if k * 2 < m - 1:
return dfs(n - 1, k)
return dfs(n - 1, m - k) ^ 1
return str(dfs(n, k))C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
char findKthBit(int n, int k) {
function<int(int, int)> dfs = [&](int n, int k) {
if (k == 1) {
return 0;
}
if ((k & (k - 1)) == 0) {
return 1;
}
int m = 1 << n;
if (k * 2 < m - 1) {
return dfs(n - 1, k);
}
return dfs(n - 1, m - k) ^ 1;
};
return '0' + dfs(n, k);
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public char findKthBit(int n, int k) {
return (char) ('0' + dfs(n, k));
}
private int dfs(int n, int k) {
if (k == 1) {
return 0;
}
if ((k & (k - 1)) == 0) {
return 1;
}
int m = 1 << n;
if (k * 2 < m - 1) {
return dfs(n - 1, k);
}
return dfs(n - 1, m - k) ^ 1;
}
}复杂度
时间
O(n)
每折叠一次,层数 n 就减 1,最多从第 n 层降到第 1 层,一共 O(n) 步,每步只做几次比较与减法,都是常数操作。注意这里的 n 是字符串的「层数」,不是串的长度。串长是 2^n - 1,可能上百万,但我们从不遍历它,只沿中点折叠,所以是 O(n) 而非 O(2^n)
空间
O(n)
按峰值算。递归 dfs 最深会嵌套 n 层,调用栈上同时压着 O(n) 个栈帧,这是空间的主要来源。除此之外只用了 m、mid 这几个常数变量,没有开数组、没有拼字符串。若改写成循环迭代,递归栈可降到 O(1),但本题 n 最大 20,O(n) 栈深完全无压力
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找出第 N 个二进制字符串中的第 K 位 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
n 能到 20,Sn 上百万位,真拼整串是 O(2^n),你的解法怎么把它降到可行?+
因为我们从头到尾没有真的拼出 Sn,也没遍历它。每次只看目标位落在当前串中点的左边、右边还是正中,据此把问题缩小成上一层 S(n-1) 的子问题,层数每次减 1。最多折叠 n 次就降到 S1,时间是 O(n);只用几个整数变量加 O(n) 的递归栈,空间也极省。串有多长跟我们无关,我们只在位置数轴上跳。
落在右半段时,那次「取反」到底从哪来?+
来自构造规则。右半段是 reverse(invert(S(n-1))),也就是把 S(n-1) 每一位先取反、再整体反过来。所以右半段第 k 位,对应的是 S(n-1) 里第 2^n - k 位「取反」之后的值。镜像负责把位置从右半折回左半,取反负责把值修正过来,两者缺一不可。我用一个翻转奇偶来累计取反次数,最后奇数次就翻、偶数次就不翻。
参考代码里用 k 与 k 减 1 按位与判断 2 的幂,跟动画的中点判断是一回事吗?+
是同一件事的两种写法。各层前缀 Si 的中点位置正好是 2、4、8 这些 2 的幂,中点恒为 1。动画为了直观,逐层下降、走到中点时显式停下;参考代码则用「k 是 2 的幂」这个位运算一步就认出它将来会落到某层中点,直接返回 1,省去了中间的下降。唯一要单独处理的是 k 等于 1,它虽是 2 的 0 次方却对应 S1 的 0,所以代码先用 k 等于 1 返回 0 把它拦下。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 找出第 N 个二进制字符串中的第 K 位 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。