JS手撕(十一) 选择排序、快速排序


JS手撕(十一) 选择排序、快速排序

选择排序

原理

选择排序原理就是每次从未排序序列中选择最小元素,放到已排序序列的末尾。

那么如何选择最小元素,并把最小元素放到已排序序列的末尾?

只需要遍历寻找最小的数,并保存最小数的索引。遍历完之后,让最小数和已排序序列的末尾互换位置即可。

图片来自菜鸟教程

JS实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function selectSort(arr) {
const len = arr.length;
let minIndex; // 保存最小数的索引

for (let i = 0; i < len - 1; i++) { // 排好前n-1个,第n个数就是剩下的最大的。
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;
}

测试:

1
2
3
4
const selectSort = require('./sort.js');

let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 26, 4, 19, 50, 48];
console.log(selectSort(arr))

性能

时间复杂度:O(n²)        

排序稳定性:不稳定

这里第一次遇到不稳定的排序。稍微举例子说明一下为什么是不稳定的。

上面一开始2*是在2之后的,排序完之后2*变成在2之前了,所以选择排序是不稳定的。

它是不稳定的关键就是让最小数和已排序序列的末尾互换位置时,可能把大小相同的数中在前面的移动到了后面去。

快速排序

原理

快速排序原理就是:

  1. 从数组中挑出一个元素,称为基准(pivot)。

  2. 将所有比基准值小的放在基准前面,所有比基准值大的放在放在基准后面。该操作称为分区操作(partition)

  3. 递归地把小于基准值地子序列和大于基准值地子序列排序

图片来自菜鸟教程

JS实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function quickSort(arr, l, r) {
if (l >= r) {
return;
}

const partionIndex = partition(arr, l, r);

// 递归排序小于基准值的子数列
quickSort(arr, l, partionIndex - 1);

// 递归排序大于基准值的子数列
quickSort(arr, partionIndex + 1, r);

return arr;
}

function partition(arr, l, r) {
let pivot = l;
let index = l + 1;

for (let i = index; i <= r; i++) {
if (arr[i] < arr[pivot]) {
// 把小于标杆的移到左边
[arr[index], arr[i]] = [arr[i], arr[index]];
index++;
}
}

// 标杆归位
[arr[index - 1], arr[pivot]] = [arr[pivot], arr[index - 1]];

// 返回标杆的位置
return index - 1;
}

测试:

优化

快速排序最坏的情况是初始序列已经有序,第一趟排序经过n-1次比较后,第一个元素仍然在原来的位置,第二趟经过n-2次比较厚,也还在原来的位置。依此类推,最后的总比较次数是(n-1) + (n-2) + ... + 1 = n(n-1)/2。即最坏时间复杂度是O(n²)

至于如何改进,那就是随机取基准。因为上面是一直取第一位为基准,所以就导致了初始序列有序的情况下时间复杂度是O(n²)。而随机取基准就能解决这种情况。

修改起来也很简单,只需要将partition函数那里的pivot修改成取随机数即可,不过还需要将arr[pivot]arr[l]切换位置,并将pivot设置为l。因为整个算法的逻辑都是按第一位是基准来写的,所以还用之前的逻辑的话,只能随机取值,并把它换到第一位。

1
2
3
4
5
6
// partition函数开始部分
// [l, r]区间取随机数,注意是闭区间
let pivot = Math.floor(Math.random() * (r - l) + l);

[arr[l], arr[pivot]] = [arr[pivot], arr[l]];
pivot = l;

JS特殊实现

主要利用concat方法能用来合并数组,所以使用concat搭配递归调用就能很方便的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function quickSort(arr) {
if (arr.length <= 1) {
// 递归退出条件
return arr;
}

// 随机取基准索引
const pivotIndex = Math.floor(Math.random() * (arr.length - 1));

// 基准值
const pivot = arr.splice(pivotIndex, 1)[0];

const left = [];
const right = [];

for (const item of arr) {
if (item < pivot) {
// 小于基准值的放在基准值左边
left.push(item);
} else {
right.push(item);
}
}

// 递归+concat实现子数组排序
return quickSort(left).concat(pivot, quickSort(right));
}

性能

时间复杂度:O(nlogn)        

排序稳定性:不稳定。因为比基准值小的时候,需要换到基准值的左边,这里会引起相同值的相对位置的变换,所以快速排序是不稳定的。


文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录