常用排序算法在软件工程中的应用

本文将介绍软件工程中常用的排序算法,包括冒泡排序、选择排序、插入排序、快速排序、归并排序和堆排序,以及它们的基本思想和实现方法。了解这些排序算法将有助于我们在软件开发过程中根据实际需求选择合适的排序方法。

1 引言

排序算法在软件工程中的应用非常广泛,它们用于对数据进行排序,以便进行进一步的处理和分析。本文将探讨六种常见的排序算法,分别是冒泡排序、选择排序、插入排序、快速排序、归并排序和堆排序。了解这些排序算法有助于我们在软件开发过程中根据实际需求选择合适的排序方法。

1.1. 冒泡排序

冒泡排序是一种简单的排序算法,通过重复地遍历要排序的序列,比较相邻元素并交换它们的顺序(如果顺序错误),直到没有元素需要交换为止。冒泡排序的时间复杂度为 O(n^2),在处理大规模数据时效率较低。

1.2. 选择排序

选择排序的基本思想是在每次遍历过程中找到序列中的最小(或最大)值,并将其与序列的首(或尾)元素交换。然后在剩余序列中继续执行相同的操作,直到整个序列有序。选择排序的时间复杂度同样为 O(n^2)。

1.3 插入排序

插入排序的工作原理类似于我们手动对扑克牌进行排序。它将待排序序列的每个元素与已排序序列中的元素逐一比较,将其插入到合适的位置。插入排序具有稳定性,时间复杂度为 O(n^2),但在部分有序的序列中,插入排序的效率较高。

1.4. 快速排序

快速排序是一种采用分治策略的排序算法。它通过选取序列中的一个基准元素(Pivot),将待排序序列分为两部分,一部分包含小于基准的元素,另一部分包含大于基准的元素。然后分别对这两部分进行递归排序。快速排序具有不稳定性,平均时间复杂度为 O(n*log(n))。

1.5 归并排序

归并排序是一种采用分治策略的排序算法。它将待排序序列递归地分成两个相等的部分,

2 相关思路和代码实现

2.1 冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法,通过重复地遍历要排序的序列,比较相邻元素并交换它们的顺序(如果顺序错误),直到没有元素需要交换为止。冒泡排序的时间复杂度为 O(n^2),在处理大规模数据时效率较低。

以下是冒泡排序的原理图:
1679107863.gif

下面也给出了一个简单的冒泡排序的逻辑实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
初始序列:[5, 2, 9, 1, 5, 6]

第1次遍历:
(5, 2) -> 2, 5, 9, 1, 5, 6
(5, 9) -> 2, 5, 9, 1, 5, 6
(9, 1) -> 2, 5, 1, 9, 5, 6
(9, 5) -> 2, 5, 1, 5, 9, 6
(9, 6) -> 2, 5, 1, 5, 6, 9

第2次遍历:
(2, 5) -> 2, 5, 1, 5, 6, 9
(5, 1) -> 2, 1, 5, 5, 6, 9
(5, 5) -> 2, 1, 5, 5, 6, 9
(5, 6) -> 2, 1, 5, 5, 6, 9

第3次遍历:
(2, 1) -> 1, 2, 5, 5, 6, 9
(2, 5) -> 1, 2, 5, 5, 6, 9
(5, 5) -> 1, 2, 5, 5, 6, 9

最终排序结果:[1, 2, 5, 5, 6, 9]

我们展示了对序列 [5, 2, 9, 1, 5, 6] 进行冒泡排序的过程。每次遍历时,相邻元素进行比较并交换(如果顺序错误)。遍历次数逐渐减少,因为每次遍历都会将最大的元素移动到正确的位置。最终,序列将按升序排列。

dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

void bubbleSort(List<int> arr) {
int n = arr.length;

for (int i = 0; i < n - 1; i++) {
bool swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if (!swapped) break;
}
}

void main() {
List<int> arr = [5, 2, 9, 1, 5, 6];
bubbleSort(arr);
print(arr); // [1, 2, 5, 5, 6, 9]
}

