目录
- 排序分类
- 交换排序 - 冒泡排序
- 交换排序 - 快速排序
- 归并排序
- 快排和归并区别
- 十大基本排序
零、排序分类
一、 交换排序 - 冒泡排序
把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置;当一个元素小于或者等于右侧相邻元素时,位置不变
function bubbleSort(list:Array<number>) {
// 注意 这里是 < length - 1 也就是说遍历次数为 n-1
for (let i = 0; i < list.length - 1; i++) {
// 这里 < length -1的原因同上 - i的原因是 有序区
for (let j = 0; j < list.length - i -1; j++) {
if( list[j] > list[j+1]) {
let temp = list[j];
list[j] = list[j+1];
list[j+1] = temp
}
}
}
return list
}
1.1) 冒泡排序优化
**改进冒泡排序: **设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
// 是否有序标识优化
function bubbleSortTwo(list: Array<number>) {
// 注意 这里是 < length - 1 也就是说遍历次数为 n-1
for (let i = 0; i < list.length - 1; i++) {
let isSorted = true; // 有序标识 每一轮都为true
for (let j = 0; j < list.length - i - 1; j++) {
if (list[j] > list[j + 1]) {
let temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
// 如果有交换发生,则不是有序的
isSorted = false;
}
}
if (isSorted) {
break;
}
}
return list;
}
function bubbleSort2(arr) {
console.time('改进后冒泡排序耗时');
var i = arr.length-1; //初始时,最后位置保持不变
while ( i> 0) {
var pos= 0; //每趟开始时,无记录交换
for (var j= 0; j< i; j++)
if (arr[j]> arr[j+1]) {
pos= j; //记录交换的位置
var tmp = arr[j]; arr[j]=arr[j+1];arr[j+1]=tmp;
}
i= pos; //为下一趟排序作准备
}
console.timeEnd('改进后冒泡排序耗时');
return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort2(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
1.2) 冒泡排序性能分析
大小相同的元素没有交换位置,所以冒泡排序是稳定的。
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
二、交换排序 - 快速排序
它的思路:首先在未排序的序列中找到最小或者最大的元素,放到排序序列的起始位置,然后再从未排序的序列中继继续寻找最小或者最大元素,然后放到已经排序序列的末尾。以此类推,直到所有元素排序完毕。
来看一个例子,用选择排序的方式排序数组[2,5,4,1,3]
。
- 第一趟,找到数组中最小的元素1,将它和数组的第一个元素交换位置
- 第二趟,在未排序的元素中找到最小的元素2,和数组的第二个元素交换位置。
选择排序2
- 第三趟,在未排序的元素中找到最小的元素3,和数组的第三个元素交换位置。
选择排序-3
- 第四趟,在未排序的元素中找到最小的元素4,和数组的第四个元素交换位置。
选择排序4
那么到这,我们的数组就是有序的了。
选择排序5
2.1) 选择排序代码实现
选择排序的思路很简单,实现起来也不难。
const quickSort1 = arr => {
if (arr.length <= 1) {
return arr;
}
//取基准点
const midIndex = Math.floor(arr.length / 2);
//取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
const valArr = arr.splice(midIndex, 1);
const midIndexVal = valArr[0];
const left = []; //存放比基准点小的数组
const right = []; //存放比基准点大的数组
//遍历数组,进行判断分配
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midIndexVal) {
left.push(arr[i]); //比基准点小的放在左边数组
} else {
right.push(arr[i]); //比基准点大的放在右边数组
}
}
//递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
return quickSort1(left).concat(midIndexVal, quickSort1(right));
};
const array2 = [5, 4, 3, 2, 1];
console.log('quickSort1 ', quickSort1(array2));
// quickSort1: [1, 2, 3, 4, 5]
2.2) 选择排序性能分析
选择排序稳定吗?
答案是不稳定的,因为在未排序序列中找到最小值之后,和排序序列的末尾元素交换。
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一。
思想
- 先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。
- 左右分别用一个空数组去存储比较后的数据。
- 最后递归执行上述操作,直到数组长度 <= 1;
特点:快速,常用。
缺点:需要另外声明两个数组,浪费了内存空间资源。
三、归并排序
思想
排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
归并排序采用的是分治思想
。
分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
四、快排和归并区别
快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ?
可以发现:
- 归并排序的处理过程是
由下而上
的,先处理子问题,然后再合并。 - 而快排正好相反,它的处理过程是
由上而下
的,先分区,然后再处理子问题。 - 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。
- 归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。
- 快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。
参考
总结
- 排序分为: 比较类排序 与 非比较类排序
- 把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置;当一个元素小于或者等于右侧相邻元素时,位置不变
- 归并排序采用的是
分治思想
。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。