整理框架
本文整体框架如下:
枚举
→ 一维数组
→ 二维数组
→ 交错数组
→ 值类型和引用类型
→ string
→ 函数
→ ref 和 out
→ params 与参数默认值
→ 函数重载
→ 递归
→ 结构体
→ 冒泡排序
→ 选择排序
Lesson 1:枚举
什么是枚举?
枚举【1】是一个被命名的整型常量集合。
当某个变量只能在几个固定选项中取值时,可以使用枚举表示,例如:
- 玩家状态【2】
- 怪物状态【3】
- 角色职业【4】
- 游戏难度【5】
- QQ 在线状态
如果直接用整数表示玩家状态:
int playerState = 1;
只看到数字 1,很难知道它代表待机、行走还是攻击。
使用枚举可以让代码含义更加清晰:
enum PlayerState
{
Idle,
Walk,
Run,
Jump
}
PlayerState state = PlayerState.Walk;
这里需要区分三个概念:
PlayerState 枚举类型
state 枚举变量
PlayerState.Walk 枚举值
枚举可以声明在 namespace、class 或 struct 中,但不能声明在函数【6】内部。
枚举的整数值
枚举成员默认从 0 开始,后面的成员依次加 1:
enum PlayerState
{
Idle, // 0
Walk, // 1
Run, // 2
Jump // 3
}
也可以主动指定数值:
enum PlayerState
{
Idle = 0,
Walk = 10,
Run = 20,
Jump = 30
}
虽然枚举在底层与整数有关,但实际编程时应优先使用枚举成员名称,避免在程序中到处使用含义不明确的数字。
枚举与 switch
枚举非常适合配合 switch 表现状态逻辑:
PlayerState state = PlayerState.Run;
switch (state)
{
case PlayerState.Idle:
Console.WriteLine("玩家正在待机");
break;
case PlayerState.Walk:
Console.WriteLine("玩家正在行走");
break;
case PlayerState.Run:
Console.WriteLine("玩家正在奔跑");
break;
case PlayerState.Jump:
Console.WriteLine("玩家正在跳跃");
break;
}
同样的方式也可以用于处理 QQ 在线状态、咖啡规格【7】、角色职业等固定选项。
枚举类型转换
枚举可以和 int 相互转换:
PlayerState state = PlayerState.Run;
int number = (int)state;
PlayerState newState = (PlayerState)number;
枚举也可以转换成字符串:
string stateName = state.ToString();
将字符串转换成枚举时,可以使用 Enum.Parse:
string input = "Walk";
PlayerState state =
(PlayerState)Enum.Parse(typeof(PlayerState), input);
如果字符串无法匹配对应的枚举成员,程序会产生错误,因此使用时要保证字符串内容正确。
Lesson 2:一维数组
什么是数组
数组【8】用于存储一组相同类型的数据。
例如,使用一个数组存储多名学生的成绩:
int[] scores = { 80, 90, 75, 88, 96 };
数组中的所有元素必须是同一种类型。
C# 中常见的数组包括:
- 一维数组【9】
- 二维数组【10】
- 交错数组【11】
一维数组的声明
一维数组有多种声明方式:
int[] arr1;
arr1 = new int[5];
int[] arr2 = new int[5];
int[] arr3 = new int[] { 1, 2, 3, 4, 5 };
int[] arr4 = { 1, 2, 3, 4, 5 };
使用 new int[5] 创建数组时,数组中会有 5 个整数元素,每个元素的初始值都是 0。
数组长度和索引
使用 Length 获取数组长度:
int[] arr = { 10, 20, 30, 40 };
Console.WriteLine(arr.Length);
数组索引【12】从 0 开始:
| 索引 | 元素 |
|---|---|
| 0 | 10 |
| 1 | 20 |
| 2 | 30 |
| 3 | 40 |
数组的最大索引为:
arr.Length - 1
访问数组元素时使用中括号:
Console.WriteLine(arr[0]);
arr[1] = 100;
如果访问不存在的索引,会产生数组越界【13】错误。
遍历数组
数组通常使用 for 循环【14】遍历:
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
这里的 i 既表示循环次数,也表示当前元素的索引。
例如,创建一个长度为 100 的数组,并让元素值与索引相同:
int[] numbers = new int[100];
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = i;
}
这段代码把数组声明、数组长度、索引和遍历结合到了一起。
数组数据统计
数组经常配合循环完成最大值、最小值、总和与平均值计算:
int[] scores = { 78, 92, 65, 88, 96 };
int max = scores[0];
int min = scores[0];
int sum = 0;
for (int i = 0; i < scores.Length; i++)
{
if (scores[i] > max)
{
max = scores[i];
}
if (scores[i] < min)
{
min = scores[i];
}
sum += scores[i];
}
float average = (float)sum / scores.Length;
Console.WriteLine("最高分:" + max);
Console.WriteLine("最低分:" + min);
Console.WriteLine("总分:" + sum);
Console.WriteLine("平均分:" + average);
最大值和最小值通常使用数组第一个元素作为初始值:
int max = scores[0];
int min = scores[0];
计算平均值时,将 sum 转换成 float,可以避免整数除法丢失小数部分。
手动反转数组
反转数组时,可以交换【15】数组前后对应位置:
int[] arr = { 1, 2, 3, 4, 5 };
for (int i = 0; i < arr.Length / 2; i++)
{
int temp = arr[i];
arr[i] = arr[arr.Length - 1 - i];
arr[arr.Length - 1 - i] = temp;
}
数组反转后变成:
5 4 3 2 1
因为每次会同时处理前后两个元素,所以循环到数组长度的一半即可。
数组增加元素
数组创建以后,长度不能直接改变。
如果需要增加元素,就要创建一个更大的数组,再把旧数据复制过去:
int[] oldArr = { 10, 20, 30 };
int[] newArr = new int[oldArr.Length + 1];
for (int i = 0; i < oldArr.Length; i++)
{
newArr[i] = oldArr[i];
}
newArr[newArr.Length - 1] = 40;
oldArr = newArr;
课程把这种处理方式称为数组“搬家”。
数组删除元素
删除元素时,也需要创建一个长度更小的新数组:
int[] oldArr = { 10, 20, 30, 40 };
int deleteIndex = 1;
int[] newArr = new int[oldArr.Length - 1];
for (int i = 0, j = 0; i < oldArr.Length; i++)
{
if (i == deleteIndex)
{
continue;
}
newArr[j] = oldArr[i];
j++;
}
oldArr = newArr;
最终数组内容为:
10 30 40
查找数组元素
可以通过遍历查找指定元素:
int[] arr = { 15, 28, 36, 42 };
int target = 36;
int targetIndex = -1;
for (int i = 0; i < arr.Length; i++)
{
if (arr[i] == target)
{
targetIndex = i;
break;
}
}
if (targetIndex == -1)
{
Console.WriteLine("没有找到");
}
else
{
Console.WriteLine("元素索引:" + targetIndex);
}
这里使用 -1 表示没有找到目标元素。
在游戏开发中,一维数组可以用于批量存储怪物、玩家、道具或者其他同类型数据。
Lesson 3:二维数组
二维数组的基本概念
二维数组可以理解为由行和列组成的数据表格。
int[,] map = new int[3, 4];
这表示创建了一个 3 行 4 列的二维数组。
二维数组也可以在声明时直接赋值:
int[,] arr =
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
这个数组有 2 行、3 列。
获取行数和列数
二维数组使用 GetLength() 获取指定维度的长度:
int rowCount = arr.GetLength(0);
int columnCount = arr.GetLength(1);
其中:
GetLength(0) 获取行数
GetLength(1) 获取列数
获取、修改和遍历元素
访问二维数组元素时,需要同时提供行索引和列索引:
Console.WriteLine(arr[0, 1]);
arr[1, 2] = 100;
二维数组通常使用双层循环【16】遍历:
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
Console.Write(arr[i, j] + " ");
}
Console.WriteLine();
}
其中:
- 外层循环控制行
- 内层循环控制列
例如,将 1~10000 依次填入一个 100 × 100 的二维数组:
int[,] numbers = new int[100, 100];
int value = 1;
for (int i = 0; i < numbers.GetLength(0); i++)
{
for (int j = 0; j < numbers.GetLength(1); j++)
{
numbers[i, j] = value;
value++;
}
}
二维数组中的位置关系
二维数组练习的重点之一,是通过行列索引判断元素位置。
例如,将主对角线【17】右上方的元素置为 0:
int[,] arr =
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 },
{ 13, 14, 15, 16 }
};
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = i + 1; j < arr.GetLength(1); j++)
{
arr[i, j] = 0;
}
}
处理后的结果为:
1 0 0 0
5 6 0 0
9 10 11 0
13 14 15 16
主对角线元素的特点是行索引与列索引相同:
int sum = 0;
for (int i = 0; i < arr.GetLength(0); i++)
{
sum += arr[i, i];
}
在二维数组中查找最大值时,可以同时记录元素的行列位置:
int max = arr[0, 0];
int maxRow = 0;
int maxColumn = 0;
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
if (arr[i, j] > max)
{
max = arr[i, j];
maxRow = i;
maxColumn = j;
}
}
}
二维数组常用于表示:
- 矩阵【18】
- 棋盘【19】
- 控制台游戏地图【20】
- 固定行列的表格数据【21】
Lesson 4:交错数组
交错数组与二维数组的区别
交错数组是“数组的数组”。
二维数组的每一行长度相同,而交错数组的每一行可以拥有不同长度:
int[][] arr = new int[3][];
arr[0] = new int[2];
arr[1] = new int[4];
arr[2] = new int[3];
也可以直接初始化:
int[][] arr =
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};
获取交错数组的行数:
Console.WriteLine(arr.Length);
获取某一行的长度:
Console.WriteLine(arr[1].Length);
访问元素时使用两个中括号:
Console.WriteLine(arr[1][2]);
arr[1][2] = 100;
遍历时,每一行都要使用自己的长度:
for (int i = 0; i < arr.Length; i++)
{
for (int j = 0; j < arr[i].Length; j++)
{
Console.Write(arr[i][j] + " ");
}
Console.WriteLine();
}
交错数组在这套课程中属于非重点知识,理解它与二维数组的区别,并掌握基本访问方式即可。
Lesson 5:值类型和引用类型
值类型的赋值特点
课程使用整数变量演示值类型【22】:
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a);
Console.WriteLine(b);
输出:
10
20
执行:
int b = a;
相当于把 a 中的值复制一份交给 b。
之后修改 b,不会影响 a,可以总结为:
它变,我不变。
引用类型的赋值特点
课程使用数组演示引用类型【23】:
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1;
arr2[0] = 100;
Console.WriteLine(arr1[0]);
Console.WriteLine(arr2[0]);
输出:
100
100
这里并没有复制一份完整数组,而是让 arr1 和 arr2 指向同一份数组数据。
因此,通过其中一个变量修改数组,另一个变量也能观察到变化:
它变,我也变。
重新创建引用类型数据
如果重新使用 new 创建数组,变量就会指向一份新数据:
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1;
arr2 = new int[] { 10, 20, 30 };
arr2[0] = 100;
Console.WriteLine(arr1[0]);
Console.WriteLine(arr2[0]);
输出:
1
100
重新创建数组以后,arr1 和 arr2 不再指向同一份数据。
本阶段需要掌握的核心结论是:
| 类型 | 赋值特点 |
|---|---|
| 值类型 | 复制一份数据 |
| 引用类型 | 可能指向同一份数据 |
重新 new | 创建新数据并改变引用 |
课程还提到了栈空间【24】、堆空间【25】和 new。现阶段重点是理解赋值和修改的结果,不需要过度深入内存实现细节。
Lesson 6:特殊引用类型 string
string 的特殊表现
string 属于引用类型,但它的赋值表现比较特殊:
string str1 = "123";
string str2 = str1;
str2 = "321";
Console.WriteLine(str1);
Console.WriteLine(str2);
输出:
123
321
看起来很像值类型的“它变,我不变”。
字符串不可变
字符串具有不可变的特点。
执行:
str2 = "321";
并不是把原来的 "123" 修改成 "321",而是让 str2 对应一个新的字符串。
因此:
str1 仍然对应 "123"
str2 重新对应 "321"
下面的代码看起来是在不断修改字符串:
string result = "";
for (int i = 0; i < 10; i++)
{
result += i;
}
实际上,每次拼接通常都会产生新的字符串结果。
现阶段只需要掌握:
string是引用类型- 字符串具有不可变特点
- 对字符串重新赋值时,原字符串本身不会被直接修改
- 频繁拼接字符串可能产生较多新数据
字符串优化方案属于后续课程内容,本阶段不展开。
Lesson 7:函数
函数的基本概念
函数本质上是一块具有名称的代码块。
它的主要作用包括:
- 封装代码
- 提高代码复用率
- 抽象程序行为
例如:
static void Welcome()
{
Console.WriteLine("欢迎学习 C#");
}
调用函数:
Welcome();
本阶段介绍的函数写在 class 或 struct 中。由于还没有正式学习类,当前程序中的函数暂时配合 static 使用。
函数基本结构
static 返回类型 函数名(参数)
{
// 函数逻辑
return 返回值;
}
例如:
static int Add(int a, int b)
{
return a + b;
}
调用函数并接收返回值【26】:
int result = Add(10, 20);
Console.WriteLine(result);
四种基本函数形式
根据是否有参数和返回值,函数可以分为四种形式:
// 无参数、无返回值
static void SayHello()
{
Console.WriteLine("Hello");
}
// 有参数、无返回值
static void PrintNumber(int number)
{
Console.WriteLine(number);
}
// 无参数、有返回值
static int GetNumber()
{
return 100;
}
// 有参数、有返回值
static int GetMax(int a, int b)
{
return a > b ? a : b;
}
void 表示函数不返回数据。
return 的作用
return 不仅可以返回函数结果,还可以提前结束函数:
static void CheckAge(int age)
{
if (age < 0)
{
Console.WriteLine("年龄不能小于 0");
return;
}
Console.WriteLine("年龄:" + age);
}
当年龄小于 0 时,函数执行到 return 就会立即结束。
使用函数封装数组统计
此前在数组中编写的统计逻辑,可以进一步封装成函数:
static float[] GetArrayInfo(int[] arr)
{
int sum = 0;
int max = arr[0];
int min = arr[0];
for (int i = 0; i < arr.Length; i++)
{
sum += arr[i];
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
}
float average = (float)sum / arr.Length;
return new float[] { sum, max, min, average };
}
通过数组,可以暂时让一个函数返回多个同类型结果:
int[] numbers = { 12, 35, 18, 46 };
float[] result = GetArrayInfo(numbers);
Console.WriteLine("总和:" + result[0]);
Console.WriteLine("最大值:" + result[1]);
Console.WriteLine("最小值:" + result[2]);
Console.WriteLine("平均值:" + result[3]);
使用函数判断质数和闰年
函数适合封装能够重复使用的判断逻辑。
判断质数:
static bool IsPrime(int number)
{
if (number < 2)
{
return false;
}
for (int i = 2; i < number; i++)
{
if (number % i == 0)
{
return false;
}
}
return true;
}
判断闰年:
static bool IsLeapYear(int year)
{
return year % 400 == 0 ||
year % 4 == 0 && year % 100 != 0;
}
函数把判断过程封装起来,调用者只需要关心返回结果。
Lesson 8:ref 和 out
普通参数为什么不能直接修改外部变量
普通值类型参数传入函数时,函数得到的是数据副本:
static void ChangeNumber(int number)
{
number = 100;
}
int value = 10;
ChangeNumber(value);
Console.WriteLine(value);
输出仍然是:
10
如果希望函数内部的修改影响外部变量,可以使用 ref 或 out。
ref 参数
static void ChangeNumber(ref int number)
{
number = 100;
}
int value = 10;
ChangeNumber(ref value);
Console.WriteLine(value);
使用 ref 时:
- 定义参数时需要写
ref - 调用函数时也需要写
ref - 传入函数前必须初始化
- 函数内部可以修改,也可以不修改
out 参数
static void GetNumber(out int number)
{
number = 100;
}
int value;
GetNumber(out value);
Console.WriteLine(value);
使用 out 时:
- 定义参数时需要写
out - 调用函数时也需要写
out - 传入前可以不初始化
- 函数内部必须赋值
两者的核心区别如下:
| 对比项目 | ref【27】 | out【28】 |
|---|---|---|
| 调用前是否必须初始化 | 必须 | 不需要 |
| 函数内部是否必须赋值 | 不强制 | 必须 |
| 是否能修改外部变量 | 可以 | 可以 |
| 常见用途 | 修改已有数据 | 返回额外结果 |
可以简单理解为:
ref:带着已有数据进入函数
out:从函数中领取一份结果
使用 out 返回额外信息
登录函数可以直接返回登录是否成功,再通过 out 返回提示信息:
static bool Login(
string inputName,
string inputPassword,
out string message)
{
string correctName = "admin";
string correctPassword = "123456";
if (inputName != correctName)
{
message = "用户名错误";
return false;
}
if (inputPassword != correctPassword)
{
message = "密码错误";
return false;
}
message = "登录成功";
return true;
}
调用:
string message;
bool result = Login(
"admin",
"123456",
out message);
Console.WriteLine(result);
Console.WriteLine(message);
Lesson 9:变长参数与参数默认值
params 变长参数
如果函数需要接收数量不确定的同类型参数,可以使用 params:
static int Sum(params int[] numbers)
{
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return sum;
}
调用时可以传入不同数量的整数:
Console.WriteLine(Sum());
Console.WriteLine(Sum(10));
Console.WriteLine(Sum(10, 20, 30));
Console.WriteLine(Sum(1, 2, 3, 4, 5));
params 的使用规则包括:
params后面必须是数组- 数组可以是任意数据类型
- 一个函数最多只能有一个
params params必须放在参数列表【29】最后- 参数数量可以为
0
例如,通过 params 计算平均值:
static float GetAverage(params int[] numbers)
{
if (numbers.Length == 0)
{
return 0;
}
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return (float)sum / numbers.Length;
}
参数默认值
参数默认值【30】也叫可选参数:
static void PrintPlayer(
string name,
int level = 1,
int health = 100)
{
Console.WriteLine("姓名:" + name);
Console.WriteLine("等级:" + level);
Console.WriteLine("生命值:" + health);
}
调用时可以省略可选参数:
PrintPlayer("勇者");
PrintPlayer("勇者", 10);
PrintPlayer("勇者", 10, 500);
没有传入数据时,函数使用默认值;传入数据时,则使用调用者提供的值。
普通参数必须放在可选参数前面:
static void Test(int a, int b = 10, int c = 20)
{
}
Lesson 10:函数重载
函数重载的基本概念
在同一个语句块中,可以声明多个同名函数。
只要参数列表不同,就可以构成函数重载【31】。
参数列表不同包括:
- 参数数量不同
- 参数类型不同
- 参数顺序不同
例如,根据参数类型重载【32】比较函数:
static int GetMax(int a, int b)
{
return a > b ? a : b;
}
static float GetMax(float a, float b)
{
return a > b ? a : b;
}
static double GetMax(double a, double b)
{
return a > b ? a : b;
}
调用时,程序会根据传入的数据类型选择对应函数:
Console.WriteLine(GetMax(10, 20));
Console.WriteLine(GetMax(3.5f, 8.2f));
Console.WriteLine(GetMax(7.8, 9.6));
重载的作用是让一组相似功能使用同一个函数名,减少函数名称数量,提高代码可读性。
重载与返回值无关
下面两个函数不能构成重载:
static int Test(int number)
{
return number;
}
static float Test(int number)
{
return number;
}
它们只有返回值类型不同,参数列表完全相同,因此会产生冲突。
函数重载判断的是参数列表,而不是返回值类型。
另外,ref 和 out 不能作为唯一的重载区别,不能只依靠一个函数使用 ref、另一个函数使用 out 来构成重载。
Lesson 11:递归函数
什么是递归
递归【33】就是函数自己调用自己。
如果函数无条件调用自己:
static void Test()
{
Test();
}
程序将无法停止,最终产生错误。
正确的递归必须具备三个条件:
- 存在结束条件
- 控制结束条件的数据不断变化
- 最终能够达到结束条件
递归调用过程
下面的函数会递归打印 0~10:
static void PrintNumber(int number)
{
if (number > 10)
{
return;
}
Console.WriteLine(number);
PrintNumber(number + 1);
}
调用:
PrintNumber(0);
每次调用时,number 都会加 1,最终达到结束条件。
递归求阶乘
阶乘具有明显的递归关系:
1! = 1
2! = 2 × 1!
3! = 3 × 2!
n! = n × (n - 1)!
代码如下:
static int Factorial(int number)
{
if (number == 1)
{
return 1;
}
return number * Factorial(number - 1);
}
调用:
Console.WriteLine(Factorial(5));
结果为:
120
这里:
number == 1是结束条件number - 1让控制数据发生变化- 数据最终会减少到
1
递归的重点不是记住某一道题,而是学会识别结束条件和数据变化规律。
Lesson 12:结构体
什么是结构体
结构体【34】是一种自定义变量类型,是数据和函数的集合。
它可以用于描述:
- 学生
- 玩家
- 怪物
- 矩形
- 其他具有一组相关信息的数据
基本语法如下:
struct 自定义结构体名
{
// 成员变量
// 构造函数
// 成员函数
}
例如,使用结构体描述学生:
enum Gender
{
Male,
Female
}
struct Student
{
public string name;
public Gender gender;
public int age;
public string className;
public string major;
}
结构体可以把原本分散的相关数据组织在一起。
public 与 private
课程介绍了两个访问修饰符【35】:
| 修饰符 | 作用 |
|---|---|
public | 外部可以访问 |
private | 只能在结构体内部访问 |
如果没有写访问修饰符,结构体成员默认为 private。
struct Player
{
public string name;
private int health;
}
外部可以访问 name,但不能直接访问 health。
结构体构造函数与 this
构造函数【36】用于方便地创建和初始化结构体数据:
struct Rectangle
{
public float width;
public float height;
public Rectangle(float width, float height)
{
this.width = width;
this.height = height;
}
}
结构体构造函数具有以下特点:
- 没有返回值
- 名称与结构体名称相同
- 当前课程使用带参数的构造函数
- 需要初始化结构体中的成员数据
- 构造函数可以重载
this 表示当前这个结构体自己:
this.width 结构体自己的成员变量
width 构造函数传入的参数
结构体中的成员函数
结构体中的成员函数【37】可以直接访问结构体自己的数据:
struct Rectangle
{
public float width;
public float height;
public Rectangle(float width, float height)
{
this.width = width;
this.height = height;
}
public float GetArea()
{
return width * height;
}
public float GetPerimeter()
{
return (width + height) * 2;
}
public void ShowInfo()
{
Console.WriteLine("面积:" + GetArea());
Console.WriteLine("周长:" + GetPerimeter());
}
}
使用:
Rectangle rectangle = new Rectangle(10, 5);
rectangle.ShowInfo();
当前阶段,结构体中的普通成员函数不需要写 static。
结构体与数组结合
结构体可以描述一只怪物,数组可以批量存储多只怪物:
struct Monster
{
public string name;
public int health;
public int attack;
public Monster(string name, int health, int attack)
{
this.name = name;
this.health = health;
this.attack = attack;
}
}
创建怪物数组:
Monster[] monsters = new Monster[10];
for (int i = 0; i < monsters.Length; i++)
{
monsters[i] = new Monster(
"小怪兽" + (i + 1),
100,
10);
}
这里综合使用了:
- 结构体
- 构造函数
- 数组
- 循环
- 字符串拼接【38】
结构体与 ref 结合
结构体属于值类型。将结构体传入函数时,如果希望函数直接修改外部结构体数据,可以使用 ref:
struct Fighter
{
public string name;
public int health;
public int attack;
public Fighter(string name, int health, int attack)
{
this.name = name;
this.health = health;
this.attack = attack;
}
}
攻击函数:
static bool Attack(
ref Fighter attacker,
ref Fighter target,
out string message)
{
target.health -= attacker.attack;
if (target.health <= 0)
{
target.health = 0;
message = attacker.name + "击败了" + target.name;
return true;
}
message =
attacker.name + "攻击了" + target.name +
",目标剩余生命值:" + target.health;
return false;
}
调用:
Fighter hero = new Fighter("奥特曼", 500, 80);
Fighter monster = new Fighter("小怪兽", 300, 30);
string message;
bool isOver = Attack(
ref hero,
ref monster,
out message);
Console.WriteLine(message);
这段代码把结构体、函数、ref 和 out 联系了起来。
Lesson 13:冒泡排序
冒泡排序的基本思路
排序【39】是把无序数据调整为指定顺序:
原数组:5 2 8 1 4
升序: 1 2 4 5 8
降序: 8 5 4 2 1
冒泡排序【40】的核心过程是:
- 比较两个相邻元素
- 如果顺序不符合要求,就交换
- 一轮比较后,让一个元素到达正确位置
- 重复多轮,直到数组有序
升序【41】冒泡排序:
int[] arr = { 5, 2, 8, 1, 4 };
for (int i = 0; i < arr.Length - 1; i++)
{
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
外层循环控制比较轮数,内层循环比较相邻元素。
冒泡排序优化
每完成一轮,就有一个元素到达最终位置,因此内层循环可以缩小比较范围:
j < arr.Length - 1 - i
还可以使用一个 bool 变量记录当前轮是否发生交换。
如果一轮比较完全没有发生交换,说明数组已经有序,可以提前结束:
static void BubbleSort(int[] arr, bool ascending)
{
for (int i = 0; i < arr.Length - 1; i++)
{
bool exchanged = false;
for (int j = 0; j < arr.Length - 1 - i; j++)
{
bool needExchange;
if (ascending)
{
needExchange = arr[j] > arr[j + 1];
}
else
{
needExchange = arr[j] < arr[j + 1];
}
if (needExchange)
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
exchanged = true;
}
}
if (!exchanged)
{
break;
}
}
}
调用时通过参数控制排序方向:
BubbleSort(arr, true); // 升序
BubbleSort(arr, false); // 降序
Lesson 14:选择排序
选择排序的基本思路
选择排序【42】每一轮都会从剩余元素中找到一个极值【43】,再把它放到当前目标位置。
升序排序时:
- 假设当前位置是最小值位置
- 向后查找真正的最小值
- 记录最小值索引
- 将最小值与当前位置交换
- 继续处理后面的元素
代码如下:
int[] arr = { 5, 2, 8, 1, 4 };
for (int i = 0; i < arr.Length - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < arr.Length; j++)
{
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
if (minIndex != i)
{
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
课程中将 minIndex 这类记录极值位置的变量形容为“中间商【44】”。
如果需要降序【45】排列,就在每一轮查找最大值。
也可以通过参数统一控制排序方向:
static void SelectionSort(int[] arr, bool ascending)
{
for (int i = 0; i < arr.Length - 1; i++)
{
int targetIndex = i;
for (int j = i + 1; j < arr.Length; j++)
{
bool foundTarget;
if (ascending)
{
foundTarget = arr[j] < arr[targetIndex];
}
else
{
foundTarget = arr[j] > arr[targetIndex];
}
if (foundTarget)
{
targetIndex = j;
}
}
if (targetIndex != i)
{
int temp = arr[i];
arr[i] = arr[targetIndex];
arr[targetIndex] = temp;
}
}
}
冒泡排序与选择排序的区别
| 对比项目 | 冒泡排序 | 选择排序 |
|---|---|---|
| 比较方式 | 比较相邻元素 | 在剩余元素中寻找极值 |
| 每轮结果 | 一个元素逐渐移动到正确位置 | 直接把极值放到目标位置 |
| 交换次数 | 一轮可能交换多次 | 一轮通常最多交换一次 |
| 基本结构 | 两层循环 | 两层循环 |
| 练习重点 | 相邻元素交换 | 极值索引记录 |
两种算法都适合练习:
- 数组遍历
- 双层循环
- 条件判断【46】
- 元素交换
- 函数封装【47】
Comments NOTHING