1. 问题背景与理解
最近在准备华为OD机试时遇到一道有趣的题目,题目要求我们利用给定的数字卡片组合出最大的数字。具体来说,小组中每个成员都持有一张卡片,卡片上是6位以内的正整数,我们需要将这些数字卡片以某种顺序连接起来,形成可能的最大数字。
这个问题看似简单,但实际蕴含着深刻的算法思想。作为一名经常参加编程竞赛的选手,我第一反应是这属于"字符串拼接求最大值"类问题,与经典的"最大数"问题类似。这类问题在各大公司的笔试面试中经常出现,比如LeetCode上的179题"最大数"就是非常相似的题目。
2. 问题分析与解题思路
2.1 问题重述
给定一个由逗号分隔的多个正整数字符串(每个数字不超过6位,最多25个数字),我们需要将这些数字以某种顺序连接起来,形成可能的最大数字字符串。
示例:
输入:"22,221"
输出:"22221"
2.2 初步思考
最直观的想法可能是将所有数字按数值从大到小排序,然后直接拼接。比如对于"22,221",按数值排序是221 > 22,所以拼接为"22122"。但示例给出的正确答案是"22221",这说明简单的数值比较排序是不正确的。
2.3 关键洞察
问题的关键在于如何定义两个数字的"大小"关系。我们不能简单地比较数字本身的数值大小,而应该比较它们在不同拼接顺序下产生的数字大小。
对于两个数字a和b,我们需要比较的是:
- 将a放在前面形成的数字ab
- 将b放在前面形成的数字ba
然后选择能产生更大数字的排列顺序。
2.4 比较函数的实现
基于上述思路,我们需要自定义一个比较函数。在Python中,可以通过functools.cmp_to_key函数将比较函数转换为key函数,用于排序。
比较函数可以这样定义:
python复制def compare(a, b):
ab = a + b
ba = b + a
if ab > ba:
return -1 # a应该排在b前面
elif ab < ba:
return 1 # b应该排在a前面
else:
return 0 # 顺序无关
2.5 边界情况考虑
在实际实现时,还需要考虑一些特殊情况:
- 输入包含多个0的情况,比如"0,0",正确输出应该是"0"而不是"00"
- 输入本身就是最大的情况,不需要任何调整
- 输入数字长度不一的情况,比如"3,30,34,5,9"
3. 完整解决方案
3.1 Python实现代码
python复制from functools import cmp_to_key
def largest_number(nums):
# 将数字转换为字符串列表
str_nums = list(map(str, nums))
# 自定义比较函数
def compare(a, b):
if a + b > b + a:
return -1
else:
return 1
# 排序
str_nums.sort(key=cmp_to_key(compare))
# 处理全0的情况
if str_nums[0] == '0':
return '0'
return ''.join(str_nums)
# 示例测试
print(largest_number([22, 221])) # 输出: 22221
print(largest_number([3, 30, 34, 5, 9])) # 输出: 9534330
print(largest_number([0, 0])) # 输出: 0
3.2 代码解析
- 输入处理:首先将输入的数字列表转换为字符串列表,方便后续的字符串拼接比较。
- 自定义比较:定义compare函数,比较两个字符串不同拼接顺序的结果。
- 排序:使用Python的sort方法配合cmp_to_key进行自定义排序。
- 边界处理:检查排序后的第一个数字是否为'0',如果是则直接返回'0',避免类似"00"的输出。
- 结果拼接:将排序后的字符串列表拼接成最终结果。
3.3 时间复杂度分析
该算法的主要时间消耗在排序步骤。假设有n个数字,平均长度为k,那么:
- 比较两个字符串的时间复杂度是O(k)
- 排序的时间复杂度是O(n log n)
- 每次比较需要O(k)时间,所以总时间复杂度是O(k * n log n)
对于题目限制的n≤25,这个复杂度是完全可接受的。
4. 测试用例与验证
为了确保我们的解决方案正确,需要设计全面的测试用例:
4.1 基本测试用例
python复制assert largest_number([22, 221]) == "22221"
assert largest_number([3, 30, 34, 5, 9]) == "9534330"
assert largest_number([0, 0]) == "0"
4.2 边界测试用例
python复制# 所有数字相同
assert largest_number([1, 1, 1]) == "111"
# 数字长度差异大
assert largest_number([1, 10, 100, 1000]) == "1101001000"
# 大数字测试
assert largest_number([999999, 999998]) == "999999999998"
4.3 性能测试用例
python复制# 最大规模测试(25个数字)
import random
nums = [random.randint(0, 999999) for _ in range(25)]
print(largest_number(nums)) # 应该快速返回结果
5. 常见问题与解决技巧
5.1 为什么不能直接按字符串降序排序?
直接按字符串降序排序在某些情况下会出错。例如:
- "3"和"30"按字符串降序排序会得到"30" > "3"
- 但实际最大组合是"330"而不是"303"
5.2 如何处理前导零?
在拼接完成后,需要检查最终结果是否以"0"开头。如果是,说明所有输入都是0,应该返回单个"0"。
5.3 如何优化比较过程?
对于大规模数据,可以预先计算每个数字的"权重"或"优先级",避免在排序时重复计算拼接结果。但在本题限制下(n≤25),这种优化不是必需的。
5.4 其他语言的实现
在Java中可以使用Comparator接口,在C++中可以使用自定义比较函数配合sort算法。核心思路是相同的。
6. 算法扩展与变种
6.1 最小数字问题
类似地,我们可以求卡片组成的最小数字。只需修改比较函数,选择拼接后较小的顺序。
python复制def smallest_number(nums):
str_nums = list(map(str, nums))
def compare(a, b):
if a + b < b + a:
return -1
else:
return 1
str_nums.sort(key=cmp_to_key(compare))
# 处理全0的情况
if str_nums[0] == '0':
return '0'
return ''.join(str_nums)
6.2 带权重的数字拼接
如果每个数字有不同的权重,需要在拼接时考虑。这可以通过修改比较函数来纳入权重因素。
6.3 限制拼接长度的最大数字
如果要求拼接后的数字不超过某一位数,问题会变得更加复杂,可能需要使用动态规划来解决。
7. 实际应用场景
这类算法在实际中有多种应用:
- 数据库查询优化:确定多个查询的最佳执行顺序
- 任务调度:安排任务以获得最大效率或收益
- 资源分配:优化资源使用顺序
- 金融领域:组合金融产品以获得最佳收益
8. 个人经验分享
在解决这个问题时,我最初犯了一个错误:直接按字符串的字典序降序排列。这在"3"和"30"的测试用例中失败了。通过这个错误,我学到了:
- 不要假设排序规则:看似合理的排序规则可能在边界情况下失效
- 全面测试的重要性:简单的测试用例可能无法暴露所有问题
- 理解问题本质:最大数字问题本质上是自定义排序问题
另一个经验是:在Python中,使用functools.cmp_to_key可以方便地实现自定义排序,这比传统的key函数更灵活,特别是在需要比较两个元素相互关系时。
最后,对于这类问题,画几个例子并手动计算往往能帮助快速发现规律。比如对于"22,221",手动尝试两种拼接方式就能立即看出哪种更大。