以此笔记记录冒泡排序、插入排序、归并排序、快速排序的实现代码,供自己以后翻阅
- 冒泡排序:稳定排序;原地排序,空间复杂度O(1),平均时间复杂度O(n^2)
function mpSort(arr) {
for (let i=0;i<arr.length;i++) {
//设立标志,当此次循环需要冒泡时,flag会置为true,否则提前跳出循环
let flag = false;
for (let j=0;j<arr.length - 1 - i;j++) {
//当前元素大于后面的元素的时候,交换位置
if(arr[j]>arr[j+1]) {
let temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true
}
}
if(!flag) {
break
}
}
return arr
}
- 插入排序:稳定排序;原地排序,空间复杂度O(1),平均时间复杂度O(n^2)
function crSort(arr) {
//我们分成两个半区,左边的是排好序的,右边是待排序的
for(let i=1;i<arr.length;i++) {
//我们取待排序区间的第一个值从右往左依次和排序区间中的每个元素作比较
let value = arr[i];
let j = i - 1;
for(;j >= 0;j--) {
//比较值大的情况,就原地交换位置
if(value < arr[j]) {
arr[j + 1] = arr[j]
} else {
break
}
}
arr[j + 1] = value;
}
return arr
}
- 归并排序:稳定排序;非原地排序,空间复杂度O(n),平均时间复杂度O(nlogn);分治思想,递归方法
function mergeSort(arr) {
//当数组分割为只有一个元素的时候直接返回该数组
if(arr.length < 2) {
return arr
}
//分割数组
let mid = Math.floor(arr.length / 2);
//slice方法返回从第一个参数到第二个参数之间这个长度的数组(不包含mid)
let leftArr = arr.slice(0,mid);
let rightArr = arr.slice(mid);
//不断递归,分割数组,同时进行排序
return mergeSort_s(mergeSort(leftArr),mergeSort(rightArr));
}
//合并方法
function mergeSort_s(leftArr,rightArr) {
let result = [];
//从前往后对比两个数组,谁小就推入谁,然后删除该元素,数组长度-1
while(leftArr.length && rightArr.length) {
if(leftArr[0] > rightArr[0]) {
//shift()方法返回数组的第一个元素,同时删除原始数组中该元素
result.push(leftArr.shift())
} else {
result.push(rightArr.shift())
}
}
//经过上面比较之后,会有一个数组清空了,那么直接将剩下一个数组的所有元素依次推入result
while(leftArr.length > 0) {
result.push(leftArr.shift())
}
while(rightArr.length > 0) {
result.push(rightArr.shift())
}
return result
}
- 快速排序:不稳定排序;原地排序,空间复杂度O(1),平均时间复杂度O(nlogn);分治思想,递归方法
PS:快速排序思路一致,但有几种写法,这里写一个写法最好理解的。缺点是需要单独开辟空间,没有做到原地排序。
写法一:非原地排序
function quickSort(arr) {
if(arr.length < 2) {
return arr
}
let mid = Math.floor(arr.length / 2);
let leftArr = [];
let rightArr = [];
//取数组中间的值为基准点,将它直接从数组中取出来
let pivot = arr.splice(mid,1)[0];
for (let i = 0;i < arr.length;i++) {
if(arr[i] > pivot) {
rightArr.push(arr[i])
} else {
leftArr.push(arr[i])
}
}
//对两边数组不断递归,最后合并两个数组,同时不要忘了插入基准点元素
return quickSort(leftArr).concat(pivot, quickSort(rightArr))
}
最近看了几篇文章,总算理解了这种写法,其实快速排序的思路就是那样,将比基准点大的放在它右边,比基准点小的放在它左边,然后如此反复递归其左右两边的数组。思路是这样,但写法有很多种,这里我用的是前后指针。
写法二:原地排序。
function quickSort(arr,left,right) {
if(left > right) {
return;
}
//设立指针和基准点(数组第一个)
let i = left;
let j = right;
let base = arr[left];
while(i < j) {
//我们从右往左找,找到比基准点小的元素
while(i < j && arr[j] >= base) {
j--
}
//再从左往右找,找到比基准点大的元素,然后交换他们的位置
while(i < j && arr[i] <= base) {
i++
}
if(i < j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
//大的丢右边小的丢左边,丢完之后,将基准点与i,j指针最后停留的元素交换位置
arr[left] = arr[i];
arr[i] = base
//然后对快排后的左右两边做同样的操作
quickSort(arr,left,i-1);
quickSort(arr,i+1,right)
}
补充:最久又学了几个排序算法,都比较特殊,对数据要求比较严苛,但是只要满足了条件,时间复杂度可以达到O(n)
1.桶排序:举个例子。对一批金额在0-50的订单进行排序,我们可以将分成几个桶【0-9,10-19,20-29,30-39,40-49】,将对应的数据放入桶内,对每个桶的数据进行快速排序。
限制条件:1.划分出来的桶要有天然的大小关系。2.每个桶内的数据量相差不能过大
2.计数排序:还是举个例子。对十万考生进行排序,我们设总分范围在0-600,于是我们分601个桶,将分数等于下标值的考生放入对应的桶中,桶内数据一致无需在排序,然后按照顺序将数据从桶内取出存放至一个数组,顺序直接就出来了。这里有一个很厉害的思维,后续将代码补充进来。
限制条件:1.数据范围不宜过大。2.仅能对正整数进行排序,如果不是正整数(比如小数)要用计数排序,那么应该保证数据在相对大小不变的情况下转换为正整数
3.基数排序:继续举个例子。对电话号码进行排序,因为电话号码的位数为11位,这个时候很明显不适合使用桶排序和计数排序。基数排序的操作简单来说就是将电话号码的相对位数进行比较,比如从末位开始,进行比较,然后排序,在对第10位比较排序直到第一位,我们将整体十一位数的数字拆分为对多个一位的数字进行比较,就符合了上面的计数排序和桶排序的条件了。
PS:当要排序的一组数据位数不相等怎么办?比如单词,我们可以采用不足位数补0的办法。
限制条件:1.要比较的一组数据要能拆分为独立的位,并且位与位之间要有能比较的关系。2.位的范围不能过大,过大的话就不满足线性排序的限制条件了,就达不到时间复杂度为O(n)的效果了
我的理解是这三种排序其实都是基于前面说的基础排序而来,桶排序也就是多了个划分为桶的步骤,然后使用快排。所以这几种算法的写法不用死记,我觉得最重要的还是他这种思想。
就比如说,对电话号码排序,在不跟我说这几种排序思想的时候我可能只知道快排,但是元素长度那么大,很明显不论使用哪种基础排序都是不合理的,而知道了基数排序的思想,我才哦~,原来还可以这样玩。这里面只有计数排序用到了一种很巧妙的新的写法,后面我会把代码补上。