【问题标题】:Read an array recursively递归读取数组
【发布时间】:2017-06-07 22:09:15
【问题描述】:

我正在学习如何将递归应用于数组。

例如,我通常以迭代方式读取数组,这样:

void read_array(int *a, int n){
        int i;
        for(i = 0; i < n; ++i)
                scanf("%d", &a[i]);
        return;
}

我想递归地读取一个数组。我写了以下函数:

void read_array(int *a, int n){
        int i = n - 1;
        if (n < 0)
                return;
        else{
                if(scanf("%d", &a[n - 1 - i]) == 1){
                        read_array(a, n - 1);
                        return;
                }
        }
}

它可以编译,但在运行时会引发分段错误错误。它让我感到困惑,因为该函数考虑了一个应该停止它的基本情况 0

【问题讨论】:

  • 首先n的初始值是多少?它是数组中的最大索引还是元素数?您为数组分配了 空间?并使用调试器逐行单步执行代码,同时监控变量及其值以及所有操作的结果。
  • 你为什么使用scanf来读取数组? printf 可以做到这一点,而不会成为 C 编程中最复杂的函数。还是我误解了您所说的“阅读”是什么意思?
  • @Worice 之所以给学生讲递归,是因为当学生后来成为老师时,可以给学生讲递归。
  • 除了玩笑之外,递归被大大高估了,在现实世界的编程中没有太多的位置。将数学和算法理论转化为实用的东西在很大程度上是一次失败的尝试。唯一的实际用途非常狭窄,例如在实现二叉搜索树时,您想要优化树占用的内存,代价是执行速度和峰值堆栈使用。 99% 认为他们找到了递归用途的人是错误的。主要是浪费时间:执行时间、学生时间、代码阅读器时间。
  • @Lundin 抱歉,这是您的意见,所以不在这里讨论这个问题。递归是一个概念,无论你想要什么,它都可以用于好的目的或坏的目的。但是你不能说这个概念不好,这是你的意见。

标签: c arrays recursion


【解决方案1】:

您对数组索引的计算是错误的。这一行:

            if(scanf("%d", &a[n - 1 - i]) == 1){

假定n初始 值,但同时,您减少 n 在每个递归步骤中。话虽如此,它不应该崩溃,而只是重复写入a 的第一个元素,因为i = n - 1n - 1 - i 始终为零。

编写这种递归的惯用方法是在i上递归:

void read_array(int *a, int n, int i)
{
    if (i < n)
    {
        if(scanf("%d", &a[i]) == 1)
        {
            read_array(a, n, i+1);
        }
    }
}

并使用i 的初始值调用它,例如read_array(a, 10, 0) 用于读取 10 元素数组。

在实践中,应避免 C 中的递归。*

* 函数式语言通常可以优化递归,C 只是使用调用堆栈,开销很大。


在这个例子中,编写一个函数的递归的理论目的在某种程度上被一个返回void的函数所破坏。如果这只是学习原理,那么函数实际上应该返回一些东西。例如,您可以创建一个功能性“列表构建器”:

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

// place the side effect in a separate function
int getValue(void)
{
    // could have `scanf()` here:
    return rand();
}

typedef struct List
{
    int a[10];
    size_t length;
} List;

// non-functional helper to get around limitations of C:
// (if it could initialize result directly with the new values, it would
// be functional)
List listAppend(List list, int val)
{
    List result = list;
    result.a[result.length++] = val;
    return result;
}

// recursive function without side effects:
List buildList(List list, int (*value)())
{
    if (list.length >= 10) return list;
    return buildList(listAppend(list, value()), value);
}

int main(void)
{
    List myList = buildList((List){0}, &getValue);

    for (size_t i = 0; i < myList.length; ++i)
    {
        printf("myList.a[%zu] is %d\n", i, myList.a[i]);
    }
}

【讨论】:

  • 感谢 Felix 提供如此优雅的解决方案以及您为我提供的详细信息。我真的不喜欢递归,但我们的教授完全喜欢。对我来说原因仍然模糊不清。无论如何,int i 应该假定与0 的值相同,对吧?此外,您的代码是否有可能超出必要地显示}
  • 复制和粘贴不适合删除您的else,我将重新格式化。是的,了解递归很重要,但我还是想说不要在 C 中这样做。
  • @FelixPalmen C 可以优化尾递归的一些(如果不是全部)情况,跳转到函数的开头而不是增加堆栈。
  • @ArturR.Czechowski 很好。我刚刚添加了一个“更好”的递归,我想这很难优化;)
  • @Worice 这里有一些更新让你看到更接近纯函数递归的东西。
【解决方案2】:

函数有bug。

由于变量i的初始化方式如下

int i = n - 1;

然后是这个调用中的第二个参数

scanf("%d", &a[n - 1 - i])

被评估为

scanf("%d", &a[n - 1 - (n - 1)])

也就是说它总是等于零

scanf("%d", &a[0])

由于使用指针a 的相同值调用递归函数,因此所有输入的值都分配给a[0]。数组的所有其他元素仍未初始化。

虽然这不能作为函数执行异常的原因。

