字节 Data 全栈岗·刷题手册

字节 Data 全栈岗 · 刷题手册

配套:《字节Data北京全栈岗面试准备》(SQL 题、前四道算法、后端八股)。本手册收录新面经里的题目,以及同类型、同难度的高频题。 所有代码都用 Java 实际运行过,结果正确。 招聘方的提醒:可能不考原题,但类型和难度差不多。一定要写出来,不能只说思路。


0. 怎么保证“写得出来”

0.1 从这几份面经看面试官怎么打分

面经里给候选人打的是四个维度:思维沟通、项目经验、基础知识、编程能力。通过的那位,编程能力的评价是“算法题能写出来,思路也可以”。写出来是底线,思路好是加分。

0.2 硬写的策略(卡住时照这个顺序走)

  1. 先确认题意和边界(30 秒):输入为空怎么办?n 超出长度怎么办?
  2. 先写能跑的版本:想不出最优解,就先写一个暴力解法,比如链表题先把值放进数组再处理。能跑的暴力解法远远好过一个没写完的最优解。写完再说“我再优化一下”。
  3. 链表题一律用虚拟头节点:ListNode dummy = new ListNode(0); dummy.next = head; 能消掉“删除的是头节点”这类特殊情况,大部分链表题都能少写一半判断。
  4. 边写边说:每写一段就说这段在干什么,面试官能跟上你的思路,卡住时也更愿意提示。
  5. 写完马上用题目给的例子手动走一遍,然后主动说复杂度和边界。

0.3 练习方法(这是关键)

看懂答案不等于会写。每道题都按这个流程练: 1. 先自己想 10 分钟,写不出就看答案; 2. 看懂后合上答案,在白纸或纯文本编辑器里从头默写,不开代码补全、不用 AI; 3. 写错了再看一眼,再合上重写,直到一次写对; 4. 第二天再默写一遍,还能一次写对才算掌握。


1. 链表题(这个岗位最爱考,6 道里占了 4 道)

1.1 删除链表的倒数第 n 个节点

思路:快慢指针。快指针先走 n 步,然后两个指针一起走;快指针走到最后一个节点时,慢指针正好停在要删除节点的前一个。用虚拟头节点处理“删除的是头节点”的情况。

复杂度:一次遍历,时间 O(L),空间 O(1)。

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);   // 虚拟头节点:删除头节点时不用特殊处理
    dummy.next = head;
    ListNode fast = dummy, slow = dummy;
    for (int i = 0; i < n; i++) {       // 快指针先走 n 步
        if (fast.next == null) throw new IllegalArgumentException("n 大于链表长度");
        fast = fast.next;
    }
    while (fast.next != null) {         // 一起走,直到快指针到最后一个节点
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;         // 慢指针的下一个就是要删的节点
    return dummy.next;                  // 不能返回 head:head 可能已经被删掉了
}

已验证:[1,2,3,4,5] 删倒数第 2 个得 [1,2,3,5];[1] 删倒数第 1 个得 [];[1,2] 删倒数第 1 个得 [1];[1,2] 删倒数第 2 个得 [2](删的是头节点)。

易错点:返回值要用 dummy.next,不能用 head,因为头节点可能被删了。

1.2 删除链表的正数第 n 个节点

思路:用虚拟头节点,走到第 n−1 个节点(要删节点的前一个),把它的 next 跳过一个。

public ListNode removeNth(ListNode head, int n) {
    if (n <= 0) throw new IllegalArgumentException("n 必须大于 0");
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode prev = dummy;
    for (int i = 1; i < n; i++) {          // 走到第 n-1 个节点
        if (prev.next == null) throw new IllegalArgumentException("n 大于链表长度");
        prev = prev.next;
    }
    if (prev.next == null) throw new IllegalArgumentException("n 大于链表长度");
    prev.next = prev.next.next;            // 跳过第 n 个节点
    return dummy.next;
}

已验证:1→3→4→8 删第 2 个得 1→4→8;删第 1 个得 3→4→8;删第 4 个得 1→3→4;n 越界会报错。

1.3 K 个一组翻转链表(这批题里最难的一道,重点练)

思路:每次先往后数 k 个节点,不够 k 个就停止(剩下的保持原样);够 k 个就把这一段原地反转,再接回原链表。

需要记住四个指针: - groupPrev:这一组的前一个节点(第一组就是虚拟头节点) - kth:这一组的第 k 个节点(反转后它变成这一组的头) - groupNext:下一组的第一个节点 - 反转时把 prev 初始化为 groupNext,反转完这一组的尾巴自然就接上了下一组

