博主去年秋招参与了星环科技的笔试流程拿到的是C卷。考完之后最大的感受是这套题和互联网大厂的那套刷题逻辑完全是两码事。星环做的是大数据基础软件从分布式存储到计算引擎再到数据仓库平台都有自研产品所以它的笔试题目不会只考察“你会不会写代码”而是更在意“你懂不懂在数据量上来之后代码和方案会不会崩”。如果你准备用刷LeetCode的思路去应付大概率会在SQL题和场景设计题上吃大亏。这篇文章我会把C卷的题型结构、各类题目的考察逻辑和典型解法、以及我自己的作答策略和复盘心得完整拆开来讲。无论你是正在准备星环或其他大数据公司笔试的应届生还是只是想了解企业级编程题到底考什么都有参考价值。1. 先给C卷画个像题量、时间和出题逻辑1.1 整体印象不是竞赛是工程能力摸底星环秋招笔试C卷的时长一般是120分钟题型以编程题为主偶尔会夹杂少量选择题或问答题。从我拿到的C卷来看编程题的量大约在4到5道整体难度相比互联网大厂要温和一些但覆盖面非常广。它不会出那种需要逆天思维才能想到解法的偏题怪题反而很多题目一眼看过去都能有思路但真正动手写的时候你会发现自己对边界条件的处理、对数据规模的理解、对分布式场景的敏感度才是真正被考察的东西。出题逻辑也很明确星环的核心产品是TDH大数据平台包括分布式SQL引擎Inceptor、流计算引擎Slipstream、数据科学平台Sophon等。笔试题目会刻意向这些业务靠拢比如SQL题考察窗口函数和聚合查询算法题里出现日志处理、Session切分、有序数据合并这类真实场景场景设计题则直接问海量数据去重、TopK、实时统计这类大数据领域的经典问题。1.2 C卷的典型题型分布根据我考到的C卷以及横向对比了其他同学分享的A卷、B卷信息整理出一张参考表题型题量考察方向参考分值占比SQL编程题1~2道窗口函数、多表关联、聚合统计、数据倾斜处理25%~30%算法编程题2~3道排序、堆、动态规划、图论、字符串处理40%~50%大数据场景设计题1道分布式原理、容量估算、去重/TopK/UV统计20%~30%这个占比不是官方数据是个人推测但方向应该差不了太多。值得注意的是C卷的算法题虽然占比高但难度上限并不高。对于长期刷题的人来说基本都能做出来关键是能不能在限定时间内做对、做完整并且考虑清楚数据规模带来的影响。1.3 为什么说这套题“很星环”举个例子你就明白了。同样是考SQL互联网大厂可能考一道简单的多表Join你就能过但星环的SQL题会故意给你一张带重复数据的明细表让你算“每个用户连续登录的最大天数”这道题用窗口函数才能优雅解决。再比如场景题大厂可能会让你讲讲缓存穿透怎么解决星环则会问你“10亿条URL用哪种方式去重能接受小概率误判时怎么做不能接受误判时又怎么做”。它考察的不是你背了多少八股而是你有没有真的理解数据量上来之后单机思维会失效这件事。2. SQL题窗口函数是星环笔试的隐形门槛2.1 一道经典的“连续登录天数”问题先说C卷里最典型的一道SQL题给定用户登录明细表user_login包含字段user_id和login_date可能存在一天多次登录的重复记录要求统计每个用户历史上连续登录的最大天数。这类题在面试中很常见但网上很多答案写得并不严谨尤其是对重复日期的去重处理经常被忽略。正确的解法是用DENSE_RANK而不是ROW_NUMBER来给每个用户的登录日期编号因为ROW_NUMBER会把同一天的重复记录编成不同序号导致后续分组计算时出现错误。核心思路是对每个用户把登录日期减去连续编号后的日期值如果日期是连续的减出来的结果一定相同。SELECT user_id, MAX(consecutive_days) AS max_consecutive_days FROM ( SELECT user_id, date_sub(login_date, rn) AS group_date, COUNT(*) AS consecutive_days FROM ( SELECT user_id, login_date, DENSE_RANK() OVER (PARTITION BY user_id ORDER BY login_date) AS rn FROM ( SELECT DISTINCT user_id, login_date FROM user_login ) t1 ) t2 GROUP BY user_id, date_sub(login_date, rn) ) t3 GROUP BY user_id;最内层的SELECT DISTINCT用来去掉一天多次登录的重复记录这是很容易漏掉的点。中间层的DENSE_RANK()按用户分区、按日期排序生成连续编号date_sub(login_date, rn)这一步是整个解法的灵魂它把每个用户的登录日期与编号做差得到的分组字段在连续登录期间是恒定值。最后按用户和分组字段聚合统计出每组的天数再取最大值。这道题其实是在考察两个能力一是DENSE_RANK和ROW_NUMBER的差异是否清楚二是能不能意识到明细数据需要先做去重。很多人觉得自己会窗口函数但真正写的时候没考虑重复登录结果全错。顺便说一句如果数据库是Hive或星环InceptorDISTINCT在子查询里的执行效率不如GROUP BY所以在实际生产环境我一般会写成GROUP BY user_id, login_date笔试里写DISTINCT更直观。2.2 分组TopNRANK、DENSE_RANK、ROW_NUMBER怎么选C卷里另一道高频SQL题是分组TopN比如“统计每个品类销量前3的商品”。这类题表面上简单但实际答题时很多人会在三个窗口函数之间选错。直接给结论函数行为说明适用场景ROW_NUMBER()相同值也会编出不同的序号只要唯一名次不关心并列RANK()相同值同排名但跳跃需要并列名次且允许后续名次空出DENSE_RANK()相同值同排名不跳跃需要并列名次且希望后续名次连续比如销量数据是100、90、90、80ROW_NUMBER的结果是1、2、3、4RANK的结果是1、2、2、4DENSE_RANK的结果是1、2、2、3。如果需求是“Top3”用DENSE_RANK能查出4条记录因为并列第二占掉了两个位置用RANK能查出3条记录但第3名空缺用ROW_NUMBER则随机挑一个第二名。大多数业务需求里“取前3个商品”用ROW_NUMBER更合理但“找出所有销量不低于第3名的商品”就要用DENSE_RANK。写标准答案SELECT category_id, product_id, sales FROM ( SELECT category_id, product_id, sales, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales DESC) AS rn FROM product_sales ) t WHERE rn 3;2.3 大数据SQL引擎下的数据倾斜答好了能拉开差距C卷的SQL题还有一个隐藏加分项就是考察你是否了解分布式SQL引擎的局限性。比如一道题要求你统计“每个渠道的点击量”数据量级是百亿级其中某个头部渠道的点击量占了总量的70%以上。这种场景下如果直接写GROUP BY channel_id单机执行聚合的那个节点会承受巨大压力其他节点都跑完了只有它还在慢慢处理整个任务的执行时间被无限拉长。解决思路是两阶段聚合第一阶段先对key加随机前缀把热点key的聚合压力打散到多个节点第二阶段去掉前缀再做一次汇总。伪代码逻辑如下# 第一阶段对key加随机前缀后局部聚合 # SELECT concat(cast(rand() * 10 as int), -, channel_id) AS tmp_key, # COUNT(*) AS cnt # FROM click_log # GROUP BY tmp_key # 第二阶段去掉前缀汇总 # SELECT substr(tmp_key, 3) AS channel_id, # SUM(cnt) AS total_cnt # FROM ( # SELECT concat(cast(rand() * 10 as int), -, channel_id) AS tmp_key, # COUNT(*) AS cnt # FROM click_log # GROUP BY tmp_key # ) t # GROUP BY channel_id笔试中你能把这个问题写出来哪怕只是简单描述两句“热点key会导致长尾任务可以通过加随机前缀打散”面试官就能判断你是真的写过大数据任务而不是只背过SQL语法。我考C卷的时候这道SQL题现场就想到数据倾斜问题在答案末尾补了一段文字说明后来面试时面试官还专门问我“你笔试里提到的两阶段聚合具体是什么思路”直接把笔试变成了面试的加分项。3. 算法题难度不在“想不出”而在“写不完”3.1 数据量大到你必须用堆合并K个有序数组C卷的算法题里有一类非常典型的题目——合并K个有序数组。题目描述很简单给定K个升序排列的整型数组长度不一定相等请合并成一个升序数组。K的范围可能是几百总元素个数可能达到百万级别。常规想法是把所有元素放进一个列表然后排序时间复杂度是O(N log N)。这在大数据场景下不是不能用但笔试出这道题的目的很明显考察你是否会用堆来做多路归并把复杂度降到O(N log K)。用Python实现的小顶堆解法import heapq def merge_k_sorted_arrays(arrays): heap [] for idx, arr in enumerate(arrays): if arr: heapq.heappush(heap, (arr[0], idx, 0)) result [] while heap: val, arr_idx, elem_idx heapq.heappop(heap) result.append(val) if elem_idx 1 len(arrays[arr_idx]): next_val arrays[arr_idx][elem_idx 1] heapq.heappush(heap, (next_val, arr_idx, elem_idx 1)) return result堆里始终只保存K个元素每次弹出最小值再压入该数组的下一个元素整个过程只需要O(N log K)的时间空间复杂度是O(K)。注意Python的heapq默认是小顶堆可以直接使用。如果要用Java就是PriorityQueue。这道题有两点容易出错一个是没有处理空数组导致解包时下标越界另一个是在循环里把整个数组的剩余部分都压进堆而不是只压入下一个元素导致堆的规模变大复杂度退化。我见过不少人在笔试时把heapq.heappush(heap, (arr[idx][elem_idx 1], idx, elem_idx 1))写进遍历数组的循环里这一下就把O(log K)的堆操作变成了O(K log K)完全失去了多路归并的意义。3.2 Session切分考的是字符串处理和边界条件另一道让我印象深刻的算法题和用户日志有关给定一批用户访问记录每条记录包含用户ID、访问时间戳Unix秒、访问耗时要求按用户切分会话。规则是如果同一用户相邻两次访问的间隔超过30分钟则认为是新会话否则属于同一会话。最后输出每个用户每个会话的开始时间、结束时间和总访问时长。整体思路不复杂按用户分组后对时间排序然后线性扫描切分会话。但实际写起来有不少细节坑。from collections import defaultdict def split_sessions(logs): # logs: list of (user_id, start_time, duration) user_records defaultdict(list) for user_id, ts, dur in logs: user_records[user_id].append((ts, ts dur)) result [] for user_id, sessions in user_records.items(): sessions.sort(keylambda x: x[0]) cur_start, cur_end sessions[0] for i in range(1, len(sessions)): ts, end sessions[i] if ts - cur_end 30 * 60: result.append((user_id, cur_start, cur_end)) cur_start, cur_end ts, end else: cur_end max(cur_end, end) result.append((user_id, cur_start, cur_end)) return result这里最容易踩的坑有三个。第一会话的结束时间应该用“访问开始时间访问耗时”而不是相邻记录的开始时间否则会话长度会被低估。第二判断是否属于同一会话用的是“下次访问的开始时间”与“当前会话的结束时间”之差而不是与上次访问开始时间之差否则一次长访问会把后续访问错误地切割开。第三同一用户的两条访问记录可能乱序给出必须先排序再切分。这类题目在LeetCode上不会出现但它几乎是所有大数据公司笔试的常客。因为Session切分是用户行为分析、留存计算、流量分析里最基础的一步实际工作中从日志清洗到指标计算都离不开它。笔试考这道题本质上是想看看你有没有处理真实日志数据的经验。3.3 拓扑排序与DAG和星环的技术栈有直接关联星环的计算引擎和调度系统都是围绕DAG有向无环图设计的所以C卷里也出现了一道跟任务依赖关系有关的题目给定N个任务和M条依赖关系任务B依赖任务A意味着A必须排在B之前执行要求给出一组合法的任务执行顺序。标准解法是Kahn算法也就是BFS式拓扑排序。先统计每个节点的入度把入度为0的节点放进队列每次从队列取出一个节点加入结果序列把它指向的所有节点的入度减1如果减到0就入队。如果最终结果序列的长度不等于节点总数说明图中存在环无法完成拓扑排序。from collections import deque def topological_sort(n, edges): indegree [0] * n graph [[] for _ in range(n)] for a, b in edges: graph[a].append(b) indegree[b] 1 q deque([i for i in range(n) if indegree[i] 0]) res [] while q: node q.popleft() res.append(node) for nxt in graph[node]: indegree[nxt] - 1 if indegree[nxt] 0: q.append(nxt) return res if len(res) n else []这道题的难点不在于算法本身而在于题目常常不会直接告诉你“这是拓扑排序”而是包装成一个“编译任务依赖”或“数据管道调度”的场景。你在笔试时要能识别出它的本质。另外如果依赖关系是字符串形式的任务ID而非数字编号记得先做一层字符串到整数的映射。4. 大数据场景设计题这道题拉开的是“背题党”和“懂行的人”的差距4.1 10亿条URL去重从容量估算说起C卷最后一道题是场景设计题给了这么个问题有10亿条URL需要去重要求内存占用尽量小并且允许小概率误判你会怎么做如果不允许任何误判又该怎么做先把数量级算清楚。10亿条URL假设每条平均长度100字节原始数据总量大约是100GB。要把这100GB的URL全部读进内存做精确去重普通单机根本做不到。即便只存URL的哈希值比如8字节一个也要8GB内存很多笔试卷子默认给的内存限制也就1到2GB直接GG。允许小概率误判的场景标准答案是布隆过滤器Bloom Filter。它用一个超大的位数组和若干个哈希函数插入URL时用多个哈希函数计算出多个位全部置1查询时同样计算这些位只要有一个位是0就说明这个URL一定不存在。需要注意的是布隆过滤器存在误判——可能把不在集合中的元素判断为在集合中——但它不会漏判也就是已经在集合中的元素一定会被识别出来。10亿个元素如果使用10个哈希函数大约需要60亿bit的位数组换算下来约750MB这个内存占用是可接受的误判率大约在千分之一量级。不允许误判的场景标准答案是哈希分片。把10亿条URL按哈希值取模分布到比如100台机器上每台机器只需要处理1000万条URL单机内存完全够用然后每台机器内部用HashSet做精确去重最后合并结果。这个方案的代价是需要分布式环境但它保证了100%准确。笔试作答时我的建议是不需要把布隆过滤器的最优哈希函数个数公式背出来但一定要把容量估算写清楚。比如“10亿条URL按每条100字节算原始数据约100GB单机无法精确去重用布隆过滤器位数组长度取600亿bit约750MB误判率可控”这样的表述比单纯说一句“用布隆过滤器”值钱得多。4.2 海量日志TopK分而治之 小顶堆TopK问题也是场景设计题的高频考点。题目一般长这样给出若干台服务器上每天产生的海量日志每行包含一个搜索词需要在有限的内存下统计出出现次数最多的前100个词。这类题的标准套路是分两步。第一步分片把所有日志按搜索词的哈希值分布到多台机器或者单机上分批读入。第二步在每个分片上用HashMap统计词频然后维护一个大小为K的小顶堆堆顶是当前第K大的词频数值。遍历完所有数据后堆里的K个元素就是出现次数最多的K个词。如果K比较小比如100小顶堆的插入和删除都是O(log K)即使统计一亿个词频条目也很快。写一个简化版的小顶堆TopK逻辑import heapq from collections import Counter def top_k_words(words, k): counter Counter(words) # 单机分批场景下手动计数更合理 heap [] for word, cnt in counter.items(): if len(heap) k: heapq.heappush(heap, (cnt, word)) elif cnt heap[0][0]: heapq.heapreplace(heap, (cnt, word)) return [word for cnt, word in heap]这里用heapq.heapreplace而不是先heappop再heappush效率更高一步操作完成替换。实际生产环境如果数据量更大连HashMap都放不下就需要追加一层分治把词按哈希分布到多个文件分别统计后再归并。这些都是大数据处理的常见套路笔试时能写出分片局部统计全局归并这三步基本就能拿高分。4.3 实时UV统计考察的是对流式处理的理解还有一道场景题是关于UV统计的有一个用户点击流数据源源不断产生需要实时统计每小时的独立访客数UV数据量非常大内存有限你会怎么设计最直观的方案是把每个用户的ID存到一个HashSet里每小时结束时输出集合大小。但当用户量达到亿级时HashSet的内存会爆炸。实际工程里常见的做法是使用**HyperLogLogHLL**这类基数估算算法它用极小的内存默认大约12KB就能统计出高达2^64量级的基数误差通常在1%以内。很多OLAP引擎和数据库内置了HLL函数比如StarRocks的HLL_UNION_AGG、PostgreSQL的approx_distinct原理就是HLL。如果笔试时想写得更深入一点可以提一下流式计算引擎的选择。星环的Slipstream和Flink在架构上一脉相承答题时提到“用Flink按小时窗口做KeyBy用户ID窗口内用HLL累加去重最终结果写入OLAP引擎”这样的方案会显得你对实时计算链路有整体认知而不是只盯着一两个数据结构。这就会和只背过“用Redis Set去重”的人明显拉开差距。5. 我的作答策略与临场复盘5.1 时间分配别把时间耗死在最后一题上120分钟做4到5道题时间其实是够的但前提是合理分配。我当时的策略是拿到卷子先花3分钟把题目全部快速过一遍把每道题的难易程度标注出来。然后按照“先SQL、再场景设计、最后算法”的顺序作答。原因是SQL题和场景设计题基本上有思路就能写拿分确定性高而算法题即使有思路也可能在调试边界条件上花掉大量时间放在最后做更稳妥。时间分配上我给自己定的界限是SQL题30分钟、场景设计题25分钟、算法题每题25到30分钟、最后留10到15分钟统一检查。实际操作下来C卷的算法题有两道在30分钟内顺利通过另外一道调试了很久最后勉强提交。如果一开始就去啃最难的算法题后面SQL和场景题很可能来不及写那损失就太大了。5.2 最容易失分的几个低级错误从我自己和一起准备秋招的同学的反馈来看C卷失分点往往不在解题思路上而在一些很基础的细节上。输入输出格式是重灾区。有些平台要求按特定格式解析输入比如“第一行是一个整数N接下来N行每行两个整数”如果你习惯了只写核心函数不写输入处理会直接判0分。考前几天建议在牛客网或类似OJ上练几道考试风格的题目适应在线笔试的输入输出套路。选错语言也是常见的坑。星环笔试通常支持Python、Java、C但这三门语言在考试环境里的表现差异很大。如果题目里出现大规模整数运算Python的int不会溢出很方便但运行速度慢Java的PriorityQueue和C的priority_queue实现堆的写法需要提前熟悉。我到后期基本只写Python因为团队对限时开发更友好。还有一个容易被忽略的点笔试平台对代码缩进和空格的处理有时很严格尤其是Python。我见过有人在试卷里把缩进混用了Tab和空格导致代码运行直接报语法错误。考前最好确认一下编辑器设置把所有缩进统一成4个空格。5.3 笔试之后的思考C卷到底想筛选什么样的人复盘完整套C卷我最大的感受是星环笔试筛选的画像很清晰它不想要只会刷题的“解题机器”而是想要懂业务场景、有工程意识、知道数据量是分布式系统第一约束条件的人。SQL题考窗口函数和去重是想看你有没有做过数据处理算法题考堆和拓扑排序是想看你对数据结构和DAG理解到不到位场景题考布隆过滤器和分片是想看你对分布式系统的敏感度。所以如果你准备投星环这类大数据基础软件公司我的建议是不要把精力全放在刷难题上重心应该放在吃透窗口函数、堆、哈希、分治这些“朴素但实用”的工具上并且一定要建立起“单机算不动怎么办”的思维习惯。笔试题目本身并不难难的是你有没有在实际项目中体会过数据量上来之后的痛苦。这种痛苦提前经历一次比刷100道题都管用。