引言
算法能力已成为软件测试工程师面试中的硬通货,尤其在一线互联网公司的技术面中,手写代码或讲解算法思路几乎成为必考环节。本文精选5道高频面试题,从题目背景到解题思路,再到代码实现与测试要点,逐一拆解。每道题都附带完整的可运行代码和测试用例,帮助你真正理解而非死记硬背。
题目背景
面试官常通过这道题考察候选人对字符串操作和边界情况的处理能力。看似简单,但隐藏的陷阱不少。
解题思路
- ►直接反转:使用双指针从两端向中间交换字符。
- ►递归反转:适合考察对递归的理解,但效率较低。
- ►API调用:Python中
s[::-1]或Java中StringBuilder.reverse()。
代码实现(Python)
python
def reverse_string(s: str) -> str:
# 将字符串转为列表,因为字符串不可变
chars = list(s)
left, right = 0, len(chars) - 1
while left < right:
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return ''.join(chars)
# 测试
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
assert reverse_string("ab") == "ba"
assert reverse_string("12345") == "54321"
测试要点
- ►空字符串:输入
""应返回""。
- ►单字符:输入
"a"应返回"a"。
- ►数字字符串:确保反转后数字顺序正确。
- ►特殊字符:如空格、标点符号,反转后位置应对调。
进阶思考
如果要求原地修改(如字符数组),则直接用双指针交换即可,无需额外空间。
---
题目背景
回文判断是字符串算法的基础,但面试官通常会加入“忽略大小写和非字母数字字符”的条件,增加难度。
解题思路
- ►双指针法:从两端向中间比较,跳过非字母数字字符。
- ►预处理法:先过滤并统一大小写,再直接比较。
代码实现(Python)
python
def is_palindrome(s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
# 跳过非字母数字字符
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
# 忽略大小写比较
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
# 测试
assert is_palindrome("A man, a plan, a canal: Panama") == True
assert is_palindrome("race a car") == False
assert is_palindrome(" ") == True # 空字符串视为回文
assert is_palindrome("0P") == False # 数字和字母的比较
assert is_palindrome("ab_a") == True # 下划线被忽略
测试要点
- ►空字符串:按题目要求,通常视为回文。
- ►纯特殊字符:如
"!@#",跳过所有字符后,left >= right,应返回True。
- ►大小写混用:如
"Aa"应返回True。
- ►数字与字母混合:如
"1a1"应返回True。
常见陷阱
- ►忘记处理
left和right在跳过字符时的越界问题。
- ►比较时未统一大小写。
---
题目背景
给定一个大小为 n 的数组,找到其中的多数元素(出现次数大于 ⌊ n/2 ⌋ 的元素)。面试官期望候选人在 O(n) 时间复杂度和 O(1) 空间复杂度下完成。
解题思路
- ►哈希表法:统计每个元素出现次数,空间复杂度 O(n)。
- ►摩尔投票法:核心思想是“抵消”,候选元素遇到相同元素票数+1,不同则-1,票数为0时更换候选。最终候选即为多数元素。
代码实现(Python)
python
def majority_element(nums: list) -> int:
candidate = None
count = 0
for num in nums:
if count == 0:
candidate = num
count = 1
elif num == candidate:
count += 1
else:
count -= 1
return candidate
# 测试
assert majority_element([3, 2, 3]) == 3
assert majority_element([2, 2, 1, 1, 1, 2, 2]) == 2
assert majority_element([1]) == 1
assert majority_element([6, 5, 5]) == 5
测试要点
- ►单元素数组:直接返回该元素。
- ►所有元素相同:如
[1,1,1],返回1。
- ►边界情况:数组长度为偶数时,多数元素仍存在(题目保证)。
- ►负数或零:正常处理,不影响算法逻辑。
为什么摩尔投票法有效?
因为多数元素出现次数超过一半,即使其他元素全部联合起来,也无法完全抵消它。
---
题目背景
这道题考察链表的基本操作和指针管理能力。面试官会关注候选人对边界条件的处理,以及是否使用递归或迭代。
解题思路
- ►迭代法:创建哨兵节点(dummy),用指针依次比较两个链表的当前节点,将较小的节点接入结果链表。
- ►递归法:比较当前节点,将较小节点的next指向递归结果。
代码实现(Python,迭代法)
python
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def merge_two_lists(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# 处理剩余节点
current.next = l1 if l1 else l2
return dummy.next
# 辅助函数:列表转链表
def list_to_linked_list(arr):
dummy = ListNode(0)
current = dummy
for val in arr:
current.next = ListNode(val)
current = current.next
return dummy.next
# 辅助函数:链表转列表
def linked_list_to_list(head):
result = []
while head:
result.append(head.val)
head = head.next
return result
# 测试
l1 = list_to_linked_list([1, 2, 4])
l2 = list_to_linked_list([1, 3, 4])
merged = merge_two_lists(l1, l2)
assert linked_list_to_list(merged) == [1, 1, 2, 3, 4, 4]
# 空链表测试
l1 = None
l2 = list_to_linked_list([0])
merged = merge_two_lists(l1, l2)
assert linked_list_to_list(merged) == [0]
# 两个空链表
merged = merge_two_lists(None, None)
assert merged is None
测试要点
- ►一个链表为空:直接返回另一个链表。
- ►两个链表都为空:返回None。
- ►链表长度不等:确保剩余节点被正确拼接。
- ►值相等的情况:合并后顺序应保持稳定(如1,1)。
常见错误
- ►忘记处理链表遍历结束后的剩余节点。
- ►未使用哨兵节点,导致头节点处理复杂。
---
题目背景
二分查找是测试工程师必须掌握的算法之一,常用于性能测试中的数据定位或自动化测试中的元素查找。面试官会考察边界条件的处理,尤其是左右指针的移动规则。
解题思路
- ►标准二分:在有序数组中,每次将搜索范围缩小一半。
- ►左闭右闭区间:初始化
left=0, right=len(nums)-1,循环条件left <= right。
- ►左闭右开区间:初始化
left=0, right=len(nums),循环条件left < right。
代码实现(Python,左闭右闭)
python
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
# 测试
assert binary_search([1, 2, 3, 4, 5], 3) == 2
assert binary_search([1, 2, 3, 4, 5], 1) == 0
assert binary_search([1, 2, 3, 4, 5], 5) == 4
assert binary_search([1, 2, 3, 4, 5], 6) == -1
assert binary_search([], 1) == -1 # 空数组
assert binary_search([1], 1) == 0 # 单元素
assert binary_search([1, 3, 5, 7], 4) == -1 # 不存在
测试要点
- ►目标在数组中间:正常返回索引。
- ►目标在数组两端:返回0或len-1。
- ►目标不存在:返回-1。
- ►空数组:立即返回-1。
- ►单元素数组:目标存在则返回0,否则返回-1。
- ►重复元素:二分查找通常返回任意一个匹配索引,但面试官可能要求返回第一个或最后一个,需要调整逻辑。
进阶变体
- ►查找第一个等于target的位置:当
nums[mid] == target时,不立即返回,而是将right = mid - 1,继续向左查找。
- ►查找最后一个等于target的位置:当
nums[mid] == target时,将left = mid + 1,继续向右查找。
---
总结
这5道算法题覆盖了字符串、数组、链表、搜索等核心数据结构,是软件测试面试中的常客。理解背后的原理和边界条件,比死记硬背代码更重要。建议在面试前,手写每道题的代码并设计完整的测试用例,确保在压力下也能清晰表达思路。
记住:面试官看重的不仅是答案正确,更是你思考问题的方式和代码的健壮性。