golang:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

func bubbleSort(arr []int) {
n := len(arr)

for i := 0; i < n-1; i++ {
swapped := false
for j := 0; j < n-i-1; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
swapped = true
}
}
if !swapped {
break
}
}
}

func main() {
arr := []int{5, 2, 9, 1, 5, 6}
bubbleSort(arr)
fmt.Println(arr) // [1, 2, 5, 5, 6, 9]
}

2.2 选择排序

选择排序(Selection Sort)是一种简单的排序算法,其基本思想是在每次遍历过程中找到序列中的最小值,并将其与序列的首元素交换。然后在剩余序列中继续执行相同的操作,直到整个序列有序。选择排序的时间复杂度为 O(n^2),在处理大规模数据时效率较低。

以下是选择排序的原理图:
1679110110.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

初始序列:[5, 2, 9, 1, 5, 6]

第1次遍历:找到最小值 1,与第1个元素 5 交换
1, 2, 9, 5, 5, 6

第2次遍历:在剩余序列 [2, 9, 5, 5, 6] 中找到最小值 2,与第2个元素 2 交换
1, 2, 9, 5, 5, 6

第3次遍历:在剩余序列 [9, 5, 5, 6] 中找到最小值 5,与第3个元素 9 交换
1, 2, 5, 9, 5, 6

第4次遍历:在剩余序列 [9, 5, 6] 中找到最小值 5,与第4个元素 9 交换
1, 2, 5, 5, 9, 6

第5次遍历:在剩余序列 [9, 6] 中找到最小值 6,与第5个元素 9 交换
1, 2, 5, 5, 6, 9

最终排序结果:[1, 2, 5, 5, 6, 9]

在选择排序的原理图中,我们展示了对序列 [5, 2, 9, 1, 5, 6] 进行选择排序的过程。每次遍历时,找到最小值并与序列的首元素交换。遍历次数逐渐减少,直到整个序列有序。

以下是使用 Dart 和 Go 语言实现选择排序的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void selectionSort(List<int> arr) {
int n = arr.length;

for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}

