第4部分 数组
4.1 数组的基本概念
4.1.1 数组元素
- 元素个数:通常情况下,数组的下标从 0 开始计数,例如:dim arr(4)表示数组的索引范围为 0 到 4,共有 5个元素
4.1.2 数组类型
- 一维数组:只包含一行或一列元素
- 二维数组:是一个表格形式的数据结构,具有行和列的概念。通过两个索引可以访问二维数组中的元素,第一个索引表示行,第二个索引表示列
- 多维数组:除了二维数组外,VBA 还支持多维数组,可以是三维、四维甚至更高维度的
- 嵌套数组:将一个数组作为另一个数组的元素之一,可以理解为数组中包含数组,由多个数组组合在一起形成的一个整体的多层次结构,需要注意的是每一个子数组的大小也可以不同
- 以下是不同的数组类型在本地窗口的展开,本地窗口展开的次数与数组的维度相关
![](https://img-blog.csdnimg.cn/direct/36ce0fbb8fd948f2b67ec2f0842d4c75.png)
![](https://img-blog.csdnimg.cn/direct/cd94d30d48724ddc99447a68bd78b847.png)
![](https://img-blog.csdnimg.cn/direct/7c77fd9dd5154da6bfb7aec3b2885a2d.png)
![](https://img-blog.csdnimg.cn/direct/6902c575fc874b4ab17463bbe3aec58f.png)
4.1.3 数组上限和下限
- UBound:函数返回数组的上限,可以指定维度,比如行或者列,未指定维度,则默认为 1
- LBound:函数返回数组的下限,可以指定维度,比如行或者列,未指定维度,则默认为 1
Dim matrix(1 To 5, 1 To 3) As Integer ' 声明一个 5 行 3 列的整数数组 Debug.Print LBound(matrix, 1) ' 输出第一维的下限(1) Debug.Print UBound(matrix, 1) ' 输出第一维的上限(5) Debug.Print LBound(matrix, 2) ' 输出第二维的下限(1) Debug.Print UBound(matrix, 2) ' 输出第二维的上限(3)
4.2 数组声明
4.2.1 静态数组
特点:数组大小固定,声明时可以指定数据元素的个数、维度以及数据类型
- 一维数组声明:
Dim arr1D(5) As Integer ' 声明一个包含 6 个元素的一维整数数组 Dim arr1D() As String ' 声明一个字符串类型的一维数组
- 多维数组声明:
Dim arr2D(5, 3) As Double ' 声明一个 6 行 4 列的二维浮点数数组 Dim arr3D(5, 3, 2) As String ' 声明一个 6 行 4 列 3 层的三维字符串数组
- 指定下标起始值:设置数组的下标从几开始计数
Dim arr1D(1 To 5) As Integer ' 声明一个从 1 到 5 的整数数组
4.2.2 动态数组
特点:在运行时可以动态调整数组大小
- 动态数组声明
Dim dynamicArr() As Integer ' 声明一个动态整数数组
- ReDim 语句:调整动态数组的大小,并且重新初始化数据内容
Dim arr() As Integer ' 声明一个动态数组 ReDim arr(5) ' 初始化数组为包含 6 个元素的数组
- ReDim Preserve语句:调整数组大小并保留数组中已有数据的语句
' 示例:把数组 arr 从长度6调整为11,并保留原数据 Dim arr(5) As Integer ' 声明一个长度为 6 的整数数组 Dim i As Integer ' 给数组赋值 For i = 0 To 5 arr(i) = i * 10 Next i ' 调整数组大小并保留数据 ReDim Preserve arr(10) ' 输出调整后的数组内容 For i = 0 To 10 Debug.Print arr(i) Next i
4.2.3 隐式类型声明
特点:省略数组声明时的数据类型
- 静态数组:例如Dim arr
- 动态数组:例如Dim arr()
4.3 数组写入
- 单元格数据赋值:例如arr = Range("A1:A3")
- 单元格数据手动写入:选中一部分单元格,按F9键可以转换为数组,再加[ ]便可以手动写入
![](https://img-blog.csdnimg.cn/direct/38544d4ada874295a007c6600863bcd0.png)
![](https://img-blog.csdnimg.cn/direct/a857271cd0a8414bb0bbd585f31c2cf1.png)
![](https://img-blog.csdnimg.cn/direct/fd09838013f44b52aae958ac0066dddf.png)
- Array 函数初始化数组:通过Array把要写入的数据连接在一起,例如arr = Array(1, 2, 3, 4)
- 注意点:以上三种方法在声明时不要声明为静态数组(即固定数组大小),并且需要把数组声明成Variant 类型,否则会报错
- 代码循环写入:通过for循环写入,需要注意数组的维度和数据类型;一般情况下循环次数等于数组的元素数量,循环的嵌套次数等于数组的维度
4.4 数组元素处理
4.4.1 Split 拆分
- 功能:用于将字符串分割为子字符串并存储在数组中
- 语法:Split(expression, delimiter, limit, compare)
- 参数:
- expression:要分割的字符串
- delimiter:分隔符,用于指定在哪些字符处分割字符串。如果省略该参数,则默认使用空格作为分隔符
- limit:可选参数,指定返回的数组的最大维度(字符串的个数)。如果省略该参数或设置为 -1,则返回包含所有分割项的数组
- compare:可选参数,用于指定字符串比较方式。可以是以下常量:
- vbBinaryCompare:二进制比较(默认),可以用数字0代替
- vbTextCompare:文本比较,不区分大小写,可以用数字1代替
4.4.2 Join 合并
- 功能:将数组元素连接成一个字符串,可以在连接时加入特殊符号标记作为分隔符
- 语法:Join(array, delimiter)
- 参数:
- array: 要连接的数组
- delimiter: 连接数组元素时使用的分隔符
- 示例:将数组 arr 中的字符串以"e"为分隔符进行分割,并用"-"连接写入新的数组 brr 中
Sub SplitAndCopyToArray() Dim arr() As String Dim brr() As String Dim str As String Dim splitArr() As String Dim i As Integer ' 假设 arr 包含需要分割的字符串 arr = Split("Apple,Orange,Banana", ",") ' 初始化 brr 数组,长度与 arr 相同 ReDim brr(0 To UBound(arr)) ' 使用 For 循环遍历 arr 中的每个元素 For i = LBound(arr) To UBound(arr) ' 使用 Split 方法将字符串分割为子字符串,并存储到 splitArr 数组中 splitArr = Split(arr(i), "e") ' 将分割后的子字符串重新组合为新的字符串,存储到 brr 数组中对应的位置 brr(i) = Join(splitArr, "-") Next i ' 输出结果,检查 brr 数组中的内容 For i = LBound(brr) To UBound(brr) Debug.Print brr(i) Next i End Sub
4.4.3 Filter 筛选
- 功能:筛选数组中符合条件的元素
- 语法:Filter(sourcearray, match, [ include, [ compare ]])
- 参数:
- sourcearray:要筛选的一维数组,如果不是一维数组,先用join把多维数组中的字符串连接起来再分割
- match:筛选条件,通常为字符串
- include:可选,True表示包含,False表示不包含
- compare:0区分大小写,1不区分大小写
- 示例:筛选数组中包含“an”的元素,并赋值给新的数组
Sub TestFilter() Dim arr As Variant Dim filteredArr As Variant ' 初始化原始数组 arr = Array("apple", "banana", "cherry", "grape", "orange") ' 调用 Filter 函数进行筛选包含"an"的元素 filteredArr = Filter(arr, "an") ' 在 Immediate 窗口显示筛选后的数组 Dim i As Integer For i = LBound(filteredArr) To UBound(filteredArr) Debug.Print filteredArr(i) Next i End Sub
4.5 清空数组
- 重新声明数组:通过重新声明数组,把数组初始化为一个空数组
Dim arr() As String ' 声明一个字符串数组 ReDim arr(0) ' 重新初始化数组,长度为0,即为空数组
- 使用Erase关键字:Erase 数组,会释放数组占用的内存,并将数组重置为空数组
Dim arr() As String ' 声明一个字符串数组 Erase arr ' 清空数组,arr现在是一个空数组
4.6 数组转置
转置前 | 转置后 | 转置次数 |
---|---|---|
单列(n行1列)二维数组 | 一维数组 | 1次 |
单行(1行n列)二维数组(先变成列二维再转一维) | 一维数组 | 2次 |
多行多列二维数组 | 多行多列二维数组(行列互换) | 1次 |
嵌套数组 | 二维数组 | 2次 |
示例:在Sheet1工作表A1:D1中写入随机数值,在本地窗口可以查看数组的状态
Sub TransposeExample()
Dim ws As Worksheet
Dim rng As Range
Dim arr1 As Variant, arr2 As Variant, flatArr As Variant
' 获取工作表对象
Set ws = ThisWorkbook.Sheets("Sheet1")
' 获取范围对象(假设范围是单行)
Set rng = ws.Range("A1:D1")
' 从范围获取二维数组(单行)
arr1 = rng.value
' 第一次转置(行变列,变成单列的二维数组)
arr2 = Application.Transpose(arr1)
' 第二次转置(单列的二维数组变成一维数组)
flatArr = Application.Transpose(arr2)
End Sub
引申:EXCEL中一列(行)转多行多列或多行多列转一列(行) - 知乎 (zhihu.com)
4.7 结合工作表函数
- INDEX 函数:
- 功能:返回指定单元格或单元格数组的值
- 语法:INDEX(array, row_num, [column_num])
- 参数:
- array:要从中返回值的数组或范围
- row_num:要返回的值所在的行号。如果省略 row_num,则需使用 column_num
- column_num:要返回的值所在的列号。如果省略 column_num,则需使用 row_num
- 备注:如果将 row_num 或 column_num 设置为 0(零),则 INDEX 将分别返回整列或整行值的数组
- 示例:Application.index(arr, 0, 1) 返回arr第一列的数组; Application.index(arr, 2, 2) 返回 arr 中第 3 行、第 2 列的值
- MATCH 函数:
- 功能:在数组或范围中查找值,并返回其位置
- 语法:MATCH(lookup_value, lookup_array, [match_type])
- 参数:
- lookup_value:要查找的值
- lookup_array:要在其中查找值的数组或范围
- match_type:可选参数,指定查找方式,有 1(小于等于)、0(等于)和 -1(大于等于)三种类型。默认为 1
- 备注:match_type为1时,数组必须以升序排序;为-1时,数组必须以降序排序
- 示例:Application.Match("Carol", brr, 0) 在数组 brr 中查找值为 "Carol" 的位置,精确匹配
- 综合使用示例:假设有一个名为 "Data" 的工作表,包含学生姓名和对应的成绩,数据如下,找到 "Carol" 的成绩并输出
代码如下:A列 B列 Name Score Alice 85 Bob 92 Carol 78 Dave 88 Ellen 95 Sub SearchStudentScore() ' 声明工作表和数组变量 Dim ws As Worksheet Dim arr As Variant Dim brr As Variant Dim searchName As String Dim nameLocation As Integer Dim score As Double ' 忽略错误并继续运行,避免学生姓名不在列表时的报错 On Error Resume Next ' 设置工作表 Set ws = ThisWorkbook.Sheets("Data") ' 将数据范围写入数组 arr = ws.Range("A2:B6").value ' 要查找的学生姓名 searchName = "Carol" ' 使用INDEX获取学生姓名列 brr = Application.index(arr, 0, 1) ' 使用MATCH查找学生姓名在数组中的位置,精确匹配 nameLocation = Application.Match(searchName, brr, 0) ' 判断学生是否存在并获取成绩 If Not IsError(studentLocation) Then ' 使用INDEX获取学生对应的成绩 score = Application.index(arr, nameLocation, 2) MsgBox searchName & "的成绩是:" & score Else MsgBox searchName & "不存在于列表中。" End If End Sub
4.8 数组排序
功能:将数组中的元素按升序或降序排列
说明:排序通常有两层循环,外层循环用于控制迭代次数,每次迭代都会将当前未排序的部分缩小一点,内层循环则用于对未排序部分中的元素进行两两比较,并根据排序规则决定是否交换它们的位置
时间复杂度:描述算法执行时间随输入数据规模增长而变化的规律
4.8.1 冒泡排序(Bubble Sort)
- 方法:重复地遍历数组,一次比较两个相邻的元素,并且如果它们的顺序错误就把它们交换
- 案例解释:以升序的方式排列数组中的数据,从第一个数开始每两个数进行比较,如果第一个数大于第二个数则进行交换,第一次循环结束后末尾的值为最大值,然后从第一个数到倒数第二数两两比较,以此类推直到排序完成
-
初始数组:
[5, 3, 8, 1, 4]
步骤:
-
第一次外循环(比较相邻元素并交换):
- 比较
5
和3
,交换:[3, 5, 8, 1, 4]
- 比较
5
和8
,不交换:[3, 5, 8, 1, 4]
- 比较
8
和1
,交换:[3, 5, 1, 8, 4]
- 比较
8
和4
,交换:[3, 5, 1, 4, 8]
- 第一次循环结束后末尾的值为最大值
- 比较
-
第二次外循环(从第一个数到倒数第二个数两两比较):
- 比较
3
和5
,不交换:[3, 5, 1, 4, 8]
- 比较
5
和1
,交换:[3, 1, 5, 4, 8]
- 比较
5
和4
,交换:[3, 1, 4, 5, 8]
- 比较
-
第三次外循环(从第一个数到倒数第三个数两两比较):
- 比较
3
和1
,交换:[1, 3, 4, 5, 8]
- 比较
3
和4
,不交换:[1, 3, 4, 5, 8]
- 比较
-
第四次外循环(从第一个数到倒数第四个数两两比较):
- 比较
1
和3
,不交换:[1, 3, 4, 5, 8]
- 比较
-
-
最终数组:[1, 3, 4, 5, 8]
- 扑克牌解释:想象你手上拿着一副杂乱无章的扑克牌,你的目标是让它们按照从小到大的顺序排列。你会从最底下开始,逐个比较相邻的牌,如果它们的顺序错了,你就交换它们的位置,直到没有任何两张牌需要交换为止。这就好像你不断地冒泡最大的牌到顶端一样,直到所有的牌都有序
- 备注:交换顺序先把移动的值a储存在一个变量中,另外一个值b移动在原先a的位置后,再把变量(a的值)赋值给b的位置
- 交换次数:取决于数组的大小n和数组中元素的顺序,最坏情况下为n*(n-1)/2次(排序顺序和数组顺序相反)
- 时间复杂度:O(n^2),不适合处理大规模数据
- 优点:实现简单,容易理解和实现
4.8.2 选择排序(Selection Sort)
- 方法:通过每次遍历数组找到最小(或最大)的元素,存放在序列的起始位置,直到整个数组排序完成
- 案例解释:以升序的方式排列数组中的数据,先假设第一个数是最小值,然后从第一个数开始和后面的每个数进行比较,找到数组中的最小值并与其交换位置,再假设第二个数是最小值和后面的数进行比较,以此类推直到排序完成
-
初始数组:
[5, 3, 8, 1, 4]
步骤:
-
第一次外循环(第一个数开始和后面的每个数进行比较,找到最小值并交换到第一个位置):
- 比较
5
和3
,最小值是3
- 比较
3
和8
,最小值是3
- 比较
3
和1
,最小值是1
- 比较
1
和4
,最小值是1
- 交换
1
和5
:[1, 3, 8, 5, 4]
- 比较
-
第二次外循环(第二个数开始和后面的每个数进行比较,找到最小值并交换到第二个位置):
- 比较
3
和8
,最小值是3
- 比较
3
和5
,最小值是3
- 比较
3
和4
,最小值是3
- 不交换:
[1, 3, 8, 5, 4]
- 比较
-
第三次外循环(第三个数开始和后面的每个数进行比较,找到最小值并交换到第三个位置):
- 比较
8
和5
,最小值是5
- 比较
5
和4
,最小值是4
- 交换
4
和8
:[1, 3, 4, 5, 8]
- 比较
-
第四次外循环(第四个数开始和后面的每个数进行比较,找到最小值并交换到第四个位置):
- 比较
5
和8
,最小值是5
- 不交换:
[1, 3, 4, 5, 8]
- 比较
-
最终数组:[1, 3, 4, 5, 8]
- 扑克牌解释:想象你还是手里有一副无序的扑克牌,但这次你打算通过每次选择最小的牌来整理它们。你会从牌堆里挑选出最小的一张,然后放到已经整理好的牌的堆顶。接着,你再从剩下的牌中选择出最小的,放到已经排好序的牌的下一张。你像这样一直重复,直到所有的牌都被选择并放置到正确的位置。
- 交换次数:取决于数组的大小n和数组中元素的顺序,最坏情况下为n-1次
- 时间复杂度:O(n^2),不适合处理大规模数据
- 优点:实现简单,不占用额外的内存空间
- 示例代码:数组arr中的元素分别为5-3-8-1-4,用两种方式分别进行排序
Sub SortArrayExample() ' 创建包含一行数据的数组 Dim rowData(1 To 5) As Integer ' 假设初始数据为:5, 3, 8, 1, 4 rowData(1) = 5 rowData(2) = 3 rowData(3) = 8 rowData(4) = 1 rowData(5) = 4 ' 输出原始数据 Debug.Print "原始数据:" PrintArray rowData Dim sortMethod As VbMsgBoxResult sortMethod = MsgBox("请选择排序方式:是-冒泡排序,否-选择排序", vbYesNo) If sortMethod = vbYes Then ' 使用冒泡排序对数组进行排序 BubbleSort rowData Else ' 使用选择排序对数组进行排序 SelectionSort rowData End If ' 输出排序后的数据 Debug.Print "排序后的数据:" PrintArray rowData End Sub ' 打印数组 Sub PrintArray(arr() As Integer) Dim i As Integer For i = LBound(arr) To UBound(arr) Debug.Print arr(i); Next i Debug.Print End Sub ' 冒泡排序算法 Sub BubbleSort(arr() As Integer) Dim n As Integer ' 数组长度 Dim i As Integer ' 外层循环计数器 Dim j As Integer ' 内层循环计数器 Dim temp As Integer ' 临时变量,用于交换数组元素 Dim swapCount As Integer ' 交换次数 n = UBound(arr) - LBound(arr) + 1 ' 获取数组长度 ' 外层循环,遍历数组 For i = 1 To n - 1 ' 内层循环,每次遍历比较相邻两个元素并交换 For j = 1 To n - i If arr(j) > arr(j + 1) Then ' 如果前一个元素大于后一个元素,则交换它们 temp = arr(j) arr(j) = arr(j + 1) arr(j + 1) = temp swapCount = swapCount + 1 ' 打印交换后的数组和外层循环次数 Debug.Print "第 " & i & " 次外层循环,第 " & swapCount & " 次交换后的数组:" PrintArray arr End If Next j Next i End Sub ' 选择排序算法 Sub SelectionSort(arr() As Integer) Dim n As Integer ' 数组长度 Dim i As Integer ' 外层循环计数器 Dim j As Integer ' 内层循环计数器 Dim minIndex As Integer ' 最小值的索引 Dim temp As Integer ' 临时变量,用于交换数组元素 Dim swapCount As Integer ' 交换次数 n = UBound(arr) - LBound(arr) + 1 ' 获取数组长度 ' 外层循环,遍历数组 For i = 1 To n - 1 minIndex = i ' 假设当前索引处的值是最小值 ' 内层循环,从下一个元素开始找到最小值的索引 For j = i + 1 To n If arr(j) < arr(minIndex) Then minIndex = j ' 更新最小值的索引 End If Next j ' 如果找到的最小值的索引不是当前位置,进行交换 If minIndex <> i Then ' 将找到的最小值与当前元素交换 temp = arr(i) arr(i) = arr(minIndex) arr(minIndex) = temp swapCount = swapCount + 1 ' 打印交换后的数组和外层循环次数 Debug.Print "第 " & i & " 次外层循环,第 " & swapCount & " 次交换后的数组:" PrintArray arr End If Next i End Sub
执行情况:
![](https://img-blog.csdnimg.cn/direct/4a14bd4784dc46c880d243f986726fd2.png)
![](https://img-blog.csdnimg.cn/direct/ccf6896b845042e88687e74e3b181c09.png)
4.8.3 插入排序(Insertion Sort)
- 方法:每次将一个待排序的元素,按其关键字的大小插入到前面已经排好序的子序列中去,直到全部插入完为止
- 案例解释:以升序的方式排列数组中的数据,外层循环从第二个数开始,和前面的每个数进行比较(从离自己最近的数开始),并决定是否交换位置,每结束一次外层循环代表这个数前面的顺序已经排列好了,然后第二次外层循环从第三个数开始,继续和前面的数比较找到合适的位置,以此类推直到外层循环到最后一个数,排序完成
-
初始数组:
[5, 3, 8, 1, 4]
步骤:
-
第一次外循环(从第二个数开始,和前面的每个数比较,将第二个元素插入到合适位置):
- 比较
5
和3
,交换:[3, 5, 8, 1, 4]
- 比较
-
第二次外循环(从第三个数开始,和前面的每个数比较,将第三个元素插入到合适位置):
- 比较
5
和8
,不交换:[3, 5, 8, 1, 4]
- 比较
-
第三次外循环(从第四个数开始,和前面的每个数比较,将第四个元素插入到合适位置):
- 比较
8
和1
,交换:[3, 5, 1, 8, 4]
- 比较
5
和1
,交换:[3, 1, 5, 8, 4]
- 比较
3
和1
,交换:[1, 3, 5, 8, 4]
- 比较
-
第四次外循环(从第五个数开始,和前面的每个数比较,将第五个元素插入到合适位置):
- 比较
8
和4
,交换:[1, 3, 5, 4, 8]
- 比较
5
和4
,交换:[1, 3, 4, 5, 8]
- 比较
-
最终数组:
[1, 3, 4, 5, 8]
-
- 扑克牌解释:想象你面前有一张桌子,上面有几张已经有序的扑克牌。你手里拿着一叠乱序的牌,你的任务是把每张牌插入到正确的位置,以使整个桌面的牌都有序。你会逐个取出手上的牌,然后在已经有序的牌中找到合适的位置,将这张牌插入进去。你会重复这个过程,直到手上的牌都被插入到了正确的位置。
- 交换次数:取决于数组的大小n和数组中元素的顺序,和冒泡排序一样最坏情况下为n*(n-1)/2次(排序顺序和数组顺序相反)
- 时间复杂度:O(n^2),对于大规模数据排序性能仍然不理想
- 优点:对于部分有序的数据较为高效,性能优于冒泡排序和选择排序
- 示例:
Sub InsertionSortExample() Dim rowData(1 To 5) As Integer ' 假设初始数据为:5, 3, 8, 1, 4 rowData(1) = 5 rowData(2) = 3 rowData(3) = 8 rowData(4) = 1 rowData(5) = 4 ' 输出原始数据 Debug.Print "原始数据:" PrintArray rowData ' 使用插入排序对数组进行排序 InsertionSort rowData ' 输出排序后的数据 Debug.Print "排序后的数据:" PrintArray rowData End Sub ' 打印数组 Sub PrintArray(arr() As Integer) Dim i As Integer For i = LBound(arr) To UBound(arr) Debug.Print arr(i); Next i Debug.Print End Sub ' 插入排序算法 Sub InsertionSort(arr() As Integer) Dim i As Integer, j As Integer 'i为外层循环计数器,j为内层循环计数器 Dim key As Integer Dim loopCount As Integer, swapCount As Integer, temp As Integer ' 记录循环次数和交换次数的变量初始化 loopCount = 0 swapCount = 0 ' 外层循环,从第二个元素开始,依次插入到已排序的部分中 For i = LBound(arr) + 1 To UBound(arr) key = arr(i) ' 当前要比较的元素 j = i - 1 ' j为当前元素的前一个元素 loopCount = loopCount + 1 ' 记录外层循环次数 ' 内层循环,将大于key的元素向后移动 Do While j >= LBound(arr) And arr(j) > key swapCount = swapCount + 1 ' 记录交换次数 temp = arr(j) arr(j) = arr(j + 1) arr(j + 1) = temp j = j - 1 If j < 1 Then Exit Do ' 已到达数组起始位置,结束内层循环 Loop ' 打印每次排序后的数组和交换次数 Debug.Print "外层循环次数: " & loopCount & ", 交换次数: " & swapCount PrintArray arr ' 调用PrintArray子过程打印当前数组 Next i End Sub
插入排序,交换次数6
4.8.4 快速排序(Quick Sort)
- 方法:通过选择一个基准点(通常为第一个元素、最后一个元素或中间元素),然后将数组分成两个部分,一部分比基准点小,另一部分比基准点大,再递归地对这两部分进行排序
- 注意:以基准点对其他数据进行排序的时候,数据之间不会进行大小比较,只考虑是否比基准点大或者小(也就是说一边的数据比基准数小,一边的数比基准点大),它们之间的顺序按照和基准点比较的先后顺序排列
- 案例解释:先选择3作为基准值,从左到右找到第一个大于基准元素的元素,并从右到左找到第一个小于或等于基准元素的元素,如果找到的两个元素位置没有交叉,则交换它们,交换后左右指针同时往内移动一位,直到两个元素位置有交叉,把基准点放在恰当的位置。此时一边的数据比基准点大,一边比基准点小,而它们之间没有排序,所以继续对左右两边的数据按照上面的方法递归排序,直到最后全部排序完成。
- 初始数组: [3, 6, 8, 10, 1, 2, 1]
- 选择基准点:选择
3
作为基准点。 -
Step 1: 划分数组
-
从左到右找到第一个大于基准元素的元素
-
从右到左找到第一个小于或等于基准元素的元素
-
如果找到的两个元素位置没有交叉,则交换它们,交换后左右指针同时往内移动一位
- 当找到的两个元素位置有交叉时,代表两边都已排序完成
- 交换 6 和 1:[3, 1, 8, 10, 1, 2, 6]
- 交换 8 和 2:[3, 1, 2, 10, 1, 8, 6]
- 交换 10 和 1: [3, 1, 2, 1, 10, 8, 6]
基准元素位置调整:将基准元素放到正确的位置- 继续从右边找到一个小于等于基准的数,交换位置,本例中为基准 3 与 1交换
- 划分结果:[1, 1, 2, 3, 10, 8, 6]
-
-
Step 2: 递归排序左子数组
- 对基准 3 左边的子数组 [1, 1, 2] 进行排序
- 选择 1 作为基准,找到的两个元素位置有交叉,基准位置调整1和1后得到:[1, 1, 2]
- 左边 [1] 和右边 [2] 已经有序,不需要进一步操作
-
Step 3: 递归排序右子数组
- 对基准 3 右边的子数组 [10, 8, 6] 进行排序。
- 选择 10 作为基准,8和6都比10小,基准位置调整10和6后得到:[6, 8, 10]
- 递归地对左边 [6, 8] 进行排序,选择 6 作为基准,把6放到正确位置(此时已经基准已经在正确位置,无需实际交换,只是代码会运行一遍)分区后得到:[6, 8]
- 左边 [6] 和右边 [8] 已经有序,不需要进一步操作。
最终结果:[1, 1, 2, 3, 6, 8, 10]
- 扑克牌解释:选取扑克牌中的一张作为“基准”值,将扑克牌分成两堆,左边的牌比基准值小,右边的牌比基准值大。然后递归地对这两堆牌进行同样的操作,最终所有牌都按从小到大的顺序排列
- 交换次数:取决于输入数组的排列情况以及选择的基准元素,本案例为4+1+1+1,一共7次
- 时间复杂度:平均情况下时间复杂度为 O(nlogn),最坏情况下时间复杂度为 O(n2),但可以通过随机化基准元素选择来缓解
- 优点:适用于处理大规模数据
- 示例:
Sub TestQuickSort() Dim arr() As Variant Dim i As Integer ' 初始化数组 arr = Array(3, 6, 8, 10, 1, 2, 1) ' 调用 QuickSort 对数组进行排序 Debug.Print "整个数组分区" QuickSort arr, LBound(arr), UBound(arr) ' 打印排序后的数组 Debug.Print "排序后的数组:" PrintArray arr End Sub Sub QuickSort(arr() As Variant, low As Long, high As Long) Dim pivot As Variant Dim i As Integer, j As Integer, swapCount As Integer Dim temp As Variant ' 如果低索引小于高索引,进行排序 If low < high Then ' 选择数组的第一个元素作为基准元素 pivot = arr(low) i = low j = high ' 开始分区过程 Do While i < j ' 从左到右找到第一个大于基准元素的元素 Do While arr(i) <= pivot And i < high i = i + 1 Loop ' 从右到左找到第一个小于或等于基准元素的元素 Do While arr(j) > pivot j = j - 1 Loop ' 如果找到的两个元素位置没有交叉,则交换它们 If i < j Then ' 交换 arr(i) 和 arr(j) temp = arr(i) arr(i) = arr(j) arr(j) = temp swapCount = swapCount + 1 ' 打印当前数组状态 Debug.Print "左指针:" & i & ",右指针:" & j & ",交换次数:" & swapCount PrintArray arr End If Loop ' 将基准元素放到正确的位置 arr(low) = arr(j) arr(j) = pivot swapCount = swapCount + 1 Debug.Print "将基准元素放到正确的位置,交换次数:" & swapCount PrintArray arr ' 递归地对左右子数组进行排序 ' 如果左子数组大小大于1,才进行排序并打印 If low < j - 1 Then Debug.Print "左边子数组递归排序" QuickSort arr, low, j - 1 End If ' 如果右子数组大小大于1,才进行排序并打印 If j + 1 < high Then Debug.Print "右边子数组递归排序" QuickSort arr, j + 1, high End If End If End Sub Sub PrintArray(arr() As Variant) Dim i As Integer ' 打印数组为一行 For i = LBound(arr) To UBound(arr) Debug.Print arr(i); Next i Debug.Print "" ' 换行 End Sub
快速排序
4.8.5 归并排序(Merge Sort)
- 什么是递归:
- 日常生活中的递归例子:拆礼物盒
- 假设你收到了一系列嵌套的礼物盒,每个盒子里还有一个更小的礼物盒,直到最里面的盒子里有真正的礼物。你需要一层一层地打开这些盒子,直到找到最终的礼物,然后再将每层盒子按顺序重新盖好。每次你打开一个盒子(进行递归调用),你都需要记住要把打开的盒子恢复原样(返回上一层递归)。最终,当你完成所有的操作时,你不仅找到了礼物,还恢复了所有的盒子。
- 结论:每次递归完成后,返回上一层递归,直到程序结束
- 判断递归是否完成:观察每一层递归是否运行到当前代码的 End Sub
- 归并排序:
- 方法:先分割后合并,将数组从中间分为两个子数组,递归地对这两个子数组进行排序;再将两个已经排好序的子数组合并为一个有序的数组
- 注意:每次递归完成后,返回上一层递归
- 案例解释:
-
Step 1: 初始化数组 arr = [7, 2, 9, 4, 1, 5]
-
调用 MergeSort arr, 0, 5(数组从索引0到索引5)
-
-
Step 2: 递归排序左半部分
-
初次调用
MergeSort(arr, 0, 5)
:mid = 0 + (5 - 0) \ 2 = 2
- 递归调用
MergeSort(arr, 0, 2)
-
第二次调用
MergeSort(arr, 0, 2)
:mid = 0 + (2 - 0) \ 2 = 1
- 递归调用
MergeSort(arr, 0, 1)
-
第三次调用
MergeSort(arr, 0, 1)
:mid = 0 + (1 - 0) \ 2 = 0
- 递归调用
MergeSort(arr, 0, 0)
(由于left
不小于right
,直接返回) - 递归调用
MergeSort(arr, 1, 1)
(由于left
不小于right
,直接返回)
-
-
Step 3: 合并左半部分
-
初次合并
Merge(arr, 0, 0, 1)
:leftArr = [7]
rightArr = [2]
- 合并后的数组为
[2, 7, 9, 4, 1, 5]
-
回到
MergeSort(arr, 0, 2)
,继续调用MergeSort(arr, 2, 2)
(由于left
不小于right
,直接返回)
-
-
Step 4: 递归排序右半部分
-
初次调用
Merge(arr, 0, 1, 2)
:leftArr = [2, 7]
rightArr = [9]
- 合并后的数组为
[2, 7, 9, 4, 1, 5]
-
回到最初调用
MergeSort(arr, 0, 5)
,继续调用MergeSort(arr, 3, 5)
:mid = 3 + (5 - 3) \ 2 = 4
- 递归调用
MergeSort(arr, 3, 4)
mid = 3 + (4 - 3) \ 2 = 3
- 递归调用
MergeSort(arr, 3, 3)
(由于left
不小于right
,直接返回) - 递归调用
MergeSort(arr, 4, 4)
(由于left
不小于right
,直接返回)
-
- Step 5: 合并右半部分
-
合并
Merge(arr, 3, 3, 4)
:leftArr = [4]
rightArr = [1]
- 合并后的数组为
[2, 7, 9, 1, 4, 5]
-
继续调用
MergeSort(arr, 5, 5)
(由于left
不小于right
,直接返回) -
合并
Merge(arr, 3, 4, 5)
:leftArr = [1, 4]
rightArr = [5]
- 合并后的数组为
[2, 7, 9, 1, 4, 5]
-
-
Step 6: 最终合并
- 最终合并
Merge(arr, 0, 2, 5)
:leftArr = [2, 7, 9]
rightArr = [1, 4, 5]
- 合并后的数组为
[1, 2, 4, 5, 7, 9]
- 最终合并
-
- 扑克牌解释:将扑克牌分成两半,分别对两半进行排序,然后合并两个有序的部分,直到所有牌都合并成一个有序的整体
- 时间复杂度:归并排序的时间复杂度是 O(n log n),因为每次分割数组的操作是 O(log n),而合并操作是 O(n)
- 适用性:归并排序适用于处理大数据集和需要稳定排序的场合,尤其是在链表结构中非常高效
- 示例:
Sub MergeSortExample() Dim arr() As Variant arr = Array(7, 2, 9, 4, 1, 5) Debug.Print "原始数组:" PrintArray arr ' 调用归并排序 MergeSort arr, LBound(arr), UBound(arr) Debug.Print "排序后的数组:" PrintArray arr End Sub Sub PrintArray(arr() As Variant) Dim i As Integer For i = LBound(arr) To UBound(arr) Debug.Print arr(i); Next i Debug.Print End Sub Sub MergeSort(arr() As Variant, left As Integer, right As Integer) If left < right Then Dim mid As Integer mid = (left + right) \ 2 ' 递归排序左半部分 MergeSort arr, left, mid ' 递归排序右半部分 MergeSort arr, mid + 1, right ' 合并两个有序部分 Merge arr, left, mid, right End If End Sub Sub Merge(arr() As Variant, left As Integer, mid As Integer, right As Integer) Dim temp() As Integer Dim i As Integer, j As Integer, k As Integer Dim n1 As Integer, n2 As Integer ' 计算两个子数组的大小 n1 = mid - left + 1 n2 = right - mid ' 创建临时数组 ReDim temp(left To right) ' 将左半部分数据拷贝到临时数组 For i = 0 To n1 - 1 temp(left + i) = arr(left + i) Next i ' 将右半部分数据拷贝到临时数组 For j = 0 To n2 - 1 temp(mid + 1 + j) = arr(mid + 1 + j) Next j ' 初始化指针 i = left ' 左半部分起始点 j = mid + 1 ' 右半部分起始点 k = left ' 合并后的数组起始点 ' 合并两个有序子数组 While i <= mid And j <= right If temp(i) <= temp(j) Then arr(k) = temp(i) i = i + 1 Else arr(k) = temp(j) j = j + 1 End If k = k + 1 Wend ' 拷贝剩余的左半部分元素 While i <= mid arr(k) = temp(i) i = i + 1 k = k + 1 Wend ' 拷贝剩余的右半部分元素 While j <= right arr(k) = temp(j) j = j + 1 k = k + 1 Wend ' 打印每次合并后的数组 Debug.Print "合并数组下标位置:" & left & " 到 " & right PrintArray arr End Sub
![](https://img-blog.csdnimg.cn/direct/5b6773af99a94257a85c6cf2248e8ac7.png)