VBA学习笔记四:数组

第4部分  数组

4.1 数组的基本概念

4.1.1 数组元素

  • 元素个数:通常情况下,数组的下标从 0 开始计数,例如:dim arr(4)表示数组的索引范围为 0 到 4,共有 5个元素

4.1.2 数组类型

  • 一维数组:只包含一行或一列元素
  • 二维数组:是一个表格形式的数据结构,具有行和列的概念。通过两个索引可以访问二维数组中的元素,第一个索引表示行,第二个索引表示列
  • 多维数组:除了二维数组外,VBA 还支持多维数组,可以是三维、四维甚至更高维度的
  • 嵌套数组:将一个数组作为另一个数组的元素之一,可以理解为数组中包含数组,由多个数组组合在一起形成的一个整体的多层次结构,需要注意的是每一个子数组的大小也可以不同
  • 以下是不同的数组类型在本地窗口的展开,本地窗口展开的次数与数组的维度相关

一维数组

          

二维数组
三维数组

          

嵌套数组

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键可以转换为数组,再加[ ]便可以手动写入
图一:选中一部分单元格
图二:按f9
图三:加[ ]可以手动写入

          

  • 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  清空数组

  1. 重新声明数组:通过重新声明数组,把数组初始化为一个空数组
    Dim arr() As String  ' 声明一个字符串数组
    ReDim arr(0)         ' 重新初始化数组,长度为0,即为空数组
    
  2. 使用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列
    NameScore
    Alice85
    Bob92
    Carol78
    Dave88
    Ellen95
    代码如下:
    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]

      步骤

      • 第一次外循环(比较相邻元素并交换):

        • 比较 53,交换:[3, 5, 8, 1, 4]
        • 比较 58,不交换:[3, 5, 8, 1, 4]
        • 比较 81,交换:[3, 5, 1, 8, 4]
        • 比较 84,交换:[3, 5, 1, 4, 8]
        • 第一次循环结束后末尾的值为最大值
      • 第二次外循环(从第一个数到倒数第二个数两两比较):

        • 比较 35,不交换:[3, 5, 1, 4, 8]
        • 比较 51,交换:[3, 1, 5, 4, 8]
        • 比较 54,交换:[3, 1, 4, 5, 8]
      • 第三次外循环(从第一个数到倒数第三个数两两比较):

        • 比较 31,交换:[1, 3, 4, 5, 8]
        • 比较 34,不交换:[1, 3, 4, 5, 8]
      • 第四次外循环(从第一个数到倒数第四个数两两比较):

        • 比较 13,不交换:[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]

      步骤

    • 第一次外循环(第一个数开始和后面的每个数进行比较,找到最小值并交换到第一个位置):

      • 比较 53,最小值是 3
      • 比较 38,最小值是 3
      • 比较 31,最小值是 1
      • 比较 14,最小值是 1
      • 交换 15[1, 3, 8, 5, 4]
    • 第二次外循环(第二个数开始和后面的每个数进行比较,找到最小值并交换到第二个位置):

      • 比较 38,最小值是 3
      • 比较 35,最小值是 3
      • 比较 34,最小值是 3
      • 不交换:[1, 3, 8, 5, 4]
    • 第三次外循环(第三个数开始和后面的每个数进行比较,找到最小值并交换到第三个位置):

      • 比较 85,最小值是 5
      • 比较 54,最小值是 4
      • 交换 48[1, 3, 4, 5, 8]
    • 第四次外循环(第四个数开始和后面的每个数进行比较,找到最小值并交换到第四个位置):

      • 比较 58,最小值是 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
    

    执行情况:

冒泡循环,交换6次
选择循环,交换2次

4.8.3 插入排序(Insertion Sort)

  • 方法:每次将一个待排序的元素,按其关键字的大小插入到前面已经排好序的子序列中去,直到全部插入完为止
  • 案例解释:以升序的方式排列数组中的数据,外层循环从第二个数开始,和前面的每个数进行比较(从离自己最近的数开始),并决定是否交换位置,每结束一次外层循环代表这个数前面的顺序已经排列好了,然后第二次外层循环从第三个数开始,继续和前面的数比较找到合适的位置,以此类推直到外层循环到最后一个数,排序完成
    • 初始数组[5, 3, 8, 1, 4]

      步骤

    • 第一次外循环(从第二个数开始,和前面的每个数比较,将第二个元素插入到合适位置):

      • 比较 53,交换:[3, 5, 8, 1, 4]
    • 第二次外循环(从第三个数开始,和前面的每个数比较,将第三个元素插入到合适位置):

      • 比较 58,不交换:[3, 5, 8, 1, 4]
    • 第三次外循环(从第四个数开始,和前面的每个数比较,将第四个元素插入到合适位置):

      • 比较 81,交换:[3, 5, 1, 8, 4]
      • 比较 51,交换:[3, 1, 5, 8, 4]
      • 比较 31,交换:[1, 3, 5, 8, 4]
    • 第四次外循环(从第五个数开始,和前面的每个数比较,将第五个元素插入到合适位置):

      • 比较 84,交换:[1, 3, 5, 4, 8]
      • 比较 54,交换:[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

        

归并排序
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值