技术笔记:算法1-2 排序(NOIP经典真题解析)
排序是算法竞赛中最基础、最核心的技能之一,它不仅是许多复杂算法的基础,也常常作为独立考点出现在各类编程竞赛中。本文将通过三道NOIP经典真题(选票统计、奖学金评选、欢乐的跳跃),讲解不同排序场景下的解题思路和代码实现技巧,帮助你掌握排序算法的实际应用。
一、 选票统计(大规模数据排序)
题目核心要求
给定n名候选人的m张选票(1 ≤ n ≤ 999,1 ≤ m ≤ 2,000,000),将所有选票按候选人编号从小到大排序并输出。
解题思路
这道题的核心挑战在于处理大规模输入数据(m最大为2e6),需要从时间和空间两方面进行优化:
- 输入优化 :使用
BufferedReader替代Scanner,因为其读取速度更快,能避免因输入过大导致的超时(TLE)。 - 排序优化 :直接使用Java内置的
Arrays.sort(),它基于经过高度优化的双枢轴快速排序算法,性能远超手写的冒泡或选择排序。 - 输出优化 :使用
StringBuilder拼接结果,最后一次性输出,减少频繁I/O操作带来的性能损耗。
完整Java代码
java
import java.util.Arrays;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
// 使用 BufferedReader 处理大规模输入,速度更快
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 读取第一行,获取候选人数量n和选票数量m
String[] firstLine = br.readLine().split(" ");
int n = Integer.parseInt(firstLine[0]);
int m = Integer.parseInt(firstLine[1]);
// 读取第二行,获取所有选票数据
String[] voteStrs = br.readLine().split(" ");
int[] votes = new int[m];
for (int i = 0; i < m; i++) {
votes[i] = Integer.parseInt(voteStrs[i]);
}
// 使用内置排序,简单高效
Arrays.sort(votes);
// 使用 StringBuilder 拼接结果,减少I/O次数
StringBuilder sb = new StringBuilder();
for (int vote : votes) {
sb.append(vote).append(" ");
}
// 输出结果,trim()去除末尾多余空格
System.out.println(sb.toString().trim());
// 关闭资源
br.close();
}
}
关键注意点
- 输入速度是关键 :对于百万级别的输入,
Scanner的速度瓶颈会非常明显,BufferedReader是更优选择。 - 内置排序的优势 :
Arrays.sort()在处理基本类型数组时效率极高,除非有特殊需求,否则不要手写排序算法。 - 减少I/O操作 :频繁调用
System.out.println()会严重拖慢程序速度,使用StringBuilder可以将多次输出合并为一次。
二、 奖学金评选(多关键字排序)
题目核心要求
给定n名学生的语文、数学、英语成绩,按以下规则评选出前5名并输出其学号和总分:
- 优先按总分从高到低排序
- 若总分相同,按语文成绩从高到低排序
- 若语文成绩也相同,按学号从小到大排序
解题思路
这是一个典型的多关键字排序问题,需要自定义排序规则。
- 数据封装 :创建一个
Student类,封装学生的学号、各科成绩和总分,便于统一管理和排序。 - 自定义排序 :实现冒泡排序(或使用
Arrays.sort()结合自定义比较器),严格按照题目给定的优先级进行比较和交换。 - 输出前5名:排序完成后,直接输出数组中前5个学生的学号和总分。
完整Java代码
java
import java.util.Scanner;
class Student {
int id;
int chinese;
int math;
int english;
int total;
}
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
Student[] students = new Student[n];
// 读取并初始化学生信息
for (int i = 0; i < n; i++) {
students[i] = new Student();
students[i].id = i + 1; // 学号从1开始
students[i].chinese = sc.nextInt();
students[i].math = sc.nextInt();
students[i].english = sc.nextInt();
students[i].total = students[i].chinese + students[i].math + students[i].english;
}
// 冒泡排序实现多关键字排序
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (shouldSwap(students[j], students[j + 1])) {
Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}
// 输出前5名学生
for (int i = 0; i < 5; i++) {
System.out.println(students[i].id + " " + students[i].total);
}
sc.close();
}
/**
* 判断两个学生是否需要交换位置
* @param a 前一个学生
* @param b 后一个学生
* @return 需要交换返回true,否则返回false
*/
private static boolean shouldSwap(Student a, Student b) {
if (a.total != b.total) {
return a.total < b.total; // 总分低的在后,需要交换
} else if (a.chinese != b.chinese) {
return a.chinese < b.chinese; // 语文成绩低的在后,需要交换
} else {
return a.id > b.id; // 学号大的在后,需要交换
}
}
}
关键注意点
- 多关键字比较的优先级:必须严格按照题目给定的顺序进行比较,先比较总分,再比较语文成绩,最后比较学号。
- 冒泡排序的稳定性:冒泡排序是稳定排序,能够保证当关键字相同时,原始顺序得以保留,这在处理学号这种次要关键字时非常重要。
- 代码的可维护性 :将比较逻辑抽离到
shouldSwap方法中,使代码结构更清晰,也更容易理解和维护。
三、 欢乐的跳跃(排序辅助验证)
题目核心要求
给定一个包含n个元素的整数数组,判断其是否符合"欢乐的跳跃"定义:数组中任意两个连续元素之差的绝对值,必须包含[1, n-1]之间的所有整数。
解题思路
这道题的巧妙之处在于,我们可以利用排序来辅助验证:
- 计算差值 :遍历数组,计算每对连续元素之差的绝对值,得到一个包含
n-1个元素的差值数组。 - 排序差值数组:对差值数组进行排序。
- 验证连续性 :检查排序后的差值数组是否恰好是
1, 2, 3, ..., n-1。如果是,则符合"欢乐的跳跃";否则不符合。
完整Java代码
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 循环处理多组输入
while (scanner.hasNext()) {
int n = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
// 处理n=1的特殊情况,直接符合条件
if (n == 1) {
System.out.println("Jolly");
continue;
}
// 计算连续元素差值的绝对值
int[] diffs = new int[n - 1];
for (int i = 0; i < n - 1; i++) {
diffs[i] = Math.abs(a[i + 1] - a[i]);
}
// 对差值数组进行排序
Arrays.sort(diffs);
// 验证差值是否连续
boolean isJolly = true;
for (int i = 0; i < n - 1; i++) {
if (diffs[i] != i + 1) {
isJolly = false;
break;
}
}
// 输出结果
System.out.println(isJolly ? "Jolly" : "Not jolly");
}
scanner.close();
}
}
关键注意点
- 特殊情况处理 :当
n=1时,数组没有连续元素,默认符合"欢乐的跳跃"定义。 - 排序的辅助作用:排序将问题转化为一个简单的线性验证,使我们能够高效地判断差值的连续性。
- 时间复杂度分析 :算法的时间复杂度主要由排序步骤决定,为O(n log n),这在题目给定的
n ≤ 1000的约束下是完全可行的。