【问题标题】:Can quicksort be implemented in C without stack and recursion?可以在没有堆栈和递归的情况下在 C 中实现快速排序吗?
【发布时间】:2019-07-27 05:59:26
【问题描述】:

我找到了这个帖子How to do iterative quicksort without using stack in c? 但建议的答案确实使用了内联堆栈数组! (只允许固定数量的额外空间)

【问题讨论】:

  • 它确实需要一些类似堆栈的数据存储。您是否想利用这些信息做点什么?
  • Quicksort 要求您进行某种簿记。如果您使用的是迭代解决方案,您需要某种类似堆栈的数据结构来跟踪您的分区。
  • 任何递归函数都可以实现为迭代函数。但作为@EugeneSh。指出,您需要实现类似于堆栈的东西。
  • @EugeneSh。我只是好奇,因为我确实找到了堆排序和归并排序的无堆栈非递归解决方案。

标签: c sorting


【解决方案1】:

reference 页面中的代码大胆声明:

堆栈我的实现不使用堆栈来存储数据...

然而,函数定义有许多自动存储的变量,其中有 2 个具有 1000 个条目的数组,最终将使用固定但大量的堆栈空间:

//  quickSort
//
//  This public-domain C implementation by Darel Rex Finley.
//
//  * Returns YES if sort was successful, or NO if the nested
//    pivots went too deep, in which case your array will have
//    been re-ordered, but probably not sorted correctly.
//
//  * This function assumes it is called with valid parameters.
//
//  * Example calls:
//    quickSort(&myArray[0],5); // sorts elements 0, 1, 2, 3, and 4
//    quickSort(&myArray[3],5); // sorts elements 3, 4, 5, 6, and 7

bool quickSort(int *arr, int elements) {

  #define  MAX_LEVELS  1000

  int  piv, beg[MAX_LEVELS], end[MAX_LEVELS], i=0, L, R ;

  beg[0]=0; end[0]=elements;
  while (i>=0) {
    L=beg[i]; R=end[i]-1;
    if (L<R) {
      piv=arr[L]; if (i==MAX_LEVELS-1) return NO;
      while (L<R) {
        while (arr[R]>=piv && L<R) R--; if (L<R) arr[L++]=arr[R];
        while (arr[L]<=piv && L<R) L++; if (L<R) arr[R--]=arr[L]; }
      arr[L]=piv; beg[i+1]=L+1; end[i+1]=end[i]; end[i++]=L; }
    else {
      i--; }}
  return YES; }

缩进样式非常混乱。这是一个重新格式化的版本:

#define MAX_LEVELS  1000

bool quickSort(int *arr, int elements) {
    int piv, beg[MAX_LEVELS], end[MAX_LEVELS], i = 0, L, R;

    beg[0] = 0;
    end[0] = elements;
    while (i >= 0) {
        L = beg[i];
        R = end[i] - 1;
        if (L < R) {
            piv = arr[L];
            if (i == MAX_LEVELS - 1)
                return NO;
            while (L < R) {
                while (arr[R] >= piv && L < R)
                    R--;
                if (L < R)
                    arr[L++] = arr[R];
                while (arr[L] <= piv && L < R)
                    L++;
                if (L < R)
                    arr[R--] = arr[L];
            }
            arr[L] = piv;
            beg[i + 1] = L + 1;
            end[i + 1] = end[i];
            end[i++] = L;
        } else {
            i--;
        }
    }
    return YES;
}

请注意,1000 很大,但对于已经排序的中等大小数组上的病理情况来说是不够的。该函数仅在大小为 1000 的数组上返回NO,这是不可接受的。

对于算法的改进版本,一个低得多的值就足够了,其中较大的范围被推入数组,循环在较小的范围上迭代。这确保了一个包含 N 个条目的数组可以处理一组 2N 个条目。它在已排序的数组上仍然具有二次时间复杂度,但至少可以对所有可能大小的数组进行排序。

这是一个经过修改和检测的版本:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_LEVELS  64

int quickSort(int *arr, size_t elements) {
    size_t beg[MAX_LEVELS], end[MAX_LEVELS], L, R;
    int i = 0;

    beg[0] = 0;
    end[0] = elements;
    while (i >= 0) {
        L = beg[i];
        R = end[i];
        if (L + 1 < R--) {
            int piv = arr[L];
            if (i == MAX_LEVELS - 1)
                return -1;
            while (L < R) {
                while (arr[R] >= piv && L < R)
                    R--;
                if (L < R)
                    arr[L++] = arr[R];
                while (arr[L] <= piv && L < R)
                    L++;
                if (L < R)
                    arr[R--] = arr[L];
            }
            arr[L] = piv;
            if (L - beg[i] > end[i] - R) { 
                beg[i + 1] = L + 1;
                end[i + 1] = end[i];
                end[i++] = L;
            } else {
                beg[i + 1] = beg[i];
                end[i + 1] = L;
                beg[i++] = L + 1;
            }
        } else {
            i--;
        }
    }
    return 0;
}

