洛谷算法1-2 排序(NOIP经典真题解析)java(持续更新)

技术笔记:算法1-2 排序(NOIP经典真题解析)

排序是算法竞赛中最基础、最核心的技能之一,它不仅是许多复杂算法的基础,也常常作为独立考点出现在各类编程竞赛中。本文将通过三道NOIP经典真题(选票统计、奖学金评选、欢乐的跳跃),讲解不同排序场景下的解题思路和代码实现技巧,帮助你掌握排序算法的实际应用。

一、 选票统计(大规模数据排序)

题目核心要求

给定n名候选人的m张选票(1 ≤ n ≤ 999,1 ≤ m ≤ 2,000,000),将所有选票按候选人编号从小到大排序并输出。

解题思路

这道题的核心挑战在于处理大规模输入数据(m最大为2e6),需要从时间和空间两方面进行优化:

  1. 输入优化 :使用BufferedReader替代Scanner,因为其读取速度更快,能避免因输入过大导致的超时(TLE)。
  2. 排序优化 :直接使用Java内置的Arrays.sort(),它基于经过高度优化的双枢轴快速排序算法,性能远超手写的冒泡或选择排序。
  3. 输出优化 :使用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();
    }
}

关键注意点

  1. 输入速度是关键 :对于百万级别的输入,Scanner的速度瓶颈会非常明显,BufferedReader是更优选择。
  2. 内置排序的优势Arrays.sort()在处理基本类型数组时效率极高,除非有特殊需求,否则不要手写排序算法。
  3. 减少I/O操作 :频繁调用System.out.println()会严重拖慢程序速度,使用StringBuilder可以将多次输出合并为一次。

二、 奖学金评选(多关键字排序)

题目核心要求

给定n名学生的语文、数学、英语成绩,按以下规则评选出前5名并输出其学号和总分:

  1. 优先按总分从高到低排序
  2. 若总分相同,按语文成绩从高到低排序
  3. 若语文成绩也相同,按学号从小到大排序

解题思路

这是一个典型的多关键字排序问题,需要自定义排序规则。

  1. 数据封装 :创建一个Student类,封装学生的学号、各科成绩和总分,便于统一管理和排序。
  2. 自定义排序 :实现冒泡排序(或使用Arrays.sort()结合自定义比较器),严格按照题目给定的优先级进行比较和交换。
  3. 输出前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; // 学号大的在后,需要交换
        }
    }
}

关键注意点

  1. 多关键字比较的优先级:必须严格按照题目给定的顺序进行比较,先比较总分,再比较语文成绩,最后比较学号。
  2. 冒泡排序的稳定性:冒泡排序是稳定排序,能够保证当关键字相同时,原始顺序得以保留,这在处理学号这种次要关键字时非常重要。
  3. 代码的可维护性 :将比较逻辑抽离到shouldSwap方法中,使代码结构更清晰,也更容易理解和维护。

三、 欢乐的跳跃(排序辅助验证)

题目核心要求

给定一个包含n个元素的整数数组,判断其是否符合"欢乐的跳跃"定义:数组中任意两个连续元素之差的绝对值,必须包含[1, n-1]之间的所有整数。

解题思路

这道题的巧妙之处在于,我们可以利用排序来辅助验证:

  1. 计算差值 :遍历数组,计算每对连续元素之差的绝对值,得到一个包含n-1个元素的差值数组。
  2. 排序差值数组:对差值数组进行排序。
  3. 验证连续性 :检查排序后的差值数组是否恰好是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();
    }
}

关键注意点

  1. 特殊情况处理 :当n=1时,数组没有连续元素,默认符合"欢乐的跳跃"定义。
  2. 排序的辅助作用:排序将问题转化为一个简单的线性验证,使我们能够高效地判断差值的连续性。
  3. 时间复杂度分析 :算法的时间复杂度主要由排序步骤决定,为O(n log n),这在题目给定的n ≤ 1000的约束下是完全可行的。

相关推荐
想进个大厂2 小时前
代码随想录day35 36
算法·leetcode·职场和发展
嵌入小生0072 小时前
数据结构 | 常用排序算法大全及二分查找
linux·数据结构·算法·vim·排序算法·嵌入式
近津薪荼2 小时前
优选算法——滑动窗口4(找子串)
c++·学习·算法
小小仙。2 小时前
IT自学第二十二天
java·开发语言
索荣荣2 小时前
Java Cookie 全面指南:从原理到 Spring Boot 实战
java·开发语言·python
红烧柯基2 小时前
nohup java -jar运行jar包时设置启动参数
java·开发语言·jar
2301_822377652 小时前
模板代码异常处理
开发语言·c++·算法
hcnaisd22 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
多恩Stone2 小时前
【3DV 进阶-12】Trellis.2 数据处理脚本细节
人工智能·pytorch·python·算法·3d·aigc