同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的
不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素,并让其它比它大的元素移动的数列的一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分
这种思路叫做分治法
流程:
每一轮的比较和交换都需要把数组全部元素都遍历一遍,时间复杂度是O(n),需要遍历logn轮,所以时间复杂度为O(nlogn)
基准元素的选择
最简单的方式是选择数列的第一个元素
这种选择在大多数情况下是没问题的,但是假如有一个原本逆序的数列,期望排序称为顺序的数列,则会出现时间复杂度退化称为O(n2)的情况
避免这种情况的做法是随机选取一个元素作为基准元素,并且让基准元素和列首元素交换位置
但是即使随机选择元素,也有极小的几率选择到数列的最大值或者最小值,所以虽然快速排序的平均时间复杂度是O(nlogn),但是最坏的情况下时间复杂度是O(n2)
元素交换
设定基准元素之后,就是要把其他元素中小于基准元素的都交换到基准元素的一边,大于基准元素的都交换到基准元素的另一边,
具体实现有两种方法:
1. 双边循环法
· 2. 单边循环法
双边循环法
详细过程如下:
原始数组如下,要求对其从小到大排序
1. 首先,选择基准元素 pivot,并且设置两个指针 left 和 right,指向数列的最左和最右两个元素
2. 进行第一次循环,从 right 指针开始,让指针指向的元素和基准元素进行比较,如果大于 pivot,则指针向左移动,如果小于等于 pivot 则 right 指针停止移动,切换 left 指针
在当前数列中,1 < 4 所以right指针直接停止移动,
切换 left 指针,让指针指向的元素和基准元素进行比较,如果小于或等于 pivot,则指针向右移动,如果大于 pivot,则 left 指针停止移动
由于left指针开始指向基准元素,判断肯定相等,所以 left 右移一位,
由于 7 > 4 则 left 指针停下,这时让 left 和 right 指针所指向的元素进行交换
3. 接下来重新切换到 right 指针 进行第二次循环,后续步骤如下:
代码如下
/**
* @param {Array<number>} arr 待交换的数组
* @param {number} startIndex 起始下标
* @param {number} endIndex 结束下标
* @returns {number} 返回基准元素位置
* @description 分治 双边循环法
*/
function partition(arr: Array<number>, startIndex: number, endIndex: number):number {
// 取第一个位置的元素作为基准元素
const pivot = arr[startIndex];
// left 指针
let left = startIndex;
// right 指针
let right = endIndex;
while(left != right) {
// 控制right指针左移
while(left < right && arr[right] > pivot) {
right--
}
// 控制left指针右移
while(left < right && arr[left] <= pivot) {
left++
}
// 如果指针不重合 交换left right 指针所指元素
if(left < right) {
let p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
// pivot 和重合点交换
arr[startIndex] = arr[left];
arr[left] = pivot;
/*
这里有个问题思索了很久:怎么保证交换的left元素 一定是小于 或者 大于 基准元素
细想下来:如上代码 left 或者 right 其中一个停止,另一个 移动过来重合
如果 left => right 则 right一定为 小于= pivot 的元素
如果 left <= right 则 一定为 小于= pivot的元素
*/
return left
}
function quickSort(arr: Array<number>, startIndex: number, endIndex: number) {
if(startIndex >= endIndex) {
return
}
// 得到基准元素的位置
let pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素 分为两部分递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
function main() {
let arr = [4, 2, 6, 2, 2, 2, 2, 8];
quickSort(arr, 0, arr.length - 1 );
console.log(arr)
}
main()
单边循环法
给出原始数组,要求对其从小到大进行排序:
选的基准元素 pivot,同时设置一个 mark 指针指向数列起始位置,这个 mark 指针代表 小于基准元素的区域边界
接下来,从基准元素的下一个位置开始遍历数组,
如果遍历到的元素大于基准元素,就继续往后遍历
如果遍历到的元素小于基准元素,则需要做两件事情:
第一、把 mark 指针右移 1 位,因为小于 pivot 的区域边界增大了 1
第二、让最新遍历到的元素和 mark 指针所在位置的元素交换位置,因为最新遍历的元素属于小于 pivot 的区域
首先遍历到元素 7,7 > 4,所以继续遍历
接下来遍历到元素 3,3 < 4,所以 mark 指针右移 1 位
随后,让元素 3 和 mark 指针所在位置的元素交换,因为 3 归属小于 pivot 区域
后续步骤如下:
代码如下:
/**
* @param {Array<number>} arr 待交换的数组
* @param {number} startIndex 起始位置
* @param {number} endIndex 结束位置
* @description 分治 单边循环法
*/
function partition(arr: Array<number>, startIndex: number, endIndex: number):number {
// 拿到基准元素
let pivot = arr[startIndex];
// 边界
let mark = startIndex;
for (let index = startIndex; index <= endIndex; index++) {
if(arr[index] < pivot) {
mark++;
let p =arr[mark];
arr[mark] = arr[index];
arr[index] = p
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark
}
function quickSort(arr: Array<number>, startIndex: number, endIndex: number){
if(startIndex >= endIndex) {
return
}
const pivotIndex = partition(arr, startIndex, endIndex);
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex)
}
function main() {
let arr = [4, 4, 6, 5, 3, 2, 8, 1];
quickSort(arr, 0, arr.length - 1);
console.log(arr)
}
main();
非递归实现
/**
* @param {Array<number>} arr 待排序的数组
* @param {number} startIndex 起始位置
* @param {number} endIndex 结束位置
* @returns 基准元素下标
* @description 分治 单边循环法
*/
function partition(arr: Array<number>, startIndex: number, endIndex: number) {
// 基准元素
let pivot = arr[startIndex]
// 边界
let mark = startIndex
for (let index = startIndex; index <= endIndex; index++) {
if(arr[index] < pivot) {
mark++;
let p = arr[mark];
arr[mark] = arr[index];
arr[index] = p
}
};
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark
}
interface StackItem {
startIndex: number;
endIndex: number;
}
function quickSort(arr: Array<number>, startIndex: number, endIndex: number) {
// 用一个栈来代替递归的调用栈
let quickSortStack:Array<StackItem> = [];
// 起止下标
let obj:StackItem = {
startIndex,
endIndex
};
quickSortStack.push(obj);
// 当栈为空时结束
while(quickSortStack.length) {
let item = quickSortStack.pop() as StackItem;
let { startIndex, endIndex } = item
let pivotIndex = partition(arr, startIndex, endIndex);
// 如果一边有两个 或者两个以上的元素 则继续入栈
if(startIndex < pivotIndex - 1) {
quickSortStack.push({
startIndex,
endIndex: pivotIndex - 1
})
}
if(endIndex > pivotIndex + 1) {
quickSortStack.push({
startIndex : pivotIndex + 1,
endIndex: endIndex
})
}
}
}
function main() {
let arr = [4, 4, 6, 5, 3, 2, 8, 1];
quickSort(arr, 0, arr.length - 1);
console.log(arr)
}
摘要总结自: 漫画算法 小灰的算法之旅