自建网站 支付宝常州建站软件
这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
 https://space.bilibili.com/8888480?spm_id_from=333.999.0.0

1. 快速排序过程详解
1.1 逻辑解释
- 先设置一个数组 
arr[] = [ ], 下标是:0 ~ (n - 1), - 然后 
0 ~ (n - 1)位置随机选择一个数字:p, - 那么就按 
p为分界线, 将<= p的数字放在左边, 将> p的数字放在右边, - 然后将 
p这个数字放在左边范围的最右侧位置, (当然可能有很多个p, 但是我们不关心, 只是放一个) - 此时这个 
p数字算是排好序了. 在整个数组位置上就已经不用变了. - 然后从这个已经排好序的数字 
p的左边和右边位置调用递归过程排序就行了. (就是重复上面的过程), 每次都能将一个数字排好序. 
1.2 代码实例
这个过程和上面的逻辑实现是一样的, 直接看就能看懂, 这里我就直接说明几个需要注意的点.
l >= r:若是l == r说明只有一个数字, 不用排序, 若是l > r说明数组不存在.Math.random()方法的使用:这个Math.random()方法返回[0, 1)之间任意的一个浮点数, 我感觉说到这里已经能说明问题了, 这个应该是谁都要掌握的, 要是不知道, 就直接随便看一下讲解就行了. 真没有什么好讲的.
public static int[] sortArray(int[] nums) {  if (nums.length > 1) {  quickSort1(nums, 0, nums.length - 1);  }  return nums;  
}  // 随机快速排序经典版(不推荐)  
public static void quickSort1(int[] arr, int l, int r) {  if (l >= r) {  return;  }  // 随机这一下,常数时间比较大  // 但只有这一下随机,才能在概率上把快速排序的时间复杂度收敛到O(n * logn)  int x = arr[l + (int) (Math.random() * (r - l + 1))];  int mid = partition1(arr, l, r, x);  quickSort1(arr, l, mid - 1);  quickSort1(arr, mid + 1, r);  
}  // 已知arr[l....r]范围上一定有x这个值  
// 划分数组 <=x放左边,>x放右边,并且确保划分完成后<=x区域的最后一个数字是x  
public static int partition1(int[] arr, int l, int r, int x) {  // a : arr[l....a-1]范围是<=x的区域  // xi : 记录在<=x的区域上任何一个x的位置,哪一个都可以  int a = l, xi = 0;  for (int i = l; i <= r; i++) {  if (arr[i] <= x) {  swap(arr, a, i);  if (arr[a] == x) {  xi = a;  }  a++;  }  }  swap(arr, xi, a - 1);  return a - 1;  
}  public static void swap(int[] arr, int i, int j) {  int tmp = arr[i];  arr[i] = arr[j];  arr[j] = tmp;  
}
 
2. partition 函数详解
 
这段代码会有两个分支, 就是 <= x 和 > x, 所以最开始, 设置 i 对传递进来的数组范围上进行遍历,
- 若是 
i遍历到的数字<= x, 就让arr[a] 和 arr[i]进行互换(这一步是到了i遍历的数字> x的时候进行交换, 为以后做铺垫), 然后将a++, i++. 此时<= x范围的的数字扩大了. - 若是 
i遍历到的数字> x, 就让a不变, i++, - 通过上述过程, 
i遍历完数组的数字之后, 可以实现将所有<= x的数字放在数组的左侧, 将所有> x的数字放在数组的右侧. - 然后将 
x数字归位到a - 1位置. 因为这样才能保证x数字在数组中已经是排好序了. - 最后返回 
a - 1下标. 
其中的 a, xi 变量, 左程云老师都在代码的注释中说明好了, 就不画蛇添足了.
// 已知arr[l....r]范围上一定有x这个值  
// 划分数组 <=x放左边,>x放右边,并且确保划分完成后<=x区域的最后一个数字是x  
public static int partition1(int[] arr, int l, int r, int x) {  // a : arr[l....a-1]范围是<=x的区域  // xi : 记录在<=x的区域上任何一个x的位置,哪一个都可以  int a = l, xi = 0;  for (int i = l; i <= r; i++) {  if (arr[i] <= x) {  swap(arr, a, i);  if (arr[a] == x) {  xi = a;  }  a++;  }  }  swap(arr, xi, a - 1);  return a - 1;  
}  
 
