JavaScript 来尝试实现一遍
排序很重要,基本很多地方都需要有它的存在,找什么第几个大的啦,将数据升序降序等等事情。
排序算法的分类
-
内部排序算法,是应用于少量数据的排序算法。一般仅使用于主存储器,包括冒泡排序、插入排序和快速排序。
-
外部排序算法,是可以应用于海量数据的排序算法。因此,可以使用于诸如硬盘驱动器和闪存盘之类的外部存储设备。归并排序是外部排序算法。
排序常用到的思路
-
递归/迭代
-
分治法
冒泡排序 --- Bubble sort
一言简之:冒泡排序的任务就是每次大循环都能实现将最小(大)的数提到最前面来,每个大循环内的小循环就是不断的比较和交换;
const bubbleSort = (arr) => {
for (let i = 0; i < arr.length; ++i) {
for (let j = i + 1; j < arr.length; ++j) {
if (arr[i] > arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
console.log(arr);
}
};
最坏和平均的时间复杂度都是 O(n^2)
,最佳时间复杂度是 O(n)
,空间复杂度因为冒泡排序是内部排序的关系,不需要额外的空间,其复杂度为 O(1)
。
插入排序 --- Insertion sort
插入排序,将要排序的数组分为两部分,已经排好序的,还没排序的;不断将未排序数组元素插入到已经排序数组中。
说人话,今天体育老师要排班级的队列,他将学生分成了两部分,一部分是排好的队伍,一个是散乱的一堆调皮学生;他预选在排好的队伍放进了一个最乖的学生,之后,他次次都去那堆无序散乱的人群中拉出一个调皮捣蛋的学生,将这个学生拉去和排好的队伍做身高比较,给他选个好位置,嘱咐他别随便乱动,不断重复这个行为直至把所有的学生都排进队里,就可以愉快地下课摸鱼了!
const insertionSort = (arr) => {
for (let i = 1; i < arr.length; i++) {
for (let j = i - 1; j > -1; j--) {
if (arr[j + 1] < arr[j]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
}
我的理解中,插入排序和冒泡排序有很多相似的地方,都有相邻位的比较以及元素的值交换,因此插入排序的时间复杂度和空间复杂度和冒泡排序的一样。其空间复杂度为 O(1)
,最坏和平均时间复杂度为 O(n^2)
,最佳时间复杂度为 O(n)
。
选择排序 --- Selection sort
罢了罢了,直接说胡话吧;体育老师今天又有个任务要帮个班级排队伍,毕竟出了名的排队能手;体育老师今天换种方式排队列,直接说了会靠目测一次次把人群中最矮(高)的人群拉出来,不断重复直至无人可拉,之后他就下班打游戏去了!
const selectionSort = (arr) => {
let minIndex;
for (let i = 0; i < arr.length; i++) {
minIndex = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j])
minIndex = j;
}
if (minIndex !== i) {
[arr[minIndex], arr[i]] = [arr[i], arr[minIndex]];
}
}
}
呀,不得不说,重新认识排序,更感觉冒泡排序,插入排序,选择排序基本需要两个操作(比较,交换);而选择排序的时间复杂度和空间复杂度也和上面两个一样的;其空间复杂度为 O(1)
,最坏和平均时间复杂度为 O(n^2)
,最佳时间复杂度为 O(n)
。
合并排序 --- Merge sort
合并排序开始就采用一种新的思路解决,就是分治法,将问题分成小块部分逐一解决;归并排序的主要概念就是对长度为 1 的数组进行排序。 因此,任务在于将数组拆分为大小为 1 的子数组,然后适当地合并它们,以便得出已排序的数组。
思路:
-
将数组元素拆分为单个元素。
-
比较各个元素并按顺序排列它们。 这会产生长度为 1 或 2 的子数组。
-
创建一个空数组。
-
比较子数组的元素并将较小的值推送到空数组。
-
如果您已从其中一个数组中提取了所有值,请将剩余的数组推送到新数组。
-
继续直到所有子数组都被覆盖并且您有一个已排序的数组。
const merge = (arr1, arr2) => {
let mergeArr = [];
if (arr1.length > 1) {
let minIndex;
for (let i = 0; i < arr1.length; i++) {
minIndex = i;
for (let j = i + 1; j < arr1.length; j++) {
if (arr1[minIndex] > arr1[j])
minIndex = j;
}
if (minIndex !== i) {
[arr1[minIndex], arr1[i]] = [arr1[i], arr1[minIndex]];
}
}
}
if (arr2.length > 1) {
let minIndex;
for (let i = 0; i < arr2.length; i++) {
minIndex = i;
for (let j = i + 1; j < arr2.length; j++) {
if (arr2[minIndex] > arr2[j])
minIndex = j;
}
if (minIndex !== i) {
[arr2[minIndex], arr2[i]] = [arr2[i], arr2[minIndex]];
}
}
}
let i = 0,
j = 0;
while (i < arr1.length && j < arr2.length) {
if (arr1[i] < arr2[j]) {
mergeArr.push(arr1[i++]);
} else
mergeArr.push(arr2[j++]);
}
while (i < arr1.length)
mergeArr.push(arr1[i++]);
while (j < arr2.length)
mergeArr.push(arr2[j++]);
return mergeArr;
}
const mergeSort = (arr) => {
if (arr.length <= 1)
return arr;
let mid = Math.ceil(arr.length / 2);
let arr1 = arr.slice(0, mid);
let arr2 = arr.slice(mid);
let arr1_subarrays = [],
sorted_arr1_subarrays = [];
let arr2_subarrays = [],
sorted_arr2_subarrays = [];
for (let i = 0; i < arr1.length; i += 2) {
arr1_subarrays.push(arr1.slice(i, i + 2));
}
for (let i = 0; i < arr2.length; i += 2) {
arr2_subarrays.push(arr2.slice(i, i + 2));
}
for (let i = 0; i < arr1_subarrays.length; i += 2) {
let result = merge(arr1_subarrays[i], arr1_subarrays[i + 1]);
result.forEach((value) => sorted_arr1_subarrays.push(value));
}
for (let i = 0; i < arr2_subarrays.length; i += 2) {
let result = merge(arr2_subarrays[i], arr2_subarrays[i + 1]);
result.forEach((value) => sorted_arr2_subarrays.push(value));
}
let result = merge(sorted_arr1_subarrays, sorted_arr2_subarrays);
return result;
}
此处的分治法,将数组先分为每两个为一组,之后将相邻数组合并起来,最终合并为一个大数组。有用到二分的思想,该算法的空间复杂度因为需要额外申请一个新数组存储中间数据,所以为 O(n)
;其最坏,最好和平均时间复杂度都为 O(n*logn)
。
快速排序 --- QuickSort
快速排序也应用了分治的思想;它的工作原理是有一个枢轴元素,它左边的元素小于它,右边的元素大于它;枢轴元素可以是数组中的任何元素。
分治思路:
-
选择一个枢轴元素。
-
将数组拆分为两个数组,左侧小于枢轴元素,右侧大于枢轴元素。
-
递归地执行上述步骤,直到我们有长度为 1 的子数组。组合子数组以产生一个已排序的数组。
const partition = (arr, left, right) => {
let pivot = arr[Math.floor(left + (right - left) / 2)],
i = left,
j = right;
while (i <= j) {
while (arr[i] < pivot)
i++;
while (arr[j] > pivot)
j--;
if (i <= j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
j--;
}
}
return i;
}
const quickSort = (arr, left, right) => {
let index;
if (arr.length > 1) {
index = partition(arr, left, right);
if (left < index - 1)
quickSort(arr, left, index - 1);
if (right > index)
quickSort(arr, index, right);
}
return arr;
}
其最坏时间复杂度是 O(n^2)
, 而其平均和最好时间复杂度以及空间复杂度都是 O(n*long)
,因为这里主要运用到了二分的套路,不断将问题拆半进行解决。
总结
JavaScript 的 sort() 方法默认使用插入排序。 这意味着在对大型数据集进行排序时不合适。 在处理大数据集时,应该考虑其他排序算法,例如归并排序,且通常,基于分而治之的排序算法会相较快些。
开发运用到排序时可以根据数据集、所需时间和可用空间找出要使用的合适排序算法。