一、快速排序思想
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
快速排序是先将一个元素排好序,然后再将剩下的元素排好序,类似二叉树的前序遍历。
时间复杂度:递归层数是log(n), 所以平均时间复杂度是O(nlog(n))。对于排序好的数组,递归层数是n,时间复杂度会降为O(n^2)
void sort(int[] nums, int lo, int hi) {
if (lo >= hi) {
return;
}
// 对 nums[lo..hi] 进行切分
// 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi]
int p = partition(nums, lo, hi);
// 去左右子数组进行切分
sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) {
return;
}
/****** 前序位置 ******/
print(root.val);
/*********************/
traverse(root.left);
traverse(root.right);
**}**
-
相关1:冒泡排序
同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端;而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分,【分治法】。
-
相关2:归并排序
归并排序就是先把左半边数组排好序,再把右半边数组排好序,然后把两半数组合并,类似二叉树的后序遍历。
// 定义:排序 nums[lo..hi]
void sort(int[] nums, int lo, int hi) {
if (lo == hi) {
return;
}
int mid = (lo + hi) / 2;
// 利用定义,排序 nums[lo..mid]
sort(nums, lo, mid);
// 利用定义,排序 nums[mid+1..hi]
sort(nums, mid + 1, hi);
/****** 后序位置 ******/
// 此时两部分子数组已经被排好序
// 合并两个有序数组,使 nums[lo..hi] 有序
merge(nums, lo, mid, hi);
/*********************/
}
// 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi]
// 合并为有序数组 nums[lo..hi]
void merge(int[] nums, int lo, int mid, int hi);
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) {
return;
}
traverse(root.left);
traverse(root.right);
/****** 后序位置 ******/
print(root.val);
/*********************/
}
二、例举简单版-快速排序(阮一峰博客)
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));
};
每次递归都会新建left、right数组,大小为n级别。 所以此算法空间复杂度是O(nlog(n))。
三、改良版-快速排序(基于阮老师博客)
var quickSort = function (arr, compare) {
if (arr.length <= 1) { return arr; } // base条件
var pivotIndex = Math.floor(arr.length / 2); // 1. 选择pivot基准
var pivot = arr[pivotIndex];
var left = [];
var right = [];
// 所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
for (var i = 0; i < arr.length; i++) {
if (i === pivotIndex) continue
if (compare(arr[i], pivot) < 0) {
left.push(arr[i]);
} else if (compare(arr[i], pivot) > 0) {
right.push(arr[i]);
} else if (compare(arr[i], pivot) === 0) {
// 保证排序的稳定性
if (i < pivotIndex) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
}
// 3. 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止
return quickSort(left, compare).concat([pivot], quickSort(right, compare));
};
// 测试
const testArr = [
{
id: 1,
number: 5
},
{
id: 2,
number: 3
},
{
id: 3,
number: 3
},
{
id: 4,
number: 3
},
{
id: 5,
number: 1
},
]
function compare (a, b) {
return a.number - b.number
}
console.log(quickSort(testArr, compare))
可以看到改良后的快速排序有这几处优点:
- 1. 排序是稳定的。(利用额外的空间,获得了排序稳定性)
-
- 优化了以下的一处逻辑。
/** 原本 **/
var pivot = arr.splice(pivotIndex, 1)[0]; // 时间复杂度是O(n)
/* 改为 */
var pivot = arr[pivotIndex];
// 并在for循环额外加上
if (i === pivotIndex) continue
但是空间复杂度依然过高O(nlog(n))!
四、原地快速排序--挖坑法(但是无法解决稳定性问题)
function quickSort(arr, startIndex, endIndex) {
if (startIndex >= endIndex) return
const pivotIndex = partition(arr, startIndex, endIndex)
// 分治法递归数组的两部分
quickSort(arr, startIndex, pivotIndex - 1)
quickSort(arr, pivotIndex + 1, endIndex)
}
function partition(arr, startIndex, endIndex) {
// 取第一个位置元素作为基准
const pivot = arr[startIndex]
// 坑的位置,初始等于pivot的位置
let index = startIndex
let left = startIndex
let right = endIndex
// left 指针和 right 指针没有交错
while (right >= left) {
// right 指针从右向左移动
while (right >= left) {
if (arr[right] < pivot) {
arr[index] = arr[right]
index = right
left++
break
}
right--
}
// left 指针从左向右移动
while (right >= left) {
if (arr[left] > pivot) {
arr[index] = arr[left]
index = left
right--
break
}
left++
}
}
arr[index] = pivot
return index
}
const arr = [3, 2, 1, 5, 6, 4]
quickSort(arr, 0, arr.length - 1)
console.log('dig', arr)
参考:zhuanlan.zhihu.com/p/63202860
五、原地快速排序--指针交换法(但是无法解决稳定性问题)
function quickSort_exchange(arr, startIndex, endIndex) {
if (startIndex >= endIndex) return
const pivotIndex = partition(arr, startIndex, endIndex)
// 分治法递归数组的两部分
quickSort_exchange(arr, startIndex, pivotIndex - 1)
quickSort_exchange(arr, pivotIndex + 1, endIndex)
}
// 指针交换法
function partition(arr, startIndex, endIndex) {
// 取第一个位置元素作为基准
const pivot = arr[startIndex]
let left = startIndex
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) {
const p = arr[left]
arr[left] = arr[right]
arr[right] = p
}
}
// pivot和指针重合点交换
arr[startIndex] = arr[left]
arr[left] = pivot
return left
}
const arr2 = [3, 2, 1, 5, 6, 4]
quickSort_exchange(arr2, 0, arr2.length - 1)
console.log('exchange', arr2)