冒泡排序算法

26 阅读5分钟

🔍 冒泡排序算法原理

想象一下气泡从水底上升:气泡在上升过程中会逐渐变大。类似地,在冒泡排序中,每一轮遍历都会让一个当前最大的元素“浮”到它最终的位置上。这个过程是通过不断比较相邻元素,并在它们顺序错误时交换来实现的。 算法通过嵌套循环实现:

  • 外层循环:控制排序的轮数。对于长度为 n的数组,最多需要 n-1轮。因为经过 n-1轮后,即使是最坏情况,剩下的一个元素也必然在正确位置。
  • 内层循环:在每一轮中,负责具体的比较和交换。它从数组开始处遍历到当前尚未排序部分的末尾,逐一比较相邻元素。

📝 基础R语言实现

我们先从最基础的代码开始,它严格遵循上述原理。

# 基础版本的冒泡排序函数
basicBubbleSort <- function(arr) {
  n <- length(arr) # 获取数组长度
  # 外层循环:控制轮数,最多n-1轮
  for (i in 1:(n-1)) {
    # 内层循环:遍历未排序部分 (1 到 n-i)
    for (j in 1:(n-i)) {
      # 比较相邻元素:如果前一个比后一个大,则交换
      if (arr[j] > arr[j+1]) {
        temp <- arr[j]    # 临时存储arr[j]的值
        arr[j] <- arr[j+1] # 将arr[j+1]的值赋给arr[j]
        arr[j+1] <- temp  # 将临时存储的值赋给arr[j+1]
      }
    }
    # 打印每一轮排序后的数组状态,便于观察学习
    print(paste("第", i, "轮后:", paste(arr, collapse = ", ")))
  }
  return(arr) # 返回排序后的数组
}

# 测试示例
test_vector <- c(64, 34, 25, 12, 22, 11, 90)
cat("排序前:", paste(test_vector, collapse = ", "), "\n")
sorted_vector <- basicBubbleSort(test_vector)
cat("排序后:", paste(sorted_vector, collapse = ", "), "\n")

代码关键点解释

  • for (i in 1:(n-1)):外层循环,n-1轮足以完成排序。
  • for (j in 1:(n-i)):内层循环,每轮结束后,末尾 i个元素已有序,无需再比较。
  • 交换操作:使用临时变量 temp是交换两个变量值的标准方法,确保数据不丢失。

🚀 冒泡排序的优化

基础版本效率不高。我们通过两种策略优化它。

优化1:提前终止

如果在一轮遍历中没有发生任何交换,说明数组已经有序,可以提前结束排序。

# 带提前终止的优化版本
optimizedBubbleSort1 <- function(arr) {
  n <- length(arr)
  for (i in 1:(n-1)) {
    # 设置一个标志位,假设本轮已经有序
    hasSwapped <- FALSE
    for (j in 1:(n-i)) {
      if (arr[j] > arr[j+1]) {
        temp <- arr[j]
        arr[j] <- arr[j+1]
        arr[j+1] <- temp
        hasSwapped <- TRUE # 发生交换,说明尚未完全有序
      }
    }
    print(paste("第", i, "轮后:", paste(arr, collapse = ", ")))
    # 如果本轮没有交换,提前结束排序
    if (!hasSwapped) {
      cat("在第", i, "轮提前结束,数组已有序。\n")
      break
    }
  }
  return(arr)
}

# 测试一个已经部分有序的数组
nearly_sorted_vector <- c(1, 2, 3, 5, 4, 6, 7)
cat("优化前排序:", paste(nearly_sorted_vector, collapse = ", "), "\n")
result1 <- optimizedBubbleSort1(nearly_sorted_vector)

优化2:记录最后交换位置

在每轮遍历中,记录最后一次发生交换的位置。这个位置之后的元素已经有序,下一轮遍历只需到这个位置即可。