public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null || k <= 1) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode groupPrev = dummy;                 // 当前组的前一个节点
    while (true) {
        // 1. 找到当前组的第 k 个节点,不够 k 个就结束
        ListNode kth = groupPrev;
        for (int i = 0; i < k && kth != null; i++) kth = kth.next;
        if (kth == null) break;
        ListNode groupNext = kth.next;          // 下一组的第一个节点

        // 2. 反转当前组。prev 从 groupNext 开始,反转后组尾直接连上下一组
        ListNode prev = groupNext, cur = groupPrev.next;
        while (cur != groupNext) {
            ListNode next = cur.next;
            cur.next = prev;
            prev = cur;
            cur = next;
        }

        // 3. 接回原链表:原来的第一个节点现在是组尾,作为下一组的 groupPrev
        ListNode oldFirst = groupPrev.next;
        groupPrev.next = kth;                   // 组头变成原来的第 k 个
        groupPrev = oldFirst;
    }
    return dummy.next;
}

已验证:[1,2,3,4,5] k=2 得 [2,1,4,3,5];k=3 得 [3,2,1,4,5];k=1 不变;[1,2,3,4] k=4 得 [4,3,2,1];[1,2,3,4,5,6] k=3 得 [3,2,1,6,5,4]。

画图讲解(面试时边画边讲):以 1→2→3→4→5、k=2 为例。第一组:groupPrev 是虚拟头,kth 是 2,groupNext 是 3。反转后是 2→1→3,虚拟头指向 2,groupPrev 移到 1。第二组同理得到 4→3→5。第三组只剩 5,不够 2 个,停止。

复杂度:每个节点被访问常数次,时间 O(n),空间 O(1)。

如果实在写不出来的保底方案:把整个链表的值读进数组,每 k 个一段在数组里反转,再写回链表。但题目要求不能只改值、必须交换节点,所以这只能作为“先写个能跑的版本”,写完要说明“这个版本改的是值,题目要求交换节点,我再改成原地反转”。

1.4 回文链表

思路: - 简单解法:把值放进数组,用双指针从两头往中间比较。时间 O(n),空间 O(n)。先写这个保底。 - 最优解(空间 O(1),加分):①快慢指针找中点;②反转后半段;③前半段和反转后的后半段逐个比较;④把后半段再反转回来,恢复原链表。

public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) return true;
    // 1. 找中点:快指针每次两步,慢指针每次一步;结束时 slow 停在前半段的最后一个节点
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 2. 反转后半段
    ListNode secondHead = reverse(slow.next);
    // 3. 比较。以后半段为准:奇数长度时中间节点归前半段,不参与比较
    ListNode p1 = head, p2 = secondHead;
    boolean ok = true;
    while (p2 != null) {
        if (p1.val != p2.val) { ok = false; break; }
        p1 = p1.next;
        p2 = p2.next;
    }
    // 4. 恢复原链表(加分点:不破坏调用方的数据)
    slow.next = reverse(secondHead);
    return ok;
}

