2019年的搜狗秋招测试工程师笔试,到现在还有人翻出来看,说明这个岗位的题目是真的有参考价值。先说清楚一件事:这里的"搜狗"是公司,不是输入法。搜狗的测试工程师岗在当年是很多人的目标,笔试分为多场,第一场以编程题和测试思维题为主。网上流传的版本大多不完整,我结合历届考生回忆和同类型大厂测开笔试题,把这一场出现的编程题按题型重新整理了一遍,加上解题思路和易错点分析,给准备测开岗笔试的同学做一个参考。
有些同学可能会问:2019年的题目,放到现在还有意义吗?答案是,意义比大多数人想象的大。测开岗位的笔试题目风格和后台开发不一样,它不追求偏题怪题,更看重编码基本功、边界条件处理、以及把"测试思维"融入代码的能力。这几个核心能力,到现在依然是搜狗、以及同类互联网公司测开岗面试的重头戏。所以这套题不是背答案用的,是拿来找感觉用的。
1. 一场测开笔试的完整画像:岗位定位与出题逻辑
1.1 为什么测开岗笔试也要写代码
先聊一个被反复问的问题:测试工程师为什么要考编程?很多同学在校招前以为测试岗就是点点点、写写用例,等拿到笔试题才发现,代码题占了半壁江山。
这件事的根源在于岗位定位。搜狗当时招的测试工程师,在实际工作中要负责自动化测试框架编写、测试工具开发、性能测试脚本编写和问题定位分析。一个连循环都不会写的人,是没办法独立完成接口自动化用例的。所以笔试环节用编程题筛掉"没有代码基础"的候选人,是效率最高的方式。
从另外一个角度看,测开岗的编程题和开发岗的编程题还是有区别的。开发岗的题目经常是"实现一个LRU缓存""实现一个跳表",追求的是数据结构的掌握程度和算法优化能力。测开岗的题目往往更贴近实际代码场景:比如字符串处理、数字校验、数组操作,而且非常喜欢考"你写的代码能不能处理极端输入",这本质上是在考察你有没有做测试的习惯。
1.2 这一场笔试的题型分布与整体难度
根据当时考生回忆和后续社区讨论,这场笔试的编程题部分,可以整理出几个清晰的题型倾向:
- 字符串操作类题目,占比最高,几乎每场都有1到2道,重点考察对字符数组和边界条件的处理。
- 排序与查找类题目,数量不多,但出现频率稳定,重点在快排、二分查找的变体。
- 数组与指针类题目,对C/C++语言栈要求高,如果是用Java或Python的同学,考法会相对温和一些。
- 代码输出与找错题,这类题是测开笔试的特色,给一段代码,让你判断输出结果或者指出bug。
- 内存与性能相关题目,搜狗当时很看重代码的底层认知,部分题目涉及内存分配、作用域和资源释放。
从难度上看,编程题整体是"中等偏基础"的,不考复杂动态规划和高级图论。但它的陷阱全藏在细节里:你的代码能不能在数组越界时不崩、字符串为空时能不能正常返回、输入的特殊情况有没有考虑到。这些细节,恰恰是做过测试的人最敏感的部分。
2. 字符串处理题:测开面试中最常见的编码题
字符串处理类题目在测开笔试里出现的频率,高到可以用"必考"来形容。原因很简单:实际测试工作中,处理报文、解析日志、校验输入格式,全是字符串战场。搜狗这一场也不例外,下面把出现过的字符串题目形态和解题策略拆开讲。
2.1 字符统计类题目:从"第一个只出现一次的字符"说起
这道题在网络流传的版本中出现过多次,原型是:给定一个字符串,找出第一个只出现一次的字符并返回它的下标,如果不存在则返回-1。比如输入"abaccdeff",第一个只出现一次的字符是'b',下标是1。
很多同学上来就写两层循环,外层遍历每个字符,内层到后面找有没有重复的。这个解法在字符串很短的时候没问题,但时间复杂度是O(n^2),而且面试官看一眼就能看出你缺乏优化意识。
推荐的解法是一次遍历记次数,二次遍历查结果:
def first_uniq_char(s: str) -> int: from collections import Counter count = Counter(s) for i, ch in enumerate(s): if count[ch] == 1: return i return -1这里有一个所有语言通用的坑:遍历字典或哈希表的时候,插入顺序和遍历顺序不一定相同。Python 3.7之后dict可以保证插入顺序,但在其他语言里不一定。所以第一次统计次数,第二次还是要重新遍历原始字符串,而不是遍历哈希表找value为1的key。如果直接遍历哈希表,你拿到的可能是"第一个出现且只出现一次的字符"的下标,但这个字符在字符串里不一定就是第一个只出现一次的字符。
如果面试官要求空间复杂度O(1)且字符串只包含小写字母,更好一点的解法是用长度为26的数组当哈希表。数据量小的时候,数组访问比标准库哈希表快一个量级。这个细节在笔试中不一定能体现出来,但在面试追问中很加分。
2.2 字符串反转与局部反转:边界条件的重灾区
字符串反转也是这场笔试的高频题,但测开岗考反转很少考最朴素的"把整个字符串倒过来",而是喜欢考变形:比如反转单词顺序。题目大概是:输入"hello world from china",输出"china from world hello"。
这个题的经典解法是"先整体反转,再逐个单词反转":
def reverse_words(s: str) -> str: return " ".join(s.split()[::-1])用Python的split切分,看起来非常简洁,但这里有一个隐藏的取舍问题。split()默认会将多个连续空格当成一个分隔符,并且自动去掉首尾空格,这在大厂实际业务场景中——比如解析用户输入的命令、处理日志中的空白——往往是有用的。但是如果题目明确要求保留空格个数,或者输入中包含标点和换行符,split()就会出现偏差。
更接近面试官期望的解法是原地反转。用双指针把每个单词单独反转:
def reverse_words_inplace(s: list) -> None: # 先整体反转 s.reverse() start = 0 for i in range(len(s) + 1): if i == len(s) or s[i] == ' ': s[start:i] = reversed(s[start:i]) start = i + 1这种方式才是考察你"指针和索引"掌握度的正题。实际工作中做日志解析、协议解析时,操作字符数组的频率远高于直接调用API。
2.3 笔试中字符串题的通用检查清单
字符串题做完了,怎么检查自己的代码对不对?我在后来带新人的时候,总结了一个字符串题检查五连:
- 空字符串输入时程序是否能正常返回,而不是空指针报错。
- 全空格输入时是否会死循环或越界。
- 首字符和尾字符是分隔符时的处理是否正常。
- 字符串只有一个字符时,反转和统计逻辑是否仍成立。
- 包含中文时,按字节操作会不会把中文字符截断。
这几个检查点,看起来简单,但实际笔试过程中丢分最多的就是空串和单字符边界。你在代码里对边界情况的处理,某程度上就是你做测试时的用例设计习惯。面试官不是只找一个能跑出正确答案的人,他要找一个"知道什么时候程序会跑挂"的人。这也正是测开岗和其他开发岗在编码上的本质区别。
3. 数组与指针:笔试中真正的失分重灾区
字符串题如果说是"大家都见过,关键看细节",那数组与指针题就是"很多人以为自己会,一写就错"的类型。搜狗这场笔试里的数组类题目,难度不大,但错法五花八门。
3.1 数组去重与双指针:会写不代表写得对
先看一道典型题:给定一个有序数组,原地删除重复出现的元素,使每个元素只出现一次,返回移除后数组的新长度。就是著名的removeDuplicates问题。
如果不用指针,很多人会写成这样:
def remove_duplicates(nums: list) -> int: s = list(set(nums)) return len(s)这样写,返回的长度是对的,但你必须注意到题目的关键词是"原地"。set方式创建了新数组,而且丢掉了顺序,如果数组不是有序的,结果完全不一样。
标准的双指针写法如下:
def remove_duplicates(nums: list) -> int: if not nums: return 0 slow = 0 for fast in range(1, len(nums)): if nums[fast] != nums[slow]: slow += 1 nums[slow] = nums[fast] return slow + 1slow指针指向"已处理区域的最后一个位置",fast指针遍历整个数组。每遇到一个新元素就放到slow的下一个位置。这个思路在后续很多复杂题目里都会用到:比如移动零、压缩字符串、去除特定元素。笔试中遇见"原地修改"三个字,第一反应就应该是双指针。
3.2 合并两个有序数组:从后往前走才是最优解
另一个在搜狗笔试中出现过的题目是合并两个有序数组。题目一般是:给你两个有序整数数组nums1和nums2,把nums2合并到nums1中,使nums1成为一个有序数组。前提是nums1有足够的空间容纳nums2的元素。
新手最常见的写法是先把nums2追加到nums1后面,然后整体排序。这种做法虽然能通过部分测试用例,但你用的是编程语言的排序API,时间复杂度O((m+n)log(m+n)),没有利用到两个数组"已经有序"这个条件。
更优的解法是从后往前合并,避免移动元素:
def merge(nums1: list, m: int, nums2: list, n: int) -> None: i, j, k = m - 1, n - 1, m + n - 1 while j >= 0: if i >= 0 and nums1[i] > nums2[j]: nums1[k] = nums1[i] i -= 1 else: nums1[k] = nums2[j] j -= 1 k -= 1从后往前填,每个元素只需要被移动一次,时间O(m+n),空间O(1)。如果从前往后合并,nums1中大量元素要反复后移,时间会退化。
这个题目在测开笔试中出现有一个特别的意义:它考察的是你能否理解"倒序遍历"的思想。测试工程师平时写自动化脚本,很少遇到这种数组物理结构上的操作。但笔试考的不是"你开发时会不会用到",而是"你的底层认知是不是完整"。面试官希望测试人员能看懂开发代码中的性能隐患,能判断一个实现是不是最优的。如果测试人员连这种标准解法都不知道,是没办法给开发提出有效优化建议的。
3.3 内存与指针:搜狗笔试的"隐形特色题"
搜狗当年的技术栈有大量C/C++业务,所以编程题里经常出现内存相关题目。这类题对只写了Java和Python的同学来说不太友好,但是你躲不开。整理网络流传题目时,最常出现的是这几个类型:
- 给定一段C代码,指出其中的内存错误。
- 实现一个简单的字符串拷贝函数,注意空指针和内存重叠。
- 判断函数返回后局部变量是否可以继续使用。
第二种类型最典型。让写一个类似strcpy的函数:
char* my_strcpy(char* dest, const char* src) { if (dest == NULL || src == NULL) { return NULL; } char* ret = dest; while (*src != '\0') { *dest++ = *src++; } *dest = '\0'; return ret; }这个写法很基础,但藏着几个可以追问的问题:
- 返回值为什么用char*,而不是void?因为支持链式调用。
- 如果dest和src指向同一块内存的开头和中间位置,会不会出问题?
- 如果src比dest的内存长,会不会越界?所以需要在调用前约定好内存长度,或者改用受控长度的版本。
搜狗笔试中这类题往往不是让你直接写完就交,而是会要求说明你的实现里如何处理极端情况。这在测试界有一个专门的说法叫"防御性编程"。我在实际工作中见过的自动化测试框架,大多数问题不是功能逻辑错了,而是空值处理不完善就上线了。这恰恰是测开人员在Code Review时必须盯住的地方。
4. 排序与查找题:稳定会写比会默写更重要
排序和查找是所有笔试的基本盘。测开岗不会考红黑树手写,但快排、二分查找、归并这类"必须烂熟于心"的算法,还是大概率出现的。搜狗这场笔试的排序查找题不算难,但有几个点值得展开讲讲。
4.1 快排的边界条件:三个等号就能要命
快速排序,面试编码题中的钉子户。很多同学能背出快排的整体框架,但一写代码就踩坑。最容易出错的就是partition函数里的边界条件。
一个常见的写法:
def quick_sort(nums: list, left: int, right: int) -> None: if left >= right: return pivot = nums[left] i, j = left, right while i < j: while i < j and nums[j] >= pivot: j -= 1 nums[i] = nums[j] while i < j and nums[i] <= pivot: i += 1 nums[j] = nums[i] nums[i] = pivot quick_sort(nums, left, i - 1) quick_sort(nums, i + 1, right)注意这里的关键点:
- 内层while必须先移动右侧j,否则结果会错乱。
- 判断条件里写"大于等于"而不是"大于",可以避免相等元素导致的越界。
- i和j的移动过程中,始终要有i < j的保护,否则会越出数组边界。
如果你在快排之前先讲清楚"选最左元素做pivot,右侧先动,相等值跳过",面试官通常就不会追问太深。但如果代码里少写一个等号,结果就可能是死循环。我当时做测开的时候,有一次做代码题库评审,翻出来的提交里十个快排有一半是在等号上面崩的。这不是技术深度问题,是严谨性问题。测开岗位对严谨性的要求,就在这种细节里。
4.2 二分查找的进阶:从找值到找位置
二分查找在搜狗笔试中出现的形态一般是:在一个有序数组中查找目标值,如果不存在返回-1;或者返回目标值第一次出现和最后一次出现的位置。
基础版没什么好说的,但有几个细节值得强调:
def binary_search(nums: list, target: int) -> int: left, right = 0, len(nums) - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return -1这个解法里最容易出问题的是两个点。
第一个点是mid的计算方式。很多教材写的是mid = (left + right) // 2,但在数组长度接近系统上限时,left + right可能溢出。用left + (right - left) // 2可以避免这个问题,Java和C++里尤其要注意,Python的int没有溢出问题,但建议还是养成这个习惯。
第二个点是while循环的条件。left <= right和left < right是两种不同的边界约定,前者配合left = mid + 1和right = mid - 1使用,后者配合left = mid和right = mid使用。最怕的是把两种混在一起写,写出来就是死循环。
扩展到查找左边界时,思路更绕:
def find_first(nums: list, target: int) -> int: left, right = 0, len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] >= target: right = mid else: left = mid + 1 return left if nums[left] == target else -1注意,查找左边界的时候需要的是left < right配合left = mid + 1和right = mid的组合。这个组合是无数人写错的地方。理解了"收缩方向"比背模板更重要。
4.3 排序查找题的测试思维:把"找Bug"的思路反哺到代码中
排序查找题在纯开发岗位上,通常答出正确解法就完事了。但在测开岗的笔试中,面试官往往会在你写完代码之后追加一个问题:你写的这个排序,如果数组里有大量重复元素,会退化吗?如果数组已经有序,快排的表现如何?
这背后的逻辑是:测试人员在评估一个算法时,要能想象它在极端输入下的行为。快排最坏情况O(n^2)的原因,不在排序本身,而在于pivot的选择策略。如果每次pivot都选到最小或最大的元素,那么数组并没有被平均划分,递归树退化成一条链。
所以,如果笔试中让你写快排,顺手可以提一句优化策略:随机选pivot或者取三数中值。一句"我知道最坏case是O(n^2),可以通过随机pivot优化"在面试官心中的加分,远比你把代码模板背得滚瓜烂熟要大得多。这是测开岗笔试里少数可以"用测试知识给编码加分"的环节。
5. 代码输出与找错题:比写代码更难得的读代码能力
搜狗第一场笔试还有一个让人头疼的板块:给一段C/C++或者Java代码,让你写输出结果,或者指出代码中的bug。这种题目在开发岗笔试中不多见,但测开岗非常喜欢。原因很简单:测试人员最核心的技能之一,是读懂一段代码并推断它的行为,然后决定怎么测它。
5.1 局部变量返回与悬垂指针:C/C++老生常谈的经典坑
网上流传的搜狗笔试题目中,有一道非常典型的C语言题,代码大概是这样的:
int* func() { int local = 42; return &local; }然后问:函数返回后,调用方拿到的指针是否有效?如果继续访问这个指针,结果是什么?
答案是在函数返回时,局部变量local的栈内存已经释放。继续通过指针访问这个地址,行为是未定义的。在没有被其他栈帧覆盖之前,你碰巧还能读到42,但这个值随时可能变。
这个题考得很基础,但是非常经典。它考察的是你是否理解栈帧生命周期、函数返回时栈空间被回收的机制。很多Python和Java背景的写手来做测开,碰到这种题就直接懵了。实际上不要求你用过C,但要求你把"值类型变量在栈上分配,栈帧退出即失效"这个底层逻辑搞明白。
我在面试别人的时候,会在这个题后面追加一个问题:如果改成下面这样呢?
int* func() { static int local = 42; return &local; }加了static之后,变量被放在静态存储区,生命周期延申到程序结束。这个指针可以安全使用。追问到这里,C语言栈扎实与否一目了然。笔试中如果遇到这种题,不要只写答案,最好把"为什么"也写在答卷上。阅卷人能看到你的思维过程。
5.2 内存泄漏与资源释放:越基础的题越暴露真实水平
另一类找错题和内存泄漏相关。搜狗笔试中出现过的场景是,多线程环境下,主线程分配了一个缓冲区,传给工作线程处理。工作线程负责处理数据但忘记释放缓冲区,主线程又无法知道何时释放。问这个程序是否存在内存问题,怎么改。
这类题在纯编码环节是写不出来的,属于分析题。但它紧紧贴着实际工作场景。解法一般是两种:一种是在工作线程内部处理完后立即释放,这是最常见的方式,但要注意处理好失败路径,处理到一半抛异常,缓冲区也要释放;另一种是引入统一的内存管理机制,或者用智能指针/容器,让所有权语义更清晰。
这个分析的价值在于,它考察的已经不只是"会不会写代码",而是"写代码的时候知不知道自己负责的资源边界在哪里"。搜索和输入法这类后端服务,都是7x24小时跑着的,一次内存泄漏可能在一个月后才会把进程拖垮,期间内存监控曲线一直缓慢爬坡。这种问题只能靠测试人员配合压测和内存分析工具定位出来。所以搜狗在笔试里考内存题,是有实际业务诉求的。
5.3 读代码能力为什么是测开的硬本事
我一直觉得,测开岗笔试里最考验人的不是"写一个功能"而是"读一段代码并找出问题"。在校招群体里,很多同学只练怎么写代码,不练怎么读代码。这导致他们上手测试工作后,面对开发提交过来的上千行改动时,完全找不到重点,只能从界面功能上点一遍了事。
读代码能力强的人,看一个函数就能预判可能出问题的位置:这个函数有没有处理空指针?这个循环的退出条件会不会永远不成立?这里取数组下标前有没有检查长度?这些问题才是测试用例设计的出发点。
如果你想为这类题做准备,最好的办法不是刷题,而是去开源项目里找一些老旧的C/C++代码,比如一些历史遗留的工具库,尝试从代码里找出潜在问题。这比背十道笔试原题都管用。读代码的速度和理解深度,是测试工程师积累的护城河,没那么容易用工具替代掉。
6. 测试用例设计思维题:编程题里最隐蔽的加分项
编程题之外,搜狗笔试第一场里还有一小部分"非典型编程题",它们用代码的壳子考测试思维。很多人看到题觉得没头绪,其实是没读懂出题人的意图。
6.1 给定一个函数让你写用例:从黑盒角度反推代码逻辑
这类题一般长这样:有一个函数int calculate(int a, int b, char op),支持加、减、乘、除四种运算,请写出你对该函数的完整测试方案。
如果只看题目,脑海里蹦出来的测试用例可能是:2+3=5、5-2=3、3*4=12、8/2=4。这个答案只能拿基础分。
完全一点的答案是分几个维度来组织用例:
- 正常功能:每种运算符至少一组正常输入。
- 边界值:操作数为0、操作数为最大整数。
- 异常输入:除数为0时程序应该怎么表现。
- 特殊字符:op传入了'%'或者'=',程序怎么处理。
- 大数场景:999999999 + 999999999是否溢出。
如果你还能说出"a和b为负数时,除法要注意取整方向和余数符号",这就达到了测开岗位的基本线。再如果,你能主动提到"这个函数在边界处用了int,测试时需要考虑整数溢出,要不要建议开发改成long或做范围校验",那面试官眼前的你就已经能独立做测试方案设计了。
这里就体现了一个关键差异:测试人员写用例,不是验证代码"能跑",而是思考代码"在什么情况下会挂",然后围绕这个问题构造用例。这个思路一旦确立,你的用例设计就不是堆数量,而是讲逻辑。
6.2 代码实现后补测试用例:白盒视角下的覆盖率思维
还有一种形态是,代码已经写好了,让你看代码,然后补测试用例。这个环节除了看功能,更看重的是覆盖率思维。
举个例子:一段判断年份是否为闰年的代码:
def is_leap(year: int) -> bool: return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)写测试用例的时候,就必须包括:
- 能被4整除但不能被100整除的年份,比如2024,期望True。
- 能被100整除但不能被400整除的年份,比如1900,期望False。
- 能被400整除的年份,比如2000,期望True。
- 不能被4整除的年份,比如2023,期望False。
- 负数年份和0,这种入参是非法还是合法,代码里没有定义。
闰年这个例子看起来简单,但它是覆盖率的绝佳教材。很多刚入行的测试看到代码只有一行,不知道从哪下手。其实一行代码也包含三个逻辑分支,每个分支都要有至少一个正向和反向用例。
这类题在搜狗笔试中不是以大题出现的,更多的是作为一个追问出现在编程题后。但它反映的能力,恰恰是目前整个行业都在追求的"测试左移"——测试人员不再等到代码完成后才介入,而是从代码设计阶段就开始分析逻辑和风险。你笔试时展现的这种分析能力,会直接映射到你入职后的工作方式里。
6.3 自动化测试脚本设计:笔试里偶尔出现的实践题
除了纯算法题和用例设计题,搜狗第一场笔试中还零星出现过一些偏脚本实践的题目。比如:给定一个登录接口的文档,让你设计一个自动化验证该接口的脚本流程。由于篇幅限制,这道题通常不会要求你写出可运行代码,而是要求你写出脚本框架和关键步骤。
这种题目的得分点一般是这样几个:
- 是否明确分步骤:准备测试数据、构造请求、发送请求、断言响应、清理环境。
- 是否包含鉴权处理:登录接口往往会返回token,后续请求需要带上token。
- 是否包含测试数据隔离:每跑一轮都要用不同的测试账号,避免脏数据互相影响。
- 是否处理失败重试和日志记录:断言失败时要能从日志里定位问题。
如果你的框架里出现了类似的字眼,哪怕没用任何测试框架,也能拿到不错的分数。这个题的意义在于它把"测试思维"和"编程能力"做了个缝合。算法题看不出你有没有做过测试,但这样一道接口测试设计题,能明显分辨出谁真正理解测开岗位是干什么的。
7. 从这场笔试带走什么:针对测开岗的备考复盘与经验
7.1 做题顺序和时间的取舍策略
根据多数考生的反馈,搜狗这场笔试的题量不小,除了编程题还有大量选择填空。所以做题节奏上,我建议拿到试卷先花两三分钟浏览全卷的题量,做到心里有数。编程题里如果有一道卡住了超过20分钟,果断做下一道,回头再来补。和开发岗笔试不一样,测开岗的笔试往往有"输出结果题"和"测试用例设计题",这些题不需要长时间跑代码,先把能拿到的分拿到。
7.2 测开笔试的复习重点排序
如果让我按投入产出比给测开笔试备考排序,大概是这样的顺序:
- 第一位是字符串操作,任何笔试前都应该把字符串相关的常见题过一遍:反转、统计、去重、单词拆分。
- 第二位是数组与排序,双指针、快排思想、归并思想、二分查找变体,都要能手写。
- 第三位是代码读题能力,找bug题、代码输出题、内存分析题,这类题是测开岗特有的分水岭。
- 第四位是用例设计题,不用刻意准备很多,但至少要形成一套完整的思路框架。
如果你还有时间,可以再看看链表的基本操作:单链表反转、快慢指针找中间节点。这类题在搜狗和其他大厂的测开笔试中也属于常客,虽然不像字符串和数组那么高频,但出现在哪一场就够喝一壶的了。
7.3 笔试之后:这道题还能用来做什么
说句实在话,这套2019年的题目在今天看来,直接命中原题的概率已经不大。但它的价值在于帮你校准自己的水平。我建议拿到一套历史题之后,不要只做一遍对个答案就算了。可以模拟真实笔试环境,给自己倒计时,用纯手写或者白板的方式完成编码题。然后把编码题当成测试对象,给自己写的代码设计测试用例,跑一跑边界条件。这个过程,就是在模拟入职之后的工作状态。
我在带新人时经常说,测开这个岗位,表面上考的是编程和测试知识,实际上考的是你有没有"穷尽可能性"的心智习惯。做题发现自己哪里不会,补上,然后把总结下来的检查清单留好,下一次笔试前过一遍。这个习惯比刷一百道题更有用。至少我当年从搜狗这场笔试带走的东西,后来在我自己的测试框架设计和问题排查中,确实帮了大忙。