快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
①. 从数列中挑出一个元素,称为”基准”(pivot)。
②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
实现思路:
快速排序的算法思想其实比较简单,先找出一个数作为基准数(通常取数组最小下标或者最大下标位置的数字)。定义两个变量作为“哨兵”,然后分别从后向前,从前向后两个方向去“探测”:
- 从后向前:寻找比基准数小的数据,如果找到,停下来
- 从前向后:寻找比基准数大的数据,如果找到,停下来
- 如果两个方向的“探测”都找到了符合要求的数据,则交换数据,继续顺着方向寻找
- 直到两个哨兵碰到一起,此时把相遇位置上的数据和基准数(即数组的中间位)交换数据
- 此时,基准数左侧的数都小于等于基准数,右侧的数都大于等于基准数
- 同样的方法去“探测”基准数左侧和右侧的数据(使用递归)
代码如下:
public static void QuickSort(int[] arr){
// 定义两个哨兵 分别为 数组的最小下边 和数组的最大下标
sort(arr,0,arr.length-1);
}
public static void sort(int[] arr , int left ,int right){
if(left<right){
// 通过基准数字将数组左右分为两个子数组
// 数组中小于基准的数放在左边 大于基准的数字调换到右边
// 递归子数组
int pvtion = partion(arr,left,right);
sort(arr,left,pvtion-1);
sort(arr,pvtion+1 , right);
}
}
public static int partion(int[] arr , int left ,int right){
int pvtion = arr[left]; //依数组中最左边的数字作为基准
while (left < right){ // 左边数组下标 小于右边数组下标
while (arr[right] >pvtion && left<right)
right--; //当右边的哨兵位置上的数字大于基准数字 哨兵左移 直达哨兵到达位于从右边开始第一个小于基准的数字
if(left< right){
// 把最右边第一个小于基准的数字换到最左边位置
// 因为我们以最左边的哨兵为基准 ,所以直接赋值到该哨兵位置
arr[left++] = arr[right]; //类似于 arr[left] = arr[right];left++;
}
while(arr[left] <= pvtion && left<right)
left++;//当左边的哨兵位置上的数字小于基准数字 哨兵右移 直达哨兵到达位于从左边开始第一个大于基准的数字
if(left <right){
arr[right--] = arr[left]; //把该大于基准的数字的换到右边
}
}
arr[left] = pvtion; //把基准数字放在中间位置
return left; // 返回中间位置的下标
}
以下是快速排序算法复杂度:
最坏时间复杂度 | |
---|---|
最优时间复杂度 | |
平均时间复杂度 | |
最坏空间复杂度 | 根据实现的方式不同而不同 |
虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常平均来看, 它的时间复杂为O(nlogn), 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高.
Tips: 同选择排序相似, 快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 因此, 快速排序并不稳定.