排序算法:快速排序,golang实现

目录

前言

快速排序

代码示例

[1. 算法包](#1. 算法包)

[2. 快速排序代码](#2. 快速排序代码)

[3. 模拟程序](#3. 模拟程序)

[4. 运行程序](#4. 运行程序)

[5. 从大到小排序](#5. 从大到小排序)

快速排序的思想

快速排序的实现逻辑

[1. 选择基准值 (Pivot)](#1. 选择基准值 (Pivot))

[2. 分区操作 (Partition)](#2. 分区操作 (Partition))

[3. 递归排序](#3. 递归排序)

循环次数测试

[假如 10 条数据进行排序](#假如 10 条数据进行排序)

[假如 20 条数据进行排序](#假如 20 条数据进行排序)

[假如 30 条数据进行排序](#假如 30 条数据进行排序)

[假设 5000 条数据,对比 冒泡、选择、插入、堆、归并](#假设 5000 条数据,对比 冒泡、选择、插入、堆、归并)

快速排序的适用场景

[1. 大数据集](#1. 大数据集)

[2. 随机数据](#2. 随机数据)

[3. 缓存友好的](#3. 缓存友好的)


前言

在实际场景中,选择合适的排序算法对于提高程序的效率和性能至关重要,本节课主要讲解"快速排序"的适用场景及代码实现。

快速排序

快速排序(Quick Sort) 是一种非常高效的排序算法,采用分治法的策略来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。其基本思想是:选择一个基准值(pivot),通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以达到整个数据变成有序序列。

代码示例

下面我们使用Go语言实现一个快速排序

1. 算法包

创建一个 pkg/algorithm.go

bash 复制代码
touch pkg/algorithm.go

(如果看过上节课的插入排序,则已存在该文件,我们就不需要再创建了)

2. 快速排序代码

打开 pkg/algorithm.go文件,代码如下

从小到大 排序

Go 复制代码
package pkg

// BubbleSort 冒泡排序
...

// SelectionSort 选择排序
...

// InsertionSort 插入排序
...

// QuickSort 快速排序
func QuickSort(arr []int, low, high int) {
	if low < high {
		// partitionIndex 是分区操作后基准的索引
		partitionIndex := partition(arr, low, high)

		// 分别对基准左侧和右侧的子数组进行快速排序
		QuickSort(arr, low, partitionIndex-1)
		QuickSort(arr, partitionIndex+1, high)
	}
}

// partition 分区操作
func partition(arr []int, low, high int) int {
	pivot := arr[high] // 选择最后一个元素作为基数
	i := low - 1

	for j := low; j < high; j++ {
		// 如果当前元素小于或等于基数
		if arr[j] <= pivot {
			i++
			// 交换 arr[i] 和 arr[j]
			arr[i], arr[j] = arr[j], arr[i]
		}
	}
	// 交换 arr[i+1] 和 arr[high] (基准值)
	arr[i+1], arr[high] = arr[high], arr[i+1]
	return i + 1
}

3. 模拟程序

打开 main.go 文件,代码如下:

Go 复制代码
package main

import (
	"demo/pkg"
	"fmt"
)

func main() {
	// 定义一个切片,这里我们模拟 10 个元素
	arr := []int{51, 224, 67, 322, 825, 103, 50, 965, 789, 601}
	fmt.Println("Original data:", arr) // 先打印原始数据
	pkg.QuickSort(arr, 0, len(arr)-1)  // 调用快速排序
	fmt.Println("New data:  ", arr)    // 后打印排序后的数据
}

4. 运行程序

bash 复制代码
go run main.go

能发现, Original data 后打印的数据,正是我们代码中定义的切片数据,顺序也是一致的。

New Data 后打印的数据,则是经过快速排序后的数据,是从小到大的。

5. 从大到小排序

如果需要 从大到小 排序也是可以的,在代码里,将两个元素比较的 小于等于符号 改成 大于等于符号 即可。

修改 pkg/algorithm.go 文件:

Go 复制代码
package pkg

// BubbleSort 冒泡排序
...

// SelectionSort 选择排序
...

// InsertionSort 插入排序
...

// QuickSort 快速排序
func QuickSort(arr []int, low, high int) {
	if low < high {
		// partitionIndex 是分区操作后基准的索引
		partitionIndex := partition(arr, low, high)

		// 分别对基准左侧和右侧的子数组进行快速排序
		QuickSort(arr, low, partitionIndex-1)
		QuickSort(arr, partitionIndex+1, high)
	}
}

// partition 分区操作
func partition(arr []int, low, high int) int {
	pivot := arr[high] // 选择最后一个元素作为基数
	i := low - 1

	for j := low; j < high; j++ {
		// 如果当前元素大于或等于基数
		if arr[j] >= pivot {
			i++
			// 交换 arr[i] 和 arr[j]
			arr[i], arr[j] = arr[j], arr[i]
		}
	}
	// 交换 arr[i+1] 和 arr[high] (基准值)
	arr[i+1], arr[high] = arr[high], arr[i+1]
	return i + 1
}

只需要一丁点的代码即可

package pkg 算第一行,上面示例中在第三十一行代码中,我们将 "<=" 改成了 ">=" ,这样就变成了 从大到小排序了

快速排序的思想

  • 分而治之:将大问题分解为小问题,然后递归地解决小问题,最后将小问题的解合并成原问题的解
  • 原地排序:快速排序是原地排序算法,它只需要一个很小的栈空间(用于递归)来进行排序,不需要额外的存储空间
  • 不稳定性:快速排序在某些情况下可能不是稳定的排序算法,因为相同元素的相对位置可能会在排序过程中改变

快速排序的实现逻辑

1. 选择基准值 (Pivot)

  • 在快速排序中,首先需要选择一个基准值(pivot)。基准值的选择对排序的效率有很大的影响,但在本文的示例代码中,我们简单地选择了数组的最后一个元素作为基准值

2. 分区操作 (Partition)

  • 分区操作是快速排序的核心。它的目的是将数组重新排列,使得所有比基准值小的元素都移到基准值的左边,所有比基准值大的元素都移到右边。分区操作完成后,基准值就处于其最终排序位置
  • 在 partition 函数中,我们使用两个指针 i 和 j,其中 i 指向小于基准值的最后一个元素的下一个位置(初始化为 low - 1), j 用于遍历数组 (从 low 开始到 high - 1)。当 arr[j] 小于或等于基准值时,我们将其与 arr[i+1] 交换,并将 i 增加 1。这样,所有小于或等于基准值的元素都被交换到了基准值的左边
  • 最后,我们将基准值 (原本在 arr[high] ) 与 arr[i + 1] 交换,此时 i + 1 就是基准值的最终位置,也是分区操作的返回值

3. 递归排序

  • 分区操作完成后,我们得到了基准值的正确位置,并且数组被分成了两部分:一部分是基准值左边的所有元素 (都比基准值小),另一部分是基准值右边的所有元素 (都比基准值大)
  • 然后,我们递归地对这两部分分别进行快速排序。这是通过调用 QuickSort 函数,并传入适当的参数 (基准值左侧和右侧的子数组的范围) 来实现的

循环次数测试

按照上面示例进行测试

假如 10 条数据进行排序

Go 复制代码
[]int{51, 224, 67, 322, 825, 103, 50, 965, 789, 601}

总计循环了 30

假如 20 条数据进行排序

Go 复制代码
[]int{997, 387, 461, 530, 979, 502, 36, 459, 99, 60, 454, 37, 182, 273, 529, 130, 315, 351, 975, 497}

总计循环了 83

假如 30 条数据进行排序

Go 复制代码
[]int{755, 247, 642, 652, 38, 587, 387, 284, 476, 924, 339, 830, 614, 534, 832, 450, 8, 641, 768, 788, 472, 750, 169, 479, 386, 124, 868, 259, 550, 613}

总计循环了 138

上面我们说到,"基准值的选择对排序的效率有很大的影响",我们修改一条,依旧使用 30 条数据,我们将其最后一位数据 613,改成其他数 120 (这个值可随便改,这里示例 120)

通过调试,循环了 151

如果将 120 改成 700

通过调试,循环了 131

假设 5000 条数据,对比 冒泡、选择、插入、堆、归并

  • 冒泡排序:循环次数 12,502,499
  • 选择排序:循环次数 12,502,499
  • 插入排序:循环次数 6,323,958
  • 快速排序:循环次数 74,236
  • 堆排序:循环次数 59,589
  • 归并排序:循环次数 60,288

快速排序的适用场景

快速排序在多种情况下都是非常高效的,特别是以下几种情况:

1. 大数据集

对于大数据集,快速排序通常比简单的排序算法 (如 冒泡排序、插入排序) 更快

2. 随机数据

当输入数组中的数据是随机分布的时,快速排序的平均时间复杂度是 O(n log n),这是非常高效的

3. 缓存友好的

快速排序通过递归地在数组的不同部分上工作,倾向于产生良好的缓存局部性,特别是在处理大数据集时

然后,快速排序在某些情况下可能不是最佳选择,例如:

  • 小数据集:对于非常小的数据集,快速排序的递归开销可能使得它不如简单的排序算法 (如 插入排序) 快
  • 几乎已经排序的数据:在这种情况下,快速排序的性能可能退化为 O(n^2),因为它依赖于分区操作来减少问题的规模。如果分区操作不能有效地减少数组的大小 (例如,基准值总是最大或最小的元素),则会导致性能下降。在这种情况下,可以考虑使用归并排序或堆排序等算法
  • 不稳定的排序需求:虽然快速排序在大多数情况下是稳定的 (如果实现得当),但在某些特定实现中可能不是。如果稳定性是排序算法的一个关键要求,可能需要考虑其他算法
相关推荐
逝去的秋风几秒前
【代码随想录训练营第42期 Day57打卡 - 图论Part7 - Prim算法与Kruskal算法
算法·图论·prim算法
QXH2000009 分钟前
数据结构—双向链表
c语言·数据结构·算法·链表
旺小仔.28 分钟前
【数据结构篇】~排序(1)之插入排序
c语言·数据结构·算法·链表·性能优化·排序算法
绎岚科技1 小时前
深度学习自编码器 - 随机编码器和解码器篇
人工智能·深度学习·算法·机器学习
jingling5551 小时前
后端开发刷题 | 数字字符串转化成IP地址
java·开发语言·javascript·算法
云边有个稻草人1 小时前
【刷题】Day5--数字在升序数组中出现的次数
开发语言·笔记·算法
ymchuangke1 小时前
评价类——熵权法(Entropy Weight Method, EWM),完全客观评价
人工智能·python·算法·机器学习·数学建模
Crossoads2 小时前
【数据结构】排序算法---希尔排序
c语言·开发语言·数据结构·算法·排序算法
skaiuijing2 小时前
巧用二级指针
c语言·开发语言·算法·架构·操作系统
jingling5552 小时前
后端开发刷题 | 最长上升子序列
java·开发语言·数据结构·后端·算法·动态规划