为什么说快速排序是树的前序遍历?

485 阅读3分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

先声明下我们的快速排序的风格:

区间使用经典的左闭右开[0,1)风格

我们先实现实现一个比较naive的快速排序。 先是快速排序的划分算法,为了简单起见,就不使用双指针的方式了,直接像以往数组去重一样做考虑,像先前那种数组去重那样考虑,假设我们有一个额外的数组,然后每次把相比于枢纽小的值放到这个额外数组中,而没被放到数组中的元素就属于另外一部分,这样分开了两部分。

然后我们这样考虑,额外数组是否是必须的,也就是如果我们使用数组本身作为额外的数组,如果像先前的数组去重一样,原来的数据就是不需要的了,可以直接覆盖掉。但是这里不行,为了即将被覆盖的数组还需要在后续的排序中使用,所以这里通过交换来完成类似的效果。这样小的数据全部会在数组的前面,而大的数据则被放到后面。而作为比较的基准的值可以交换到中间,这样就完成了快速排序需要的划分功能。 代码实现像下面这样:


def partition(self, arr, low, high):
    pivot = high - 1
    p = low
    for i in range(low, pivot):  # [low,pivot)
        if arr[i] < arr[pivot]:
            arr[i], arr[p] = arr[p], arr[i]
            p += 1
    arr[pivot], arr[p] = arr[p], arr[pivot]
    print(arr)
    return p

有了这个函数,实际上快速排序基本上可以说已经完成了,因为快速排序做的就是它的排序思路就是不排序,每次都做划分,当分到不能分的时候,也就完成了排序。 所以我们像下面这样写快速排序的代码

def quick_sort(self, arr: List, low: int, high: int):
    if high - low > 1:
        pivot = self.partition(arr, low, high)
        self.quick_sort(arr, low, pivot)
        self.quick_sort(arr, pivot + 1, high)

这个实现比较naive,因为没有引入随机化。

接下来从树的角度来看一下这个过程,顺便分析一下它的复杂度。 如果把划分的过程看作选取树节点的值的话,后续的两个递归调用可以看作访问左子树和右子树。这也是典型的dfs的过程。不过我们还有另一种看法,假设是这个过程是一个建立树的过程,递归调用是分别建立左右子树的过程。这样可能更合适。如果有兴趣的话,力扣上倒是有一个与这个过程非常像的题目,不过它不用分区,而是把有序数组变成一个二叉搜索树,所以更简单些。

如果是建立树的话,经典的极端情况就是二叉树会退化成链表,会失去树的层次优势。对于快排也是这样,当退化成链表时,每次都是仅仅比上一层少1个元素参与比较。而由于划分函数的复杂性是线性的,所以复杂性就和自然数的和成正比,也就是平方阶。

引入随机化就是为了避免每次建树或者说排序像上述的情况,使每层以指数形式减少待处理的元素数量。这样复杂度就降低到了对数阶。

既然是前序遍历,自然也少不了用栈模拟这个经典的做法,在此我们同样可以写一个naive的栈式快排。

def quick_sort_stack(self, arr: List):
    stack = [(0, len(arr))]
    while stack:
        low, high = stack.pop()
        pivot = self.partition(arr, low, high)
        if pivot - low > 1:
            stack.append((low, pivot))
        if high - pivot > 1:
            stack.append((pivot + 1, high))

如有批评指正,欢迎评论区留言。