这道题不能按数值大小或普通字符串顺序排序。对于两个数字字符串 a 和 b,只需比较:
a + b > b + a,就把 a 放在 b 前面。a + b < b + a,就把 b 放在 a 前面。按这个规则排序后连接所有字符串,就是能够组成的最大数。
给定一组非负整数 nums,重新排列每个数的顺序,使它们连接后组成最大的整数。每个整数不可拆分。
由于结果可能非常大,必须返回字符串。
示例 1:
示例 2:
题目约束:
1 <= nums.length <= 100。0 <= nums[i] <= 10^9。按数值降序排列 [3, 30] 会得到:
但正确顺序是:
普通字符串降序也不可靠。例如 "121" 在普通字典序中大于 "12",但:
所以 "12" 应放在 "121" 前面。决定顺序的不是单个字符串本身,而是两种拼接结果。
定义:
例如:
a | b | a + b | b + a | 顺序 |
|---|---|---|---|---|
"3" | "30" | "330" | "303" | "3" 在前 |
"34" | "3" | "343" | "334" | "34" 在前 |
"12" | "121" | "12121" | "12112" | "12" 在前 |
"12" | "1212" | "121212" | "121212" | 任意顺序 |
a + b 和 b + a 的长度始终相同,并且只包含数字,所以可以直接使用字符串字典序比较,不需要转换成数值。
考虑任意排列中相邻的两个字符串 a 和 b,其余前缀和后缀分别记为 P 和 S:
如果 a + b < b + a,交换二者后得到:
两个完整字符串的公共前缀 P 相同,公共后缀 S 也不影响首次出现差异的位置。因为 b + a 更大,所以交换后的完整结果一定更大。
因此,任何最优排列都不能包含违反比较规则的相邻逆序对。按照 a + b 与 b + a 的规则排序,会消除所有这样的逆序对,所以得到全局最大结果。
排序比较器必须保持一致,不能出现 a 应在 b 前、b 应在 c 前,但 c 又应在 a 前的循环关系。
关系:
等价于比较无限周期字符串:
普通字典序具有传递性,因此这个拼接比较规则也具有传递性,可以安全地交给排序算法使用。若两种拼接相等,交换它们不会改变最终字符串。
不能把拼接结果转换成 Number 后相减:
题目允许单个数字达到 10^9,两个数字拼接后最多有 20 位,已经超过 JavaScript 的安全整数范围。不同字符串可能被舍入成相同的 Number。
例如:
正确结果是 a + b 更大,但这两个字符串转换成 Number 后会得到相同的浮点数。比较器会错误地返回 0。
正确做法是直接比较等长字符串:
以 nums = [3, 30, 34, 5, 9] 为例:
"9" + "5" > "5" + "9",所以 "9" 在 "5" 前。"5" + "34" > "34" + "5",所以 "5" 在 "34" 前。"34" + "3" > "3" + "34",所以 "34" 在 "3" 前。"3" + "30" > "30" + "3",所以 "3" 在 "30" 前。排序结果:
连接后得到:
参考实现来源:doocs/leetcode,按本文的字符串比较方式重新整理,避免 JavaScript 大整数精度问题;原项目采用 CC BY-SA 4.0。
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 转成字符串 | nums.map((num) => String(num)) | 便于拼接比较,同时不修改输入数组 |
| 生成两种顺序 | ab = a + b、ba = b + a | 判断两个元素谁应排在前面 |
| 自定义排序 | ab > ba ? -1 : 1 | 按能产生更大拼接结果的顺序排列 |
| 处理全零 | values[0] === '0' | 将 "000" 规范化为 "0" |
| 连接结果 | values.join('') | 返回可能超过数值范围的字符串 |
如果排序后第一个字符串是 "0",说明所有元素都是 0。因为任意非零字符串与 "0" 比较时都会排在 "0" 前面。
因此:
而不是:
只检查排序后的第一个元素即可,不需要删除结果中的每一个前导零。
"0"。[0, 1, 0] → "100"。"12" 与 "121"。"12" 与 "1212",任意顺序都产生相同结果。Number: 拼接值可能超过安全整数范围。nums。设数组长度为 n,单个整数转换后的最大位数为 k:
O(nk log n)。排序需要 O(n log n) 次比较,每次拼接和比较至多处理 O(k) 个字符。O(nk)。字符串数组保存所有数字的字符串形式;排序算法自身的额外空间取决于 JavaScript 引擎,但不会改变该上界。题目中 k <= 10,把位数视为常数时,也可以简写为时间 O(n log n)、额外空间 O(n)。返回字符串占用的空间不计入额外空间。
数值大小只描述单个数字,不描述拼接后的相对贡献。例如 30 > 3,但 "330" > "303",所以 3 应在 30 前面。
a + b 和b + a?两个元素在最终结果中相邻时只有这两种顺序。选择更大的拼接顺序,无论它们前后还有什么内容,都不会让完整结果变小。
如果一个排列中存在相邻逆序对 a、b,并且 a + b < b + a,交换它们会严格增大完整字符串。不断消除逆序对后得到的排序结果不存在可改进的相邻交换,因此是全局最优。
满足。a + b >= b + a 等价于比较 a 和 b 的无限周期字符串;字典序具有传递性,所以不会形成循环比较关系。
Number(ba) - Number(ab)?两个输入拼接后最多有 20 位,超过 JavaScript 的安全整数范围,转换时可能丢失低位差异,使本应有先后顺序的元素被错误地视为相等。
"0" 就代表全是零?任何非零字符串按照拼接规则都会排在 "0" 前面。如果 "0" 仍位于首位,就不可能存在非零元素。
a + b === b + a 怎么办?返回 0 即可。这类字符串通常具有相同的重复模式,例如 "12" 和 "1212";无论谁在前,局部乃至最终拼接结果都相同。
O(n log n)?排序比较器不是常数成本:它需要拼接并比较最多 2k 个字符。完整表达是 O(nk log n);只有把题目限定的最大位数 k <= 10 视为常数时,才简写为 O(n log n)。
"0"。O(1),忽略字符串数组和排序空间。a + b > b + a,就让 a 排在 b 前面。先只回答第 1 题,再展开后续问题:
a + b 和 b + a 就能决定两个元素的相对顺序?[121, 12] 时,为什么 "12" 应排在 "121" 前面?[0, 0, 0] 需要单独返回 "0"?Number(b + a) - Number(a + b) 会比较失败的用例,并解释失败原因。