C# 数据结构和算法 :03 数组和排序(二)
bigegpt 2025-01-06 11:20 4 浏览
多维数组
C#语言中的数组不必只有一个维度。也可以创建二维数组。正如你将看到的,多维数组非常有用,并且在开发各种应用程序时经常被使用。
想象一个二维数组
如果你想想象一个二维数组,可以休息一下,闭上眼睛,玩数独游戏。如果你不知道这是什么,数独是一种流行的游戏,它要求你用1到9的数字填满一个9x9棋盘的空格。然而,每一行、每一列以及每一个3x3的格子只能包含唯一的数字。惊喜——这个棋盘形成了一个二维数组!你可以通过指定它的行和列来指向棋盘上的任何位置,就像在二维数组的情况下一样。如果你有点厌倦了用铅笔和纸解决这样的谜题,那么请看第9章,动手实践,你将学习如何创建一个解决数独谜题的算法!
下面展示了一个存储整数值的二维数组示例:
首先,你需要声明并初始化一个具有5行3列的二维数组,如下所示的代码行:
int[,] numbers = new int[5, 3];
numbers[0, 0] = 9; (...)
你也可以以一种略有不同的方式将声明与初始化结合起来:
int[,] numbers = new int[,]
{
{ 9, 5, -9 },
{ -11, 4, 0 },
{ 6, 115, 3 },
{ -12, -9, 71 },
{ 1, -6, -1 }
};
访问二维数组中特定元素的方式需要一个小的解释。让我们来看以下示例:
int number = numbers[2, 1];
numbers[1, 0] = 11;
在代码的第一行中,获取了第三行(索引等于2)和第二列(索引等于1)的值(即115),并将其设置为number变量的值。另一行将第二行和第一列中的-11替换为11。
现在你已经了解了一维和二维数组,让我们继续学习三维数组。你知道如何理解这个结构吗?
想象一个三维数组
如果你想更好地想象一个三维数组,启动一个你可以用积木创建建筑物的游戏。你将每个积木放置在棋盘上指定的位置,使用X和Y坐标。然而,你还可以建造建筑物的下一层,所以你也可以指定积木的Z坐标。在这种情况下,你在一个三维世界中操作,使用三维数组!
下面的图中展示了一个三维数组的例子:
如果你想创建一个三维数组,你可以使用以下代码:
int[,,] numbers = new int[3, 2, 3];
剩余的操作可以像处理不同维度的数组一样进行。当然,在访问数组的特定元素时,你需要指定三个索引。
到目前为止,你已经了解了一维、二维和三维数组。但是,使用四维数组可能吗?当然可以!
想象一个四维数组
想象一个四维数组可能不太容易,但让我们尝试一下!再次想象我们之前提到的三维游戏棋盘,但是内容会根据你在游戏中的等级而变化。这样,你可以使用X、Y和Z坐标来访问三维世界中的特定方块。为了得到目标值,你需要使用另一个维度,即提供你当前的等级。这样,根据第四维度,你会得到不同的结果。不那么难,对吧?
你可以使用以下代码行声明这样一个数组:
int[,,,] numbers = new int[5, 4, 3, 2];
如果你需要更多的维度,你可以应用它们。然而,请记住,使用更多的维度可能会难以理解,而且你的代码在未来可能更难以跟踪和维护。
介绍多维数组的话题已经结束,让我们来看一些例子。这些例子将向你展示如何在现实世界中使用这样的数据结构。
例子 - 乘法表
这个第一个例子展示了在二维数组上执行基本操作以呈现乘法表。它将1到10范围内所有整数值的乘积结果存储在数组中,并在控制台中呈现它们:
让我们来看看数组的声明和初始化:
int[,] results = new int[10, 10];
这里,创建了一个具有10行和10列的二维数组,并将其元素初始化为默认值——即零。当数组准备好后,你用乘法的结果填充它,并在控制台中展示结果。这样的任务可以使用两个for循环来完成,如下所示:
for (int i = 0; i < results.GetLength(0); i++)
{
for (int j = 0; j < results.GetLength(1); j++)
{
results[i, j] = (i + 1) * (j + 1);
Console.Write(#34;{results[i, j],4}");
}
Console.WriteLine();
}
在前面的代码中,你可以看到调用在结果数组上的 GetLength 方法。这个方法返回特定维度中的元素数量——也就是说,第一个(当传递0作为参数时)和第二个(传递1作为参数时)。在这两种情况下,根据数组初始化时指定的值,都返回了10。代码的另一个重要部分是设置元素值的方式。为此,你必须提供两个索引。
将乘法结果转换为字符串值后,它们的长度各不相同,从一个字符(如2*2的结果4)到三个字符(10*10的结果100)。为了改善它们的展示效果,你需要将每个结果都写成4个字符的长度。因此,如果整数值占用的空间较少,就应该添加前导空格。例如,1将显示为带有三个前导空格(___1,其中_代表空格),而100将只显示一个空格(_100)。你可以通过在插值字符串中使用适当的复合格式字符串(即,,4)来实现这一目标。
示例 - 游戏地图
另一个例子是一个程序,它展示了一个游戏的地图。这个地图是一个长方形,有6行和8列。数组中的每个元素指定了一种地形类型,比如草地、沙地、水或砖块(也被称为墙)。地图上的每个位置都应该用特定的颜色显示(比如草地用绿色),同时使用一个自定义字符来描绘地形类型(比如水用≈表示),如下图所示:
让我们开始创建两个辅助方法,这些方法使得根据地形类型获取特定颜色和字符成为可能(分别命名为GetColor和GetChar)。这些方法的代码如下:
ConsoleColor GetColor(char terrain)
{
return terrain switch
{
'g' => ConsoleColor.Green,
's' => ConsoleColor.Yellow,
'w' => ConsoleColor.Blue,
_ => ConsoleColor.DarkGray
};
}
char GetChar(char terrain)
{
return terrain switch
{
'g' => '\u201c',
's' => '\u25cb',
'w' => '\u2248',
_ => '\u25cf'
};
}
正如你所见,GetColor方法的代码是不言自明的。然而,GetChar方法根据字符的值(g、s、w或b)返回适当的Unicode字符。例如,在水的情况下,返回的是'\u2248'值,这是≈字符的表示。
让我们来看看代码的其余部分。在这里,你配置了地图,并且在控制台中展示它。代码如下:
using System.Text;
char[,] map = {
{ 's', 's', 's', 'g', 'g', 'g', 'g', 'g' },
{ 's', 's', 's', 'g', 'g', 'g', 'g', 'g' },
{ 's', 's', 's', 's', 's', 'b', 'b', 'b' },
{ 's', 's', 's', 's', 's', 'b', 's', 's' },
{ 'w', 'w', 'w', 'w', 'w', 'b', 'w', 'w' },
{ 'w', 'w', 'w', 'w', 'w', 'b', 'w', 'w' }
};
Console.OutputEncoding = Encoding.UTF8;
for (int r = 0; r < map.GetLength(0); r++)
{
for (int c = 0; c < map.GetLength(1); c++)
{
Console.ForegroundColor = GetColor(map[r, c]);
Console.Write(GetChar(map[r, c]) + " ");
}
Console.WriteLine();
}
Console.ResetColor();
这段代码不应该需要额外的注释或解释。只需记住,在控制台输出中使用Unicode值时,不要忘记通过将OutputEncoding属性设置为Encoding.UTF8来选择UTF-8编码。你可以使用ForegroundColor属性为控制台设置前景色。如果你想将这种颜色重置为默认颜色,只需像最后一行中展示的那样调用ResetColor方法即可。
到目前为止,你已经了解了单维和多维数组,但本书还剩下一种变体需要介绍,那就是锯齿数组。让我们继续阅读以了解更多关于它们的信息。
锯齿数组
本书中要描述的数组的最后一种变体是锯齿数组,也被称为数组的数组。听起来很复杂,但幸运的是,它非常简单。锯齿数组可以理解为一个单维数组,其中每个元素都是另一个数组。当然,这些内部数组可以有不同的长度,甚至可以未初始化。
想象一个不规则数组
如果你想更好地想象一个不规则数组,暂时停止阅读这本书,打开你的日历,并切换其视图以展示整个年份。它包含365或366个盒子,取决于年份。对于每一天,你都有不同数量的会议。在某些日子里,你有三个会议,而在其他日子,只有一个甚至零个。你的假期在日历中标记并为会议预留。你可以很容易地想象到不规则数组在这个情况下的应用。每一天的盒子都是这个数组的一个元素,它包含一个数组,里面是特定一天组织的会议数据。如果这一天是在你的假期中,相关的条目没有初始化。这使得不规则数组更容易可视化。
一个不规则数组的例子在下图中展示:
这个锯齿数组包含四个元素。第一个包含一个有两个元素的数组(9和5)。第二个元素包含一个有三个元素的数组(0, -3和12)。第三个未初始化(null),而最后一个是一个只有一个元素的数组(54)。
在继续示例之前,值得一提的是声明和初始化锯齿数组的方式,因为它与我们已经描述的数组有所不同。让我们来看一下以下代码片段:
int[][] numbers = new int[4][];
numbers[0] = new int[] { 9, 5 };
numbers[1] = new int[] { 0, -3, 12 };
numbers[3] = new int[] { 54 };
这段代码可以用集合表达式简化,如下所示:
int[][] numbers = new int[4][];
numbers[0] = [9, 5];
numbers[1] = [0, -3, 12];
numbers[3] = [54];
在第一行中,我们声明了一个包含四个元素的一维数组。每个元素都是另一个整数值的一维数组。当执行第一行代码时,numbers数组被初始化为默认值,即null。因此,我们需要手动初始化特定的数组和元素,如下三行代码所示。值得注意的是,第三个元素没有被初始化。
你也可以用不同的方式编写前面的代码,如下所示:
int[][] numbers =
{
new int[] { 9, 5 },
new int[] { 0, -3, 12 },
null!,
new int[] { 54 }
};
不仅如此——还有一个更短的变体可用:
int[][] numbers =
[
[9, 5],
[0, -3, 12],
null!,
[54]
];
如何从不规则数组中访问特定元素?让我们看看:
int number = numbers[1][2];
numbers[1][1] = 50;
第一行代码将number变量的值设置为12——即,从数组中获取第三个元素(索引等于2)的值,这是锯齿数组的第二个元素。另一行代码将数组中的第二个元素的值从-3更改为50,这是锯齿数组的第二个元素。
现在我们已经介绍了锯齿数组,让我们来看一个例子。
示例 – 年度运输计划
在这个示例中,你将学习如何开发一个程序,该程序为你全年的交通制定计划。对于每个月的每一天,应用程序都会选择一种可用的交通方式,比如开车、乘公交、乘地铁、骑自行车,或者干脆步行。最终,程序将展示生成的计划,如下图所示:
首先,让我们声明一个枚举类型,其中包含代表各种交通工具类型的常量:
public enum MeanEnum { Car, Bus, Subway, Bike, Walk }
代码的下一部分如下:
Random random = new();
int meansCount = Enum.GetNames<MeanEnum>().Length;
int year = DateTime.Now.Year;
MeanEnum[][] means = new MeanEnum[12][];
for (int m = 1; m <= 12; m++)
{
int daysCount = DateTime.DaysInMonth(year, m);
means[m - 1] = new MeanEnum[daysCount];
for (int d = 1; d <= daysCount; d++)
{
int mean = random.Next(meansCount);
means[m - 1][d - 1] = (MeanEnum)mean;
}
}
首先,创建了Random类的一个新实例。这将用于从可用的交通方式中随机选择一种。
接下来的一步是获取可用交通类型的数量。
然后,创建了一个不规则数组。假设它有12个元素,代表当前年份的所有月份。
接下来,使用for循环遍历一年中的所有月份。在每次迭代中,使用DateTime的DaysInMonth静态方法获取天数。不规则数组的每个元素是一个单维数组,包含MeanEnum值。这样的内部数组的长度取决于一个月中的天数。例如,一月设置为31个元素,四月设置为30个元素。
下一个for循环遍历月份中的所有天数。在这个循环内,你随机选择一种交通方式,并将其设置为不规则数组中某个元素的值。
代码的下一部分与在控制台中展示计划有关:
string[] months = GetMonthNames();
int nameLength = months.Max(n => n.Length) + 2;
for (int m = 1; m <= 12; m++)
{
string month = months[m - 1];
Console.Write(#34;{month}:".PadRight(nameLength));
for (int d = 1; d <= means[m - 1].Length; d++)
{
MeanEnum mean = means[m - 1][d - 1];
(char character, ConsoleColor color) = Get(mean);
Console.ForegroundColor = ConsoleColor.White;
Console.BackgroundColor = color;
Console.Write(character);
Console.ResetColor();
Console.Write(" ");
}
Console.WriteLine();
}
首先,使用GetMonthNames方法创建一个包含月份名称的一维数组,稍后将展示并描述该方法。然后,将nameLength变量的值设置为存储月份名称所需的最长文本长度。为此,使用Max扩展方法从包含月份名称的集合中找到最长的文本长度。得到的结果增加了2,以预留冒号和空格的空间。
使用for循环遍历锯齿数组的所有元素——即所有月份。在每次迭代中,将月份名称显示在控制台上。接下来的for循环用于遍历锯齿数组当前元素的所有项——即月份的所有天数。对于每一天,都会设置适当的前景色和背景色,并显示一个合适的字符。这两个颜色和字符由Get方法返回,该方法以MeanEnum值作为参数。稍后将展示这个方法的实现。
现在,让我们来看看GetMonthNames方法的实现:
string[] GetMonthNames()
{
CultureInfo culture = new("en");
string[] names = new string[12];
foreach (int m in Enumerable.Range(1, 12))
{
DateTime firstDay = new(DateTime.Now.Year, m, 1);
string name = firstDay.ToString("MMMM", culture);
names[m - 1] = name;
}
return names;
}
这段代码是不言自明的,但让我们专注于调用Range方法的那一行。它返回一个从1到12的整数集合。因此,我们可以将它与foreach循环一起使用,而不是使用简单的从1到12的for循环。只需将其视为解决同一问题的另一种方式。
最后,值得一提的是Get方法。它允许我们使用一个方法而不是两个,即为给定的运输类型返回一个字符和颜色。通过以值元组的形式返回数据,代码更短更简单,如下所示:
(char Char, ConsoleColor Color) Get(MeanEnum mean)
{
return mean switch
{
MeanEnum.Bike => ('B', ConsoleColor.Blue),
MeanEnum.Bus => ('U', ConsoleColor.DarkGreen),
MeanEnum.Car => ('C', ConsoleColor.Red),
MeanEnum.Subway => ('S', ConsoleColor.Magenta),
MeanEnum.Walk => ('W', ConsoleColor.DarkYellow),
_ => throw new Exception("Unknown type")
};
}
数组在本章中无处不在!现在我们已经学习了这种数据结构及其C#实现的相关主题,我们可以专注于一些与数组紧密相关的算法,即排序算法。你准备好了解其中的一些了吗?如果是的话,让我们继续进入下一节。
相关推荐
- 5分钟搭建公网https网页文件服务器,免费权威TLS证书
-
请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习前言本文主要讲解如何快速搭建一个https网页文件服务器,并免费申请权威机构颁发的tls证...
- nginx负载均衡配置(nginx负载均衡配置两个程序副本)
-
Nginx是什么没有听过Nginx?那么一定听过它的“同行”Apache吧!Nginx同Apache一样都是一种WEB服务器。基于REST架构风格,以统一资源描述符(UniformResources...
- 19《Nginx 入门教程》Nginx综合实践
-
今天我们将基于Nginx完成两个比较有用的场景,但是用到的Nginx的配置非常简单。内部Yum源搭建内部Pip源搭建1.实验环境ceph1centos7.6内网ip:172.16....
- Nginx性能调优与优化指南(nginx优化配置大全)
-
Nginx性能调优需要结合服务器硬件资源、业务场景和负载特征进行针对性优化。以下是一些关键优化方向和具体配置示例:一、Nginx配置优化1.进程与连接数优化nginxworker_process...
- C++后端开发必须彻底搞懂Nginx,从原理到实战(高级篇)
-
本文为Nginx实操高级篇。通过配置Nginx配置文件,实现正向代理、反向代理、负载均衡、Nginx缓存、动静分离和高可用Nginx6种功能,并对Nginx的原理作进一步的解析。当需...
- 【Nginx】史上最全的Nginx配置详解
-
Nginx服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里,http块又包括http全局块和server块。Nginx是非常重要的负载均衡中间件,被广泛应用于大型...
- 【Nginx】Nginx 4种常见配置实例(nginx基本配置与参数说明)
-
本文主要介绍nginx4种常见的配置实例。Nginx实现反向代理;Nginx实现负载均衡;Nginx实现动静分离;Nginx实现高可用集群;Nginx4种常见配置实例如下:一、Nginx反向代理配...
- 使用nginx+allure管理自动化测试报告
-
allure在自动化测试中经常用来生成漂亮的报告,但是网上及官网上给出的例子都仅仅是针对单个测试用例文件的形式介绍的,实际使用中,自动化测试往往需要包含不止一个产品或项目,本文介绍如何使用nginx+...
- nginx配置文件详解(nginx配置文件详解高清版)
-
Nginx是一个强大的免费开源的HTTP服务器和反向代理服务器。在Web开发项目中,nginx常用作为静态文件服务器处理静态文件,并负责将动态请求转发至应用服务器(如Django,Flask,et...
- SpringCloud Eureka-服务注册与发现
-
1.Eureka介绍1.1学习Eureka前的说明目前主流的服务注册&发现的组件是Nacos,但是Eureka作为老牌经典的服务注册&发现技术还是有必要学习一下,原因:(1)一些早期的分布式微服...
- 微服务 Spring Cloud 实战 Eureka+Gateway+Feign+Hystrix
-
前言我所在项目组刚接到一个微服务改造需求,技术选型为SpringCloud,具体需求是把部分项目使用SpringCloud技术进行重构。本篇文章中介绍了Eureka、Gateway、Fe...
- 深度剖析 Spring Cloud Eureka 底层实现原理
-
你作为一名互联网大厂后端技术开发人员,在构建分布式系统时,是不是常常为服务的注册与发现而头疼?你是否好奇,像SpringCloudEureka这样被广泛使用的组件,它的底层实现原理到底是怎样的...
- 热爱生活,喜欢折腾。(很热爱生活)
-
原文是stackoverflow的一则高票回答,原文链接可能之前也有人翻译过,但是刚好自己也有疑惑,所以搬运一下,个人水平有限所以可能翻译存在误差,欢迎指正(如侵删)。尽管classmethod和st...
- GDB调试的高级技巧(详细描述gdb调试程序的全过程)
-
GDB是我们平时调试c/c++程序的利器,查起复杂的bug问题,比打印大法要好得多,但是也不得不说,gdb在默认情况下用起来并不是很好用,最近学习到几个高级点的技巧,分享下:一美化打印先上个例子...
- Arduino 实例(二十三)Arduino 给Python 编译器发送信息
-
1首先Python需要安装Pyserial库,在命令提示符中输入pipintallpyserial若是遇到提示‘pip‘不是内部或外部命令,也不是可运行的程序或批处理文件,则需要设置环境变...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- skip-name-resolve (63)
- linuxlink (65)
- pythonwget (67)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)