javascript 排序算法实现
冒泡排序
- 冒泡排序的实现思路就是将第i个元素和i+1上对应的元素进行比较,如果第一个arr[i] 大于arr[i+1]就交换他们的位置
- 冒泡排序会使用双层循环,第一层循环用于界定比较的范围,每比较完一轮,最后一个元素一定是arr中的最大值,所以在下一轮比较中就可以不考虑第n位,比较的范围就变成了0到n-1,第三次的范围就变成了0到n-2,以此类推经过n次比较第0位的数就只arr中的最小值,第二层循环用于比较排序
- 关于算法时间复杂度,冒泡排序的算法时间复杂度是O(n^2) ~ 实现:
// 交换方法
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function bubbleSort(arr){
if(arr === null || arr.length < 2) return;
for(let end = arr.length - 1; end > 0; end--){
for(let i = 0; i < end; i++){
if(arr[i] > arr[i+1]) {
swap(arr, i, i+1)
}
}
}
}
选择排序
- 选择排序的实现思路就是每次都去找到一个最小值放在当前位置上,例如,在0
n-1范围内找到一个最小值放在位置0上, 接下来在1n-1范围内找到一个最小值放在1位置上,在 2n-1范围内找到最小值放在2位置上,至此02位置上的值已经排好序了 - 选择排序也需要使用双层循环,第一层同样是控制排序的范围,因为我们已排好序的部分就不用考虑了,第二层巡皇就负责找到最小值的索引下标,实现选择排序的过程中我们需要定义一个标记最下值位置的变量。
- 关于选择排序的算法时间复杂度是O(n^2)
! 选择排序和冒泡排序算法时间复杂度都是O(n^2), 但是选择排序是一种不稳定的排序算法,比如给定一个序列6 9 6 0 8, 在第一趟比较时我们会找到0与第一个6交换,在接下来的比较过程中,第一个6和第二个6的原序就被打乱了, 选择排序的最好和最坏算法时间复杂度都是O(n^2)
实现~
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function selectSort(arr) {
if(arr === null || arr.length < 2) return;
for(let i = 0; i < arr.length - 1; i++){
let minIndex = i;
for(let j = 0; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex)
}
}
插入排序
- 将 i + 1位上的数与i位上的数进行比较,如果arr[i+1] 小于i位上的数就交换
- 同理插入排序也是双层循环的,外层循环控制排序范围,内层循环负责比较交换
- 插入排序的算法时间复杂度平均复杂度是O(n^2),最坏时间复杂度也就是需要比较 1 + 2 + 3 + ... + (N - 1)次等差数列求和N^2 / 2,此时的时间复杂度是O(n^2),最好时间复杂度就。是给定的数组是有序的,排每插入一个元素,只需要考查前一个元素,此时的时间复杂度是O(n)
! 插入排序与选择排序和冒泡排序算法复杂度虽然都是O(n^2),但是插入排序与数据状况是有关系的,数据有序的情况下,算法时间复杂度就低。但是冒泡排序与数据状况是没有关系的,
~实现
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function insertSort(arr){
if(arr === null || arr.length < 2) return;
for(let i = 1; i < arr.length; i++){
for(let j = i - 1; j > 0 && arr[j] > arr[j+1]; j--) {
swap(arr, j, j+1);
}
}
}
归并排序
归并排序算法实现的思路是:
- 找到给定目标数组arr的中间位置索引,将数据样本分割为两个部分进行排序,记中间位置索引为mid
- 将分割好的数组分别进行排序,确保左右两边的数组都是有序的,这样就可以使用外排的方式为数组排序
- 模拟C语言中的'指针'对左右两边的数组进行外排,
- 准备一个辅助数组,用于存放左右两侧数组外排的结果
- 判断左右两个数组外派过程中的边界条件,某一个数组先完成一轮比较,就将另外一个数组的数据拷贝到辅助数组中即可
- 最后将辅助数组中的数据拷贝到目标数组arr中,归并排序将就完成了
如果你觉得难以理解,可以举个例子:
// 目标数组
let arr = [5,3,7,4,9,1]
// 找到中间索引位置
let mid = (0 + 5) / 2 = 2;
// 所以将数组分割为两个部分[5,3,7]和[4,9,1],然后将这两个数组排序
let arrLeft = [3,5,7]
let arrRight = [1,4,9]
// 准备一个辅助数组, 数组长度为arr.length
let helpArr = new Array(6);
// 令p1指向左侧数组的起始位置,p2指向右侧数组的起始位置,然后开始比较p1和p2索引所对应元素的值
/**
一下是归并排序的过程:
* 刚开始p1指向3,p2指向1, 此时arr[p1] > arr[p2] 将arr[p2]存到辅助数组中[1];
* 然后p1指向不变, p2++, p2指向4, 此时arr[p1] < arr[p2] 将arr[p1]存到辅助数组中,此时辅助数组为[1,3];
* 接下来p2指向不变, p1++, p1指向5, p2指向4, arr[p1] > arr[p2] ,将arr[p2]存到辅助数组中[1, 3, 4];
* 接下来p1指向不变,p2++, p1指向5, p2指向9, arr[p1] < arr[p2], 将arr[p1]存到辅助数组中[1,3,4,5];
* 接下来p1不变, p2++, 此时p2越界了,直接将未越界的左侧数组拷贝到helpArr中[1,3,4,5,7]
*/
~现在先用代码完整的实现一下归并排序,然后再进行归并排序的复杂度分析
function mergeSort(arr) {
if(arr === nnull || arr.length < 2) return;
mergeProcess(arr, 0, arr.length-1);
}
/**
* @param {*number} L 排序起始位置
* @param {*number} R 排序终止位置
*/
function mergeProcess(arr, L, R) {
// 停止递归的条件是 L = R
if(L === R) return;
// 中间位置索引
let mid = (L + R) / 2;
// 左侧数组排序
mergeProcess(arr, L, mid);
// 右侧数组排序
mergeProcess(arr, mid + 1, R);
// 排序过程的具体实现
merge(arr, L, mid, R);
}
function merge(arr, L, mid, R) {
// 创建一个长度为R-L + 1的辅助数组
let helpArr = new Array(R - L + 1);
let i = 0;
// 左侧数组起始位置
let p1 = L;
// 右侧数组起始位置
let p2 = mid + 1;
while(p1 <= mid && p2 <= R) {
helpArr[i++] = arr[p1++] < arr[p2++] ? arr[p1++] : arr[p2++];
}
// 将没有越界的数组中剩余的数据拷贝到辅助数组中
while(p1 <= mid) {
// 右侧越界
helpArr[i++] = arr[p1++];
}
while(p2 <= R) {
// 左侧越界
helpArr[i++] = arr[p2++]
}
// 将辅助数组中的数据拷贝到arr中
for(let j = 0; j < helpArr.length; j++) {
arr[L+j] = helpArr[j]
}
}
归并排序算法复杂度分析
- 归并排序将总的数据样本量N划分我两部分,左侧和右侧各N/2, 加上最后将辅助数组拷贝到原数组的复杂度O(N), 总的算法复杂度为 T(N) = 2T(N/2) + O(N);
- 归并排序算法使用了递归短发,所以尝试使用递归算法的算法复杂度分析方法来进行分析,由于递归时两个子过程的划分是一致的,所以可以用master公式来分析 T(N) = aT(b*(N/2)) + O(N^d);递归算法中a=2, b = 1, d = 1, log(b, a) = d 所以归并排序的复杂度为 T(N) = N*logN;
! 归并排序的时间复杂度O(N*lonN)要由于复杂度为O(N^2)的算法