int testsort(int *a, size_t size, const char *desc) {
    clock_t t = clock();
    size_t i;

    if (quickSort(a, size)) {
        printf("%s: quickSort failure\n", desc);
        return 1;
    }
    for (i = 1; i < size; i++) {
        if (a[i - 1] > a[i]) {
            printf("%s: sorting error: a[%zu]=%d > a[%zu]=%d\n",
                   desc, i - 1, a[i - 1], i, a[i]);
            return 2;
        }
    }
    t = clock() - t;
    printf("%s: %zu elements sorted in %.3fms\n",
           desc, size, t * 1000.0 / CLOCKS_PER_SEC);
    return 0;
}

int main(int argc, char *argv[]) {
    size_t i, size = argc > 1 ? strtoull(argv[1], NULL, 0) : 1000;
    int *a = malloc(sizeof(*a) * size);
    if (a != NULL) {
        for (i = 0; i < size; i++)
            a[i] = rand();
        testsort(a, size, "random");
        for (i = 0; i < size; i++)
            a[i] = i;
        testsort(a, size, "sorted");
        for (i = 0; i < size; i++)
            a[i] = size - i;
        testsort(a, size, "reverse sorted");
        for (i = 0; i < size; i++)
            a[i] = 0;
        testsort(a, size, "constant");
        free(a);
    }
    return 0;
}

输出:

random: 100000 elements sorted in 7.379ms
sorted: 100000 elements sorted in 2799.752ms
reverse sorted: 100000 elements sorted in 2768.844ms
constant: 100000 elements sorted in 2786.612ms

这是一个稍微修改过的版本,更能抵抗病理病例:

#define MAX_LEVELS  48

int quickSort(int *arr, size_t elements) {
    size_t beg[MAX_LEVELS], end[MAX_LEVELS], L, R;
    int i = 0;

    beg[0] = 0;
    end[0] = elements;
    while (i >= 0) {
        L = beg[i];
        R = end[i];
        if (R - L > 1) {
            size_t M = L + ((R - L) >> 1);
            int piv = arr[M];
            arr[M] = arr[L];

            if (i == MAX_LEVELS - 1)
                return -1;
            R--;
            while (L < R) {
                while (arr[R] >= piv && L < R)
                    R--;
                if (L < R)
                    arr[L++] = arr[R];
                while (arr[L] <= piv && L < R)
                    L++;
                if (L < R)
                    arr[R--] = arr[L];
            }
            arr[L] = piv;
            M = L + 1;
            while (L > beg[i] && arr[L - 1] == piv)
                L--;
            while (M < end[i] && arr[M] == piv)
                M++;
            if (L - beg[i] > end[i] - M) {
                beg[i + 1] = M;
                end[i + 1] = end[i];
                end[i++] = L;
            } else {
                beg[i + 1] = beg[i];
                end[i + 1] = L;
                beg[i++] = M;
            }
        } else {
            i--;
        }
    }
    return 0;
}

输出:

random: 10000000 elements sorted in 963.973ms
sorted: 10000000 elements sorted in 167.621ms
reverse sorted: 10000000 elements sorted in 167.375ms
constant: 10000000 elements sorted in 9.335ms

作为结论:

  • 是的,无需递归即可实现快速排序,
  • 不,没有任何本地自动存储就无法实现,
  • 是的,只需要固定数量的额外空间,但这仅仅是因为我们生活在一个小世界,其中数组的最大大小受可用内存的限制。本地对象的大小为 64 可以处理比 Internet 大小更大的数组,比当前的 64 位系统可以处理的要大得多。

【讨论】:

  • 用“是的,只需要恒定数量的额外空间”来狡辩:虽然这是一个实际问题,但这是因为要排序的输入的大小在实践中是有限的。比如说,宇宙中的电子数量,这个数量不会太多,无法通过合理的额外存储量来支持。
  • @JohnBollinger:确实我们生活在一个小世界里。
【解决方案2】:

显然,如here 所述,可以仅使用恒定数量的额外空间来实现非递归快速排序。这建立在 Sedgewick 的 work 之上,用于快速排序的非递归公式。它本质上不是保留边界值(低和高),而是执行线性扫描来确定这些边界。

