您的问题的答案是,是的,可以在不迭代的情况下反转数组。问题本身的措辞可能模棱两可,但问题的精神是显而易见的:可以使用递归算法;在这个意义上,recursive的含义也完全没有歧义。
如果在与顶级公司的面试中,你被问到这个问题,那么下面的伪代码就足以证明你真正理解什么是递归:
function reverse(array)
if (length(array) < 2) then
return array
left_half = reverse(array[0 .. (n/2)-1])
right_half = reverse(array[(n/2) .. (n-1)])
return right_half + left_half
end
例如,如果我们有一个包含 16 个元素的数组,其中包含拉丁字母的前 16 个字母 [A]..[P],则上述反向算法可以可视化如下:
Original Input
1. ABCDEFHGIJKLMNOP Recurse
2. ABCDEFGH IJKLMNOP Recurse
3. ABCD EFGH IJKL MNOP Recurse
4. AB CD EF GH IJ KL MN OP Recurse
5. A B C D E F G H I J K L M N O P Terminate
6. BA DC FE HG JI LK NM PO Reverse
7. DCBA HGFE LKJI PONM Reverse
8. HGFEDCBA PONMLKJI Reverse
9. PONMLKJIHGFEDCBA Reverse
Reversed Output
使用递归算法解决的任何问题都遵循分而治之范式,即:
问题被划分为 [两个或更多] 子问题,其中每个子问题小于原始问题,但可以以与原始问题类似的方式解决(划分 )。
问题被划分为 [两个或更多] 子问题,其中每个子问题都是独立的,可以递归解决,如果足够小,也可以直接解决(征服 )。
问题被划分为 [两个或更多] 子问题,其中这些子问题的结果被组合以给出原始问题的解决方案(Combine)。 p>
上面用于反转数组的伪代码严格满足上述条件。因此,它可以被认为是一种递归算法,我们可以毫无疑问地声明,可以在不使用迭代的情况下对数组进行反转。
其他背景信息
迭代、递归实现和递归算法的区别
递归实现意味着算法是递归的,这是一个常见的误解。它们不是等价的。这是一个明确的解释,包括对上述解决方案的详细解释。
什么是迭代和递归?
早在 1990 年,计算机科学领域最受尊敬的三位现代算法分析学者Thomas H. Cormen、Charles E. Leiserson 和Ronald L. Rivest,发表了他们广受好评的Introduction to Algorithms .在这本书中,它代表了 200 多本受人尊敬的文本的汇集,并且 20 多年来一直被用作世界上大多数一流大学教授算法的第一个也是唯一一个文本,Mssrs . Cormen、Leiserson 和 Rivest 明确说明了什么构成 迭代 以及什么构成 递归。
在他们对两种经典排序算法插入排序和合并排序的分析和比较中,他们解释了迭代和递归算法(有时称为当iteration 的经典数学概念在相同的上下文中使用时,增量算法可以消除歧义。
首先,插入排序被归类为迭代算法,其行为总结如下:
对子数组 A[1..j-1] 进行排序后,我们将单个项 A[j] 插入到适当的位置,生成排序后的数组 A [1..j].
来源:Introduction to Algorithms - Cormen, Leisersen, Rivest, 1990 MIT Press
该语句将迭代算法归类为依赖于算法先前执行(“迭代”)的结果或状态的算法,并且此类结果或状态信息随后用于解决当前迭代的问题。
另一方面,归并排序被归类为递归算法。递归算法符合称为分而治之的处理范例,这是一组区分递归算法与非递归算法的操作的三个基本标准。如果在处理给定问题期间,算法可以被认为是递归的:
问题被划分为 [两个或更多] 子问题,其中每个子问题小于原始问题,但可以以与原始问题类似的方式解决(划分 )。
问题被划分为 [两个或更多] 子问题,其中每个子问题可以递归解决,如果足够小,则可以直接解决(Conquer)。
问题被划分为 [两个或更多] 子问题,其中这些子问题的结果被组合以给出原始问题的解决方案(Combine)。 p>
参考:Introduction to Algorithms - Cormen, Leisersen, Rivest, 1990 MIT Press
迭代算法和递归算法都会继续工作,直到达到终止条件。插入排序的终止条件是第 j 项已正确放置在数组 A[1..j] 中。分治算法的终止条件是范式的准则 2“触底”,即子问题的大小达到足够小的大小,无需进一步细分即可解决。
请务必注意,分而治之范式要求子问题必须能够以与原始问题类似的方式解决,以允许递归。由于原始问题是一个独立问题,没有外部依赖项,因此子问题也必须是可解决的,就好像它们是没有外部依赖项的独立问题一样,特别是在其他子问题上。这意味着分治算法中的子问题应该自然独立。
相反,同样重要的是要注意迭代算法的输入基于算法的先前迭代,因此必须按顺序考虑和处理。这会在迭代之间产生依赖关系,从而阻止算法将问题划分为可以递归解决的子问题。例如,在插入排序中,您不能将项目 A[1..j] 分成两个子集,这样在 A[j 的数组中的排序位置] 在所有项目 A[1..j-1] 被放置之前决定,因为 A[j] 的真正正确位置可能会移动,而任何 A[ 1..j-1] 正在被自己放置。
递归算法与递归实现
对递归这个术语的普遍误解源于一个普遍的错误假设,即某些任务的递归实现自动意味着问题已经解决使用递归算法。递归算法与递归实现不同,而且从来都不是。
递归实现涉及一个函数或一组函数,它们最终调用自己,以便以与解决整个任务完全相同的方式解决整个任务的子部分。递归 算法(即那些满足分而治之范式的算法)非常适合递归实现。但是,递归算法可以仅使用迭代构造(如 for(...) 和 while(...))来实现,因为所有算法(包括递归算法)最终都会重复执行某些任务以获得结果。
这篇文章的其他贡献者已经完美地证明了迭代算法可以使用递归函数来实现。事实上,对于涉及迭代直到满足某个终止条件的一切,递归实现都是可能的。底层算法中没有 Divide 或 Combine 步骤的递归实现等效于具有标准终止条件的迭代实现。
以插入排序为例,我们已经知道(并且已经证明)插入排序是一种迭代算法。但是,这并不妨碍插入排序的递归实现。实际上,可以很容易地创建递归实现,如下所示:
function insertionSort(array)
if (length(array) == 1)
return array
end
itemToSort = array[length(array)]
array = insertionSort(array[1 .. (length(array)-1)])
find position of itemToSort in array
insert itemToSort into array
return array
end
可以看出,实现是递归的。但是,插入排序是一种迭代算法,我们知道这一点。那么,我们怎么知道即使使用上面的递归实现,我们的插入排序算法也没有变成递归的呢?让我们将分而治之范式的三个标准应用于我们的算法并进行检查。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都小于原始问题,但可以以与原始问题类似的方式解决。
YES:排除长度为 1 的数组,将项 A[j] 插入到数组中适当位置的方法与用于插入的方法相同将所有先前的项目 A[1..j-1] 插入到数组中。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都是独立的,可以递归解决,如果足够小,也可以直接解决。
否:项目 A[j] 的正确放置完全依赖包含 A[1..j 的数组-1] 项和正在排序的项。因此,在处理数组的其余部分之前,不会将 item A[j](称为 itemToSort)放入数组中。
-
问题被划分为 [两个或更多] 子问题,这些子问题的结果被组合起来以给出原始问题的解决方案。
NO:作为一种迭代算法,在任何给定的迭代中只能正确放置一项 A[j]。空间 A[1..j] 没有被划分为 A[1], A[2]...A[j] 都正确的子问题独立放置,然后将所有这些正确放置的元素组合起来得到排序数组。
显然,我们的递归实现并没有使插入排序算法本质上是递归的。实际上,在这种情况下,实现中的递归起到了流控制的作用,允许迭代继续进行,直到满足终止条件。因此,使用递归实现并没有将我们的算法变成递归算法。
在不使用迭代算法的情况下反转数组
既然我们了解了什么使算法具有迭代性,以及什么使算法具有递归性,那么我们如何“不使用迭代”来反转数组呢?
有两种方法可以反转数组。这两种方法都要求您提前知道数组的长度。迭代算法因其效率而受到青睐,其伪代码如下:
function reverse(array)
for each index i = 0 to (length(array) / 2 - 1)
swap array[i] with array[length(array) - i]
next
end
这是一个纯粹的迭代算法。让我们通过将其与决定算法的递归性的分而治之范式进行比较来检验为什么我们可以得出这个结论。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都小于原始问题,但可以以与原始问题类似的方式解决。
是:数组的反转被分解为最细的粒度、元素,并且每个元素的处理与所有其他处理的元素相同。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都是独立的,可以递归解决,如果足够小,也可以直接解决。
YES:数组中元素 i 的反转是可能的,而不需要元素 (i + 1)(例如)已经反转与否。此外,数组中元素i的反转不需要其他元素反转的结果才能完成。
-
问题被划分为 [两个或更多] 子问题,这些子问题的结果被组合起来以给出原始问题的解决方案。
NO:作为一种迭代算法,每个算法步骤只执行一个计算阶段。它不会将问题划分为子问题,也不会将两个或多个子问题的结果合并得到一个结果。
我们上面第一个算法的上述分析证实它不符合分而治之范式,因此不能被认为是递归算法。然而,由于标准 (1) 和标准 (2) 都得到满足,显然递归算法是可能的。
关键在于我们迭代解决方案中的子问题具有最小可能的粒度(即元素)。通过将问题划分为越来越小的子问题(而不是从一开始就追求最细粒度),然后合并子问题的结果,可以使算法递归。
例如,如果我们有一个包含 16 个元素的数组,其中包含拉丁字母 (A..P) 的前 16 个字母,则递归算法在视觉上看起来如下所示:
Original Input
1. ABCDEFHGIJKLMNOP Divide
2. ABCDEFGH IJKLMNOP Divide
3. ABCD EFGH IJKL MNOP Divide
4. AB CD EF GH IJ KL MN OP Divide
5. A B C D E F G H I J K L M N O P Terminate
6. BA DC FE HG JI LK NM PO Conquer (Reverse) and Merge
7. DCBA HGFE LKJI PONM Conquer (Reverse) and Merge
8. HGFEDCBA PONMLKJI Conquer (Reverse) and Merge
9. PONMLKJIHGFEDCBA Conquer (Reverse) and Merge
Reversed Output
从顶层开始,16 个元素逐渐分解为大小完全相同的较小子问题(级别 1 到 4),直到我们达到子问题的最细粒度;正向排列的单位长度数组(第 5 步,单个元素)。此时,我们的 16 个数组元素看起来仍然是有序的。但是,它们同时也是反转的,因为单个元素数组本身也是反转数组。然后将单元素数组的结果合并得到八个长度为 2 的反向数组(步骤 6),然后再次合并得到四个长度为 4 的反向数组(步骤 7),依此类推,直到我们的原始数组被重构反过来(步骤 6 到 9)。
递归算法反转数组的伪代码如下:
function reverse(array)
/* check terminating condition. all single elements are also reversed
* arrays of unit length.
*/
if (length(array) < 2) then
return array
/* divide problem in two equal sub-problems. we process the sub-problems
* in reverse order so that when combined the array has been reversed.
*/
return reverse(array[(n/2) .. (n-1)]) + reverse(array[0 .. ((n/2)-1)])
end
如您所见,该算法将问题分解为子问题,直到达到可立即给出结果的子问题的最细粒度。然后在合并结果时反转结果以提供反转的结果数组。虽然我们认为这个算法是递归的,但让我们应用分治算法的三个标准来确认。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都小于原始问题,但可以以与原始问题类似的方式解决。
是:在第 1 层反转数组可以使用与第 2、3、4 或 5 层完全相同的算法来完成。
-
问题被划分为 [两个或更多] 子问题,其中每个子问题都是独立的,可以递归解决,如果足够小,也可以直接解决。
YES:每个不是单位长度的子问题都通过将问题分成两个独立的子数组并递归地反转这些子数组来解决。单位长度数组是可能的最小数组,它们本身是反转的,因此提供了终止条件和保证的第一组组合结果。
-
问题被划分为 [两个或更多] 子问题,这些子问题的结果被组合起来以给出原始问题的解决方案。
是:第 6、7、8 和 9 级的每个问题都仅由上一级的结果组成;即他们的子问题。在每个级别反转数组会导致整体反转结果。
可以看出,我们的递归算法通过了分而治之范式的三个标准,因此可以被认为是真正的递归算法。因此,可以在不使用迭代算法的情况下反转数组。
有趣的是,我们原来的数组反转迭代算法可以使用递归函数实现。这种实现的伪代码如下:
function reverse(array)
if length(array) < 2
return
end
swap array[0] and array[n-1]
reverse(array[1..(n-1)])
end
这类似于其他海报提出的解决方案。这是一个递归实现,因为定义的函数最终会调用自身以对数组中的所有元素重复执行相同的任务。但是,这不会使算法递归,因为没有将问题划分为子问题,也没有将子问题的结果合并到给出最终结果。在这种情况下,递归只是被用作流控制结构,并且在算法上可以证明总体结果以完全相同的顺序执行相同的步骤序列,与为解决方案。
这就是迭代算法、递归算法和递归实现之间的区别。