面试中最常见的排序算法--快排篇

649 阅读4分钟

这是我参与更文挑战的第 19 天,活动详情查看: 更文挑战

快速排序

快速排序也是我们在算法书里面认识的老朋友了。前面介绍了归并排序与二叉树的后序遍历有非常类似的地方,那么快速排序又和什么遍历相似呢?

是二叉树的前序遍历!前序遍历有两个重要的特点:

  • 拿到根结点的信息
  • 将根结点的信息,传递给左右子树

对于排序来说,有序性就是信息。因此,我们要做的事情就是把能拿到的有序信息,传递给左子数组和右子数组。

分析快排

1. 有序性的传递

对于快排而言,它传递有序性的手段就是将选择一个数 x,并且利用这个数,将数组分成三部分:

  • 小于 x 的部分放在数组的最前面
  • 等于 x 的部分放在数组的中间
  • 大于 x 的部分放在数组的最后面

2. 左右子数组的处理

此时,可以把小于 x 的部分当成二叉树中的左子树,大于 x 的部分当成二叉树的右子树。等于 x 的部分当成二叉树的根结点。

那么接下来要做的事情就是像前序遍历一样,递归地处理左子数组和右子数组。

相对于二叉树的前序遍历来说,快速排序也有它自己的特点:

  • 根结点的处理,需要执行“三路切分”操作,将一个数组切分为三段;
  • 左右子区间是由切分动态生成的,并不像二叉树那样由指针固定。

可以用伪代码表示如下:

function 前序遍历/快速排序():
    获取根结点的信息
    将根结点的信息传递左右子树/左右子数组

那么前序遍历/快速排序的考点就可以总结为以下 3 点:

  • 如何划分子结构
  • 获取根结点的信息
  • 如何将根结点的信息,传递给左右子树/左右子数组。

那么接下来我们就从上图中展示的三个方面入手,并且与二叉树的前序遍历的代码对照着一起看。

实操快排

1. 划分

首先我们看一下如何划分子数组。对于二叉树而言,子树的划分是天然的,已经在数据结构里面约定好了,比如 TreeNode.left、TreeNode.right。

但是对于数组而言,切分的时候,如果想到达最优的效率,那么将数组切为平均的两半效率应该是最高的(可以联想到二叉平衡树的效率)。但是快排不能保证选择一个数,就一定能将数组切分成为两半。

切分的结果如下:

利用x将数组 A[]切分为三段,[小于x的部分,等于x的部分,大于x的部分]
左子树 = [小于x的部分] = [b, l)根结点 = [等于x的部分] = [l, i)右 子树 = [大 于x的部分] = [i, e)

2. 子数组的递归

由于这里是排序,就需要分别对左子数组和右子数组进行排序。如果你还能记得“二叉树的前序遍历”,那么对子数组的排序应该也只需要递归就可以了。

// 二叉树的前序遍历拿左右子树的信息
preOrder(root.left);
preOrder(root.right);

快速排序则需要这么写:

// 快速排序去拿左右子数组的信息
qsort(a, b, l);
qsort(a, i, e);

最后,我们还需要考虑一下边界情况:

  • 当 b >= e 的时候,说明这个区间是一个空区间,没有必要再排序;
  • 当 b + 1 == e 的时候,说明只有一个元素,也没有必要排序。

以上两种边界情况可以对应到当二叉树为空,以及二叉树只有一个结点的情况。

完整代码

让我们一起写一下快速排序的代码吧(解析在注释里):

// 交换数组中两个元素的值 
void swap(int[] A, int i, int j) {
  int t = A[i];
  A[i] = A[j];
  A[j] = t;
}
// 将数组[b, e)范围的元素进行排序 void qsort(int[] A, int b, int e) {
  // 像二叉树一样,如果空树/只有一个结点,那么不需要再递归了 
  // 如果给定的区间段为空,或者只有一个结点。 
  if (b >= e || b + 1 >= e) {
    return;
  }
  // 取数组中间的元素作为x
  final int m = b + ((e - b) >> 1);
  final int x = A[m];
  // 三路切分
  int l = b, i = b, r = e - 1;
  while (i <= r) {
    if (A[i] < x) {
      swap(A, l++, i++);
    } else if (A[i] == x) {
      i++;
    } else {
      swap(A, r--, i);
    }
  }
  // 像二叉树的前序遍历一样,分别遍历左子树与右子树。
  qsort(A, b, l);
  qsort(A, i, e);
}
// 主函数,将数组nums排序 
void quickSort(int[] nums) {
  if (nums == null)
    return;
  qsort(nums, 0, nums.length);
}

复杂度分析:快速排序在较优情况下是 O(NlgN),在较差情况下是 O(N2)。