3. Partition 函数优化
 
3.1 优化逻辑
在经典的 Partition 函数中, 只是将所有的位置分成了两个区域, 一次只能是调整好一个数字, 但是有可能在一个数组中, 可能有很多个相同的数字, 比如在随机选择之后, 我们选择了 x, 但是这个 x 在这个数组中, 有很多个, 那我们干脆可以将所有与 x 相等的数字都排好序, 所以对应的:可以将这个数组分为三个区域:左边是 < x 的区域, 中间是 == x 的区域, 右边是 > x 的区域, 这样就有可能一次将数组中的多个数字排好序.
3.2 代码实例
讲解:我们随机选择的一个数字:x, 还是和原来一样, 设置一个 i 遍历数组中的所有数字, 将传进来的数组的最左边设置为 l, 最右边设置为:r, 分为三种情况.
- 若是 
< x, 则交换a 和 i, 然后将a++, i++. - 若是 
== x, 则只是i++. - 若是 
> x, 则交换b 和 i, 然后将b--, i 不变. - 按照上述步骤, 可以将数组分为三个部分.
 
这个的时间复杂度为:O(n), 因为只是用 i 遍历了整个数组.
需要注意的是:这个函数是有返回值的, 只是用全局变量进行等效果的替换了. 使用的方式和意义及其注意点左程云老师都在注释里说明白了.
// 随机快速排序改进版(推荐)  
public static void quickSort2(int[] arr, int l, int r) {  if (l >= r) {  return;  }  // 随机这一下,常数时间比较大  // 但只有这一下随机,才能在概率上把快速排序的时间复杂度收敛到O(n * logn)  int x = arr[l + (int) (Math.random() * (r - l + 1))];  partition2(arr, l, r, x);  // 为了防止底层的递归过程覆盖全局变量  // 这里用临时变量记录first、last  int left = first;  int right = last;  quickSort2(arr, l, left - 1);  quickSort2(arr, right + 1, r);  
}  // 荷兰国旗问题  
public static int first, last;  // 已知arr[l....r]范围上一定有x这个值  
// 划分数组 <x放左边,==x放中间,>x放右边  
// 把全局变量first, last,更新成==x区域的左右边界  
public static void partition2(int[] arr, int l, int r, int x) {  first = l;  last = r;  int i = l;  while (i <= last) {  if (arr[i] == x) {  i++;  } else if (arr[i] < x) {  swap(arr, first++, i++);  } else {  swap(arr, i, last--);  }  }  
}
 
4. 时间复杂度分析

这个随机行为的时间复杂度需要用期望来估计, 这个已经在前面的时间复杂度章节中说明过了.
4.1 最差情况
最慢的情况是:arr[] = [1, 2, 3, 4, 5, 6, 7], 若是选择了一个最右侧的数字 7, 那就要将左边所有的数字遍历一遍, 将 7 放到正确位置之后, 选择 6, 继续进行遍历, 最后都排好序了, 这个的时间复杂度是:O(n^2).
空间复杂度是:O(n), 因为按照最差情况, 递归过程压的层数是 n 层.
4.2 最优情况
最优情况是随机选择之后的数字在整个数组的最中间位置(按数值大小). 所以递归过程是两个相同的子过程 (近似看成), 然后 Partition 过程的时间复杂度是:O(n), 所以根据 master 公式,
 时间复杂度 == 2 * T(N/2) + O(n) == O(n * log(n)).
空间复杂度:因为此时是最优的情况, 所以对应的递归过程也是一个最好的情况, 递归到最底层之后, 返回, 然后进行另一个递归过程, 此时占用的空间是原来最底层的函数使用过的, 是重复利用了的空间, 所以假设数组长度是:n, 所以这个就是一个二分的过程, 空间复杂度:O(log(n)).
当然也有可能是一般情况, 有可能还是比较靠近两侧, 或者是比较靠近中间, 但不是最优情况, 也不是最差情况.
所以根据期望, 最后的时间复杂度是:O(n * log(n)), 空间辅助度是:O(log(n)). 而且这个不是最好情况的时间复杂度和空间复杂度, 这是数学家们根据所有的情况统计之后, 进行严密的论证之后的答案, 是可以和最好的情况相等, 但是不是最好情况直接用的.
注意:这个证明过程很复杂, 不用掌握也不影响后续的学习.
