zoukankan      html  css  js  c++  java
  • 排序算法杂谈(四) —— 快速排序的非递归实现

    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 参数,来达到扩充堆栈空间的目的,但是一般不推荐这么做,因为这个影响是整体的。
    • 从代码的角度,如果循环能够解决问题,那么就使用循环;如果递归能解决问题,那么就使用递归,没有必要特意去做两者的转换。
  • 相关阅读:
    git爬坑不完全指北(二):failed to push some refs to ‘XXX’的解决方案
    javascript精雕细琢(三):作用域与作用域链
    javascript精雕细琢(二):++、--那点事
    git爬坑不完全指北(一):Permission to xxx.git denied to user的解决方案
    深入浅出CSS(三):隐藏BOSS大盘点之默认属性小总结
    读书笔记
    MPP5运维手册
    HTML自闭合(self-closing)标签
    Mysql JDBC的通信协议(报文的格式和基本类型)
    详解 & 0xff 的作用
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/9118376.html
Copyright © 2011-2022 走看看