void main() {
List<int> arr = [5, 2, 9, 1, 5, 6];
selectionSort(arr);
print(arr); // [1, 2, 5, 5, 6, 9]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

package main

import "fmt"

func selectionSort(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ {
minIndex := i
for j := i + 1; j < n; j++ {
if arr[j] < arr[minIndex] {
minIndex = j
}
}
if minIndex != i {
arr[i], arr[minIndex] = arr[minIndex], arr[i]
}
}
}

func main() {
arr := []int{5, 2, 9, 1, 5, 6}
selectionSort(arr)
fmt.Println(arr) // [1, 2, 5, 5, 6, 9]
}

在这两个实现中,我们使用了相似的逻辑。selectionSort 函数遍历待排序数组,找到最小值并与序列的首元素交换。遍历次数逐渐减少,直到整个序列有序。注意,我们使用变量 minIndex 来记录当前最小值的索引,在每次遍历后更新此变量。

2.3 插入排序

插入排序(Insertion Sort)是一种简单的排序算法,逐个遍历待排序序列中的元素,将每个元素插入到已排序序列中的适当位置,使得插入后的序列仍然有序。插入排序在处理部分有序数据时表现较好,时间复杂度为 O(n^2),在处理大规模数据时效率较低。

以下是插入排序的原理图:
1679110779.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
初始序列:[5, 2, 9, 1, 5, 6]

第1次遍历:将 2 插入到已排序序列 [5] 中的适当位置
2, 5, 9, 1, 5, 6

第2次遍历:将 9 插入到已排序序列 [2, 5] 中的适当位置
2, 5, 9, 1, 5, 6

第3次遍历:将 1 插入到已排序序列 [2, 5, 9] 中的适当位置
1, 2, 5, 9, 5, 6

第4次遍历:将 5 插入到已排序序列 [1, 2, 5, 9] 中的适当位置
1, 2, 5, 5, 9, 6

第5次遍历:将 6 插入到已排序序列 [1, 2, 5, 5, 9] 中的适当位置
1, 2, 5, 5, 6, 9

最终排序结果:[1, 2, 5, 5, 6, 9]

在插入排序的原理图中,我们展示了对序列 [5, 2, 9, 1, 5, 6] 进行插入排序的过程。每次遍历时,将当前元素插入到已排序序列中的适当位置。遍历次数逐渐减少,直到整个序列有序。

以下是使用 Dart 和 Go 语言实现插入排序的代码示例:

Dart 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void insertionSort(List<int> arr) {
int n = arr.length;

for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;

while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}

void main() {
List<int> arr = [5, 2, 9, 1, 5, 6];
insertionSort(arr);
print(arr); // [1, 2, 5, 5, 6, 9]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func insertionSort(arr []int) {
n := len(arr)

for i := 1; i < n; i++ {
key := arr[i]
j := i - 1

for j >= 0 && arr[j] > key {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}

func main() {
arr := []int{5, 2, 9, 1, 5, 6}
insertionSort(arr)
fmt.Println(arr) // [1, 2, 5, 5, 6, 9]
}

在这两个实现中,我们使用了相似的逻辑。insertionSort 函数遍历待排序数组,将每个元素插入到已排序序列中的适当位置。遍历次数逐渐减少,直到整个序列有序。我们使用变量 key 保存当前元素值,然后使用内部的 while 循环向前遍历已排序序列,将所有大于 key 的元素后移一位,为 key 腾出适当的插入位置。

2.4 快速排序

快速排序(Quick Sort)是一种高效的排序算法,采用分治策略将待排序序列划分为较小和较大的两个子序列,然后递归地排序这两个子序列。快速排序的平均时间复杂度为 O(n*log(n)),在处理大规模数据时具有较好的性能。

以下是快速排序的原理图:
1679111210.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
初始序列:[5, 2, 9, 1, 5, 6]

选取基准值 5,划分序列:
1, 2, 5, 9, 5, 6

对子序列 [1, 2] 和 [9, 5, 6] 进行递归排序:

[1, 2] 已经有序
选取基准值 9,划分序列 [9, 5, 6]:
6, 5, 9

对子序列 [6, 5] 进行递归排序:
选取基准值 6,划分序列:
5, 6

最终排序结果:[1, 2, 5, 5, 6, 9]

以下是使用 Dart 和 Go 语言实现快速排序的代码示例:

Dart 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void quickSort(List<int> arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}

int partition(List<int> arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;

for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;

return i + 1;
}

void main() {
List<int> arr = [5, 2, 9, 1, 5, 6];
quickSort(arr, 0, arr.length - 1);
print(arr); // [1, 2, 5, 5, 6, 9]
}

Go 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

func quickSort(arr []int, low, high int) {
if low < high {
pivotIndex := partition(arr, low, high)
quickSort(arr, low, pivotIndex-1)
quickSort(arr, pivotIndex+1, high)
}
}

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[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]

return i + 1
}

func main() {
arr := []int{5, 2, 9, 1, 5, 6}
quickSort(arr, 0, len(arr)-1)
fmt.Println(arr) // [1, 2, 5, 5, 6, 9]
}

在这两个实现中,我们使用了相似的逻辑。quickSort 函数递归地对待排序数组进行排序。首先,在 partition 函数中选取基准值并对序列进行划分,将小于基准值的元素放在左侧,大于基准值的元素放在右侧。然后,在 quickSort 函数中递归地对划分后的子序列进行排序,直到整个序列有序。

2.4.1 快速排序优化项:

快速排序(Quick Sort)是一种高效的排序算法,但仍然存在优化的空间。以下是一些常见的优化方法:

优化基准值选择:
默认情况下,快速排序通常选择序列的第一个、最后一个或中间元素作为基准值。但这种选择方法在处理已排序或部分有序的数据时可能导致不平衡的划分,从而降低排序性能。为了解决这个问题,可以使用三数取中法(Median-of-Three)或随机选择基准值,使得基准值的选择更加均衡,减少不平衡划分的概率。

小数组使用插入排序:
在递归划分过程中,当子数组的大小较小时,插入排序在处理这些小规模数据时表现更优。因此,可以设定一个阈值,当子数组的大小小于该阈值时,使用插入排序而不是快速排序。

使用尾递归优化:
在实现快速排序时,可以使用尾递归优化,以减少递归栈的深度。通过在每次递归调用时处理较小的子数组,可以将递归栈的最大深度限制在 O(log n)。

并行化:
快速排序是一种可以并行化的算法。在多核处理器系统中,可以利用多个处理器同时对不同的子数组进行排序,从而进一步提高排序性能。

这些优化方法可以根据实际需求和场景选择使用,以提高快速排序在特定条件下的性能。

2.5 归并排序

归并排序(Merge Sort)是一种采用分治策略的排序算法。它将待排序序列递归地划分为两个子序列,然后将这两个子序列合并为一个有序序列。归并排序具有稳定性,且其时间复杂度为 O(n*log(n)),在处理大规模数据时具有较好的性能。

以下是归并排序的原理图:
1679112271.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
初始序列:[5, 2, 9, 1, 5, 6]

递归地将序列划分为两个子序列:
[5, 2, 9] 和 [1, 5, 6]

继续递归地划分子序列:
[5] 和 [2, 9]
[1] 和 [5, 6]

合并子序列 [2, 9] 和 [5, 6]:
[2, 5, 6, 9]

合并子序列 [5] 和 [2, 5, 6, 9]:
[2, 5, 5, 6, 9]

最终排序结果:[1, 2, 5, 5, 6, 9]

以下是使用 Dart 和 Go 语言实现归并排序的代码示例:

Dart 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void mergeSort(List<int> arr, int low, int high) {
if (low < high) {
int mid = (low + high) ~/ 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}

void merge(List<int> arr, int low, int mid, int high) {
List<int> temp = List.filled(high - low + 1, 0);
int i = low, j = mid + 1, k = 0;

while (i <= mid && j <= high) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}

while (i <= mid) {
temp[k++] = arr[i++];
}

while (j <= high) {
temp[k++] = arr[j++];
}

for (int t = 0; t < temp.length; t++) {
arr[low + t] = temp[t];
}
}

void main() {
List<int> arr = [5, 2, 9, 1, 5, 6];
mergeSort(arr, 0, arr.length - 1);
print(arr); // [1, 2, 5, 5, 6, 9]
}

golang:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

package main

import "fmt"

func mergeSort(arr []int, low, high int) {
if low < high {
mid := (low + high) / 2
mergeSort(arr, low, mid)
mergeSort(arr, mid+1, high)
merge(arr, low, mid, high)
}
}

func merge(arr []int, low, mid, high int) {
temp := make([]int, high-low+1)
i, j, k := low, mid+1, 0

for i <= mid && j <= high {
if arr[i] <= arr[j] {
temp[k] = arr[i]
k++
i++
} else {
temp[k] = arr[j]
k++
j++
}
}
for i <= mid {
temp[k] = arr[i]
k++
i++
}

for j <= high {
temp[k] = arr[j]
k++
j++
}

for t := 0; t < len(temp); t++ {
arr[low+t] = temp[t]
}

}

func main() {
arr := []int{5, 2, 9, 1, 5, 6}
mergeSort(arr, 0, len(arr)-1)
fmt.Println(arr) // [1, 2, 5, 5, 6, 9]
}

在这两个实现中,我们使用了相似的逻辑。mergeSort 函数递归地将待排序数组划分为两个子序列,然后使用 merge 函数将这两个子序列合并为一个有序序列。在合并过程中,我们使用了一个临时数组 temp,通过比较两个子序列的元素大小,将较小的元素放入 temp,并依次类推,直到所有元素都已经合并。最后,将临时数组 temp 的内容复制回原始数组 arr