首先说一下快速排序的概念,他是什么?
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序n个项目要O(nLogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序O(nLogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成\
概念没啥说的,简单来说就是一种排序算法,重要的是具体实现,简单点就是三个步骤
1. 从数组中选择一个元素作为基准点
2. 排序数组,所有比基准值小的元素摆放在左边,而大于基准值的摆放在右边。每次分割结束以后基准值会插入到中间去。
3. 最后利用递归,将摆放在左边的数组和右边的数组在进行一次上述的1和2操作
方法一 好理解,但是力扣 [912排序数组] 提交不了了
所以根据上面三个步骤可以很快写出一个容易理解的js代码,不过就是第一步选一个元素作为基准值,看了很多文章,特别是阮神也是用数组中间的一位数作为标杆元素pivot = arr.splice(Math.floor(arr.length / 2), 1)[0];
,但是我看有人也有用第一个数,但是这种写法在力扣提交不成功,具体原因我也没有搞清楚,搜了很多都没有人说,希望有大神看到这里给我解答??
还有一点儿讨论比较多的就是这个中间元素取出来之后会改变原数组,为什么不直接pivot = arr[Math.floor(arr.length / 2), 1)];拿这个值,这也是我最先想到的,但是实际会过不去,有人说这样将会出现无限递归的错误,具体可以考虑下面这段话
如果你希望通过把该基准值放入left即小数组里面,那么若该基准值是所以数组中最大的那一个,那么此时相当于所有元素进入left数组,右数组为空,继续递归则进入死循环
反之,你想把基准值放入right数组里,若基准值是数组最小值,同理死循环。
感觉,死循环大概率会出现,但是也有不会出现的情况,但是算法力求精准呀。
下面是阮神2011年写的文章,思路一看了其他几个人的写法,跟阮神都基本上一样,只不过现在在力扣这个代码,提交不了了,真是...
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
方法二 单指针
方法二的思路,
1 找个基准着把数组分成两个部分. 就是把原数组按照一个标杆儿元素分开为两部分,左边比标杆儿元素小,右边二比标杆元素大.标杆元素默认取数组的第一个.最后标杆所在数组位置
2 然后递归,根据表杆元素分成的两个大小数组重新递归
重点就是第一步的实现,也就是下面代码partition函数的实现,具体参看下面代码的详细注释
方法2.1 最左为标杆
/*
* left为需要数组中参与排序的起始点;right为数组中参与排序的终止点;
* left如果有传数字那么就为left,没有传参则为0;
* right如果有传参那么就为right,没有传参则为len-1;
* 有传参可能会部分排序可能不会排序,没传参默认排序整个数组;
* pIndex 为分组界限;*/
var sortArray = function quickSort(nums, leftI = 0, rightI = nums.length - 1) {
// 递归种植条件 如果需要排序的起始索引小于终止索引则执行排序
if(rightI > leftI) {
const pIndex = partition(nums, leftI, rightI);
// 递归左右 重新排序
sortArray(nums, leftI, pIndex -1);
sortArray(nums, pIndex + 1, rightI)
}
return nums;
}
function partition(arr, left, right) {
let pivot = left, index = pivot + 1;
// 依次对比标杆元素pivot(也就是下标为第一个的left元素)右边的所有元素
for(let i = index; i <= right; i ++) {
if(arr[pivot] > arr[i]) {
[arr[i], arr[index]] = [arr[index], arr[i]];
// 交换完以后index 需要+1 进行下一轮的对比
index ++;
}
}
// 因为是从left的下一个元素开始两辆对比,所以left元素位置没有动
// for循环结束以后第一个标杆元素右边到index-1的位置的所有元素都比标杆元素小,这个时候也要把标杆儿元素也最后一个比标杆元素小的元素交换位置 .
[arr[pivot], arr[index -1]] = [arr[index - 1], arr[pivot]];
// 目前代码到这儿 这样就实现了index-1(此时变成标杆儿元素的位置)左边都比其小右边都比其大
return index - 1
}
上面代码是以左边为标杆比较,也可以以右边第一个为标杆来比较,partition函数代码如下仅作为学习参考,实际测试以右边为标杆的代码比左边为标杆的代码平均用时 基本上多了5秒
方法2.2 最右为标杆儿 20220908提交过了,现在20220916又提交不过,那这个作为参考
// --------- 这个方法力扣跑不过了,
function partition(arr, left, right) {
let pivot = right;
// 这一步 加不加都行
// let index = left;
// 依次对比标杆元素pivot(也就是下标是最后一个(right)数组元素素 左边边的所有元素
for(let i = left ; i < right; i ++) {
if(arr[pivot] > arr[i]) {
[arr[i], arr[left]] = [arr[left], arr[i]];
// 交换完以后 left 需要+1 进行下一轮的对比
left ++;
}
}
// 因为是从right的前一个元素开始两辆对比,所以right元素位置没有动
// for循环结束以后第一个标杆元素右边到 left-1的位置的所有元素都比标杆元素小,这个时候也要把标杆儿元素也最后一个比标杆元素小的元素交换位置 .
[arr[pivot], arr[left]] = [arr[left ], arr[pivot]];
// 目前代码到这儿 这样就实现了 left也就是index(此时变成标杆儿元素的位置)左边都比其小右边都比其大
return left
}
方法三 双指针,最右为标杆儿
上面方法二中的第二种写法,以右边为标杆儿,然后往左移动比较,上面2.2是以1个指针left从左往右走的.其实也可以从两边往中间走,这样会更节约时间,具体实现如下
方法3.1
const sortArray = (nums: number[], left = 0, right = nums.length - 1) => {
if (left >= right) return nums
const j = partition(nums,left, right);
sortArray(nums, left, j - 1)
sortArray(nums, j + 1, right)
return nums
}
function partition(nums, left, right){
let i = left, j = right - 1;
while(i <= j) {
if(nums[i] > nums[right]) {
[nums[i], nums[j]] = [nums[j], nums[i]];
j--
} else {
i ++
}
}
j ++;
[nums[j], nums[right]] = [nums[right], nums[j]];
return j;
}
方法3.2
这个思路最后返回的下标是j, j是从右往左移动的, 但是交换j和right前一步还需要j在往右回退一步,多一步j++.但是经过我摸索前面一步交换可以让i和最后right坐标交换,最后直接返回i坐标就行了,也不用多一步操作i了.
function partition(nums, left, right){
let i = left, j = right - 1;
while(i <= j) {
if(nums[i] > nums[right]) {
[nums[i], nums[j]] = [nums[j], nums[i]];
j--
} else {
i ++
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
}
方法四 双指针,最左为标杆儿
sortArray代码是一样的,主要是partition函数的时候方法3都是以右边为标杆双指针,所以我想着是不是以左边为标杆也是双指针,最后代码如下
function partition(arr, left, right) {
let i = left + 1, j = right;
while (i <= j) {
if (arr[i] > arr[left]) {
[arr[i], arr[j]] = [arr[j], arr[i]];
j--;
} else {
i++;
}
}
[arr[j], arr[left]] = [arr[left], arr[j]]
return j;
}
总结,单指针走的话,从左边往右时间更少.双指针和单指针从左往右耗时差不多.
参考文章
1 www.ruanyifeng.com/blog/2011/0…
2 www.jianshu.com/p/f5b157a97… \