前言
聊一聊快排的实现思路、性能
快排相对冒泡排序的巧妙之处
首先,快排在大部分情况下比冒泡排序的性能更好,即大部分时候,快排的划分(遍历/比较)次数比冒泡排序要少。
冒泡排序的特点是数组中任意两个数都会发生一次比较,进而最终确定一个数跟其他数的大小关系。
显然,这其中有一些多余的操作。因为,如果一个数x
大于另一个数y
,那么x
势必会大于所有小于等于y
的数,所以,x
完全没必要跟所有小于等于y
的数进行比较就能确定x跟这些数的大小关系。而快排就是利用了这个特点从而使得大部分时候其性能都要优于冒泡排序。
快排的实现思路
由上面的巧妙之处的描述可知,我们应该在数组中找到一个数,这里暂且称这个数为”基准值”,能够把数组划分成两部分,两部分跟这个数有绝对的大小关系(左大右小或者左小右大),且这两个部分数据量越均匀越好(不均匀问题也不大,只要不出现一边为空的情况,就都是一个数量级)。
而我们只需要一次遍历就可以得到数组中所有数跟基准值的大小关系,同时也知到了基准值在数组中的最终位置。接下来只需要递归这个过程直到数组不可拆分即可。
最后上代码:
public int[] sortArray(int[] nums) {
doSort(nums, 0, nums.length - 1);
return nums;
}
public void doSort(int[] arr, int left, int right) {
if (left < right) {
int partition = partition(arr, left, right);
doSort(arr, left, partition - 1);
doSort(arr, partition + 1, right);
}
}
public int partition(int[] arr, int left, int right) {
int i = new Random().nextInt(right - left) + left;
//基准值
int pivot = arr[i];
//把基准值跟最后一个值做交换,那么right就成了一个空位,可以用来直接赋值而不用再保存其值
swap(arr, i, right);
int currLeft = left;
int currRight = right;
while (currLeft < currRight) {
while (currLeft < currRight) {
if (arr[currLeft] > pivot) {
//赋值前currRight是空位,可以直接赋值,赋值后因为currLeft的值已经被保存下来了,所以currLeft成了空位
// 这时就可以从右往左再继续找比基准值小的数来赋值到currLeft这个空位上
arr[currRight] = arr[currLeft];
//赋值完成后currRight对应的值肯定比基准值大,所以后面的循环从currRight左边一位依次往左找到第一个比基准值小的数
currRight--;
break;
}
currLeft++;
}
while (currLeft < currRight) {
if (arr[currRight] < pivot) {
arr[currLeft] = arr[currRight];
currLeft++;
break;
}
currRight--;
}
}
//把基准值放到准确的位置
arr[currLeft] = pivot;
//返回基准值应该在的位置
return currLeft;
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快排的性能
每一次递归,都会挑出一个基准值来对原数组进行分区。也就是说,递归的每一层划分(访问元素)的次数都比上一层少一次。所以,每一层的划分是同一个数量级,即原数组的大小n
。
另外,数组递归的层数跟递归的大小有关,所以只要不是极端情况,每一层拆分出来的子分区(子数组)跟原数组势必是一个倍数的关系。
假设这个倍数是
c
(
1
2
≤
c
<
1
)
c(\frac{1}{2}\leq c < 1)
c(21≤c<1) (取多的那一边),那么,最终的层数h
显然满足
n
∗
c
h
=
1
n*c^h=1
n∗ch=1(最后无法拆分为止)。所以
h
=
log
c
(
1
n
)
h=\log_c(\frac{1}{n})
h=logc(n1) ,从而得出
h
=
log
1
c
n
h=\log_\frac{1}{c}n
h=logc1n (
1
<
1
c
≤
2
1<\frac{1}{c}\le2
1<c1≤2)。
所以最终的划分次数/时间复杂度 O = n h = n ∗ l o g 1 c n O = nh=n*log_\frac{1}{c}n O=nh=n∗logc1n ,而 n ∗ l o g 1 c n n*log_\frac{1}{c}n n∗logc1n跟 n ∗ l o g 2 n n*log_2n n∗log2n是一个数量级的。所以也就可以认为:快排在非极端情况下的时间复杂度是 n ∗ l g n n*lgn n∗lgn。
精确的推导过程可参考《算法导论》第7章第3节。