排序算法
学习排序算法的目的
写排序算法系列的初衷是将其作为数据结构和算法刷题的过度;一方面是对之前的数据结构的复习,另一方面作为算法刷题的一个前序。
从整个算法学习的累加过程来看,排序算法是基础算法;从数据结构学习的后续来看,排序算法作为数据结构的巩固和练习是一个不错的选择。
要学习哪些排序算法
将常见的排序算法按照简单和高级分类:
- 简单排序算法:冒泡排序,选择排序,插入排序
- 高级排序算法:希尔排序,快速排序,计数排序,基数排序,归并排序,堆排序,桶排序
系列文章的结构
每一种算法文章按照下面的结构组织:
-
- 排序算法介绍
-
- 排序算法详细的实现过程
-
- 实现过程对应的伪代码
-
- 使用typescript实现排序算法
-
- 使用c实现排序算法
-
- 此算法的复杂度分析
使用伪代码和两种不同的编程语言实现的目的在于尽可能的减少编程语言对算法思想的影响。
简单排序算法
简单排序算法如其名,比较简单,所以在此一并介绍。
冒泡排序算法
1. 介绍
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地比较相邻的两个元素,如果顺序错误就交换它们,直到没有任何需要交换的元素。这样每一轮比较都会将最大(或最小)的元素“冒泡”到数组的末尾。
2. 实现过程
- 从数组的第一个元素开始,比较它与下一个元素的大小。
- 如果顺序错误(即当前元素大于下一个元素),则交换它们的位置。
- 继续向后比较,直到最后一个元素。
- 重复以上步骤,但是不再考虑已经排好序的末尾部分。
- 重复执行上述步骤,直到整个数组排序完成。
3. 伪代码
procedure bubbleSort(arr: array)
n = length(arr)
for i from 0 to n-1 do
// 对相邻元素进行比较并交换
for j from 0 to n-i-1 do
if arr[j] > arr[j+1] then
swap(arr[j], arr[j+1])
end if
end for
end for
end procedure
4. ts实现
function bubbleSort(arr: number[]): number[] {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
// 对相邻元素进行比较并交换
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换位置
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
// 示例用法
const array = [5, 2, 8, 4, 1];
const sortedArray = bubbleSort(array);
console.log(sortedArray); // 输出 [1, 2, 4, 5, 8]
5. c实现
#include <stdio.h>
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 对相邻元素进行比较并交换
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;
}
}
}
}
// 示例用法
int main() {
int array[] = {5, 2, 8, 4, 1};
int n = sizeof(array) / sizeof(array[0]);
bubbleSort(array, n);
printf("Sorted Array: ");
for (int i = 0; i < n; i++) {
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
6. 复杂度分析
- 交换次数:冒泡排序的核心操作是相邻元素的比较和交换,每次比较都可能需要进行一次交换。在最坏的情况下,即待排序数组逆序排列时,需要进行 n(n-1)/2 次交换,其中 n 是待排序数组的长度。
- 比较次数:在每一轮内循环中,需要比较相邻元素的大小,共进行 n-i-1 次比较,其中 i 表示当前轮数。因此,总的比较次数为 (n-1) + (n-2) + ... + 1 = n(n-1)/2。
- 空间复杂度:冒泡排序是原地排序算法,不需要额外的存储空间,所以空间复杂度为 O(1)。
- 时间复杂度:冒泡排序的时间复杂度主要取决于比较次数。在最坏情况下,需要进行 n(n-1)/2 次比较,所以时间复杂度为 O(n^2)。在最好情况下,当待排序数组已经有序时,只需进行 n-1 次比较,时间复杂度为 O(n)。平均情况下,冒泡排序的时间复杂度仍然为 O(n^2)。
选择排序算法
1. 介绍
选择排序(Selection Sort)是一种简单的排序算法。它的工作原理是每次从未排序的部分中选择最小(或最大)的元素,并将其放到已排序部分的末尾。
2. 实现过程
- 首先,在未排序部分中找到最小(或最大)的元素。
- 将该最小(或最大)元素与未排序部分的第一个元素交换位置,将其放到已排序部分的末尾。
- 接着,缩小未排序部分的范围,继续重复以上步骤,直到整个数组排序完成。
3. 伪代码
procedure selectionSort(arr: array)
n = length(arr)
for i from 0 to n-1 do
// 找到未排序部分的最小元素的索引
minIndex = i
for j from i+1 to n do
if arr[j] < arr[minIndex] then
minIndex = j
end if
end for
// 将最小元素与未排序部分的第一个元素交换位置
swap(arr[i], arr[minIndex])
end for
end procedure
4. ts实现
function selectionSort(arr: number[]): number[] {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
// 示例用法
const array = [5, 2, 8, 4, 1];
const sortedArray = selectionSort(array);
console.log(sortedArray); // 输出 [1, 2, 4, 5, 8]
5. c实现
#include <stdio.h>
void selectionSort(int arr[], int n) {
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;
}
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
// 示例用法
int main() {
int array[] = {5, 2, 8, 4, 1};
int n = sizeof(array) / sizeof(array[0]);
selectionSort(array, n);
printf("Sorted Array: ");
for (int i = 0; i < n; i++) {
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
6. 复杂度分析
- 交换次数:在每一轮内循环中,选择排序通过找到未排序部分的最小(或最大)元素,并将其与未排序部分的第一个元素进行交换。因此,在最坏的情况下,即待排序数组逆序排列时,需要进行 n-1 次交换,其中 n 是待排序数组的长度。
- 比较次数:在每一轮内循环中,选择排序需要比较未排序部分的每个元素与当前最小(或最大)元素进行比较。因此,总的比较次数为 (n-1) + (n-2) + ... + 1 = n(n-1)/2。
- 空间复杂度:选择排序是原地排序算法,不需要额外的存储空间,所以空间复杂度为 O(1)。
- 时间复杂度:选择排序的时间复杂度主要取决于比较次数。在最坏情况下,需要进行 n(n-1)/2 次比较,所以时间复杂度为 O(n^2)。无论输入数据的初始顺序如何,选择排序的比较次数始终相同。因此,选择排序的最好情况时间复杂度和平均情况时间复杂度也都是 O(n^2)。
插入排序算法
1. 介绍
插入排序是一种简单直观的排序算法,它通过构建有序序列,对未排序的元素依次进行插入操作,从而达到排序的目的。
2. 实现过程
- 将第一个元素作为已排序序列,将剩余的元素作为未排序序列。
- 从未排序序列中取出一个元素,记为current。
- 从已排序序列中的最后一个元素开始,依次与current比较。
- 若当前元素大于current,则将当前元素后移一位。
- 重复步骤4,直到找到current应该插入的位置。
- 将current插入到正确位置上。
- 重复步骤2到步骤6,直到所有元素都被插入到已排序序列中。
3. 伪代码
procedure insertionSort(array)
for i = 1 to length(array) - 1
current = array[i]
j = i - 1
while j >= 0 and array[j] > current
array[j + 1] = array[j]
j = j - 1
end while
array[j + 1] = current
end for
end procedure
4. ts实现
function insertionSort(array: number[]): number[] {
for (let i = 1; i < array.length; i++) {
let current = array[i];
let j = i - 1;
while (j >= 0 && array[j] > current) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = current;
}
return array;
}
5. c实现
#include <stdio.h>
void insertionSort(int array[], int length) {
for (int i = 1; i < length; i++) {
int current = array[i];
int j = i - 1;
while (j >= 0 && array[j] > current) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = current;
}
}
int main() {
int array[] = {5, 2, 8, 3, 1};
int length = sizeof(array) / sizeof(array[0]);
insertionSort(array, length);
printf("Sorted array: ");
for (int i = 0; i < length; i++) {
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
6. 复杂度分析
- 交换次数:在插入排序过程中,每次找到当前元素的正确位置时,可能需要进行多次交换操作。最坏情况下,当待排序序列为逆序时,每个元素都需要与已排序序列中的所有元素比较并交换位置,因此交换次数为O(n^2)。
- 比较次数:在插入排序过程中,每次找到当前元素的正确位置时,需要进行一次或多次比较操作。最坏情况下,当待排序序列为逆序时,每个元素都需要与已排序序列中的所有元素比较,因此比较次数也为O(n^2)。
- 空间复杂度:插入排序算法只需要使用常数级别的额外空间存储临时变量,因此空间复杂度为O(1)。
- 时间复杂度:插入排序算法的时间复杂度取决于交换次数和比较次数的总和。最好情况下,当待排序序列已经有序时,只需要进行n-1次比较,而无需进行交换操作,时间复杂度为O(n)。最坏情况下,当待排序序列为逆序时,需要进行n(n-1)/2次比较和交换操作,时间复杂度为O(n^2)。平均情况下,插入排序的时间复杂度也为O(n^2)。
简单排序算法对比
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
从上表可以得出以下结论:
- 冒泡排序、选择排序和插入排序都是简单的排序算法,适用于小规模数据或部分有序的数据。
- 从平均时间复杂度和最坏时间复杂度来看,这三种排序算法都属于O(n^2)级别的复杂度,对于大规模数据效率较低。
- 选择排序在所有情况下的时间复杂度都为O(n^2),而冒泡排序和插入排序在最好情况下的时间复杂度较优化。
- 从空间复杂度来看,这三种排序算法都只需要常数级别的额外空间。
- 从稳定性来看,冒泡排序和插入排序是稳定的排序算法,相等元素的相对顺序在排序后不会改变;而选择排序是不稳定的排序算法,相等元素的相对顺序可能会发生改变。