# 进一步优化:记录无序边界
optimizedBubbleSort2 <- function(arr) {
  n <- length(arr)
  sortBorder <- n - 1 # 初始无序边界为最后一个元素
  lastSwapIndex <- n - 1 # 记录最后一次交换的位置
  
  for (i in 1:(n-1)) {
    hasSwapped <- FALSE
    # 内层循环只需遍历到当前的无序边界
    for (j in 1:sortBorder) {
      if (arr[j] > arr[j+1]) {
        temp <- arr[j]
        arr[j] <- arr[j+1]
        arr[j+1] <- temp
        hasSwapped <- TRUE
        lastSwapIndex <- j # 更新最后一次交换的位置
      }
    }
    sortBorder <- lastSwapIndex # 更新下一轮的无序边界
    print(paste("第", i, "轮后:", paste(arr, collapse = ", "), "| 下一轮边界:", sortBorder))
    
    if (!hasSwapped) {
      cat("提前结束于第", i, "轮\n")
      break
    }
    # 如果无序边界为0,说明全部有序
    if (sortBorder == 0) break
  }
  return(arr)
}

# 测试
test_vector2 <- c(5, 2, 8, 1, 9, 3)
cat("排序前:", paste(test_vector2, collapse = ", "), "\n")
result2 <- optimizedBubbleSort2(test_vector2)

💡 算法性能与应用

时间复杂度分析

情景时间复杂度解释
最坏情况O(n²)数组完全逆序,需要所有n(n-1)/2次比较
最好情况O(n)数组已经有序,优化后一轮扫描即可结束
平均情况O(n²)需要大约n²/2次比较

空间复杂度

冒泡排序是原地排序,只需要常数O(1)的额外空间(用于临时变量temp和循环计数器等)。

稳定性

冒泡排序是稳定的排序算法。只有当arr[j] > arr[j+1]时才交换,相等元素不交换,相对顺序保持不变。

优缺点与适用场景

  • 优点:实现简单,代码易理解,稳定,原地排序。
  • 缺点:平均效率较低,不适合大规模数据集。
  • 适用场景:教学演示、小规模数据排序、或作为更复杂算法的子过程。对于数据量较大的情况,应考虑快速排序、归并排序等更高效的算法。

🛠️ 实战演练与调试

让我们通过一个具体例子,一步步跟踪算法的执行过程,加深理解。

# 详细的调试版本,打印每一步比较
debugBubbleSort <- function(arr) {
  n <- length(arr)
  cat("初始数组:", paste(arr, collapse = ", "), "\n\n")
  
  for (i in 1:(n-1)) {
    cat("=== 第", i, "轮排序 ===\n")
    hasSwapped <- FALSE
    
    for (j in 1:(n-i)) {
      cat("  比较位置", j, "和", j+1, ": ", arr[j], "vs", arr[j+1])
      
      if (arr[j] > arr[j+1]) {
        temp <- arr[j]
        arr[j] <- arr[j+1]
        arr[j+1] <- temp
        hasSwapped <- TRUE
        cat(" -> 交换后: ", paste(arr, collapse = ", "), "\n")
      } else {
        cat(" -> 无需交换\n")
      }
    }
    
    cat("第", i, "轮结果:", paste(arr, collapse = ", "), "\n\n")
    
    if (!hasSwapped) {
      cat("本轮无交换,排序完成!\n")
      break
    }
  }
  return(arr)
}

# 运行调试示例
debug_vector <- c(5, 2, 8, 1, 9)
final_result <- debugBubbleSort(debug_vector)

练习建议

  1. 尝试修改:将条件arr[j] > arr[j+1]改为arr[j] >= arr[j+1],观察稳定性变化。
  2. 降序排序:修改比较条件,实现从大到小的排序。
  3. 性能对比:使用system.time()函数比较基础版和优化版在处理较大随机数组时的性能差异。

💎 总结

冒泡排序是理解排序思想的绝佳起点。关键在于掌握其相邻比较与交换的核心机制,以及如何通过提前终止记录边界进行优化。虽然在实际应用中效率不高,但作为编程入门和算法思维的训练非常有价值。