- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
本文以 doocs/leetcode 仓库中 179. Largest Number 英文题解 与 中文题解 为骨架,结合仓库内 7 种语言的
Solution.*源码,系统讲解"给定一组非负整数,重新排列每个数的顺序(每个数不可拆分)使之组成最大整数"这一经典题目的贪心思路、比较器设计与边界处理,并给出可直接运行的 Python3 / Java / C++ / Go / C# / TypeScript / JavaScript 七语言实现。读完本文,你将掌握"拼接比较法"(比较a+b与b+a决定先后)这一通用排序技巧,以及它在不同语言中落地为自定义比较器的完整套路。
一、题目回顾:把非负整数排列成最大数
原题描述(见 README_EN.md 与 README.md):
给定一组非负整数
nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。由于结果可能非常大,所以需要返回一个字符串而不是整数。
示例
示例 1:
输入:nums = [10,2] 输出:"210"示例 2:
输入:nums = [3,30,34,5,9] 输出:"9534330"约束条件
| 约束 | 取值 |
|---|---|
| 数组长度 | 1 <= nums.length <= 100 |
| 元素取值范围 | 0 <= nums[i] <= 10^9 |
| 返回值 | 字符串(而非整数,避免溢出) |
题目本身很短,但"如何定义两个数字之间的先后关系"是这道题真正的考点——它既不等于数值大小关系,也不等于普通的字典序关系。
二、核心难点:为什么"数值排序"与"普通字典序"都不成立
题解文档的"思考"部分(README.md)开门见山地指出:
拼接出一个最大整数。按数值或普通字典序排序都不对:
9应排在98前面,因为998 > 989。
下面逐一拆解这两种看似自然的思路为何失败。
反例一:按数值降序排列
直觉上,把大的数字放前面似乎合理,但示例 1 已经给出了反例:[10, 2]按数值降序得到[10, 2],拼接为"102";而把2放在10前面得到"210",显然更大。原因是数字的"位数"也会影响拼接后的整体长度与高位取值,单纯比较数值大小忽略了这一点。
反例二:按普通字典序降序排列
把数字转成字符串后按字典序降序,同样不正确。题解文档给出的关键反例是9与98:
- 按字典序,"98" > "9"(因为比较到第 2 位时 "8" 与字符串 "9" 已结束,字典序中 "98" 更大),所以
98会排在9前面; - 但拼接结果
"998"明显大于"989",即9应当排在98前面。
类似的还有3与30:"330"大于"303",所以3应排在30前,而普通字典序会得到相反结论。示例 2 的最终答案"9534330"也印证了这一点——34排在3前面,是因为"343" > "334"。
结论:需要一种"拼接视角"的自定义排序
既然数值大小和字典序都无法直接刻画"谁在前能拼出更大结果",就必须针对两两拼接后的字符串大小来定义顺序。
三、贪心策略:以a+b与b+a的大小决定先后
核心思想
题解文档给出的解法思路非常精炼(README.md):
比较
a+b与b+a即可决定两串的先后,整体按这个比较排序再拼接;若最高位是0,说明全是0,返回"0"。
也就是说,对于任意两个数字字符串a与b:
- 若
a + b > b + a,说明把a放在b前面能拼出更大的数,因此a应排在b之前; - 若
a + b < b + a,则b应排在a之前。
把这一比较规则作为排序的比较器,对全部字符串排序,再将结果拼接起来,即可得到全局最大数。整个流程可以概括为三步:
- 转字符串:将每个整数
v转为字符串(保留其原有的位数信息); - 自定义排序:按
a + b与b + a的大小关系降序排列; - 拼接与边界处理:拼接所有字符串;若排序后首位是
"0",说明所有数字都是0,直接返回"0"。
为什么两两比较可以导出全局最优
这是一个典型的贪心正确性论证思路:若排序后的序列满足"任意相邻两个字符串x、y都有x + y >= y + x",则任意一对相邻元素的交换都不会让拼接结果变大;而任意一个不同的排列都可以通过一系列相邻交换从当前序列变换得到,因此当前序列在所有排列中拼接结果最大,即为全局最优。
还有一个值得注意的细节:当a + b == b + a时(例如数字1与11,两者拼接结果都是"111"),谁先谁后都不影响最终答案,因此比较器无需区分这种情况。
边界条件:全零数组必须返回"0"
如果不做处理,[0, 0]这类输入排序后拼接会得到"00",而正确结果应为"0"。由于非负整数转字符串时只有0自身会产生以0开头的字符串,因此排序后只要首位字符串是"0",就说明数组中所有元素都是0,直接返回"0"即可。这是各语言实现中几乎一致采用的判断方式。
四、多语言实现精讲(仓库源码对照)
仓库为该题提供了 7 种语言的独立源码文件(目录 solution/0100-0199/0179.Largest Number),与题解文档中的代码块一一对应。下面逐一讲解各语言如何落地上述贪心策略。
Python3:cmp_to_key自定义比较
源码见 Solution.py:
class Solution: def largestNumber(self, nums: List[int]) -> str: nums = [str(v) for v in nums] nums.sort(key=cmp_to_key(lambda a, b: 1 if a + b < b + a else -1)) return "0" if nums[0] == "0" else "".join(nums)要点说明:
- Python 的
list.sort不支持直接传入二元比较函数,需要借助functools.cmp_to_key将旧式比较函数包装为排序键;本地运行需补充from functools import cmp_to_key与from typing import List; - 比较函数语义:当
a + b < b + a时返回1(表示a排在b之后),否则返回-1(a排在b之前),等价于按拼接结果降序; - 排序完成后,若
nums[0] == "0",说明全部元素为0,返回"0",否则直接"".join(nums)拼接。
Java:Lambda 比较器
源码见 Solution.java:
class Solution { public String largestNumber(int[] nums) { List<String> vs = new ArrayList<>(); for (int v : nums) { vs.add(v + ""); } vs.sort((a, b) -> (b + a).compareTo(a + b)); if ("0".equals(vs.get(0))) { return "0"; } return String.join("", vs); } }要点说明:
- 通过
v + ""把整数转为字符串存入列表; - 比较器写成
(b + a).compareTo(a + b):若b + a大于a + b,返回正数,a排到b之后,从而保证拼接更大的组合排在前面,与 Python 版语义一致; String.join("", vs)完成拼接;首位为"0"时返回"0"。
C++:Lambda 直接比较拼接串
源码见 Solution.cpp:
class Solution { public: string largestNumber(vector<int>& nums) { vector<string> vs; for (int v : nums) vs.push_back(to_string(v)); sort(vs.begin(), vs.end(), [](string& a, string& b) { return a + b > b + a; }); if (vs[0] == "0") return "0"; string ans; for (string v : vs) ans += v; return ans; } };要点说明:
- 用
to_string(v)完成整数转字符串; sort的 Lambda 直接返回a + b > b + a,即"拼接更大者在前",作为升序排序的比较规则;- 拼接时用
ans += v累积,同样在vs[0] == "0"时提前返回。
Go:sort.Slice自定义比较
源码见 Solution.go:
func largestNumber(nums []int) string { vs := make([]string, len(nums)) for i, v := range nums { vs[i] = strconv.Itoa(v) } sort.Slice(vs, func(i, j int) bool { return vs[i]+vs[j] > vs[j]+vs[i] }) if vs[0] == "0" { return "0" } return strings.Join(vs, "") }要点说明:
strconv.Itoa(v)完成整数转字符串;sort.Slice的 less 函数返回vs[i]+vs[j] > vs[j]+vs[i],表示"拼接更大者在前";strings.Join(vs, "")拼接,全零时返回"0"。
C#:手写递归比较器(避免拼接字符串)
源码见 Solution.cs,C# 版本实现方式与其他语言略有不同——它没有在比较器中显式拼接字符串,而是手写了一个按位比较、递归处理公共前缀的IComparer<string>:
public class Comparer: IComparer<string> { public int Compare(string left, string right) { return Compare(left, right, 0, 0); } private int Compare(string left, string right, int lBegin, int rBegin) { var len = Math.Min(left.Length - lBegin, right.Length - rBegin); for (var i = 0; i < len; ++i) { if (left[lBegin + i] != right[rBegin + i]) { return left[lBegin + i] < right[rBegin + i] ? -1 : 1; } } if (left.Length - lBegin == right.Length - rBegin) { return 0; } if (left.Length - lBegin > right.Length - rBegin) { return Compare(left, right, lBegin + len, rBegin); } else { return Compare(left, right, lBegin, rBegin + len); } } } public class Solution { public string LargestNumber(int[] nums) { var sb = new StringBuilder(); var strs = nums.Select(n => n.ToString(CultureInfo.InvariantCulture)).OrderByDescending(s => s, new Comparer()); var nonZeroOccurred = false; foreach (var str in strs) { if (!nonZeroOccurred && str == "0") continue; sb.Append(str); nonZeroOccurred = true; } return sb.Length == 0 ? "0" : sb.ToString(); } }要点说明:
- 比较器先逐位比较两串的公共前缀;若前缀相同,则把较长串的剩余部分与较短串继续递归比较,逻辑上与比较
a + b、b + a等价,但避免了创建拼接字符串; - 主逻辑通过
OrderByDescending按该比较器降序排列; - 边界处理采用了另一种等价写法:拼接时跳过前导
"0",若最终StringBuilder为空则返回"0",同样能正确处理[0, 0]这类全零输入。
TypeScript:数值化比较
源码见 Solution.ts:
function largestNumber(nums: number[]): string { nums.sort((a, b) => { const [ab, ba] = [String(a) + String(b), String(b) + String(a)]; return +ba - +ab; }); return nums[0] ? nums.join('') : '0'; }要点说明:
- 比较器将
a + b与b + a两个拼接字符串通过一元+转为数值后相减:+ba - +ab > 0表示ba > ab,即b应排在a前; - 全零判断改用
nums[0] ? ... : '0':排序后首位为0(数值意义上的0)即返回"0"; - 需要注意,
nums[i]最大可达10^9,两个拼接串最长约 20 位,已超出 JavaScriptNumber的安全整数范围(Number.MAX_SAFE_INTEGER,约 9.007e15),数值化比较在高位场景下存在潜在的精度风险;在题目给定的数据范围内该实现可正确通过,但这也是部分语言选择纯字符串比较的原因之一。
JavaScript:与 TypeScript 相同的思路
源码见 Solution.js,实现与 TypeScript 版完全一致,只是省略了类型标注:
function largestNumber(nums) { nums.sort((a, b) => { const [ab, ba] = [String(a) + String(b), String(b) + String(a)]; return +ba - +ab; }); return nums[0] ? nums.join('') : '0'; }五、复杂度分析
在约束1 <= nums.length <= 100、0 <= nums[i] <= 10^9的前提下,从上述实现可以推断:
- 时间开销:排序为算法主体。比较次数为
O(n log n);每次比较涉及长度不超过约 10 位的两个字符串拼接(a + b、b + a,合并长度不超过约 20 位,可视为常数级),因此整体时间复杂度为O(n log n),对于n <= 100的规模非常轻松; - 空间开销:需要存放转换后的字符串数组,空间复杂度为
O(n);部分语言实现中排序器与拼接过程还会产生少量临时字符串。
六、小结与仓库资源导航
本题的核心套路可以浓缩为三点:
- 不要比较数值或字典序,而要比较
a + b与b + a两个拼接结果; - 用自定义比较器完成降序排序,各语言实现方式各异(
cmp_to_key、Lambda、sort.Slice、手写IComparer等),但语义完全一致; - 全零输入返回
"0",避免出现"00"这类前导零结果。
如果你希望进一步研究该题在仓库中的完整形态,可以参考以下文件:
- 中文题解:README.md
- 英文题解:README_EN.md
- 各语言实现(与题解文档代码块一一对应):
- Solution.py
- Solution.java
- Solution.cpp
- Solution.go
- Solution.cs
- Solution.ts
- Solution.js
"拼接比较法"(a + b与b + a定序)不仅是这道题的答案,也是面试中处理"数字拼接取极值"类问题(如排序数组排成最小数等变体)的通用思维模型,值得熟练掌握。
- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
相关推荐
LeetCode 179. 最大数(Largest Number)题解:基于 a+b 与 b+a 比较的自定义排序法
LeetCode 179. 最大数(Largest Number)题解:基于 a+b 与 b+a 比较的自定义排序法 本篇技术指南围绕 LeetCode 第 1
示例工程LeetCode-Go 题解 179. Largest Number:拼接比较排序法求解最大数
LeetCode Go 题解 179. Largest Number:拼接比较排序法求解最大数 本篇以 LeetCode Go 仓库中 179. Largest
示例工程LeetCode 179 Largest Number 题解:字符串拼接比较器与排序算法在 leetcode1/leetcode 仓库中的多语言实现
LeetCode 179 Largest Number 题解:字符串拼接比较器与排序算法在 leetcode1/leetcode 仓库中的多语言实现 本篇技术指
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考