原理
快速排序算法是基于分治思想的,在递归过程中,每次从待排序区间选取一个元素(这个值的位置随意,可以是两端,也可以是中间,也有为了处理基准值而专门写一个方法的)作为基准记录,遍历整个区间,将所有比基准记录小的元素移动到基准记录的左边,而所有比基准记录大的元素移动到基准记录的右边。之后分别对基准记录的左边和右边两个区间进行快速排序,直至将整个区间排序完成。
以下讨论不同的几种实现方式,都以从小到大排序为准。
基准值最左/最右+双指针
覆蓋值(标准算法)
这种方法将基准值先保存下来,用两个指针分别从左边和右边遍历待排序区间,注意,此处根据基准值在左边或右边的情况不同:
如果选取最左边的值为基准值,则需要用比它小的值来放在基准值原来的位置,这个值一定是从右侧开始遍历取到的,因为左边指针遇到比基准值大的停下来,右边指针遇到比基准值小的停下来,所以首先走右边指针,将它指向的值覆蓋基准值的位置,然后走左边,遇到比基准值大的数值,覆蓋右边指针指的位置,这样依此覆蓋和往中间遍历,直到左右指针相遇,这时把基准值放到这个下标位置,完成一轮排序,左边的都比它小,右边的都比它大,具体实现如下:
function quikSort1(arr, left, right) {
if (left > right) {return;}
var pivot = arr[left];
var l = left, r = right;
while (l < r) {
while (r > l && arr[r] >= pivot) { r--; }
arr[l] = arr[r];
while (l < r && arr[l] <= pivot) { l++; }
arr[r] = arr[l];
}
arr[r] = pivot;
if (left < r) { quikSort1(arr, left, r-1); }
if (right > l) { quikSort1(arr, l+1, right); }
}
同理,如果是取右边值为基准值,则先走左边指针:
function quikSort1(arr, left, right) {
if (left > right) {return;}
var pivot = arr[right];
var l = left, r = right;
while (l < r) {
while (l < r && arr[l] <= pivot) { l++; }
arr[r] = arr[l];
while (r > l && arr[r] >= pivot) { r--; }
arr[l] = arr[r];
}
arr[r] = pivot;
if (left < r) { quikSort1(arr, left, r-1); }
if (right > l) { quikSort1(arr, l+1, right); }
}
这里有一个细节,在
arr[r] = arr[j]
之类的覆蓋结束后,可以把被覆蓋的位置加一或减一即r--
,因为这个位置被覆蓋了,可以直接跳过从下一位开始比较。
交换值(两头交换法)
在这种实现方式中,可以使用不断覆蓋值,最后把基准值放进来的方法,也有先保存l和r的下标位置,然后直接交换这两个位置数值的方法,在这种情况下,基准值不会被覆蓋,也不需要在一轮比较结束后插入,但指针下标在交换后需要各自增长一位。实现如下:
function quickSort2(arr, left, right) {
var pivot = arr[left], i = left, j = right; // 以最左边的数为基准,i是左zhizhen,往右走,j是右指针,往左走
do{
while (i <= j && arr[i] < pivot) {
i++;
}// 从左边寻找比基准数大的数
while (i <= j && arr[j] > pivot) {
j--;
}// 从右边寻找比基准数小的数
if (i <= j) {
// 找完了,交换
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i ++;
j --;
// 指针继续挪动,然后比较交换,直到指针相遇
}
}while(i <= j);
// 此时已经分成左右两部分,左边比基准小,右边比基准大
if (left < j) {
//对左边部分排序
quickSort2(arr, left, j);
}
if (i < right) {
//对右边部分排序
quickSort2(arr, i, right);
}
}
基准值最左/最右+单指针遍历(两头交换法的另一种实现)
这种实现方式的思路是,以最左或最右为基准值,假设将待排序区间分为大于小于基准值的两个区间S1和S2,遍历整个区间把数值划分到这两个区域中。
首先设定一个下标m,指向S1的起始点,也就是基准值的位置,该下标表示S1区间的结束位置(一开始就是0,然后随着区间变大而增加),然后使用一个下标指针k从m的位置开始依次遍历,遇到比基准值小的,就把m增加一位,然后交换k与m的值,这样逐渐把比基准值小的值都挪到靠近基准值的位置,比基准值大的则不处理,一趟结束后,m左边的都比基准值小,右边都比基准值大,这时交换基准值和m下标上的值,让基准值处于S1和S2的分界处,然后递归排序左边到到m,和m到右边两个区域。具体实现:
function quickSort4(arr, left, right) {
var pivot = arr[left]; // p 为基准值
var m = left; // 用S1 and S2 表示基准值两边两个空的区域,m表示排好序区域最右侧的位置下标
for (var k = left+1; k <= right; k++) { // 从第二位开始遍历后面的值
if (arr[k] < pivot) { // 比基准值小
m++; // S1区域增加一个元素
// 交换该值和存储指数值
var temp = arr[k];
arr[k] = arr[m];
arr[m] = temp;
} // 对大于基准值的不做处理
}
// 交换m位置的值和基准值(m此时在S1和S2交界处)
var temp = arr[left];
arr[left] = arr[m];
arr[m] = temp;
if(left< right) {
quickSort4(arr, left, m-1);
quickSort4(arr, m+1, right);
}
}
也可以拆分成两个函数:
function partition(arr, left, right) {
var pivot = arr[left]; // 基准值
var m = left;
for (var k = left+1; k <= right; k++) {
if (arr[k] < pivot) { // 比基准值小
m++; // 存储指数+1
var temp = arr[k];
arr[k] = arr[m];
arr[m] = temp;
} // notice that we do nothing in case 1: arr[k] >= p
}
var temp = arr[left];
arr[left] = arr[m];
arr[m] = temp;
return m; // 返回存储指数位置,用于快速排序
}
function quickSort3(arr, left, right) {
if (left < right) {
var pivotIdx = partition(arr, left, right); // O(N)
// a[left..right] ~> a[left..pivotIdx–1], pivot, a[pivotIdx+1..right]
quickSort3(arr, left, pivotIdx-1); // recursively sort left subarray
// a[pivotIdx] = pivot is already sorted after partition
quickSort3(arr, pivotIdx+1, right); // then sort right subarray
}
}
该实现方式来自VISUALGO
简单粗暴的使用数组
来自快速排序(Quicksort)的Javascript实现
这种方式是选取一个基准值,然后新建两个数组,把大于和小于基准值的分别放在两个数组中,递归排序两个数组,然后合并。
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));
};