快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。是一种分治思想的应用。
快速排序是通过递归调用“切分”来实现的。
一、切分(partition)
假设j是一个切分点,给定数组为a,切分点j要满足以下三个条件:
1、对于j,a[j]已经排定
2、a[lo]到a[j-1]都不大于a[j]
3、a[j+1]到a[hi]都不小于a[j]
切分的实现方法:首先取数组第一个元素为切分元素pivot=a[lo],之后用左指针从数组左端到数组右端扫描,直到找到一个不小于pivot的元素,同样用右指针从数组右端到左端扫描,找到一个不大于pivot的元素,之后交换两个指针元素的位置,如此下去直到两个指针相遇停止。这样就可以保证左指针左侧的元素都不大于pivot,右指针右侧的元素都不小于pivot,这样交换pivot与a[右指针],然后返回右指针,这样右指针的位置即为切分点,切分元素为pivot。
举个例子,假设给定数组是a = [7 3 5 8 1 4 10 13],以第一个元素7为切分元素。
可以看到最后切分元素的右边子数组的元素都大于切分元素,左边子数组的元素都小于切分元素,再对左右两个子数组分别切分,不断地重复这个过程,就能实现快速排序。
private static int partition(int[] a, int lo, int hi) {
// 取分割点,分割点左边元素都小于分割元素,右边元素都大于分割元素
int i = lo, j = hi + 1;
while (true) {
// 由左及右寻找大于等于lo的元素位置
while (a[++i] < a[lo]) if (i == hi) break;
// 由右及左找到小于等于lo的元素位置
while (a[--j] > a[lo]) if (j == lo)break;
// 当左右指针相遇时停止扫描
if (i >= j) break;
// 否则交换左右指针元素
shuffle.swap(a, i, j);
}
// 交换切割元素与右指针元素
shuffle.swap(a, lo, j);
return j;
}
二、快速排序
前面说过快排就是递归调用切分来实现的,递归停止条件就是数组元素为1时返回,子问题是找到切分点,排序切分点左右子数组sort(lo,j-1)、sort(j+1, hi)。
private static void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int j = partition(a, lo, hi); // 找到数组的分割点
sort(a, lo, j-1);
sort(a, j+1, hi);
}
三、代码
Java:
import java.util.Arrays;
public class QuickSort {
// 实现快速排序
public static void sort(int[] a) {
shuffle.Shuffle(a); // 随机打乱输入数组,从而降低对输入的依赖
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int j = partition(a, lo, hi); // 找到数组的分割点
sort(a, lo, j-1);
sort(a, j+1, hi);
}
private static int partition(int[] a, int lo, int hi) {
// 取分割点,分割点左边元素都小于分割元素,右边元素都大于分割元素
int i = lo, j = hi + 1;
while (true) {
// 由左及右寻找大于等于lo的元素位置
while (a[++i] < a[lo]) if (i == hi) break;
// 由右及左找到小于等于lo的元素位置
while (a[--j] > a[lo]) if (j == lo)break;
// 当左右指针相遇时停止扫描
if (i >= j) break;
// 否则交换左右指针元素
shuffle.swap(a, i, j);
}
// 交换切割元素与右指针元素
shuffle.swap(a, lo, j);
return j;
}
public static void main(String[] args) {
int[] a = {3,2,6,7,1};
sort(a);
System.out.println(Arrays.toString(a));
}
}
python:
class Sort:
def __init__(self, nums):
self.nums = nums
def sort(self, nums):
length = len(nums)
print("所给数组为:%s"%nums)
self.quicksort(nums, 0, length-1)
def quicksort(self, nums, lo, hi):
if(lo >= hi): return
part = self.partition(nums, lo, hi)
self.quicksort(nums, lo, part-1)
self.quicksort(nums, part+1, hi)
def partition(self, nums, lo, hi):
print(nums[lo:hi+1])
left = lo + 1
right = hi
while True:
while (nums[lo] < nums[left]):
if (left == hi):
break
left += 1
while (nums[lo] > nums[right]):
if (right == lo):
break
right -= 1
if(left >= right):
break
self.exch(nums, left, right)
self.exch(nums, lo, right)
print("切分点为: %d"% nums[right])
return right
def exch(self, nums, left, right):
temp = nums[left]
nums[left] = nums[right]
nums[right] = temp
四、时间复杂度分析
参考:https://www.cnblogs.com/fengty90/p/3768827.html
最好情况:选择切分点时每次都将数组切分的很均匀,如果要排序的数组有n个元素,那么递归树的深度即为+1 (这个不理解可以回忆一下完全二叉树的深度),即需要递归log2n次,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去因此最好情况下快排的时间复杂度为O(nlogn)
最坏情况:
当待排序的序列为正序或逆序排列时,且每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为 ,最终其时间复杂度为O(n^2)。