nlogn级别的排序算法(2)快速排序
1. 基本思路
通过一趟排序,要将数据分成独立的两部分,其中一部分的所有数据都比另一部分中的所有数据大。为此,每次取第一个数据作为中间值,每一趟排序的目的就是使中间值位于正确的位置,所有该位置左边的数据都比中间值小,所有该位置右边的数据都比中间值大。然后以中间值为界,只要左右两边的数据量大于1,即可分别进行相同的排序操作,递归直到完成整个数组的排序。
2. javaScript实现
/**
* 快速排序迭代过程
* @param {Array} arr 排序数组
* @param {Number} lIndex 待排序部分的左边界下标
* @param {Number} rIndex 待排序部分的右边界下标
*/
function quickSort(arr,lIndex,rIndex){
if (lIndex<rIndex) {
var flag = partition(arr,lIndex,rIndex);
quickSort(arr,lIndex,flag-1);
quickSort(arr,flag+1,rIndex);
}
}
/**
* 将排序部分的第一个元素v放到正确位置的过程,
* v放置正确后,v左边的元素全部都小于或等于v,v右边的元素全部都大于或等于v
* @param {[Array]} arr 待排序的数组
* @param {[Number]} lIndex 待排序部分的左边界下标
* @param {[Number]} rIndex 待排序部分的右边界下标
* @return {[Number]} 返回调整之后,v所在位置的下标
*/
function partition(arr,lIndex,rIndex){
var j = lIndex;
var v = arr[lIndex];
for (var i = lIndex + 1; i <= rIndex; i++) {
if (arr[i] < v) {
arr.changeData(i,j+1);
j++;
}
}
arr.changeData(j,lIndex);
return j;
}
//交换数组元素
Array.prototype.changeData = function(indexOne,indexTwo){
var temp = this[indexOne];
this[indexOne] = this[indexTwo];
this[indexTwo] = temp;
};
var arr = [5,3,1,6,2,7,4,9];
quickSort(arr,0,arr.length);
console.log(arr);
3. 快速排序与归并排序的效率比较
使用100,0000数据量的随机数组进行测试,改进后的归并排序和上述快速排序算法所需要的时间分别为:
排序方法 | 归并排序(改进) | 快速排序(基本算法) |
---|---|---|
用时 | 0.453s | 0.304s |
使用1000数据量的近乎有序的数组进行测试,改进后的归并排序和上述快速排序算法所需要的时间分别为:
(此处无法使用1000以上的数据量进行测试,由于递归调用过多,会报出调用栈溢出的错误)
排序方法 | 归并排序(改进) | 快速排序(基本算法) |
---|---|---|
用时 | 0.001s | 0.005s |
由此可见,当待排序的数组为随机数据时,快速排序算法较之归并排序的算法效率要高很多。但是,当待排序的数组为近乎有序的数组时,快速排序的效率则会大幅降低。这是因为,上述快速排序每次都使用第一个数组作为分界点,当数组完全有序时,每次以分界点为界,会将数组划分非常不平衡的两部分,左边部分数据量只有1,而右边部分的数据量只在原来的基础上减少了1,如此,其排序效率会降低到n^2级别。
4. 快速排序的改进:随机化快速排序
综合以上分析,要改进快速排序算法遇见近乎有序数组排序效率低的问题,只需要改变分界点的选择方法即可。即由每次选择数组第一个元素变为每次随机选择数组元素。改进代码如下(仅在原来的基础上添加了依一句交换位置的代码):
function partition(arr,lIndex,rIndex){
//交换数组lIndex位置与[lIndex,rIndex]范围内任意元素的位置
arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
var j = lIndex;
var v = arr[lIndex];
for (var i = lIndex + 1; i <= rIndex; i++) {
if (arr[i] < v) {
arr.changeData(i,j+1);
j++;
}
}
arr.changeData(j,lIndex);
return j;
}
5. 快速排序的改进:双路快速排序
替换测试数组用例,当数组范围为[1,10],数据量为10000时(即数组中存在大部分重复数据且每一个数组的重复量都很大),归并排序和改进后的快速排序,其排序用时分别如下,此时,快速排序算法的效率再次降低。这是因为,数组中的元素每次实际上是被分为了小于v的部分和大于或等于v的部分。对于每一个”基准”元素来说,重复的元素太多了,如果我们选的”基准”元素稍微有一点的不平衡,那么就会导致两部分的差距非常大;即时我们的”基准”元素选在了一个平衡的位置,但是由于等于”基准”元素的元素也非常多,也会使得序列被分成两个及其不平衡的部分,那么在这种情况下快速排序就又会退化成O(n^2)级别的排序算法。
排序方法 | 归并排序 | 快速排序 |
---|---|---|
排序时间 | 0.006s | 0.049s |
由此产生了双路快速排序算法,其基本思路为,设置两个标记i,j,将小于v的元素放在i标记的左侧,大于v的元素放在j标记的右侧。i标记从左到右开始遍历,j元素从右到左开始遍历,当i元素遇到大于或等于v的元素,j元素遇到小于或等于v的元素时,则将i和j调换位置。当i>j时,一次partition结束,此时,j已经指向了最右侧的小于v的元素,只需要将第一个元素与j指向的元素调换位置即可。实现代码如下:
* 双路快速排序算法
* @param {[Array]} arr 待排序的数组
* @param {[Number]} lIndex 待排序部分的左边界下标
* @param {[Number]} rIndex 待排序部分的右边界下标
*/
function quickSort2(arr,lIndex,rIndex){
if (lIndex >= rIndex) {
return;
}
var flag = partition2(arr,lIndex,rIndex);
quickSort2(arr,lIndex,flag-1);
quickSort2(arr,flag+1,rIndex);
return arr;
}
function partition2(arr,lIndex,rIndex){
arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
var v = arr[lIndex];
var i = lIndex + 1;
var j = rIndex;
while(true){
//此处如果将<改为<=,则效率又会降低到普通快速排序一般,为什么呢??
while(i <= rIndex && arr[i] < v) i++;
while(j >= lIndex+1 && arr[j] > v) j--;
if (i>j) {
break;
}
arr.changeData(i,j);
i++;
j--;
}
arr.changeData(lIndex,j);
return j;
}
改进后,对数组范围为[1,10],数据量为100000的大量重复数组进行排序,用时如下:
排序方法 | 归并排序 | 随机快速排序 | 双路快速排序 |
---|---|---|---|
用时 | 0.042 | 0.651 | 0.008 |
真棒,双路快速排序甚至超过了归并排序。
6. 快速排序改进:三路快速排序
双路快速排序将数组分为了大于等于v和小于等于v两部分,而实际上,等于v的部分也占据了很大比例。由此衍生出了三路快速排序算法,即将数组分为大于v、等于v、小于v三个部分。其代码如下:
function quickSort3Ways(arr,lIndex,rIndex){
if (lIndex>=rIndex) {
return;
}
var leftAndRightObj = partition3(arr,lIndex,rIndex);
quickSort3Ways(arr,lIndex,leftAndRightObj[0]);
quickSort3Ways(arr,leftAndRightObj[1],rIndex);
return arr;
}
function partition3(arr,lIndex,rIndex){
arr.changeData(lIndex,Math.floor(Math.random()*(rIndex-lIndex+1)+lIndex));
var v = arr[lIndex];
var lt = lIndex;
var i = lIndex+1;
var rt = rIndex+1;
while(i < rt){
if (arr[i] < v) {
arr.changeData(i,lt+1);
i++;
lt++;
}else if (arr[i] === v){
i++;
}else {
arr.changeData(i,rt-1);
rt--;
}
}
arr.changeData(lIndex,lt);
return [lt,rt];
}
三路快速排序在针对含有大量重复元素的数组时,比双路快速排序具有更好的性能。以下是数组长度为1,000,000,数据范围为[1-10]的数组,用不同的方法排序的耗时:
排序方法 | 归并排序 | 双路快速排序 | 三路快速排序 |
---|---|---|---|
用时 | 0.385 | 0.13 | 0.042 |