排序算法杂谈(四) —— 快速排序的非递归实现

1. 前提

排序算法(七) —— 快速排序

排序算法杂谈(三) —— 归并排序的非递归实现

 

 

2. 快速排序与归并排序的递归

快速排序(Quick Sort)与归并排序(Merge Sort)虽然都采用了递归地思想,但是其递归地本质却有所不同。

  • 快速排序,手动划分,自然有序。
  • 归并排序,自然两分,手动合并。

 

快速排序,是先通过划分(partition)算法,将数组两分,划分的过程中,比主元(pivot)小的数字全部被划分到了左侧,比主元大的数字全部被划分到了右侧。

然后对两分的数组进行递归。当数组两侧的长度均小于等于1,那么数组就自然有序了。

归并排序,是将原数组二等分,直到被等分的数组长度小于等于1,那么被等分的数组就有序了,然后对这等分的数组进行合并。

 

所以说,快速排序与归并排序,正好代表了递归的两种典型,如果将递归的过程看做是一颗二叉树,那么:

  • 快速排序:下层递归的实现,依赖上层操作的结果。(只有父节点操作完成,才能对子节点进行递归)
  • 归并排序:上层递归的操作,依赖下层递归的结果。(只有子节点全部操作完成,才可以操作父节点)

 

 

3. 快速排序非递归实现的堆栈模型 stack 与 记录模型 record

 

快速排序这种,优先操作,然后递归的特点,大大简化了构造目标堆栈模型的难度。

在归并排序中,不难发现,其构造目标堆栈模型的过程,是不断入栈的过程,最后一次性地处理堆栈信息。

 

相反,在快速排序中,目标堆栈是一个不断 入栈-出栈 的过程,在出栈的过程中,就对数据进行处理,没有必要再最后一次性处理。

而且,由于划分具有不稳定性,所以没有办法给出确切的堆栈模型。

 

快速排序的递归过程,只需要关心其左边与右边的坐标:

    private static class Record {
        int left;
        int right;

        private Record(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

 

 

 4. 快速排序非递归的过程

 

快速排序非递归的执行过程中,只需要一个堆栈空间,其运行过程如下:

  • 对原数组进行一次划分,分别将左边的 Record 和 右边的 Record 入栈 stack。
  • 判断 stack 是否为空,若是,直接结束;若不是,将栈顶 Record 取出,进行一次划分。
  • 判断左边的 Record 长度(这里指 record.right - record.left + 1)大于 1,将左边的 Record 入栈;同理,右边的 Record。
  • 循环步骤 2、3。

 

于是,有如下代码:

public final class QuickSortLoop extends BasicQuickSort {

    private Stack<Record> stack = new Stack<>();

    @Override
    public void sort(int[] array) {
        int left = 0;
        int right = array.length - 1;
        if (left < right) {
            int pivot = partitionSolution.partition(array, left, right);
            if (pivot - 1 >= left) {
                stack.push(new Record(left, pivot - 1));
            }
            if (pivot + 1 <= right) {
                stack.push(new Record(pivot + 1, right));
            }
            while (!stack.isEmpty()) {
                Record record = stack.pop();
                pivot = partitionSolution.partition(array, record.left, record.right);
                if (pivot - 1 >= record.left) {
                    stack.push(new Record(record.left, pivot - 1));
                }
                if (pivot + 1 <= record.right) {
                    stack.push(new Record(pivot + 1, record.right));
                }
            }
        }
    }

    private static class Record {
        int left;
        int right;

        private Record(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }
}

 

如果 Record 模型过于简单,可以直接通过入栈-出栈 具体的数据来简化这个过程。

 

 

 5. 关于递归转循环需要知道的事情

 

通过归并排序和快速排序非递归实现的讲解,似乎将其转化为循环是一个更佳的做法,其实不然,它只适用于特定的场景。

关于这种方法,需要有如下的认知:

  • 递归的代码,在很多时候比循环的代码更加容易理解。
  • 递归转循环,在效率上并没有提高。相反,由于增加了构造堆栈模型的过程,其消耗的时间更多。
  • 只有当递归的层数过多,而导致 StackOverFlow 的问题出现,才考虑使用递归转循环的方法。
  • 可以通过调整 JVM 参数,来达到扩充堆栈空间的目的,但是一般不推荐这么做,因为这个影响是整体的。
  • 从代码的角度,如果循环能够解决问题,那么就使用循环;如果递归能解决问题,那么就使用递归,没有必要特意去做两者的转换。

 

posted @ 2018-05-31 18:53  Gerrard_Feng  阅读(10959)  评论(0编辑  收藏  举报