【问题标题】:Converting an iterative function to a recursive function without changing parameters在不更改参数的情况下将迭代函数转换为递归函数
【发布时间】:2021-08-15 04:56:36
【问题描述】:

我想将迭代模板函数 getSmallest 转换为递归函数,而不更改 main 中的任何内容(不更改函数参数等),因为在课堂上我们被教导要始终保持函数的公共接口相同(所以如果我们在一个大项目中工作,我们不会开始改变破坏整个程序的事情)

这是我要转换的程序:

// PRE: 0 <= start < end <= length of arr
// PARAM: arr = array of integers
//        start = start index of sub-array
//        end = end index of sub-array + 1
// POST: returns index of smallest value in arr{start:end}

template <class T>
int getSmallest(T arr[], int start, int end) {
    int smallest = start;
    for (int i = start + 1; i < end; ++i) {
        if (arr[i] < arr[smallest]) {
            smallest = i;
        }
    }
    return smallest;
}

过去几个小时我一直在搜索课堂笔记、互联网和 stackoverflow 以寻求任何帮助,但这一切似乎与我的问题无关,所以我在这里问。

这是我想出的最好的尝试:

template <class T>
int getSmallest(T arr[], int start, int end)
{
    int smallest = 0; //this should only run on the first recursion, not the rest
    i = start;
        if (i == end-1)
        {
            return smallest;
        }
        else
        {
            if (arr[i] < arr[smallest])
            {
                smallest = i;
            }
            return getSmallest<T>(arr, i, end);
        }
    
}

我似乎无法让 int smallest = 0; 仅在第一次递归时运行,而当我的程序编译时,它在功能上毫无用处。

任何帮助将不胜感激。谢谢!

【问题讨论】:

  • 你不能。递归函数将具有类似int getSmallest(Iterator start, Iterator end, T currentmin) 的签名。您可以实现此递归函数,并在内部调用具有旧签名的函数。
  • 哦,这很有道理,难怪我感觉我一直在撞墙。谢谢!
  • @EOF 递归解决方案是possible,不改变函数签名。
  • @GuardedGauntlet447 递归 getSmallest() 有问题。也许你想要if (i == end-0)? (它还有其他问题。)
  • @chux-ReinstateMonica Ok:“具有相同行为的递归解决方案(O(1) 内存使用[需要消除尾调用,C 不保证],O(n) 运行时)没有签名更改是不可能的”。现在开心吗?

标签: c++ arrays recursion


【解决方案1】:

不是 depth O(N) 的递归函数(如 OP 的尝试),不如深度 O(log(N)) 的递归函数?

每次递归将数组一分为二。

int getSmallest(T arr[], int start, int end) {
  if (start + 1 == end) {
    return start; 
  }
  int mid = start + (end-start)/2; // mid = (start + end)/2 may overflow
  int left = getSmallest(arr, start, mid + 1);
  int right = getSmallest(arr, mid, end); 
  return arr[left] < arr[right] ? left : right;
}

【讨论】:

  • 如果您假设数据已预先排序(排序),您只会得到O(log(N))。在这种情况下,您也可以提供一个O(1) 函数,它只返回第一个元素。如果数据未预先排序,则您的函数与原始函数一样 O(N),因为您需要查看数组的两半(所有 N 值)。如果您考虑堆栈深度,这完全取决于您的编译器是否有尾调用优化以及您是否使用了正确的优化标志。
  • @BitTickler 我们在谈论两件不同的事情。即使没有优化,此代码也具有 O(log(N)) 的 递归深度,这与其他案例深度为 O(n) 的答案不同。 运行时间为 O(n)。答案并没有声称运行时间是 O(log(N))。
  • 我刚刚运行它,它无法将 9 排序为长度为 10 的数组(数字 0 1 2 3 4 5 6 7 8 9),但它对于字符串和 int 都非常有效,除 9 之外的每个 nnumber 都已排序。谢谢!
  • @GuardedGauntlet447 我读错了“end = end index of subarray + 1”。代码调整。
