摘要
本文主要归纳整理常见排序算法的原理以及实现,对部分内容进行适度展开。
环境
- Google Chrome 91.0.4472.114(64 位)
资源
准备工作
- 以下算法的讲解以及实现部分,均以下面的数组作为初始数组。
- 算法的测试采用
html
内嵌JS
代码的方式,HTML
均使用以下代码作为模板,
algorithm.html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>algorithm</title></head>
<body>
<script>
function print_array(arr) {
for (let key in arr){
if (typeof(arr[key]) == 'array' || typeof(arr[key]) == 'object') {
print_array(arr[key])
} else {
document.write(key + ' = ' + arr[key] + '<br>')
}
}
}
let arr = new Array(11, 23, 0, 46, 8, 18)
print_array(arr)
document.write('<br>')
document.write('<br>')
print_array(arr)
</script>
</body>
</html>
正式开始
冒泡排序
原理
- 在
len
范围(len
初始为数组的长度)内,依次比较相邻的元素,如果前一个比后一个大,则交换他们两个。 len
每次减1
。- 重复
步骤1~2
,直到len
为1
。
简单直观的排序算法,每次的步骤1
都保证了最大的元素放到了数组的末端。
我倒觉得这种排序算法更像是最大元素不断沉底的过程,叫沉底排序更为贴切。
时间复杂度:
O
(
n
2
)
O(n^{2})
O(n2)
步骤
len
初始为数组的长度。
{–len范围– | –len范围– | –len范围– | –len范围– | –len范围– | –len范围–} |
---|
11 | 23 | 0 | 46 | 8 | 18 |
- 在
len
范围内,依次比较相邻的元素,如果前一个比后一个大,则交换他们两个。
{–len范围– | –len范围– | –len范围– | –len范围– | –len范围– | –len范围–} |
---|
11 | 0 | 23 | 46 | 8 | 18 |
{–len范围– | –len范围– | –len范围– | –len范围– | –len范围– | –len范围–} |
---|
11 | 0 | 23 | 8 | 46 | 18 |
{–len范围– | –len范围– | –len范围– | –len范围– | –len范围– | –len范围–} |
---|
11 | 0 | 23 | 8 | 18 | 46 |
len
每次减1
。
{–len范围– | –len范围– | –len范围– | –len范围– | –len范围–} | |
---|
11 | 0 | 23 | 8 | 18 | 46 |
- 重复
步骤2~3
,直到len
为1
。
实现
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let len = (arr.length - 1) - i
for (let j = 0; j < len; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j + 1]
arr[j + 1] = arr[j]
arr[j] = temp
}
}
}
}
bubbleSort(arr)
选择排序
原理
- 将整个数组视为未排序序列,找到其中的最小值。
- 将找到的最小值交换到数组的起始位置,并将其视为已排序序列。
- 在剩余未排序序列中找到最小值,并放到已排序序列的末尾。
- 不断重复
步骤3
,直到所有未排序序列中的元素,均已放到已排序序列中。
简单直观的排序算法,每次从数组中取出一个最小值,依次按顺序排好。
时间复杂度:
O
(
n
2
)
O(n^{2})
O(n2)
步骤
- 将整个数组视为未排序序列,找到其中的最小值。
{–未排序序列– | –未排序序列– | –未排序序列– | –未排序序列– | –未排序序列– | –未排序序列–} |
---|
11 | 23 | 0 | 46 | 8 | 18 |
| | 最小值 | | | |
- 将找到的最小值交换到数组的起始位置,并将其视为已排序序列。
{–已排序序列–} | {–未排序序列– | –未排序序列– | –未排序序列– | –未排序序列– | –未排序序列–} |
---|
0 | 23 | 11 | 46 | 8 | 18 |
| | | | | |
- 在剩余未排序序列中找到最小值。
{–已排序序列–} | {–未排序序列– | –未排序序列– | –未排序序列– | –未排序序列– | –未排序序列–} |
---|
0 | 23 | 11 | 46 | 8 | 18 |
| | | | 最小值 | |
- 将找到的最小值交换到已排序序列的末尾。
{–已排序序列– | –已排序序列–} | {–未排序序列– | –未排序序列– | –未排序序列– | –未排序序列–} |
---|
0 | 8 | 11 | 46 | 23 | 18 |
| | | | | |
- 重复
步骤3~4
,直到所有未排序序列中的元素,均已放到已排序序列中。
实现
function selectionSort(arr) {
let len = arr.length
for (let i = 0; i < len - 1; i++) {
let minIndex = i
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
let temp = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = temp
}
}
selectionSort(arr)
直接插入排序
原理
- 将数组的第一个元素看做一个已排序好的有序表。
- 将有序表的下一个元素作为待插入元素,用
tmp
记录。 - 从有序表的最后一个元素开始,向前寻找插入位置,比
tmp
大的元素都要向后移动(插入位置是有序表中第一个比tmp
小的元素之后,或是有序表的起始位置)。插入后有序表的长度加1,并且依然有序(有序表中,插入位置右边的元素都比待插入元素大,左边的元素都比带插入元素小)。 - 重复
步骤2~3
,直到没有待插入的元素,整个数组排序完成。
每次步骤2~3
都在不断扩充有序表。
就像玩扑克牌的时候,我们每抽到一张牌,都是将它插入到当前手牌中的合适位置。
时间复杂度:
O
(
n
2
)
O(n^{2})
O(n2)
步骤
- 将数组的第一个元素看做一个已排序好的有序表。将有序表的下一个元素作为待插入元素,用
tmp
记录(tmp = 23
)。
- 从有序表的最后一个元素开始,向前寻找插入位置,比
tmp
大的元素都要向后移动。
(此次比较,由于有序表中最后一个元素就比tmp
小,所以没有向后移动的操作)
- 在插入位置,放入
tmp
。有序表的长度加1,并且依然有序。
{–有序表– | –有序表–} | | | | |
---|
11 | 23 | 0 | 46 | 8 | 18 |
| 插入位置 | | | | |
- 将有序表的下一个元素作为待插入元素,用
tmp
记录(tmp = 0
)。
{–有序表– | –有序表–} | 待插入元素 | | | |
---|
11 | 23 | 0 | 46 | 8 | 18 |
| | | | | |
- 从有序表的最后一个元素开始,向前寻找插入位置,比
tmp
大的元素都要向后移动
(此次比较,由于有序表中的元素都比tmp
大,所以插入位置就是有序表的起始位置)。
{–有序表– | –有序表–} | | | | |
---|
11 | 11 | 23 | 46 | 8 | 18 |
插入位置 | | | | | |
- 在插入位置,放入
tmp
。有序表的长度加1,并且依然有序。
{–有序表– | –有序表– | –有序表–} | | | |
---|
0 | 11 | 23 | 46 | 8 | 18 |
插入位置 | | | | | |
- 重复
步骤4~6
,直到没有待插入元素。
实现
function insertSort(arr){
for (let i = 1; i < arr.length; i++) {
let tmp = arr[i]
let j = i - 1
while (j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j]
j--
}
arr[j + 1] = tmp
}
}
insertSort(arr)
快速排序
原理
- 找一个基准值
tmp
。 - 把数组中比
tmp
小的都放在tmp
的左边。 - 把数组中比
tmp
大的都放在tmp
的右边。 - 把
tmp
左边的数据看成一个“数组”,把tmp
右边的数据看成一个“数组”,分别重复步骤1~4
。 - 直到左右“数组”都剩下1个元素,整个数组排序完成。
每次步骤1~4
都实现了数组元素以基准值tmp
左右划分,左边的小,右边的大。
其实,同时也是找到了基准值tmp
在数组中的正确位置。
时间复杂度:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))
步骤
low
记录数组起始的索引,high
记录数组结束的索引,以数组的第一个元素作为基准值,并用tmp
存储(tmp = 11
)。现在low的位置相当于空出来了,要找一个比tmp
小的值放在这里。
- 从
high
的位置开始不断向前找,找到一个比tmp
小的值 (8 < 11
)
low
的位置存储high
找到的值,
现在相当于high
的位置又空出来了,要找一个比tmp
大的值放在这里。
- 从
low
的位置开始不断向后找,找到一个比tmp
大的值 (23 > 11
)
high
的位置存储low
找到的值
- 重复
步骤2~5
,接下来几步的变化
- 当
low
与high
在同一个位置时结束寻找,此时将tmp
的值放在此处,
以下这个过程,实现了tmp
左边的都比其小,右边的都比其大。
- 再将
tmp
的左右分别看成“数组”,重复步骤1~8
,直到左右看做的“数组”都只有1个或0个元素。
将tmp
左边看做数组,
将tmp
右边看做数组,
实现
function quickSort(arr, low, high) {
if (low < high) {
let index = getIndex(arr, low, high)
quickSort(arr, low, index - 1)
quickSort(arr, index + 1, high)
}
}
function getIndex(arr, low, high) {
let tmp = arr[low]
while (low < high) {
while (low < high && arr[high] >= tmp) {
high--
}
arr[low] = arr[high];
while (low < high && arr[low] <= tmp) {
low++
}
arr[high] = arr[low]
}
arr[low] = tmp
return low
}
quickSort(arr, 0, arr.length - 1)
希尔排序
原理
- 找一个间隔
gap
。 - 把数组按照
gap
分成多组。 - 对每个分组进行
直接插入排序
。 - 缩小
gap
的取值,重复步骤2~4
。 - 直到
gap
的取值为0
时,整个数组排序完成。
每次步骤2~4
都实现了分组内的有序,分组不断变少,最后整个数组成为一组,对这一组进行直接插入排序,整个数组排序完成。
时间复杂度:
O
(
n
2
)
O(n^{2})
O(n2)
步骤
gap
初始为数组长度 / 2
,gap
的值向下取整。
gap == 3
group1 | group2 | group3 | group1 | group2 | group3 |
---|
11 | 23 | 0 | 46 | 8 | 18 |
- 对每组进行直接插入排序,
group1 | group2 | group3 | group1 | group2 | group3 |
---|
11 | 8 | 0 | 46 | 23 | 18 |
- 每次缩小
gap
都再次除以2
,
gap == 1
group1 | group1 | group1 | group1 | group1 | group1 |
---|
11 | 8 | 0 | 46 | 23 | 18 |
- 重复
步骤2~3
,直到gap
的取值为0
时,整个数组排序完成。
实现
function shellSort(arr) {
for (let gap = Math.floor(arr.length / 2); gap > 0; gap = Math.floor(gap /= 2)) {
for (let i = gap; i < arr.length; i++) {
let tmp = arr[i]
let j = i - gap
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j]
j -= gap
}
arr[j + gap] = tmp
}
}
}
shellSort(arr)