【讨论】:

    【解决方案3】:

    嗯,它可以,因为我在 fortran IV 中实现了快速排序(这是很久以前的事了,在语言支持递归之前 - 这是为了赌注)。但是,您确实需要在某个地方(一个大数组就可以)在您进行个别工作时记住您的状态。

    递归要容易得多...

    【讨论】:

    • 也没有堆栈?
    • fortran iv 没有递归也没有堆栈
    • 不仅仅是函数调用的隐式堆栈……任何堆栈……
    • 当你说你需要一个大数组时,你实际上有一个堆栈。
    • 嗯,不。你有一个状态数组。这是OP要求的。他没有说“不使用任何内存”,而是说“不使用堆栈”
    【解决方案4】:

    根据定义,快速排序是一种“分而治之”的搜索算法,其思想是将给定的数组拆分为更小的分区。因此,您将问题划分为更容易解决的子问题。 在不使用递归的情况下使用快速排序时,您需要某种结构来存储您当时未使用的分区。 这就是postanswer 使用数组来使快速排序非递归的原因。

    【讨论】:

    • 用于簿记的内联数组实际上模仿了用于相同目的的独立堆栈的使用;但我的意图是将额外空间限制为仅恒定大小
    【解决方案5】:

    可以在没有堆栈和递归的情况下在 C 中实现快速排序吗?

    快速排序需要从每个非平凡分区向前遵循两条路径:每个(子)分区的新分区。需要将有关先前分区的信息(结果分区之一的边界)传递到每个新分区。那么,问题是这些信息在哪里?特别是,当程序在另一个分区上运行时,有关一个分区的信息保存在哪里?

    对于串行算法,答案是信息存储在堆栈或队列或其中之一的功能等效物上。总是,因为这些是我们为所需目的服务的数据结构的名称。特别是,递归是一种特殊情况,而不是替代方案。在递归快速排序中,数据存储在调用堆栈中。对于迭代实现,您可以在形式上实现堆栈,但也可以使用简单且相对较小的数组作为临时堆栈。

    但是堆栈和队列等价物可以走得更远。例如,您可以将数据附加到文件中以供以后回读。您可以将其写入管道。您可以通过通信网络将其异步传输给自己。

    如果你想发疯,你甚至可以嵌套迭代来代替递归。这将对可以处理的数组的大小施加一个硬上限,但并不像您想象的那么严格。通过一些小心和一些技巧,您可以处理具有 25 循环嵌套的十亿个元素的数组。这么深的巢穴会丑陋和疯狂,但仍然可以想象。人类可以手写。在这种情况下,一系列嵌套循环范围及其块范围变量可用作堆栈等价物。

    所以答案取决于“无堆栈”的确切含义:

    • 是的,您可以改用队列,但它需要具有与要排序的元素大致相同的容量;
    • 是的,您可以使用数组或其他某种顺序数据存储来模拟正式的堆栈或队列;
    • 是的,您可以将合适的等效堆栈直接编码到程序结构中;
    • 是的,您可能会想出其他更深奥的堆栈和队列版本;
    • 但是不,如果没有填充多级数据存储角色的东西(通常使用堆栈或等效堆栈),您将无法执行快速排序。

    【讨论】:

    • @rcgldr 选择算法在这里无济于事,因为您指出它本身是递归的,并且在结构上与 QuickSort 本身非常相似。
    • @john-bollinger 我已经完善了我的问题,我有兴趣删除 QuickSort 中的递归,但只愿意支付恒定数量的额外存储!因此,在这种情况下,即使是对数大小的堆栈(或类似堆栈)也是不允许的
    • @codeR,您预计需要排序多少数据? O(log n) 和 O(1) 之间的差异在实践中不太可能对您有意义。几个 64 元素的辅助数组应该足以快速排序一个 EB 大小的输入,而无需递归。
    • 但是,不,@codeR,虽然您可以保留一个很小的固定存储量,这足以满足您可能期望排序的任何输入,在抽象的算法分析中感觉,快速排序需要 O(log n) 开销。相对于需要 O(n^2) 步骤进行排序但只有 O(1) 开销的替代方案,这是使其性能得到改进的权衡之一。
    • @JohnBollinger - 我删除了我之前的评论。在我看来,嵌套循环将使用类似于固定大小堆栈的循环局部变量,现在有一个答案是使用不需要深度嵌套循环的固定大小数组。
    猜你喜欢
    • 2011-12-30
    • 2023-03-19
    • 2018-07-25
    • 2021-10-02
    • 2015-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-02
    相关资源
    最近更新 更多