1. 商品多属性排序问题解析
最近在准备华为OD机考时遇到一个很有意思的排序问题 - 商品推荐的多属性排序。这个问题模拟了电商平台常见的商品排序场景,非常具有实际应用价值。今天我就来详细拆解这个问题的解决思路,并提供Java、Python、Go、C++和JavaScript五种语言的实现方案。
1.1 问题背景与需求
电商平台在展示商品时,通常需要根据多个属性进行综合排序。比如双十一选购坚果时,平台可能同时考虑价格、销量、好评率等多个维度。本题要求我们实现这样的多属性排序功能:
- 输入n个商品,每个商品有m个属性
- 每个属性可以指定升序或降序排列
- 当某属性值相同时,继续比较下一个属性
- 最终输出完整排序后的商品列表
1.2 输入输出示例分析
以题目给出的示例为例:
输入:
code复制4 3
1 -1 1
2 2 2
2 3 3
4 4 4
4 4 5
这表示:
- 有4个商品,每个商品有3个属性
- 排序规则:第1个属性降序(1),第2个属性升序(-1),第3个属性降序(1)
- 然后是4个商品的具体属性值
输出:
code复制4 4 5
4 4 4
2 2 2
2 3 3
这个结果是如何得出的呢?我们来看排序过程:
- 首先按第1个属性降序排列:
- 4 > 2,所以两个4开头的商品排前面
- 两个4开头的商品第1属性相同,继续比较第2个属性(升序)
- 4 == 4,继续比较第3个属性(降序)
- 5 > 4,所以4 4 5排在4 4 4前面
- 两个2开头的商品:
- 第1属性都是2
- 比较第2属性(升序):2 < 3
- 所以2 2 2排在2 3 3前面
2. 算法设计与实现思路
2.1 核心排序逻辑
这个问题本质上是一个多关键字排序问题,可以通过自定义比较器来实现。具体思路:
- 读取排序规则,确定每个属性的排序方向(升序/降序)
- 为商品列表实现自定义比较函数:
- 依次比较每个属性
- 如果当前属性不相等,根据排序方向返回比较结果
- 如果相等,继续比较下一个属性
- 使用语言提供的排序函数,传入自定义比较器进行排序
2.2 关键实现细节
在实现过程中有几个需要注意的关键点:
- 排序规则的存储:需要将第二行的排序方向保存下来,供比较函数使用
- 比较函数的实现:需要正确处理升序和降序的不同比较逻辑
- 属性值的比较顺序:必须严格按照属性顺序依次比较
- 性能考虑:当n较大时(题目中n<=100000),需要使用高效的排序算法
提示:大多数语言的标准库排序函数时间复杂度为O(nlogn),对于n=100000来说完全够用。
3. 多语言实现方案
下面我将给出五种语言的完整实现代码,并解释关键部分。
3.1 Java实现
java复制import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] orders = new int[m];
for (int i = 0; i < m; i++) {
orders[i] = sc.nextInt();
}
int[][] products = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
products[i][j] = sc.nextInt();
}
}
Arrays.sort(products, (a, b) -> {
for (int i = 0; i < m; i++) {
if (a[i] != b[i]) {
return orders[i] * (a[i] - b[i]);
}
}
return 0;
});
for (int[] product : products) {
for (int j = 0; j < m; j++) {
System.out.print(product[j] + " ");
}
System.out.println();
}
}
}
Java实现要点:
- 使用
Arrays.sort配合lambda表达式实现自定义比较器 - 比较器中通过遍历属性,根据排序方向调整比较结果
orders[i] * (a[i] - b[i])巧妙处理了升序降序:当order=1时降序,order=-1时升序
3.2 Python实现
python复制n, m = map(int, input().split())
orders = list(map(int, input().split()))
products = [list(map(int, input().split())) for _ in range(n)]
products.sort(key=lambda x: tuple(o * v for o, v in zip(orders, x)))
for p in products:
print(' '.join(map(str, p)))
Python实现要点:
- 利用Python的稳定排序特性
- 通过生成排序键的tuple实现多属性排序
o * v技巧同样用于处理升序降序- 代码非常简洁,体现了Python的优势
3.3 C++实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> orders(m);
for (int i = 0; i < m; ++i) {
cin >> orders[i];
}
vector<vector<int>> products(n, vector<int>(m));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
cin >> products[i][j];
}
}
sort(products.begin(), products.end(), [&orders](const vector<int>& a, const vector<int>& b) {
for (int i = 0; i < orders.size(); ++i) {
if (a[i] != b[i]) {
return orders[i] * (a[i] - b[i]) > 0;
}
}
return false;
});
for (const auto& p : products) {
for (int val : p) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
C++实现要点:
- 使用lambda表达式作为自定义比较函数
- 注意C++的sort比较函数返回的是a是否应该排在b前面
- 使用vector存储数据,避免手动内存管理
3.4 Go实现
go复制package main
import (
"bufio"
"fmt"
"os"
"sort"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
nm := strings.Fields(scanner.Text())
n, _ := strconv.Atoi(nm[0])
m, _ := strconv.Atoi(nm[1])
scanner.Scan()
orderStr := strings.Fields(scanner.Text())
orders := make([]int, m)
for i := range orderStr {
orders[i], _ = strconv.Atoi(orderStr[i])
}
products := make([][]int, n)
for i := 0; i < n; i++ {
scanner.Scan()
productStr := strings.Fields(scanner.Text())
product := make([]int, m)
for j := range productStr {
product[j], _ = strconv.Atoi(productStr[j])
}
products[i] = product
}
sort.Slice(products, func(i, j int) bool {
a, b := products[i], products[j]
for k := 0; k < m; k++ {
if a[k] != b[k] {
return orders[k]*(a[k]-b[k]) < 0
}
}
return false
})
for _, p := range products {
for _, val := range p {
fmt.Printf("%d ", val)
}
fmt.Println()
}
}
Go实现要点:
- Go的sort.Slice需要自定义Less函数
- 注意Go的输入处理相对繁琐
- 比较逻辑与其他语言类似,但语法有所不同
3.5 JavaScript实现
javascript复制const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let n, m;
let orders = [];
let products = [];
let lineCount = 0;
rl.on('line', (line) => {
if (lineCount === 0) {
[n, m] = line.trim().split(' ').map(Number);
lineCount++;
} else if (lineCount === 1) {
orders = line.trim().split(' ').map(Number);
lineCount++;
} else {
const product = line.trim().split(' ').map(Number);
products.push(product);
if (products.length === n) {
products.sort((a, b) => {
for (let i = 0; i < m; i++) {
if (a[i] !== b[i]) {
return orders[i] * (b[i] - a[i]);
}
}
return 0;
});
products.forEach(p => {
console.log(p.join(' '));
});
rl.close();
}
}
});
JavaScript实现要点:
- 使用Node.js的readline模块处理输入
- 数组的sort方法可以传入自定义比较函数
- 比较逻辑与其他语言一致
- 注意异步输入处理的方式
4. 算法复杂度与优化分析
4.1 时间复杂度分析
本算法的主要时间消耗在排序步骤:
- 排序时间复杂度:O(nlogn)
- 每次比较的时间复杂度:O(m)
- 总时间复杂度:O(m * nlogn)
对于题目给定的约束条件(m<=10, n<=100000):
- m很小,可以视为常数
- nlogn ≈ 100000 * 17 ≈ 1.7百万次操作
- 在现代计算机上完全可以接受
4.2 空间复杂度分析
- 需要存储所有商品属性:O(n*m)
- 排序可能需要额外空间(取决于语言实现)
- 对于n=100000, m=10,大约需要4MB内存(假设int为4字节)
4.3 可能的优化方向
虽然当前算法已经足够高效,但在极端情况下还可以考虑:
- 避免存储所有数据:如果内存非常紧张,可以考虑流式处理
- 并行排序:对于非常大的n,可以考虑并行排序算法
- 基数排序:如果属性值范围有限,可能适用
不过对于机考场景,上述优化通常没有必要,标准实现已经足够。
5. 常见问题与调试技巧
在实际编码和调试过程中,可能会遇到以下问题:
5.1 排序结果不正确
可能原因:
- 排序方向处理错误(升序降序混淆)
- 属性比较顺序错误
- 数据类型不匹配(如将字符串当数字比较)
解决方法:
- 打印中间结果,验证排序规则是否正确应用
- 编写小规模测试用例手动验证
- 检查数据类型转换
5.2 大输入超时
可能原因:
- 使用了低效的排序算法(如冒泡排序)
- 比较函数实现不够高效
- 输入输出处理不当
解决方法:
- 确保使用语言标准库的高效排序函数
- 优化比较函数,尽早返回结果
- 使用快速的IO方法(如C++的ios::sync_with_stdio(false))
5.3 边界条件处理
需要特别注意的边界情况:
- 所有商品属性完全相同
- 只有一个商品或一个属性
- 属性值为极大/极小值
经验分享:在机考中,务必手动测试边界用例,这是常见的失分点。我通常会准备几个小测试用例,包括最小输入、最大输入、全部相同值等情况。
6. 实际应用扩展
虽然这个问题是机考题目,但它的解决方案在实际开发中很有价值:
- 电商排序系统:可以扩展为支持用户自定义排序规则的商品展示系统
- 数据分析:处理需要多列排序的数据报表
- 游戏排行榜:实现综合多个指标的玩家排名
如果要在生产环境中使用,还可以考虑以下增强:
- 支持更复杂的排序规则(如权重组合)
- 添加缓存机制提高性能
- 实现分页加载避免一次性处理大量数据
通过这个机考题目的练习,我们不仅掌握了多属性排序的算法实现,也学习了一个在实际开发中非常有用的技术模式。