【解决方案2】:

在 C++ 中有 lambda 函数之前(c++11 和更新版本有它们),除了创建带有状态的额外参数的辅助函数(例如,累加器或最小值或任何想要的)之外,别无他法.其他语言,包括陈旧的 Pascal 语言,都有嵌套函数。

template <class T>
T smallest(const T* first, const T* last) {
  // iterative implementation
}

在 lambda 函数的帮助下变成:

template <class T>
T smallest(const T* first, const T* last) {
  T result = 0; // dubious in itself, because we do not want to assume too much about what T actually is.
  auto loop = [&result,last] (const T* current) {
     if (current < last) {
        if (*current < result) {
           result = *current;
        }
        loop(current + 1);
     }
  };
  loop(first);
  return result; 
}

因为只有在我们不对T 做太多假设的情况下,实现才是真正的通用,所以T result = 0; 行已经太多了,我们需要为每种可能的类型提供一些通用的0。即使这样也是错误的,因为如果我们有一个包含负数的数组,你需要尽可能小的值而不是 0 才能使整个事情正常工作。 我们还默默假设为 T 定义了一个 operator&lt;(T x, T y) 并且它是我们想要的(假设我们想将它用于 T = std::string - 区分大小写的比较?utf8 感知?不区分大小写?)。

因此,即使更改代码中的接口通常不是一个好主意,但有时应该有所改进。因为,让我们承认吧——这个函数的设计很糟糕,因为它试图向我们推销比它真正能够给予的更多的通用性。

template <class T>
T* smallest(const T* first, const T* last) {
  if (first == last)
     return last;
  auto loop = [] (const T* first, const T* last, T* smallest) -> T* {
    if (first != last) {
       if (*first < *smallest) {
         return loop( first + 1, last, first);
       } else {
         return loop( first + 1, last, smallest);
       }
    } else {
       return smallest;
    }
  };
  return loop(first + 1, last, first);
}

是一种改进,因为它只假设operator&lt;(...),但不需要对“T 的最小可能值”做出假设。它还更清楚地显示了递归的“状态”是如何传递的(从而使这个实现尾递归)。如果幸运的话,c++ 编译器对此进行了优化并避免了不必要的堆栈使用。

下一级改进是添加一个额外的参数,允许将比较函数传递给smallest。因为我们也可以将“大于”传递给这个函数,所以我们可以尝试找到一个更通用的名称。

template <class T>
T* selectLast( const T* first, const T* last, bool (*predicate)(const T*, const T*)) {
// ...
}

template <class T, class Pred>
T* selectLast( const T* first, const T* last, Pred predicate) {
// ...
}

或者我们可以进行更多的泛化(因为我们已经处于高级函数的领域)并且只提供一个 reduce/fold 函数。由于这样的函数已经存在,我们将使用 C++ 标准库头文件 &lt;numeric&gt; 中的 std::reduce()

