排序算法快速排序
Posted 程序员思语
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法快速排序相关的知识,希望对你有一定的参考价值。
在做前端工作的时候常常会对数组做处理,也经常会用到排序,今天开始将对常用排序算法做总结,顺便复习一些数据结构知识。
友情提示:阅读本文大概需要 35分钟
前言
快速排序,是从冒泡排序演变而来的算法,也属于交换排序,但是比冒泡算法高效很对,因此得名,也是最经典的排序算法之一,它的核心思想就是分治(小伙伴还记不记得前面介绍的Fish-Redux的核心思想也是分治),快速排序拥有良好的时间复杂度,平均为O(nlog2n),最差为O(n2)。
快速排序
快速排序通过元素之间的比较和交换位置来达到排序的目的,不同的是,冒泡排序在每一轮只把一个元素冒泡到数组的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。这是分治思想的主要体现,原数组在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)。
题目背景
// 栗子
var array = [2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2]
JS实现
// JS
function quickSort(arr) {
let piovtIndex = Math.floor(arr.length/2);
if(arr.length<=1) return arr;
let piovt = arr.splice(piovtIndex,1)[0];
let _left = []
let _right = []
for (var i = 0; i < arr.length; i++) {
let item = arr[i];
if(item<piovt) {
_left.push(item)
} else {
_right.push(item)
}
}
return quickSort(_left).concat([piovt],quickSort(_right))
}
C++实现
// C++
#include <iostream>
using namespace std;
void quickSort(int* arr, int start, int end) {
if (start < end) {
int i = start, j = end;
while (i < j) {
while (arr[i] <= arr[j] && i < j) {
j--;
}
if (i < j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
i++;
}
while (arr[i] < arr[j] && i < j) {
i++;
}
if (i < j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
j--;
}
}
quickSort(arr, i + 1, end);
quickSort(arr, start, i - 1);
}
}
int main()
{
int arr[] = {
2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2
};
quickSort(arr, 0, 17);
for (int i = 0; i < 18; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
// 或者
#include <iostream>
using namespace std;
void quickSort(int* arr, int start, int end)
{
if (start < end) {
int i = start, j = end, store = arr[start];
while (i < j) {
while (i < j && arr[j] >= store) {
j--;
}
if (i < j) {
arr[i] = arr[j];
}
while (i < j && arr[i] < store) {
i++;
}
if (i < j) {
arr[j] = arr[i];
}
}
arr[i] = store;
quickSort2(arr, i + 1, end);
quickSort2(arr, start, i - 1);
}
}
int main() {
int arr[] = {
2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2
};
quickSort(arr, 0, 17);
for (int i = 0; i < 18; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
局限性:
如果出现一种原本逆序的数组,采用最原始的快速排序的画,就无法发挥分治法的优势了,在最坏情况下,快速排序需要进行N轮比较,时间复杂度会退化到 O(N^2);此外,如果随机选取的基准元素是数组中的最大值or最小值也会到导致分治思想瓦解。
为了避免以上的情况,这个基本元素pivot的选取就需要分情况,pivot的选取直接影响排序的优劣。
①.最简单的方式是选择数列的第一个元素;这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,这种排序效率将降到最低。
②.随机选择一个元素作为基准元素;这样即使在数列完全逆序的情况下,也可以有效地将数列分成两部分,但是这样基本元素随机性较强,时间复杂度也不稳定,平均时间复杂度是 O(nlogn),如果选中了数组中最大值or最小值,最坏情况下的时间复杂度是 O(n^2)。
③.挖坑法,用法是闲选定基准元素 pivot,并记住这个位置 index,这个位置相当于一个“坑”。并且设置两个指针 left 和 right,指向数列的最左和最右两个元素;接下来,从right 指针开始,把指针所指向的元素和基准元素做比较。如果比 pivot 大,则 right 指针向左移动;如果比 pivot 小,则把right所指向的元素填入坑中,最终实现比基准元素小的元素全部移到 left 区域,比基准元素大的元素的移动到 right 区域,从而实现排序。
public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 用分治法递归数组的两部分
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
// 坑的位置,初始化等于 pivot 的位置
int index = startIndex;
// 大循环在左右指针重合或者交错时结束
while(right >= left) {
// right 指针从右向左进行比较
while(right >= left) {
if(arr[right] < pivot) {
arr[left] = arr[right];
index = right;
left++;
break;
}
right--;
}
// left指针从左向右进行比较
while(right >= left) {
if(arr[left] > pivot) {
arr[right] = arr[left];
index = left;
right--;
break;
}
left++;
}
}
arr[index] = pivot;
return index;
}
public static void main(String[] args) {
int[] arr = new int[] {2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
④. 指针交换法,定义两个指针,一前一后,前面指针找比基数小的数,后面指针找比基数大的数,前面的指针找到后,将前后指针所指向的数据交换,当前面的指针遍历完整个数组时,将基数值与后指针的后一个位置的数据进行交换,然后以后指针的后一个位置作为分界,然后将数组分开,进行递归排序。
// 实现
public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 递归结束条件:startIndex大等于 endIndex的时候
if(startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1; endIndex);
}
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while(left != right) {
// 控制 right 指针比较并且左移
while(left < right && arr[right] > pivot) {
right--;
}
// 控制right指针比较并且右移
while(left < right && arr[left] <= pivot) {
left++;
}
// 交换left和right指向的元素
if(left < right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
// pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}
public static void main(String[] args) {
int[] arr = new int[] {2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
⑤.非递归的快速排序
public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 用一个集合栈来代替 递归的函数栈
Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();
// 整个数列的起止下标,以哈希的形式入栈
Map<K, V> rootParam = new HashMap();
rootParam.put("startIndex", startIndex);
rootParam.put("endIndex", endIndex);
quickSortStack.push(rootParam);
// 循环结束条件:栈为空时结束
while(!quickSortStack.isEmpty()) {
// 栈顶元素出栈,得到起止下标
Map<String, Integer> param = quickSortStack.pop();
// 得到基准元素位置
int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
// 根据基准元素分成两部分,把每个部分的起止下标入栈
if(param.get("startIndex") < pivotIndex - 1) {
Map<String, Integer> leftParam = new HashMap<String, Integer>();
leftParam.put("startIndex", param.get("startIndex"));
leftParam.put("endIndex", pivotIndex - 1);
quickSortStack.push(leftParam);
}
if(pivotIndex + 1 < param.get("endIndex")) {
Map<String, Integer> rightParam = new HashMap<String, Integer>();
rightParam.put("startIndex", pivotIndex + 1);
rightParam.put("endIndex", param.get("endIndex"));
quickSortStack.push(rightParam);
}
}
}
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startInxdex];
int left = startIndex;
int right = endIndex;
while(left != right) {
// 控制 right 指针比较并且左移
while(left < right && arr[right] > pivot) {
right--;
}
// 控制 right 指针比较并且右移
while(left < right && arr[left] <= pivot) {
left++;
}
// 交换left 和 right 指向的元素
if(left < right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
// pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}
public static void main(String[] args) {
int[] arr = new int[] {2, 9, 4, 3, 8, 6, 2, 1, 5, 4, 7, 3, 8, 4, 9 , 3, 5, 2};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
总结
在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度: O(n^2)
最优时间复杂度: O(n log n)
平均时间复杂度: O(n log n)
但当元素个数比较少时(10^2的数量级左右),快排的速度跟冒泡相比并没有快很多,还有如果要排序的元素大部分都已经是排好顺序了时,快排效率会下降,但是其最坏情况是N^2(当元素全部是已经排好的顺序时),一般情况(也即平均效率)是N*Log2(N),最好情况是N(当元素全部是逆序时),快排的特点是元素越乱排序速度越快,所以可以看出,虽然元素少时使用快排并没有很大优势,但是在快排的最坏情况跟冒泡、选择排序(冒泡、选择排序其复杂度不受元素顺序影响,永远为N^2)一样,所以快排永远是最快的。
最后
今天的 排序算法之快速排序 就分享到这里,有问题欢迎大家留言,谢谢 ~
以上是关于排序算法快速排序的主要内容,如果未能解决你的问题,请参考以下文章