问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

从CSP-J真题看插入排序:竞赛中的最优选择

创作时间:
2025-01-22 01:16:25
作者:
@小白创作中心

从CSP-J真题看插入排序:竞赛中的最优选择

在编程竞赛中,选手们经常面临快速处理小规模数据的任务。插入排序作为一种简单有效的排序方法,非常适合这种场景。本文将详细介绍如何利用插入排序算法在比赛中快速解决问题,包括其基本原理、代码实现以及一些实用的小技巧。通过这些实战技巧,参赛者可以在短时间内完成高质量的排序任务,提高比赛成绩。

01

插入排序的基本原理与特点

插入排序是一种简单直观的排序算法,其核心思想是通过构建有序序列,将未排序部分的元素逐个插入到已排序序列中。以下是它的基本步骤:

  1. 初始化:假设数组的第一个元素已排好序。
  2. 遍历与比较:从第二个元素开始,将其与已排序序列中的元素依次比较,找到合适位置后插入。
  3. 重复操作:继续从未排序部分取元素并插入已排序序列,直至整个数组有序。

插入排序的主要特点包括:

  • 简单易实现:代码结构清晰,易于理解和编写。
  • 稳定性:在插入过程中,相同大小的元素相对顺序保持不变,因此插入排序是稳定的排序算法。
  • 空间效率:是原地排序算法,仅需常数级别的额外存储空间,空间复杂度为O(1)。

然而,插入排序的时间复杂度为O(n^2),在大规模数据下表现不佳。因此,在编程竞赛中,它通常适用于以下场景:

  • 小规模数据集:数据量较小时,实现简单且占用内存少的优势明显。
  • 部分有序的数据:对几乎已排序的数组效率较高。
  • 教学与演示:逻辑清晰、易于理解,适合初学者学习基础排序原理。
02

编程竞赛中的应用场景

在编程竞赛中,插入排序常常出现在需要快速处理小规模数据的题目中。例如,在CSP-J 2021竞赛中有一道关于插入排序的题目,要求支持两种操作:修改数组元素和查询元素在排序后的位置。这道题目展示了插入排序在实际竞赛中的应用。

题目描述如下:

假设比较两个元素的时间为O(1),则插入排序可以以O(n^2)的时间复杂度完成长度为n的数组的排序。不妨假设这n个数字分别存储在a1, a2, …, an之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:

这下面是 C/C++ 的示范代码:

for (int i = 1; i <= n; i++)
    for (int j = i; j >= 2; j--)
        if (a[j] < a[j-1]) {
            int t = a[j-1];
            a[j-1] = a[j];
            a[j] = t;
        }

这下面是 Pascal 的示范代码:

for i:=1 to n do
    for j:=i downto 2 do
        if a[j]<a[j-1] then
            begin
                t:=a[i];
                a[i]:=a[j];
                a[j]:=t;
            end;

为了帮助小Z更好地理解插入排序,小Z的老师H老师留下了这么一道家庭作业:

H老师给了一个长度为n的数组a,数组下标从1开始,并且数组中的所有元素均为非负整数。小Z需要支持在数组a上的Q次操作,操作共两种,参数分别如下:

1 x v:这是第一种操作,会将a的第x个元素,也就是ax的值,修改为v。保证1 ≤ x ≤ n,1 ≤ v ≤ 10^9。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作

2 x:这是第二种操作,假设 H 老师按照上面的伪代码对a数组进行排序,你需要告诉 H 老师原来a数组的第x个元素,也就是ax,在排序后的新数组所处的位置。保证1 ≤ x ≤ n。注意这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影响后续的操作

H老师不喜欢过多的修改,所以他保证类型1的操作次数不超过5000。

小Z没有学过计算机竞赛,因此小Z并不会做这道题。他找到了你来帮助他解决这个问题。

输入格式:

第一行,包含两个正整数n, Q,表示数组长度和操作次数。

第二行,包含n个空格分隔的非负整数,其中第i个非负整数表示ai。

接下来Q行,每行2 ∼ 3个正整数,表示一次操作,操作格式见【题目描述】。

输出格式:

对于每一次类型为2的询问,输出一行一个正整数表示答案。

样例 #1:

样例输入 #1:

3 4
3 2 1
2 3
1 3 2
2 2
2 3

样例输出 #1:

1
1
2

样例解释 #1:

在修改操作之前,假设H老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是3, 2, 1。

在修改操作之后,假设H老师进行了一次插入排序,则原序列的三个元素在排序结束后所处的位置分别是3, 1, 2。

