聊一聊快速排序(JS)
快速排序
基本思路
双指针+递归分治(本质是一个创建二叉树搜索树的过程)
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
我的理解
上面的基本思路是参考网络上大佬的文章整理的出来的,我来说说我的理解。
-
在将要排序的数据中选取一个数作为基准数,将这些数据中比所选取的基准数小的数放在所选取基准数的左边为左数组,将比所选取基准数大的数组放在右边为右数组。
-
通过递归的方式重复循环1中的过程达到排序的目的。
下面是我的代码
let testArray = [3, 1, 2, 5, 6, 4];
let quickSort = (array) => {
if (array.length < 2) return array;
let leftArray = [];
let rightArray = [];
let baseDigit = array[0];
array.forEach(element => {
if (element < baseDigit) {
leftArray.push(element);
} else if (element > baseDigit) {
rightArray.push(element);
}
});
return quickSort(leftArray).concat(baseDigit, quickSort(rightArray))
};
quickSort(testArray);
某乎上一篇文章的思路
基本思路跟我上述理解大同小异,主要来看看这篇文章具体的实现过程。下面借用原文的图来讲解(原文的图做的很好就不单独画图了,主要讲一讲原文没解释需要注意的地方,和对该篇文章做一个补充),底部附原文链接。
1.数组[2,3,1,5,6,4],创建两指针,一个只想头一个指向尾,再确定一个基准数。
(注意:为了方便后面递归是能够确定基准数,这里基准数选取,第一个数或者最后一个数)
2.开始第一次的递归处理,尾指针先从右往左扫,扫到第一个小于(注意是小于,而不是小于等于哦)基准数的位置停住,这时候头指针再从左往右扫,扫到第一个大于基准数的位置停住,这时候是下面的图示状态:
(注意:这里如果基准数选区的第一个数,应该尾指针先往左侧扫,若基准数选取为最后一个属则,应是头指针向往右扫)
交换两个指针所指的数,成为了下面的状态:
3.两个数交换完毕,右指针此时指的是arr[2] = 3, 左指针指着arr[1] = 1;交换完毕后右指针继续从当前位置往左扫,扫到1的时候发现和左指针相遇了,那么这个时候就结束左右指针的扫描,左右指针同时指着arr[1] = 1,即:
此时退出循环扫描的过程,交换基准数与左右指针同时所指的数的位置,开头说了,基准数我选择的是arr[0] = 2, 指针指的是arr[1] = 1; 交换过后就变成了:
这时候就发现基准数已经出现在了它排完序后应该在的位置(排完序后是[1,2,3,4,5,6],2出现在了第2位),比这个基准数小的数组出现在了它的左边([1]出现在了2的左边),比基准数大的出现在了它的右边([3,5,6,4]出现在了2的右边)。
4.之后的过程就是对左右数组的分别递归处理。
function quickSort(arr, begin, end) {
//递归出口
if(begin >= end)
return;
var l = begin; // 左指针
var r = end; //右指针
var temp = arr[begin]; //基准数,这里取数组第一个数
//左右指针相遇的时候退出扫描循环
while(l < r) {
//右指针从右向左扫描,碰到第一个小于基准数的时候停住
while(l < r && arr[r] >= temp)
r --;
//左指针从左向右扫描,碰到第一个大于基准数的时候停住
while(l < r && arr[l] <= temp)
l ++;
//交换左右指针所停位置的数
[arr[l], arr[r]] = [arr[r], arr[l]];
}
//最后交换基准数与指针相遇位置的数
[arr[begin], arr[l]] = [arr[l], arr[begin]];
//递归处理左右数组
quickSort(arr, begin, l - 1);
quickSort(arr, l + 1, end);
}
var arr = [2,3,4,1,5,6]
quickSort(arr, 0, 5);
console.log(arr)
百科上的思路
百科上的思路跟上述某乎文章基本一致,不过再细节方面不同,这里主要讲已将它们不同的地方,详情请参考原文。(需注意之处也在和上文相同不在赘述)
主要的不同之处在于再上述2,3步骤。
百科上给的方式是:假设让右指针先扫,扫到了比基准数小的,就讲该数与基准数值交换位置,此时左指针指向基准数,再让左指针往右扫描,扫到比基准数大的交换左右指针数值,两指针相遇时直接退出这次递归,通过这样的的方式来达到第一次递归的目的。
上文中则是:假设让右指针先扫,扫到了比基准数小的,指针停住,再让左指针往右扫描扫到比基准数大的数再停住,然后交换两指针指向的值,反复调用,两指针相遇时与基准数的数值进行交换。
相对于理解来说我认为是,百科的方式更容易理解(其实是我先理解了百科的方式让后想到了自己的思路,最后才理解了某乎的方式)。
const quickSort = (array) => {
const sort = (arr, left = 0, right = arr.length - 1) => {
if (left >= right) {//如果左边的索引大于等于右边的索引说明整理完毕
return
}
let i = left
let j = right
const baseVal = arr[j] // 取无序数组最后一个数为基准值
while (i < j) {//把所有比基准值小的数放在左边大的数放在右边
while (i < j && arr[i] <= baseVal) { //找到一个比基准值大的数交换
i++
}
arr[j] = arr[i] // 将较大的值放在右边如果没有比基准值大的数就是将自己赋值给自己(i 等于 j)
while (j > i && arr[j] >= baseVal) { //找到一个比基准值小的数交换
j--
}
arr[i] = arr[j] // 将较小的值放在左边如果没有找到比基准值小的数就是将自己赋值给自己(i 等于 j)
}
arr[j] = baseVal // 将基准值放至中央位置完成一次循环(这时候 j 等于 i )
sort(arr, left, j-1) // 将左边的无序数组重复上面的操作
sort(arr, j+1, right) // 将右边的无序数组重复上面的操作
}
const newArr = array.concat() // 为了保证这个函数是纯函数拷贝一次数组
sort(newArr)
return newArr
}
性能
既然这里给出了三种方式来实现快排,那我们就来测试一下性能
由于百科的方法有问题再5位数以上会报错10000后面不测试百科方法
第一个数我的方法
在1000个相同随机数的情况下
在100000个相同随机数的情况下
结论
从性能上讲是某乎的方法更高。
附测试代码
// 我的方法
let myQuickSort = (array) => {
if (array.length < 2) return array;
let leftArray = [];
let rightArray = [];
let baseDigit = array[0];
array.forEach(element => {
if (element < baseDigit) {
leftArray.push(element);
} else if (element > baseDigit) {
rightArray.push(element);
}
});
return myQuickSort(leftArray).concat(baseDigit, myQuickSort(rightArray))
};
// 某乎的方法
let moHu = (arr, begin, end) => {
//递归出口
if (begin >= end)
return;
var l = begin; // 左指针
var r = end; //右指针
var temp = arr[begin]; //基准数,这里取数组第一个数
//左右指针相遇的时候退出扫描循环
while (l < r) {
//右指针从右向左扫描,碰到第一个小于基准数的时候停住
while (l < r && arr[r] >= temp)
r--;
//左指针从左向右扫描,碰到第一个大于基准数的时候停住
while (l < r && arr[l] <= temp)
l++;
//交换左右指针所停位置的数
[arr[l], arr[r]] = [arr[r], arr[l]];
}
//最后交换基准数与指针相遇位置的数
[arr[begin], arr[l]] = [arr[l], arr[begin]];
//递归处理左右数组
moHu(arr, begin, l - 1);
moHu(arr, l + 1, end);
};
//百科的方法
let baiKe = (array) => {
let sort = (arr, left = 0, right = arr.length - 1) => {
if (left >= right) {//如果左边的索引大于等于右边的索引说明整理完毕
return
}
let i = left;
let j = right;
const baseVal = arr[j];// 取无序数组最后一个数为基准值
while (i < j) {//把所有比基准值小的数放在左边大的数放在右边
while (i < j && arr[i] <= baseVal) { //找到一个比基准值大的数交换
i++
}
arr[j] = arr[i]; // 将较大的值放在右边如果没有比基准值大的数就是将自己赋值给自己(i 等于 j)
while (j > i && arr[j] >= baseVal) { //找到一个比基准值小的数交换
j--
}
arr[i] = arr[j] // 将较小的值放在左边如果没有找到比基准值小的数就是将自己赋值给自己(i 等于 j)
}
arr[j] = baseVal; // 将基准值放至中央位置完成一次循环(这时候 j 等于 i )
sort(arr, left, j - 1); // 将左边的无序数组重复上面的操作
sort(arr, j + 1, right) // 将右边的无序数组重复上面的操作
};
const newArr = array.concat();// 为了保证这个函数是纯函数拷贝一次数组
sort(newArr);
return newArr
};
// 生成一个1-count的随机数组
let createTestArray = (count) => {
let temArray = [];
while (count > 0) {
temArray.unshift(count);
count--;
}
let i = temArray.length;
while (i) {
let j = Math.floor(Math.random() * i--);
[temArray[j], temArray[i]] = [temArray[i], temArray[j]];
}
return temArray;
};
// 测试
let testQuickSort = (name, func, arr, moHu) => {
if (!!moHu) {
console.time(name);
func(arr, moHu.begin, moHu.end);
console.timeEnd(name);
return;
}
console.time(name);
func(arr);
console.timeEnd(name);
};
// 生成1-100000的随机数组
const testArray = createTestArray(100000);
testQuickSort('myQuickSort', myQuickSort, testArray);
testQuickSort('moHu', moHu, testArray, {begin: 0, end: 99999});
// testQuickSort('baiKe', baiKe, testArray);
最后
最后记一笔,快速排序是不稳定排序也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
文章连接:
某乎:微软前端社招笔试详解
百科:百度百科