【讨论】:

    【解决方案3】:

    转换迭代函数的一般方法是将每个循环替换为辅助函数。例如:

    for (int i = start + 1; i < end; i++) {
        if (arr[i] < arr[smallest]) {
            smallest = i;
        }
    }
    

    我们首先将其重写为 while 循环:

    int i = start + 1;
    while (i < end) {
        if (arr[i] < arr[smallest) {
            smallest = i;
        }
        i++;
    }
    

    然后我们问自己“我们在循环中使用循环之前定义的哪些变量?”答案:arriendsmallest。这些成为我们函数的输入。

    然后我们问自己:在循环中修改了什么状态,我们稍后在循环外使用?答:smallest 的值。这成为我们的输出。

    因此我们编写了以下辅助函数:

    int helper_function(int i, int end, T arr[], int smallest) {
        if (i < end) {
            if (arr[i] < arr[smallest]) {
                smallest = i;
            }
            i = i + 1;
            return helper_function(i, end, arr, smallest);
        } else {
            return smallest;
        }
    }
    

    可以改写成

    int helper_function(int i, int end, T arr[], int smallest) {
        return i < end ? helper_function(i + 1, 
                                         end, 
                                         arr, 
                                         arr[i] < arr[smallest] ? i : smallest)
                       : smallest;
    }
    

    并将循环替换为

    smallest = helper_function(i, end, arr, smallest);
    

    所以这个阶段的代码是

    int helper_function(int i, int end, T arr[], int smallest) {
        return i < end ? helper_function(i + 1, 
                                         end, 
                                         arr, 
                                         arr[i] < arr[smallest] ? i : smallest)
                       : smallest;
    }
    
    int get_smallest(T arr[], int start, int end) {
        int smallest = start;
        int i = start + 1;
        smallest = helper_function(i, end, arr, smallest);
        return smallest;
    }
    

    当然,我们可以大大简化get_smallest - 例如,我们不需要重新定义smallest 然后返回它。所以我们得到get_smallest的以下代码:

    int get_smallest(T arr[], int start, int end) {
        return helper_function(start + 1, end, arr, start);
    }
    

    如果有不止一层循环嵌套,你会怎么做?从最外面的循环开始,一次一个地摆脱循环。

    请注意,在这种特殊情况下,可以使用不同的递归算法来解决问题。该算法看起来像这样:

    int get_smallest(T arr[], int start, int end) {
        if (start + 1 < end) {
            int result = get_smallest(arr, start + 1, end);
            return arr[result] < arr[start] ? result : start;
        } else {
            return start;
        }
    }
    

    这是可能的,因为min 函数是关联的。

    【讨论】:

    • 这非常有用,谢谢!!! tbh 这是比任何实验室讲师给出的更彻底的解释:)
    • @GuardedGauntlet447 很高兴能提供帮助!
    【解决方案4】:

    有三个条件:

    1. 数组没有元素。
    2. 数组有 1 个元素。
    3. 数组有多个元素

    第一个条件我们可以返回-1,第二个条件我们返回第一个元素的索引,第三个条件我们将当前最小值与从数组其余部分找到的最小元素进行比较,然后返回最小的索引:

    template<class T>
    int getSmallest(T arr[], int start, int end) {
      if (end - start == 0)
        return -1;
      if (end - start == 1)
        return start;
      int idx = getSmallest(arr, start + 1, end);
      if (arr[start] < arr[idx])
        return start;
      return idx;
    }
    

    【讨论】:

    • 非常类似于我现在的学习方式,它确实保持基本并且有帮助,谢谢!
    • 你能解释一下它是如何工作的吗?我试图理解为什么会这样,但我无法完全理解它
    • @GuardedGauntlet447 假设数组是a={3}getSmallest(a, 0, 1) 将返回3 的索引,即0。对于a={3,2},我们认为第一个元素是迄今为止的最小值,因此我们必须在数组的其余部分中搜索最小的元素,然后与第一个元素进行比较。这就是getSmallest(a, start+1, end) 所做的。您会注意到,由于数组的其余部分只剩下一个元素,因此它返回最后一个元素的索引,即idx=1。然后arr[start] &lt; arr[idx] 用于查找两者中的最小值(即索引1 处的2)。 (续)
    • @GuardedGauntlet447(续)假设数组是a={3,2,1}。我们认为第一个元素是迄今为止最小的,然后搜索数组的其余部分。从前面的示例中,我们知道getSmallest() 将在 2 元素数组上做什么。它将返回2,这是1 的索引。然后我们将arr[start](即3)与arr[idx](即1)进行比较。这适用于任何大小的数组。
    • @GuardedGauntlet447 递归可能会令人困惑,但我学到的一个技巧是不要将函数调用视为递归调用。试想一下,这是一个常规的函数调用,它会返回正确的答案。所以int idx = getSmallest(arr, start + 1, end) 将返回最小元素的索引,然后我们使用idx 将它与第一个元素进行比较——不应该想到它是递归的。
    猜你喜欢
    • 2020-08-08
    • 2015-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多