注意虽然此时a2 = a3,但是我们不能将其视为相同的元素

数据范围:

对于所有测试数据,满足1 ≤ n ≤ 8000,1 ≤ Q ≤ 2 × 10^5,1 ≤ x ≤ n,1 ≤ v, ai ≤ 10^9。

对于所有测试数据,保证在所有Q次操作中,至多有5000次操作属于类型一。

各测试点的附加限制及分值如下表所示。

测试点 n Q 特殊性质
1 ∼ 4 10 10 无
5 ∼ 7 8000 2 × 10^5 无
8 ∼ 10 8000 2 × 10^5 无
11 ∼ 14 8000 2 × 10^5 无

这道题目展示了插入排序在编程竞赛中的实际应用。由于数据规模较小(n ≤ 8000),插入排序的时间复杂度O(n^2)在可接受范围内。同时,题目要求支持元素修改和查询排序后位置的操作,这需要在排序过程中记录元素的原始位置信息,而插入排序的稳定性恰好可以保证这一点。

03

实战技巧

在编程竞赛中使用插入排序时,可以采用以下技巧来优化性能和简化代码:

  1. 减少元素移动次数:在插入过程中,可以使用一个临时变量存储待插入元素,避免多次交换操作。例如:
void SortInsert(int* a, int n) {
    for (int i = 0; i < n - 1; i++) {
        int end = i;
        int tmp = a[end + 1];
        while (end >= 0) {
            if (a[end] > tmp) {
                a[end + 1] = a[end];
                end--;
            } else {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}
  1. 与其他算法的对比:在选择排序算法时,需要考虑数据规模和特性。对于小规模数据或部分有序的数据,插入排序通常比快速排序、归并排序等更高效。例如,在上述CSP-J 2021的题目中,由于数据规模较小且需要记录元素的原始位置,插入排序是最佳选择。

  2. 特殊场景处理:在某些情况下,可能需要对插入排序进行特殊处理。例如,在上述题目中,通过pair存储元素值和原下标,以便在修改元素值后重新排序。此外,还可以使用额外的数组记录元素的当前位置,以便快速响应查询操作。

04

代码实现与示例

以下是插入排序的基本代码实现:

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

结合上述CSP-J 2021的题目,完整的代码实现如下:

#include <cstdio>
#include <algorithm>
#include <utility>
#define N 10005
using namespace std;

int n, q;
pair<int, int> a[N];
int op, x, y;
int pos[N];

void sort_once() {
    for (int i = 1; i < n; ++i)
        if (a[i] > a[i + 1]) {
            swap(pos[a[i].second], pos[a[i + 1].second]);
            swap(a[i], a[i + 1]);
        }

    for (int i = n - 1; i > 0; --i)
        if (a[i] > a[i + 1]) {
            swap(pos[a[i].second], pos[a[i + 1].second]);
            swap(a[i], a[i + 1]);
        }
}

int main() {
    scanf("%d%d", &n, &q);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i].first);
        a[i].second = pos[i] = i;
    }

    for (int i = 1; i <= n; ++i)
        sort_once();

    while (q-- && scanf("%d%d", &op, &x)) {
        if (op == 1) {
            scanf("%d", &y);
            for (int i = 1; i <= n; ++i)
                if (a[i].second == x) {
                    a[i].first = y;
                    break;
                }
            sort_once();
        } else
            printf("%d\n", pos[x]);
    }

    return 0;
}

这段代码展示了如何在编程竞赛中灵活运用插入排序。通过使用pair存储元素值和原下标,以及通过额外数组记录元素位置,可以高效地处理元素修改和查询操作。

05

总结与建议

插入排序在编程竞赛中虽然不如快速排序、归并排序等高级算法高效,但在特定场景下(如小规模数据、部分有序数据)具有独特优势。其简单易实现、稳定性好等特点,使其成为竞赛选手的重要工具。在学习和使用插入排序时,建议重点关注以下几点:

  1. 理解原理:掌握插入排序的基本思想和步骤,理解其时间复杂度和空间复杂度。
  2. 适用场景:明确插入排序在小规模数据和部分有序数据中的优势。
  3. 实战技巧:学会通过临时变量减少元素移动,以及在特殊场景下(如需要记录元素位置)的处理方法。

通过深入理解插入排序的原理和应用场景,选手们可以在编程竞赛中更加灵活地运用这一经典算法,提高解题效率和代码质量。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号