可能是使用了大数组,堆栈太小,无法递归调用函数。

在任何情况下,函数都可以通过以下方式更简单、更正确地定义

size_t read_array( int *a, size_t n )
{
    return n && scanf( "%d", a ) == 1 ? 1 + read_array( a + 1, n - 1 ) : 0;
}   

考虑到输入可能会被用户打断。在这种情况下,函数返回数组的已初始化元素的数量。

这是一个演示程序。

#include <stdio.h>

size_t read_array( int *a, size_t n )
{
    return n && scanf( "%d", a ) == 1 ? 1 + read_array( a + 1, n - 1 ) : 0;
}   

#define N   10

int main(void) 
{
    int a[N];

    size_t n = read_array( a, N );

    for ( size_t i = 0; i < n; i++ ) printf( "%d ", a[i] );
    putchar( '\n' );

    return 0;
}

如果输入数字序列

0 1 2 3 4 5 6 7 8 9

那么输出将是

0 1 2 3 4 5 6 7 8 9

【讨论】:

  • 注意:这个版本不是尾递归的,1 + read_array( a + 1, n - 1 )这里需要等待函数返回。
  • @Stargateur 你为什么这样决定?这完全取决于编译器。
  • 无尾递归与编译器无关:stackoverflow.com/questions/310974/….
  • @Stargateur 你错了。它直接取决于编译器。例如编译器不支持尾递归。
  • 不是你说的优化,尾递归是事实,这个函数是不是尾递归,点。一些编译器可以在优化调用后将非尾递归函数变成尾递归。但这不是重点。你的函数不是尾递归的,这还不错,只是一个事实。这个链接更好stackoverflow.com/questions/33923/what-is-tail-recursion
【解决方案3】:

例子:

int read_array_aux(int *i, int *n) {
  if (i == n) {
    return 0;
  }
  if (scanf("%d", i) != 1) {
    return -1;
  }
  return read_array_aux(i + 1, n);
}

int read_array_aux2(int *a, size_t i, size_t n) {
  if (i == n) {
    return 0;
  }
  if (scanf("%d", a + i) != 1) {
    return -1;
  }
  return read_array_aux2(a, i + 1, n);
}

int read_array(int *a, size_t n) {
  return read_array_aux(a, a + n);
  // return read_array_aux2(a, 0, n);
}

【讨论】:

    【解决方案4】:

    首先,条件n&lt;0 是错误的。可能这是段错误的原因。

    另外,为什么还要费心计算索引呢?在递归处理任何类型的列表时,值得掌握列​​表的头部(列表的第一个元素)和尾部(除了头部之外的所有元素)的概念。因此,递归填充数组将被定义为(在伪代码中):

    void read_array() {
       read_head();
       read_tail();
    }
    

    什么是头?它是当前数组的第一个元素。尾巴是什么?从下一个元素开始的数组。因此,read_tail 等价于 read_array,但开头向前移动了一个元素。

    最后,将所有内容集中到一个地方:

    void read_array(int *a, int n) {
       if(n<=0) {
           return;
       } else {
           if(scanf("%d", a) == 1) {
               read_array(a+1,n-1);
           }
       }
    }
    

    【讨论】:

    • 非常“Fsharpish”的解释!谢谢!
    • 非常“Fsharpish”的解释!您认为与 Felix 提出的建议的主要区别是什么?
    • Felix 解决方案通常更好,因为它很容易针对不同的语言进行修改,但它需要传递三个参数而不是两个参数:数组、索引和大小。我的建议适用于 C,因为它使用指针算法,结果只需要传递两个参数:数组的余数和数组余数的大小。选择更适合您的。
    【解决方案5】:

    正如其他答案所提到的,您对n 的处理会导致问题。您可以从sz == 0 的基本情况返回0,否则返回下一个递归调用的结果,或者如果scanf() 失败,则返回-1。在每次递归调用时,递增 a 并递减 sz。应检查调用函数中返回的值是否存在输入错误:0 成功,-1 失败。

    请注意,这是一个tail recursion,应该由大多数优秀的编译器进行优化。

    #include <stdio.h>
    
    int read_array(int *a, size_t sz);
    
    int main(void)
    {
        int arr[5];
        puts("Enter array elements:");
        if (read_array(arr, 5) != 0) {
            fprintf(stderr, "Input error\n");
        } else {
            for (size_t i = 0; i < 5; i++) {
                printf("%8d", arr[i]);
            }
            putchar('\n');
        }
    
        return 0;
    }
    
    int read_array(int *a, size_t sz)
    {
        if (sz == 0 ) {
            return 0;
        }
        if (scanf("%d", a) == 1){
            return read_array(a + 1, sz - 1);
        } else {
            return -1;
        }
    }
    

    示例交互:

    Enter array elements:
    1 2 3 4 5
           1       2       3       4       5
    
    Enter array elements:
    1 2 3 x 5
    Input error
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-28
      • 1970-01-01
      • 2016-03-07
      • 1970-01-01
      • 1970-01-01
      • 2011-02-25
      相关资源
      最近更新 更多