八大排序算法
是指常用的八种排序算法,它们包括:
-
冒泡排序(Bubble Sort):通过不断交换相邻元素的位置,将最大(或最小)的元素逐渐"冒泡"到最后(或最前)的位置。
-
选择排序(Selection Sort):每次从未排序的部分中选择最小(或最大)的元素,并将其放置在已排序部分的末尾。
-
插入排序(Insertion Sort):逐个将元素插入到已排序序列的适当位置,直到所有元素都被插入完毕。
-
希尔排序(Shell Sort):将待排序的序列按照一定的间隔分成若干子序列,对子序列进行插入排序,然后逐渐缩小间隔,直至间隔为1,完成最后的排序。
-
归并排序(Merge Sort):将待排序序列不断划分为较小的子序列,然后将这些子序列两两合并,直到最终只剩下一个有序序列。
-
快速排序(Quick Sort):通过一次划分操作将序列分成两部分,其中一部分的元素都比另一部分小(或大),然后递归地对两部分进行排序。
-
堆排序(Heap Sort):将待排序序列构建成一个堆(大顶堆或小顶堆),然后依次将堆顶元素与最后一个元素交换并调整堆,重复这个过程直到整个序列有序。
-
计数排序(Counting Sort):通过确定每个元素在序列中的位置来排序,需要额外的辅助空间来记录元素出现的次数。
这些排序算法在不同场景下有不同的优劣势,选择适当的排序算法可以提高排序效率。
优缺点
下面是一个表格,用于统计八大排序算法的优点和缺点:
排序算法 | 优点 | 缺点 |
---|---|---|
冒泡排序 | 简单易懂,实现简单 | 效率较低,对于大规模数据排序较慢 |
选择排序 | 实现简单,不占用额外空间 | 效率较低,对于大规模数据排序较慢 |
插入排序 | 对于小规模数据排序效率较高,稳定 | 对于大规模数据排序较慢 |
希尔排序 | 相对于简单插入排序,效率较高 | 实现较为复杂,需要选择合适的增量序列 |
归并排序 | 稳定,对于大规模数据排序效率较高 | 需要额外的空间来存储临时数组,实现稍复杂 |
快速排序 | 效率较高,适用于大规模数据排序 | 对于已基本有序的数据,效率较低 |
堆排序 | 效率较高,适用于大规模数据排序 | 需要额外的空间来构建堆,实现稍复杂 |
计数排序 | 稳定,对于数据值分布均匀的情况下,效率较高 | 需要额外的空间来记录元素出现次数,仅适用于非负整数的排序 |
需要注意的是,这些优点和缺点是相对的,对于不同的应用场景和数据特征,排序算法的表现可能会有所不同。因此,在选择排序算法时,需要综合考虑数据规模、数据分布、稳定性要求等因素。
图解链接
排序过程图解
实现
使用 JavaScript 实现八大排序算法的示例代码
- 冒泡排序(Bubble Sort):
冒泡排序(Bubble Sort)是一种简单的排序算法,它的基本思想是通过相邻元素的比较和交换,将最大(或最小)的元素逐渐"冒泡"到序列的末尾(或最前)位置。
冒泡排序的逻辑和原理如下:
-
首先,比较相邻的两个元素,如果前一个元素大于后一个元素,则交换它们的位置,使得较大的元素"冒泡"到右侧。
-
对每一对相邻元素进行同样的操作,依次比较相邻的两个元素,将较大的元素向右移动,直到最后一个元素。
-
针对剩下的未排序部分,重复以上步骤,每一轮都将最大的元素"冒泡"到当前未排序部分的末尾。
-
重复执行上述步骤,直到所有元素都排序完毕。
冒泡排序的过程可以用以下伪代码表示:
冒泡排序(arr):
n = arr.length
for i from 0 to n - 1:
for j from 0 to n - 1 - i:
if arr[j] > arr[j + 1]:
交换 arr[j] 和 arr[j + 1] 的位置
在每一轮内层循环中,相邻的两个元素进行比较,如果前一个元素大于后一个元素,则交换它们的位置。通过多轮的比较和交换,较大的元素逐渐向右移动,最终达到排序的目的。
冒泡排序的特点是简单易懂,但效率相对较低,尤其在处理大规模数据时。它的时间复杂度为 O(n^2),其中 n 是待排序序列的长度。同时,冒泡排序是一种稳定的排序算法,相等元素的相对位置在排序前后不会改变。
js代码实现:
function bubbleSort(arr) {
const len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换位置
}
}
}
return arr;
}
思考:为什么 i的结束是len - 1 而不是len
- 选择排序(Selection Sort):
选择排序(Selection Sort)是一种简单直观的排序算法,其基本思想是每次从未排序的部分中选择最小(或最大)的元素,然后将其放到已排序部分的末尾(或开头)。
选择排序的逻辑和原理如下:
-
首先,找到数组中最小(或最大)的元素,记为当前轮次的最小值(或最大值)。
-
将最小(或最大)值与未排序部分的第一个元素交换位置,将最小值放到已排序部分的末尾(或最大值放到已排序部分的开头)。
-
对剩余未排序的部分重复以上步骤,每一轮选择并交换出一个最小(或最大)值,直到所有元素排序完毕。
选择排序的过程可以用以下伪代码表示:
选择排序(arr):
n = arr.length
for i from 0 to n - 1:
minIndex = i
for j from i + 1 to n - 1:
if arr[j] < arr[minIndex]:
minIndex = j
交换 arr[i] 和 arr[minIndex] 的位置
在每一轮外层循环中,首先假设未排序部分的第一个元素是当前轮次的最小值(或最大值)。然后,通过内层循环遍历未排序部分的剩余元素,如果找到比当前最小值(或最大值)更小(或更大)的元素,更新最小值(或最大值)的索引。
完成内层循环后,将当前轮次的最小值(或最大值)与未排序部分的第一个元素进行交换,将其放到已排序部分的末尾(或开头)。这样,已排序部分逐渐增加,未排序部分逐渐减少,直到所有元素排序完毕。
选择排序的时间复杂度为 O(n^2),其中 n 是待排序序列的长度。它是一种不稳定的排序算法,相等元素的相对位置在排序前后可能会改变。
js代码实现:
function selectionSort(arr) {
const 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;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; // 交换位置
}
return arr;
}
- 插入排序(Insertion Sort):
插入排序(Insertion Sort)是一种简单直观的排序算法,其基本思想是将未排序部分的元素逐个插入到已排序部分的合适位置,从而构建有序序列。
插入排序的逻辑和原理如下:
-
将数组的第一个元素视为已排序部分,其余元素视为未排序部分。
-
从未排序部分选择第一个元素,将其插入到已排序部分的合适位置,使得已排序部分仍然保持有序。
-
重复以上步骤,每次从未排序部分选择一个元素,并将其插入到已排序部分的合适位置,直到所有元素都被插入到已排序部分。
插入排序的过程可以用以下伪代码表示:
插入排序(arr):
n = arr.length
for i from 1 to n - 1:
current = arr[i]
j = i - 1
while j >= 0 and arr[j] > current:
arr[j + 1] = arr[j]
j = j - 1
arr[j + 1] = current
在每一轮外层循环中,从未排序部分选择一个元素,记为 current
。然后,通过内层循环从已排序部分的末尾开始向前遍历,将比 current
大的元素向后移动一个位置,为 current
找到合适的插入位置。
内层循环会一直进行,直到找到 current
的正确插入位置或者已经遍历到已排序部分的开头。
最后,将 current
插入到正确的位置上,即 j + 1
的位置,这样已排序部分的长度增加了一。
重复执行以上步骤,每次选择并插入一个元素,直到所有元素都被插入到已排序部分。
插入排序的时间复杂度为 O(n^2),其中 n 是待排序序列的长度。它是一种稳定的排序算法,相等元素的相对位置在排序前后不会改变。
js代码实现:
function insertionSort(arr) {
const len = arr.length;
for (let i = 1; i < len; i++) {
let current = arr[i];
let j = i - 1;
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
return arr;
}
- 希尔排序(Shell Sort):
希尔排序(Shell Sort),也称为缩小增量排序,是插入排序的一种改进算法。它通过将数组分割成多个子序列来进行排序,并逐步缩小子序列的间隔,最终将整个数组变为有序。
希尔排序的逻辑和原理如下:
-
首先,选择一个增量(间隔)序列,通常是按照一定规则确定的,比如初始增量为数组长度的一半,并逐步缩小增量。
-
根据选定的增量,将数组划分为多个子序列,每个子序列包含相隔增量个位置的元素。
-
对每个子序列应用插入排序,使得每个子序列都变得部分有序。
-
缩小增量,重复步骤 2 和步骤 3,直到最后增量为 1,此时进行最后一次插入排序,将整个数组排序。
希尔排序的过程可以用以下伪代码表示:
希尔排序(arr):
n = arr.length
增量 = n / 2
while 增量 > 0:
for i from 增量 to n - 1:
temp = arr[i]
j = i
while j >= 增量 and arr[j - 增量] > temp:
arr[j] = arr[j - 增量]
j = j - 增量
arr[j] = temp
增量 = 增量 / 2
在每一轮外层循环中,通过增量将数组划分为多个子序列,然后对每个子序列应用插入排序。内层循环使用插入排序算法,逐个将未排序部分的元素插入到已排序部分的正确位置上。
重复执行以上步骤,不断缩小增量直到增量为 1,最后进行一次完整的插入排序,将整个数组排序。
希尔排序的时间复杂度取决于增量序列的选择,但最坏情况下为 O(n^2),其中 n 是待排序序列的长度。希尔排序是一种不稳定的排序算法,相等元素的相对位置在排序前后可能会改变。
js代码实现:
function shellSort(arr) {
const len = arr.length;
let gap = Math.floor(len / 2);
while (gap > 0) {
for (let i = gap; i < len; i++) {
let current = arr[i];
let j = i - gap;
while (j >= 0 && arr[j] > current) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = current;
}
gap = Math.floor(gap / 2);
}
return arr;
}
- 归并排序(Merge Sort):
归并排序(Merge Sort)是一种基于分治法的排序算法,其核心思想是将待排序的序列不断拆分为较小的子序列,然后将这些子序列逐步合并为有序序列,最终得到完全有序的序列。
归并排序的逻辑和原理如下:
-
首先,将待排序的序列均匀地分割为两个子序列,直到每个子序列只剩一个元素(认为单个元素已经是有序的)。
-
逐步将相邻的子序列进行合并,合并时比较两个子序列的首个元素,将较小(或较大)的元素放入临时数组中。
-
继续合并较小的子序列,直到所有的子序列合并为一个完整的有序序列。
归并排序的过程可以用以下伪代码表示:
归并排序(arr):
if arr.length <= 1:
return arr
middle = arr.length / 2
left = 归并排序(arr[0:middle])
right = 归并排序(arr[middle:])
return 合并(left, right)
合并(left, right):
result = []
while left.length > 0 and right.length > 0:
if left[0] <= right[0]:
result.append(left[0])
left = left[1:]
else:
result.append(right[0])
right = right[1:]
if left.length > 0:
result.append(left)
if right.length > 0:
result.append(right)
return result
在归并排序中,首先将待排序的序列不断拆分为两个子序列,然后分别对这两个子序列进行递归地归并排序。
在合并操作中,通过比较两个子序列的首个元素,将较小(或较大)的元素放入结果数组中,并将相应子序列的索引后移。当一个子序列的所有元素都被放入结果数组后,将另一个子序列中剩余的元素直接追加到结果数组中。
重复执行以上步骤,直到所有子序列合并为一个完整的有序序列。
归并排序的时间复杂度为 O(nlogn),其中 n 是待排序序列的长度。归并排序是一种稳定的排序算法,相等元素的相对位置在排序前后不会改变。
js代码实现:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const merged = [];
let i = 0,
j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
merged.push(left[i]);
i++;
} else {
merged.push(right[j]);
j++;
}
}
while (i < left.length) {
merged.push(left[i]);
i++;
}
while (j < right.length) {
merged.push(right[j]);
j++;
}
return merged;
}
6.快速排序(Quick Sort):
js代码实现:
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (i === Math.floor(arr.length / 2)) {
continue;
}
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
- 堆排序(Heap Sort):
快速排序(Quick Sort)是一种常用的排序算法,它使用分治法来对待排序的序列进行排序。快速排序的核心思想是选择一个基准元素,将序列中小于基准的元素放在基准的左边,大于基准的元素放在基准的右边,然后对左右两个子序列分别进行递归排序,最终完成整个序列的排序。
快速排序的逻辑和原理如下:
-
选择一个基准元素,通常是待排序序列的第一个元素或者随机选择一个元素。
-
将序列中小于基准的元素放在基准的左边,大于基准的元素放在基准的右边,使得基准元素所在的位置最终确定。
-
对基准元素左边的子序列和右边的子序列分别进行递归快速排序。
-
重复以上步骤,直到每个子序列只剩下一个元素,此时整个序列排序完成。
快速排序的过程可以用以下伪代码表示:
快速排序(arr, left, right):
if left < right:
pivotIndex = 分区(arr, left, right)
快速排序(arr, left, pivotIndex - 1)
快速排序(arr, pivotIndex + 1, right)
分区(arr, left, right):
pivot = arr[left] // 选择第一个元素作为基准
i = left + 1
j = right
while True:
while i <= j and arr[i] < pivot:
i = i + 1
while i <= j and arr[j] > pivot:
j = j - 1
if i > j:
break
交换 arr[i] 和 arr[j] 的位置
交换 arr[left] 和 arr[j] 的位置
return j
在快速排序中,首先选择一个基准元素,然后通过分区操作将小于基准的元素放在基准的左边,大于基准的元素放在基准的右边。
分区操作使用两个指针 i
和 j
分别从左边和右边向中间移动,寻找需要交换的元素对。当找到需要交换的元素对时,交换它们的位置。
重复进行分区操作,直到左右指针相遇,此时基准元素所在的位置确定。然后对基准元素左边的子序列和右边的子序列分别进行递归快速排序,最终完成整个序列的排序。
快速排序的时间复杂度取决于划分的平衡性,平均情况下为 O(nlogn),最坏情况下为 O(n^2),其中 n 是待排序序列的长度。快速排序是一
种不稳定的排序算法,相等元素的相对位置在排序前后可能会改变。
js代码实现:
function heapSort(arr) {
const len = arr.length;
// 构建最大堆
for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
heapify(arr, len, i);
}
// 依次取出堆顶元素,并调整堆
for (let i = len - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]]; // 将堆顶元素与末尾元素交换
heapify(arr, i, 0);
}
return arr;
}
function heapify(arr, len, i) {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]]; // 交换位置
heapify(arr, len, largest);
}
}
- 计数排序(Counting Sort):
计数排序(Counting Sort)是一种线性时间复杂度的排序算法,适用于待排序序列的取值范围较小且已知的情况。计数排序的核心思想是统计每个元素出现的次数,然后根据统计信息将元素放回到正确的位置上,从而实现排序。
计数排序的逻辑和原理如下:
-
首先,统计待排序序列中每个元素的出现次数,并将统计结果存储在一个辅助数组中。
-
根据统计结果,计算每个元素在排序后的序列中的位置信息。位置信息可以通过累加前面元素的出现次数得到。
-
创建一个与待排序序列长度相同的结果数组,用于存储排序后的序列。
-
遍历待排序序列,根据统计结果将每个元素放入结果数组的正确位置上。
计数排序的过程可以用以下伪代码表示:
计数排序(arr):
n = arr.length
maxVal = 找出序列中的最大值
count = new Array(maxVal + 1)
result = new Array(n)
// 统计每个元素的出现次数
for i from 0 to n - 1:
count[arr[i]] = count[arr[i]] + 1
// 计算每个元素在排序后序列中的位置信息
for i from 1 to maxVal:
count[i] = count[i] + count[i - 1]
// 将元素放回到正确的位置上
for i from n - 1 to 0:
result[count[arr[i]] - 1] = arr[i]
count[arr[i]] = count[arr[i]] - 1
return result
在计数排序中,首先找出待排序序列中的最大值 maxVal
,根据 maxVal
创建一个辅助数组 count
,用于统计每个元素的出现次数。
接下来,通过遍历 count
数组,累加前面元素的出现次数,计算每个元素在排序后序列中的位置信息。
然后,创建一个与待排序序列长度相同的结果数组 result
,用于存储排序后的序列。
最后,从待排序序列的末尾开始遍历,根据统计结果将每个元素放入结果数组的正确位置上,并更新统计数组的计数信息。
计数排序的时间复杂度为 O(n+k),其中 n 是待排序序列的长度,k 是序列中元素的取值范围。计数排序是一种稳定的排序算法,相等元素的相对位置在排序前后不会改变。但需要注意,计数排序对于取值范围较大的情况,会占用较大的内存空间。
js代码实现:
function countingSort(arr) {
const len = arr.length;
if (len <= 1) {
return arr;
}
const max = Math.max(...arr);
const countArr = new Array(max + 1).fill(0);
const sortedArr = [];
for (let i = 0; i < len; i++) {
countArr[arr[i]]++;
}
for (let i = 0; i < countArr.length; i++) {
while (countArr[i] > 0) {
sortedArr.push(i);
countArr[i]--;
}
}
return sortedArr;
}
这些示例代码展示了如何使用 JavaScript 实现八大排序算法。你可以使用这些代码进行测试和验证,并观察排序算法的执行过程。
补充:
- 插入排序的时间复杂度是O(n2),所以不仅仅对小规模数据效率高,对于接近有序的大规模数据也有较高效率,因为插入元素所需移动的次数少。
- 希尔排序的时间复杂度是O(nlogn),实际效率可能高于简单插入排序,需要选择一个合适的增量序列才能发挥出高效率,增量序列的选择比较关键。
- 归并排序的空间复杂度是O(n),需要额外空间的同时也产生了额外的数组拷贝操作,实际效率略低于表格所说的“效率较高”。
- 快速排序的时间复杂度是O(nlogn),但是选择枢轴的不恰当可能导致极端时间复杂度变为O(n2),所以对近乎有序的数据效率就会变低,这一点与表格描述的“对于已基本有序的数据,效率较低”有些差异。
- 堆排序更准确的空间复杂度应该是O(1),因为除了排序使用的堆结构外不需要其他额外空间,所以不需要像表格所说的“需要额外的空间”。
- 计数排序的时间复杂度和空间复杂度均为O(n+k),k为数据值域大小,所以更准确的描述应该是“当k为O(n)时,时间和空间复杂度均为O(n)”。
图解链接
排序过程图解