编程入门必刷题:从“马里奥找银币”掌握数组排序与中位数算法

发布时间:2026/8/7 14:27:22
编程入门必刷题:从“马里奥找银币”掌握数组排序与中位数算法 1. 问题引入从“马里奥找银币”到编程入门的第一道坎刚接触编程的新手尤其是从C或Python开始学起的朋友大概率都见过一类题目给你一堆数字让你找出中间大小的那个。这类问题在各大在线评测平台OJ的“入门”分类里出镜率极高比如这个“1585: 【入门】马里奥找中等的银币”。题目名字听起来挺有故事性马里奥兄弟在蘑菇王国里捡金币是家常便饭但这次要找的是“中等”的银币这就有点意思了。为什么这类题目会成为入门必刷因为它几乎完美地串联起了编程初学者需要掌握的几个最核心、最基础的概念数据的输入输出、数组或列表的基本操作、排序以及条件判断。别看它简单很多人在第一次面对时还是会卡在几个关键点上数组下标是从0开始还是1开始排序之后怎么取“中间”那个数如果有偶数个银币题目要求的“中等”到底指什么这些疑惑不解决后面学习更复杂的算法和数据结构就会根基不稳。我自己带过不少编程新手发现他们往往不是不会写for循环而是在处理这类“找中位数”或“找第K大”的变形题时对数据结构的理解是模糊的。他们会把数字杂乱地存进一个变量里或者试图用一堆if-else去硬比较代码写得又长又容易出错。这道“马里奥找银币”题就是一个绝佳的练习场让我们把“数组”这个编程世界里最基础的容器彻底搞明白。接下来我们就抛开马里奥的冒险故事直接切入核心看看如何用代码思路解决这个找“中等银币”的问题并在这个过程中把数组相关的知识点夯扎实。2. 核心需求解析什么是“中等的银币”题目描述通常很简洁马里奥收集了N枚银币每枚银币都有一个面值。他不想拿最大的可能太显眼被库巴抢走也不想拿最小的可能不值钱就想找出那枚大小“中等”的银币。在编程题中这个“中等”在绝大多数情况下指的就是数学和统计学中的中位数。这里需要严格区分两个概念平均数和中位数。平均数Mean是所有银币面值加起来除以个数它容易受到极端大或极端小的值影响。比如银币面值是[1, 2, 3, 100]平均数是26.5但这个数显然不能代表这堆银币的“中等”水平。而中位数Median则不同它是将一组数据按大小顺序排列后处于中间位置的那个数。它能更好地反映数据的“典型”水平不受极端值干扰。所以题目的核心需求可以翻译为给定一个包含N个整数的数组找出这个数组的中位数。具体到操作步骤就是以下三步输入读取银币的数量N然后读取N个银币的面值存入一个数组。处理将这个数组按照从小到大的顺序排序。输出从排序后的数组中找出并输出位于最中间的那个元素的值。但这里藏着一个关键的细节也是新手最容易栽跟头的地方数组元素个数N的奇偶性。这直接决定了“中间位置”到底是一个还是两个。如果N是奇数中位数是唯一的就是排序后位于第(N1)/2个位置的数注意这里说的是“第几个”从1开始计数。对应到数组下标从0开始就是索引为(N-1)/2的元素。如果N是偶数严格意义上的中位数是中间两个数的平均值。但在很多入门级OJ题目中为了简化输出避免处理小数题目会明确约定输出第N/2个大的数或者第N/2 1个大的数。这一点至关重要必须仔细阅读题目的输出描述。对于“马里奥找中等的银币”这类典型题通常的约定是当N为偶数时输出排序后第N/2个元素即两个中间值里较小的那个。例如数组[1,3,5,7]排序后就是它本身N4为偶数则输出第2个元素索引为1也就是3。因此在动手编码前我们必须像侦探一样审题明确题目对“中等”的确切定义。假设本题遵循常见约定偶数N时输出第N/2个那么我们的目标就非常清晰了。3. 从零开始环境准备与数组基础在开始写解题代码之前我们得先把“战场”布置好。对于C选手推荐使用轻量级的Dev-C、Code::Blocks或者功能强大的Visual Studio Code配合MinGW编译器。Python选手则简单得多自带的IDLE或任何你喜欢的编辑器如VS Code、PyCharm都可以。这里我以C和Python两种语言并行讲解方便不同起点的朋友理解。首先我们来彻底搞懂今天的主角——数组。你可以把数组想象成超市门口那一排排的寄存柜。每个柜子都有一个唯一的编号索引从0开始0号柜、1号柜、2号柜...。你要存东西数据就告诉电脑“帮我把这个数存到5号柜”。之后想取用同样说“我要5号柜里的东西”电脑就能瞬间给你找出来。这种通过编号直接访问的方式速度极快是数组最大的优势。在C中声明一个能装100个整数的柜子数组是这样写的int coins[100];。这行代码就相当于订做了100个连在一起的、只能放整数的柜子名字叫coins。在Python中我们更常使用列表List它比传统数组更灵活声明简单coins []或coins list()这是一个空的储物架可以随时往上面放东西。输入数据是解题的第一步。以C为例我们需要先知道有多少枚银币N然后依次读取每个银币的面值。这个过程通常用一个for循环完成int n; cin n; // 读取银币数量 int coins[100]; // 假设N不超过100 for (int i 0; i n; i) { cin coins[i]; // 将读取的面值依次放入数组的0,1,2,...号位置 }注意循环变量i从0开始到n-1结束这正是因为数组索引从0开始。coins[i]就表示数组coins的第i个元素第i1个柜子。Python的写法更简洁n int(input()) coins [] for _ in range(n): coins.append(int(input())) # 将输入的数字添加到列表末尾或者使用更Pythonic的列表推导式coins [int(input()) for _ in range(n)]。这里有一个初学者常犯的错误数组越界。比如你声明了int coins[10]却试图访问coins[10]或coins[-1]。C不会在运行时每次都帮你检查这个错误一旦越界就可能读写到其他内存区域的数据导致程序崩溃或出现难以预料的错误。Python的列表虽然相对安全但索引超出范围也会直接抛出IndexError异常。所以时刻牢记你的循环边界和数组大小是写好代码的第一课。4. 关键算法实现排序与中位数选取数据已经安安稳稳地放进了数组或列表里下一步就是给这些银币排个队从小到大站好。排序是计算机科学中最基础、最重要的操作之一。对于入门题我们不需要自己手写复杂的快速排序或归并排序直接使用编程语言提供的“工具箱”里的排序函数是最明智、最高效的选择。4.1 使用内置排序函数在C中algorithm头文件里的sort函数是绝对的主力。它的用法非常简单sort(起始地址, 结束地址)。对于我们存储了有效数据的数组coins它的起始地址是coins数组名本身代表首地址结束地址是coins n指向第n个元素的下一个位置。更常见的写法是sort(coins, coins n);。如果你用的是vector动态数组写法是sort(vec.begin(), vec.end());。在Python中列表有一个内置的sort()方法它会直接修改原列表coins.sort()。如果你想得到一个新的排序后的列表而不改变原列表可以使用sorted()函数sorted_coins sorted(coins)。注意sort()默认是按升序从小到大排列。如果需要降序C的sort可以传入第三个参数greaterint()Python的sort可以设置参数reverseTrue。但本题只需要升序。4.2 精准定位“中等”银币排序完成后银币们已经按面值从小到大排好队了。现在我们需要找出站在最中间的那个或那两个。这就是中位数选取逻辑。根据之前的需求分析我们需要判断N的奇偶性N为奇数中位数位置是第(N1)/2个。由于数组索引从0开始所以对应的索引是mid_index (n - 1) / 2。在整数运算中(n-1)/2和n/2在n为奇数时结果是相同的因为整除会向下取整但为了逻辑清晰我更喜欢用n / 2C整数除法。例如n5排序后数组索引为0,1,2,3,4中位数是索引2的元素而5/22。N为偶数根据常见题目约定我们取第N/2个元素注意是第几个不是索引。那么对应的索引是mid_index n / 2 - 1。因为第1个元素索引是0第2个元素索引是1所以第n/2个元素的索引是n/2 - 1。例如n4排序后数组索引0,1,2,3我们取第2个索引1的元素。因此我们可以用一个简洁的三元表达式或条件判断来完成索引计算int mid_index; if (n % 2 1) { // 奇数 mid_index n / 2; // C整数除法5/22 } else { // 偶数 mid_index n / 2 - 1; } // 输出 coins[mid_index]Python的写法类似if n % 2 1: mid_index n // 2 # 使用整除运算符 else: mid_index n // 2 - 1 print(coins[mid_index])4.3 完整代码示例与逐行分析下面给出C和Python的完整参考代码并附上关键注释。C版本#include iostream #include algorithm // 引入sort函数所在的头文件 using namespace std; int main() { int n; int coins[100]; // 题目一般会给出N的范围假设最大为100 // 1. 输入数据 cin n; for (int i 0; i n; i) { cin coins[i]; } // 2. 排序 sort(coins, coins n); // 对数组的前n个元素进行排序 // 3. 计算中位数索引并输出 int mid_index; if (n % 2 1) { // 奇数情况例如n5, 索引应为2 (0,1,【2】,3,4) mid_index n / 2; // 整数除法5/22 } else { // 偶数情况例如n4, 输出第2个大的数索引为1 (0,【1】,2,3) mid_index n / 2 - 1; } cout coins[mid_index] endl; return 0; }Python版本# 1. 输入数据 n int(input()) coins [] for _ in range(n): coins.append(int(input())) # 2. 排序 (原地修改) coins.sort() # 3. 计算中位数索引并输出 if n % 2 1: # 奇数情况 mid_index n // 2 # 整除5//22 else: # 偶数情况 mid_index n // 2 - 1 print(coins[mid_index])这两段代码逻辑完全一致。Python版本更简短得益于其动态类型和强大的内置方法。C版本则需要显式声明数组大小和类型并引入相关头文件。5. 深入理解数组排序的“黑盒”与时间开销我们轻松地调用了sort()函数问题就解决了。但作为一个有追求的学习者我们有必要掀开这个“黑盒”的一角看看里面发生了什么以及为什么要用内置排序而不是自己写。sort()函数通常实现了一种名为内省排序Introsort的混合算法。它结合了快速排序Quicksort、堆排序Heapsort和插入排序Insertion Sort的优点。简单来说它在大部分情况下使用快速的快速排序当递归深度过深可能退化为O(n²)慢速情况时切换到稳定的堆排序保证最坏情况下的性能而对小数据段则使用简单的插入排序。这种设计使得C的sort在平均和最坏情况下时间复杂度都是O(N log N)并且通常经过高度优化速度极快。Python的list.sort()方法使用的是Timsort算法这是一种为现实世界数据设计的、稳定且适应性强的排序算法。它同样具有O(N log N)的时间复杂度并且在处理部分有序的数据时效率极高。为什么时间复杂度O(N log N)很重要让我们做个对比。如果自己写一个最简单的冒泡排序它的时间复杂度是O(N²)。当N1000时O(N log N)的算法大约需要10000次操作而O(N²)则需要100万次操作效率差了两个数量级。在OJ系统中面对大量测试数据使用低效的排序算法很可能导致“时间超限”TLE。因此直接使用内置排序函数是入门乃至进阶阶段的最优解我们应把精力放在理解问题本质和算法逻辑上而不是重复造轮子。对于本题输入规模N通常很小入门题一般N1000所以即使你用冒泡排序也能通过。但养成使用高效工具的习惯对未来解决更复杂的问题有百利而无一害。6. 常见“翻车”点与调试技巧即便思路清晰代码简单新手在实现时还是会遇到各种意想不到的问题。下面我总结几个最常见的“坑”并给出解决方法。6.1 数组大小声明不足这是C/C选手的经典错误。题目说“N不超过10000”你却只写了int coins[100];。当输入数据真的达到10000时程序就会发生数组越界可能导致程序崩溃Segmentation Fault或输出错误结果。务必根据题目描述的数据范围来声明足够大的数组或者直接使用vector动态数组它可以自动扩容。// 安全做法根据题目范围声明或使用vector const int MAX_N 10000 5; // 多加一点习惯性好 int coins[MAX_N]; // 或者 #include vector vectorint coins(n); // 声明一个大小为n的vector6.2 索引计算错误这是逻辑错误的重灾区。主要体现在奇偶判断和索引转换上。混淆“第几个”和“索引”牢记“第k个”元素的索引是k-1。在计算mid_index时心里要默默推演一下小例子。比如N3排序后数组为[a, b, c]中位数是第2个b索引是1。你的公式n/2在整数除法下等于1正确。奇偶条件写反if (n % 2 1)是判断奇数。如果不小心写成if (n % 2 0)那就全反了。写完代码后用N3和N4两组数据自己心算测试一下。6.3 输入格式陷阱OJ的输入格式有时不是简单的“先输入N再输入N个数”。可能存在多组测试数据或者一行内用空格隔开多个数字。对于本题的简单格式使用cin 或input()循环读取即可。但如果题目说“第二行包含N个用空格隔开的整数”那么读取方式需要改变。C可以依然用循环cin coins[i]因为cin会跳过空格和换行符。也可以使用getline读取整行再用字符串流分割。Python可以使用input().split()一次性读取一行并分割成字符串列表再转换为整数。# 假设输入格式为第一行n第二行n个空格隔开的数 n int(input()) coins list(map(int, input().split())) # 一行代码完成读取和转换这种方法更简洁但要确保第二行确实有且仅有n个数否则会出错。6.4 调试技巧打印中间结果当你觉得程序逻辑没错但输出不对时最朴素的调试方法就是打印中间结果。在排序后、输出前把整个数组打印出来看看。// C 调试输出 sort(coins, coins n); for(int i0; in; i) cout coins[i] ; cout endl; // 看看排序对吗 // ... 然后计算和输出中位数# Python 调试输出 coins.sort() print(Sorted array:, coins) # 看看排序对吗 # ... 然后计算和输出中位数通过观察排序后的数组你可以立刻验证排序是否正确以及你计算出的mid_index指向的是不是你心目中的那个“中间”元素。这是定位逻辑错误最快的方法。7. 举一反三数组类入门题的解题通法通过“马里奥找银币”这道题我们可以提炼出一套解决类似数组入门题的通用思维框架。这类题目通常围绕数组的增、删、查、改、排序、统计这几个基本操作。7.1 问题抽象与建模拿到题目后第一步是剥离故事背景将问题抽象为对数组的操作。比如“求最大值/最小值” - 遍历数组用变量记录当前最大/最小值。“求平均值” - 遍历数组求和然后除以个数。“统计某个数出现的次数” - 遍历数组与目标数比较并计数。“找出第K大的数” - 排序后取索引为k-1的元素或使用更高效的快速选择算法。“数组去重” - 排序后相邻比较或使用集合set数据结构。7.2 核心步骤分解无论题目怎么变解题代码通常遵循一个清晰的流程我称之为“输入-处理-输出”三部曲数据输入与存储明确输入格式正确地将数据读入到数组或列表中。这是所有操作的基础。核心逻辑处理根据抽象出的问题对数组进行相应的操作遍历、排序、统计等。这是算法的核心。结果格式化输出按照题目要求的格式有时是单个数字有时是一行数有时需要四舍五入保留小数输出结果。7.3 以“求数组最大值”为例我们快速走一遍这个流程。问题输入n个数输出其中的最大值。抽象遍历数组维护一个当前最大值变量。步骤输入n和数组。初始化max_value 数组第一个元素或一个非常小的数。从第二个元素开始遍历数组如果当前元素coins[i] max_value则更新max_value coins[i]。输出max_value。C实现#include iostream using namespace std; int main() { int n, max_val; cin n; int arr[100]; for(int i0; in; i) cin arr[i]; max_val arr[0]; // 假设第一个最大 for(int i1; in; i) { // 从第二个开始比较 if(arr[i] max_val) { max_val arr[i]; } } cout max_val endl; return 0; }Python实现n int(input()) arr list(map(int, input().split())) max_val arr[0] for num in arr[1:]: # 从第二个元素开始遍历 if num max_val: max_val num print(max_val) # 更Pythonic的写法直接使用内置函数 max(arr)掌握了这个“输入-处理-输出”的框架和数组的基本操作你就能解决一大类编程入门题了。关键在于多练习将各种问题映射到这个框架里并熟练运用循环、条件判断和数组索引。8. 从数组到更高级的数据结构一个自然的延伸当我们熟练使用数组后很快就会遇到它的局限性。比如在“马里奥找银币”问题中如果银币数量N非常大比如上百万并且我们需要频繁地在中间插入或删除银币数组的效率就会很低因为需要移动大量元素。这时我们就需要了解更高级的数据结构。8.1 向量Vector—— 动态数组C中的vectorPython列表本质上就是动态数组是对普通数组的完美升级。它具备数组随机访问快的优点还能动态增长。声明时无需指定固定大小使用push_back()添加元素。在大多数需要数组的场景下直接使用vector是更安全、更方便的选择。#include vector vectorint coins; // 一个空的动态数组 int n, val; cin n; for(int i0; in; i) { cin val; coins.push_back(val); // 在末尾添加元素 } sort(coins.begin(), coins.end()); // 排序8.2 集合Set与映射Map—— 快速查找与去重如果题目要求“找出唯一不同的那枚银币”或者“统计每种面值银币出现的次数”数组遍历就显得笨拙。C的set集合可以自动去重和排序map映射可以存储键值对如面值-出现次数。Python中对应的有set和dict字典。去重setint s(coins.begin(), coins.end());一行代码就能得到所有不重复的面值。统计频率# Python 使用字典统计 freq {} for coin in coins: freq[coin] freq.get(coin, 0) 1 # 现在 freq 里存储了每个面值出现的次数8.3 字符串数组与字符处理热词中提到了“二维字符数组”、“奈芙莲有一个字符串数组”这指向了另一个常见类型字符串数组。在C中字符串可以用char str[]数组或string类表示。处理字符串数组如多个单词通常需要二维数组或vectorstring。这类问题常涉及字符串比较、排序字典序、查找子串等操作。例如对一组名字按字典序排序其核心思路和对整数排序一模一样只是比较规则从“数值大小”变成了“字典序”使用sort函数依然可以轻松完成。理解数组是理解所有这些更复杂结构的基石。当你对数组在内存中的连续存储、索引访问有了深刻理解后再学习链表非连续存储、树层次结构、图网状结构时就能通过对比抓住它们的本质区别和适用场景。编程学习就是这样从一个扎实的点开始逐步连成线再拓展成面。