【问题标题】:Quicksort: Iterative or Recursive快速排序:迭代或递归
【发布时间】:2012-09-15 05:12:02
【问题描述】:

我了解了快速排序以及如何在递归和迭代方法中实现它。
在迭代方法中:

  1. 将范围 (0...n) 推入堆栈
  2. 使用枢轴对给定数组进行分区
  3. 弹出顶部元素。
  4. 如果分区(索引范围)包含多个元素,则将分区(索引范围)推入堆栈
  5. 做以上3个步骤,直到堆栈为空

递归版本是wiki中定义的正常版本。

我了解到递归算法总是比迭代算法慢。
那么,就时间复杂度而言,哪种方法更受欢迎(内存不是问题)?
哪一个足够快,可以在编程比赛中使用?
c++ STL sort() 是否使用递归方式?

【问题讨论】:

  • 您已经回答了自己。递归版本更短更清晰。迭代更快,让您模拟递归调用堆栈。
  • 但我的教授告诉我们递归堆栈深度与我们用于存储分区范围的堆栈相同。那么,迭代的速度有多快?
  • @sabari:您关于递归更快的假设是错误的。我对这些假设进行了统计测试,并根据结果编辑了答案。
  • 您可以使用队列而不是堆栈来实现快速排序的迭代版本。算法不需要额外的存储空间是 LIFO。堆栈方法更类似于快速排序常用的递归描述,但这实际上并不是算法的固有部分。 LIFO 结构可能会提供更好的参考局部性,因此它可能会更快,因为它对缓存更友好。
  • 我想区别在于堆空间与 staxk 空间之一。垃圾回收的惩罚很高。

标签: algorithm recursion quicksort iteration


【解决方案1】:

就(渐近)时间复杂度而言 - 它们都是相同的。

“递归比迭代慢”——这句话背后的原因是递归堆栈的开销(在调用之间保存和恢复环境)。
但是 - 这些是固定数量的操作,而不会改变“迭代”的数量。

递归和迭代快速排序都是O(nlogn)平均情况O(n^2)最坏情况


编辑:

只是为了好玩,我运行了一个带有附加到帖子的 (java) 代码的基准测试,然后我运行了wilcoxon statistic test,以检查运行时间确实不同的概率是多少