private ListNode reverse(ListNode head) {
    ListNode prev = null, cur = head;
    while (cur != null) {
        ListNode next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

已验证:[1,2,2,1] 为 true,而且判断完链表恢复成 [1,2,2,1];[1,2] 为 false;[1,2,3,2,1] 为 true;单个节点和空链表为 true;[1,2,3] 为 false。

隐藏考点:奇数长度的中间节点怎么处理;判断完要恢复链表;多线程同时读这个链表时,临时修改会有问题,这种情况就用数组解法。


2. 字符串、栈、哈希

2.1 有效的括号

思路:用栈。遇到左括号,就把对应的右括号压栈;遇到右括号,就弹出栈顶比较,不一样或者栈是空的就返回 false。最后栈必须为空。

public boolean isValid(String s) {
    if (s == null || s.length() % 2 == 1) return false;  // 长度为奇数一定无效,提前返回
    Deque<Character> stack = new ArrayDeque<>();          // 用 ArrayDeque,不用过时的 Stack 类
    for (char c : s.toCharArray()) {
        if (c == '(') stack.push(')');                    // 压入期望出现的右括号,比较时更简单
        else if (c == '[') stack.push(']');
        else if (c == '{') stack.push('}');
        else if (stack.isEmpty() || stack.pop() != c) return false;
    }
    return stack.isEmpty();                               // 还有没闭合的左括号也是无效
}

已验证:()、()[]{}、([])、{[]} 为 true;(]、([)]、(、)( 为 false。

复杂度:时间 O(n),空间 O(n)。

隐藏考点:右括号多出来时栈是空的,要先判断再弹出;最后要检查栈为空;长度为奇数可以提前返回;Java 里推荐用 ArrayDeque 而不是 Stack(Stack 是同步的,性能差)。

2.2 最长公共前缀

思路:纵向扫描。以第一个字符串为基准,逐个位置检查所有字符串在这个位置的字符是否相同;某个字符串到头了或者字符不同,就返回到当前位置为止的前缀。

public String longestCommonPrefix(String[] strs) {
    if (strs == null || strs.length == 0) return "";
    String first = strs[0];
    for (int i = 0; i < first.length(); i++) {
        char c = first.charAt(i);
        for (int j = 1; j < strs.length; j++) {
            // 其他字符串到头了,或者这个位置的字符不同
            if (i >= strs[j].length() || strs[j].charAt(i) != c) {
                return first.substring(0, i);
            }
        }
    }
    return first;   // 第一个字符串整体就是公共前缀
}

已验证:[“flower”,“flow”,“flight”] 得 “fl”;[“dog”,“racecar”,“car”] 得 "“;只有一个字符串时返回它本身;含空字符串时返回”“;["abc","abcd"] 得”abc"。

复杂度:时间 O(所有字符串的字符总数),最坏情况每个字符都比较一次;空间 O(1)。

追问:「另一种思路?」把字符串排序后,只需要比较第一个和最后一个字符串的公共前缀(字典序最远的两个)。但排序是 O(n log n),不一定更快。

2.3 无重复字符的最长子串

见《全栈岗面试准备》§3.4,这里给数组版本(字符都在 ASCII 范围内时更快):

public int lengthOfLongestSubstring(String s) {
    int[] last = new int[128];       // 每个字符最后一次出现的位置
    Arrays.fill(last, -1);
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        if (last[c] >= left) left = last[c] + 1;  // 重复字符在窗口内,左边界跳过它
        last[c] = right;
        best = Math.max(best, right - left + 1);
    }
    return best;
}

已验证:abcabcbb 为 3,bbbbb 为 1,abba 为 2,空字符串为 0。


3. SQL:查找重复的电子邮箱

SELECT email AS Email
FROM Person
WHERE email IS NOT NULL       -- 空值不算重复
GROUP BY email
HAVING COUNT(*) > 1;

另一种写法(自连接,面试官可能让你写第二种):

SELECT DISTINCT p1.email AS Email
FROM Person p1
JOIN Person p2 ON p1.email = p2.email AND p1.id <> p2.id;

两种写法的对比:GROUP BY 只扫一遍表,效率高,推荐;自连接在数据量大时开销大,但思路直观。

隐藏考点(实测发现的,主动讲加分): 1. 空值:GROUP BY 会把所有空邮箱归为一组,如果有两条空邮箱,结果里会出现一个空值,所以要加 WHERE email IS NOT NULL。 2. 大小写和空格:MySQL 常用的排序规则默认不区分大小写,A@b.com 和 a@b.com 会被当成同一个邮箱;业务上如果要区分,就要注意排序规则;反过来,如果要把前后带空格的也算重复,可以用 LOWER(TRIM(email)) 分组。 3. 追问「删除重复的邮箱,只保留 id 最小的一条」:

DELETE p1 FROM Person p1
JOIN Person p2 ON p1.email = p2.email AND p1.id > p2.id;

4. 同类型高频题(有余力再练,按优先级)

优先级 题目 核心思路(一句话)
★★★ 反转链表 三个指针:prev、cur、next
★★★ 合并两个有序链表 虚拟头节点,每次接上较小的那个
★★★ 环形链表(判断是否有环) 快慢指针,相遇就有环
★★★ 链表的中间节点 快慢指针,快的走两步慢的走一步
★★★ 两数之和 哈希表存“值到下标”,查 target 减当前值
★★ 相交链表 两个指针走完自己的链表就换到对方的链表,相遇点就是交点
★★ 反转链表的第 m 到 n 个节点 虚拟头节点,找到第 m−1 个,后面逐个头插
★★ 删除排序链表中的重复元素 当前值等于下一个值就跳过下一个
★★ LRU 缓存 哈希表加双向链表;或者直接用 Java 的 LinkedHashMap
★★ 最小栈 再用一个栈同步记录当前最小值
★ 字母异位词分组 排序后的字符串作为哈希表的 key
★ 二分查找 注意 mid = left + (right - left) / 2 防溢出,以及循环条件

5. 面经里新出现的八股题(简洁答法)

MySQL

Redis

消息队列

网络

Java 和 Spring

工具

其他

项目题提醒

面经里通过的候选人讲了一个很典型的接口性能调优故事:数据大盘接口 7 到 8 秒,用 Arthas 定位到慢 SQL,用 EXPLAIN 分析后优化索引、冗余字段,再加 Redis 缓存。这种故事面试官很爱听。请从你自己的经历里找一个真实的性能优化案例(比如美团查询接口的限流和批量查询、阿里的某个慢查询),按“现象 → 定位工具 → 原因 → 方案 → 效果”整理好。一定要用真实经历,不要照搬别人的。


6. 练习计划(到面试前)

天 题目 要求
1 链表 4 道:删倒数第 n 个、删正数第 n 个、K 个一组翻转、回文链表 每道默写 3 遍;K 个一组至少写 5 遍
2 括号、最长公共前缀、无重复子串、重复邮箱 SQL;上一份文档的 4 道算法 每道默写 3 遍
3 第 4 节的五道三星题 每道限时 15 分钟,写完自己用例子走一遍
4 第 1、2 天的题全部重新默写一遍;八股快速过一遍 每道一次写对,否则当天再写
面试前一晚 链表 4 道再写一遍 保持手感

最后一句:面试时不管多紧张,先写出一个能跑的版本。写出来了,哪怕不是最优解也能及格;只讲思路,就是零分。

由 HiveGPT 托管 · 举报