结果可能是决定性的(P_VALUE=2.6e-34,https://en.wikipedia.org/wiki/P-value。请记住,P_VALUE 是 P(T >= t | H),其中 T 是检验统计量,H 是原假设)。但答案不是你所期望的。
迭代求解的平均值为 408.86 ms,递归求解的平均值为 236.81 ms

(注意 - 我使用 Integer 而不是 int 作为 recursiveQsort() 的参数 - 否则递归会取得更好的效果,因为它不必将很多整数装箱,这也很耗时- 我这样做是因为迭代解决方案别无选择,只能这样做。

因此 - 您的假设不正确,递归解决方案比 P_VALUE=2.6e-34 的迭代解决方案更快(对于我的机器和 java 而言)。

public static void recursiveQsort(int[] arr,Integer start, Integer end) { 
    if (end - start < 2) return; //stop clause
    int p = start + ((end-start)/2);
    p = partition(arr,p,start,end);
    recursiveQsort(arr, start, p);
    recursiveQsort(arr, p+1, end);

}

public static void iterativeQsort(int[] arr) { 
    Stack<Integer> stack = new Stack<Integer>();
    stack.push(0);
    stack.push(arr.length);
    while (!stack.isEmpty()) {
        int end = stack.pop();
        int start = stack.pop();
        if (end - start < 2) continue;
        int p = start + ((end-start)/2);
        p = partition(arr,p,start,end);

        stack.push(p+1);
        stack.push(end);

        stack.push(start);
        stack.push(p);

    }
}

private static int partition(int[] arr, int p, int start, int end) {
    int l = start;
    int h = end - 2;
    int piv = arr[p];
    swap(arr,p,end-1);

    while (l < h) {
        if (arr[l] < piv) {
            l++;
        } else if (arr[h] >= piv) { 
            h--;
        } else { 
            swap(arr,l,h);
        }
    }
    int idx = h;
    if (arr[h] < piv) idx++;
    swap(arr,end-1,idx);
    return idx;
}
private static void swap(int[] arr, int i, int j) { 
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

public static void main(String... args) throws Exception {
    Random r = new Random(1);
    int SIZE = 1000000;
    int N = 100;
    int[] arr = new int[SIZE];
    int[] millisRecursive = new int[N];
    int[] millisIterative = new int[N];
    for (int t = 0; t < N; t++) { 
        for (int i = 0; i < SIZE; i++) { 
            arr[i] = r.nextInt(SIZE);
        }
        int[] tempArr = Arrays.copyOf(arr, arr.length);
        
        long start = System.currentTimeMillis();
        iterativeQsort(tempArr);
        millisIterative[t] = (int)(System.currentTimeMillis()-start);
        
        tempArr = Arrays.copyOf(arr, arr.length);
        
        start = System.currentTimeMillis();
        recursvieQsort(tempArr,0,arr.length);
        millisRecursive[t] = (int)(System.currentTimeMillis()-start);
    }
    int sum = 0;
    for (int x : millisRecursive) {
        System.out.println(x);
        sum += x;
    }
    System.out.println("end of recursive. AVG = " + ((double)sum)/millisRecursive.length);
    sum = 0;
    for (int x : millisIterative) {
        System.out.println(x);
        sum += x;
    }
    System.out.println("end of iterative. AVG = " + ((double)sum)/millisIterative.length);
}

【讨论】:

  • 您的测试主要显示 Stack 类的工作效率,而不是快速排序的迭代版本的效率。您可以使用更多的静态、自写堆栈,例如 64 个元素,并且推送和弹出操作会快得多。我想这会比递归更快!而且如果实现得当,最坏的情况下可以对 2^64 个元素进行排序,这在实际应用中已经足够了。
  • @Argeman:感谢您的评论。这实际上是基准测试的重点 - 堆栈解决方案非常依赖于堆栈实现,而调用堆栈已经针对其目的进行了相当多的优化,因此递归解决方案不太可能比迭代解决方案更糟糕,除非你花大量时间针对特定目的优化堆栈(我怀疑差异是否足以值得花时间)。正如我所说,这只是一个“有趣的测试”——答案背后的主要思想仍然存在——渐近时间复杂度是相同的。
  • P.S.损坏的打印格式(而不是使用Arrays.toString())是为了适应fon.hum.uva.nl/Service/Statistics/Wilcoxon_Test.html 中wilcoxon 在线计算器所期望的输入
  • @amit 你是如何运行“Wilcoxon 符号秩测试”的...你用过的任何工具吗?
  • @Geek:我把我使用的在线工具作为评论(以 P.S 开头的那个)。 PythonXY 也将其实现为scipy.stats.wilcoxon
【解决方案2】:

这就是我在 Javascript 中提出的解决方案。我认为它有效。

function qs_iter(items) {
    if (!items || items.length <= 1) {
        return items
    }
    var stack = []
    var low = 0
    var high = items.length - 1
    stack.push([low, high])
    while (stack.length) {
        var range = stack.pop()
        low = range[0]
        high = range[1]
        if (low < high) {
            var pivot = Math.floor((low + high) / 2)
            stack.push([low, pivot])
            stack.push([pivot+1, high])
            while (low < high) {
                while (low < pivot && items[low] <= items[pivot]) low++
                while (high > pivot && items[high] > items[pivot]) high--
                if (low < high) {
                    var tmp = items[low]
                    items[low] = items[high]
                    items[high] = tmp
                }
            }
        }
    }
    return items
}

如果发现错误请告诉我:)

【讨论】:

    【解决方案3】:

    递归并不总是比迭代慢。快速排序就是一个很好的例子。以迭代方式执行此操作的唯一方法是创建堆栈结构。因此,如果我们使用递归,则以其他方式与编译器执行相同的操作,并且您可能会比编译器做得更糟。如果您不使用递归(将值弹出并将值推送到堆栈),也会有更多的跳转。

    【讨论】:

    • 为什么要跳起来才能弹出和推送?
    • 如果它不是内联的,你(在大多数情况下)需要调用一些函数,所以它需要调用。
    • 只是确认一下,您是说使用运行时堆栈(递归)比用户创建的堆栈结构更有效吗? @amit 你有什么想法吗?
    • 我认为效率较低。因为您需要分配足够的堆来构建堆栈(如果可用内存用完,甚至需要重新分配),您需要将位置存储在某个地方,您需要检查极端情况(空/满堆栈)等。
    • 很久以前我看到应用程序崩溃过多,以及递归调用的编译器限制,新的日子还是这样吗?内存呢?仅仅因为硬件更好,并不意味着我们不应该在意......只要看看android world,你就会看到有多少应用程序和服务在运行,有些是新手写的,导致手机崩溃......跨度>
    猜你喜欢
    • 2020-08-14
    • 1970-01-01
    • 2014-09-05
    • 1970-01-01
    • 2013-10-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多