수색…
비고
재귀를 사용하면 각 재귀 함수 호출이 스택에 추가되므로 코드에 심각한 영향을 줄 수 있습니다. 호출 수가 너무 많으면 StackOverflow 예외가 발생할 수 있습니다. 대부분의 "자연 재귀 함수"는 for
, while
또는 foreach
루프 구문으로 작성 될 수 있으며, 그렇게 멋지 거나 영리 하지 않으면 더 효율적으로 작성됩니다.
항상 두 번 생각하고 재귀를 신중하게 사용하십시오 - 왜 사용하는지 확인하십시오.
- 재귀 호출 수가 과도 하지 않다는 것을 안다면 재귀를 사용해야합니다.
- 과도한 수단, 사용 가능한 메모리 양에 따라 다름
- 재귀는 코드 버전이 명확하고 깨끗하기 때문에 반복적이거나 루프 기반 함수보다 읽기 쉽습니다. 종종 더 깨끗하고 더 간결한 코드 (코드 줄이 더 작음)를 제공하기 때문에 이러한 경우가 종종 있습니다.
- 하지만 효율성이 떨어질 수 있습니다. 예를 들어 피보나치 재귀에서 시퀀스의 n 번째 숫자를 계산하려면 계산 시간이 기하 급수적으로 증가합니다!
더 많은 이론을 원한다면 다음을 읽어보십시오 :
객체 구조를 재귀 적으로 설명합니다.
재귀는 메서드가 자체를 호출 할 때 발생합니다. 특정 조건이 충족 될 때까지 그렇게 할 것이고 메서드가 정상적으로 종료되고 메서드가 호출 된 지점으로 돌아갑니다. 그렇지 않은 경우 너무 많은 재귀 호출로 인해 스택 오버플로 예외가 발생할 수 있습니다.
/// <summary>
/// Create an object structure the code can recursively describe
/// </summary>
public class Root
{
public string Name { get; set; }
public ChildOne Child { get; set; }
}
public class ChildOne
{
public string ChildOneName { get; set; }
public ChildTwo Child { get; set; }
}
public class ChildTwo
{
public string ChildTwoName { get; set; }
}
/// <summary>
/// The console application with the recursive function DescribeTypeOfObject
/// </summary>
public class Program
{
static void Main(string[] args)
{
// point A, we call the function with type 'Root'
DescribeTypeOfObject(typeof(Root));
Console.WriteLine("Press a key to exit");
Console.ReadKey();
}
static void DescribeTypeOfObject(Type type)
{
// get all properties of this type
Console.WriteLine($"Describing type {type.Name}");
PropertyInfo[] propertyInfos = type.GetProperties();
foreach (PropertyInfo pi in propertyInfos)
{
Console.WriteLine($"Has property {pi.Name} of type {pi.PropertyType.Name}");
// is a custom class type? describe it too
if (pi.PropertyType.IsClass && !pi.PropertyType.FullName.StartsWith("System."))
{
// point B, we call the function type this property
DescribeTypeOfObject(pi.PropertyType);
}
}
// done with all properties
// we return to the point where we were called
// point A for the first call
// point B for all properties of type custom class
}
}
평범한 영어로 재귀
재귀는 다음과 같이 정의 할 수 있습니다.
특정 조건이 충족 될 때까지 자신을 호출하는 메서드입니다.
훌륭하고 간단한 재귀 예제는 주어진 숫자의 계승을 얻을 수있는 방법입니다.
public int Factorial(int number)
{
return number == 0 ? 1 : n * Factorial(number - 1);
}
이 메소드에서는 메소드가 인수 number
취하는 것을 볼 number
있습니다.
단계별 :
주어진 예를 들어, Factorial(4)
-
number (4) == 1
입니까? - 아니? return
4 * Factorial(number-1)
(3) - 이 메서드는 다시 한 번 호출되기 때문에
Factorial(3)
을 새 인수로 사용하여 첫 번째 단계를 반복합니다. - 이것은
Factorial(1)
이 실행될 때까지 계속되며number (1) == 1
은 1을 반환합니다. - 전반적으로 계산은
4 * 3 * 2 * 1
"빌드하고"결국 24를 반환합니다.
재귀를 이해하는 핵심은이 메서드가 새로운 인스턴스 를 호출한다는 것입니다. 리턴 한 후 호출하는 인스턴스의 실행이 계속됩니다.
재귀를 사용하여 디렉토리 트리 가져 오기
재귀를 사용하는 방법 중 하나는 파일 시스템 디렉토리 트리와 같은 계층 적 데이터 구조를 탐색하고 트리의 레벨 수 또는 각 레벨의 객체 수를 모르는 것입니다. 이 예에서는 디렉토리 트리에서 재귀를 사용하여 지정된 디렉토리의 모든 하위 디렉토리를 찾고 콘솔에 전체 트리를 인쇄하는 방법을 보여줍니다.
internal class Program
{
internal const int RootLevel = 0;
internal const char Tab = '\t';
internal static void Main()
{
Console.WriteLine("Enter the path of the root directory:");
var rootDirectorypath = Console.ReadLine();
Console.WriteLine(
$"Getting directory tree of '{rootDirectorypath}'");
PrintDirectoryTree(rootDirectorypath);
Console.WriteLine("Press 'Enter' to quit...");
Console.ReadLine();
}
internal static void PrintDirectoryTree(string rootDirectoryPath)
{
try
{
if (!Directory.Exists(rootDirectoryPath))
{
throw new DirectoryNotFoundException(
$"Directory '{rootDirectoryPath}' not found.");
}
var rootDirectory = new DirectoryInfo(rootDirectoryPath);
PrintDirectoryTree(rootDirectory, RootLevel);
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
}
}
private static void PrintDirectoryTree(
DirectoryInfo directory, int currentLevel)
{
var indentation = string.Empty;
for (var i = RootLevel; i < currentLevel; i++)
{
indentation += Tab;
}
Console.WriteLine($"{indentation}-{directory.Name}");
var nextLevel = currentLevel + 1;
try
{
foreach (var subDirectory in directory.GetDirectories())
{
PrintDirectoryTree(subDirectory, nextLevel);
}
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine($"{indentation}-{e.Message}");
}
}
}
이 코드는 디렉토리 가져 오기와 관련된 문제를 처리하기위한 예외 검사를 포함하기 때문에이 작업을 완료하는 데 최소한의 것보다 다소 복잡합니다. 아래에서 코드의 세부 사항을 각각의 작은 부분으로 찾아 볼 수 있습니다.
Main
:
main 메소드는 루트 디렉토리에 대한 경로로 사용되는 문자열로 사용자로부터 입력을받습니다. 그런 다음이 문자열을 매개 변수로 사용하여 PrintDirectoryTree
메서드를 호출합니다.
PrintDirectoryTree(string)
:
이것은 실제 디렉토리 트리 인쇄를 처리하는 두 가지 방법 중 첫 번째 방법입니다. 이 메소드는 루트 디렉토리에 대한 경로를 나타내는 문자열을 매개 변수로 사용합니다. 경로가 실제 디렉토리인지 검사하고 그렇지 않은 경우 DirectoryNotFoundException
을 던집니다. 그러면 DirectoryNotFoundException
이 catch 블록에서 처리됩니다. 경로가 실제 디렉토리이면 경로에서 DirectoryInfo
객체 rootDirectory
가 생성되고 두 번째 PrintDirectoryTree
메소드는 rootDirectory
객체와 RootLevel
값이 0 인 정수 상수)을 사용하여 호출됩니다.
PrintDirectoryTree(DirectoryInfo, int)
:
이 두 번째 방법은 작업의 정면을 처리합니다. 매개 변수로 DirectoryInfo
와 정수를 사용합니다. DirectoryInfo
는 현재 디렉토리이고, 정수는 루트와 관련된 디렉토리의 깊이입니다. 읽기 쉽게, 출력은 다음과 같이 보이도록 현재 디렉토리의 각 레벨에 대해 들여 쓰기됩니다.
-Root
-Child 1
-Child 2
-Grandchild 2.1
-Child 3
현재 디렉토리가 인쇄되면, 그 서브 디렉토리가 검색되고,이 메소드는 각각의 디렉토리에 대해 현재보다 1의 깊이 레벨 값으로 호출됩니다. 그 부분은 재귀입니다 : 그 자체를 호출하는 메소드. 이 프로그램은 트리의 모든 디렉토리를 방문 할 때까지이 방식으로 실행됩니다. 하위 디렉토리가없는 디렉토리에 도달하면 메소드가 자동으로 리턴됩니다.
이 메소드는 또한 현재 디렉토리의 서브 디렉토리 중 하나가 시스템에 의해 보호되는 경우 Throw되는 UnauthorizedAccessException
을 포착합니다. 일관성을 위해 현재 들여 쓰기 수준에서 오류 메시지가 인쇄됩니다.
아래의 방법은이 문제에 대한보다 기본적인 접근 방법을 제공합니다.
internal static void PrintDirectoryTree(string directoryName)
{
try
{
if (!Directory.Exists(directoryName)) return;
Console.WriteLine(directoryName);
foreach (var d in Directory.GetDirectories(directoryName))
{
PrintDirectoryTree(d);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
여기에는 첫 번째 접근 방식의 특정 오류 검사 또는 출력 형식이 포함되지 않지만 실제로는 동일한 작업을 수행합니다. DirectoryInfo
와는 반대로 문자열 만 사용하므로 권한과 같은 다른 디렉토리 속성에 대한 액세스를 제공 할 수 없습니다.
피보나치 서열
재귀를 사용하여 피보나치 수열에서 수를 계산할 수 있습니다.
어떤 i> 0에 대해서 F (n) = F (n-2) + F (n-1)의 수학 이론을 따르면,
// Returns the i'th Fibonacci number
public int fib(int i) {
if(i <= 2) {
// Base case of the recursive function.
// i is either 1 or 2, whose associated Fibonacci sequence numbers are 1 and 1.
return 1;
}
// Recursive case. Return the sum of the two previous Fibonacci numbers.
// This works because the definition of the Fibonacci sequence specifies
// that the sum of two adjacent elements equals the next element.
return fib(i - 2) + fib(i - 1);
}
fib(10); // Returns 55
요인 계산
숫자의 계승표 (예 :!와 같이 표시된!)는 해당 숫자와 1을 곱한 값입니다. 예를 들어, 9! = 9 x 8! = 9 x 8 x 7! = 9 × 8 × 7 × 6 × 5 × 4 × 3 × 2 × 1이다.
그래서 재귀를 사용하는 코드에서 :
long Factorial(long x)
{
if (x < 1)
{
throw new OutOfRangeException("Factorial can only be used with positive numbers.");
}
if (x == 1)
{
return 1;
} else {
return x * Factorial(x - 1);
}
}
PowerOf 계산
주어진 숫자의 거듭 제곱 계산은 재귀 적으로도 수행 할 수 있습니다. 기본 번호 감안할 때 n
및 지수 e
, 우리는 지수의 감소에 의해 덩어리에 문제를 분리해야 할 필요가 e
.
이론적 인 예 :
- 2 ² = 2x2
- 2³ = 2x2x2 또는 2³ = 2² x 2
여기에는 우리의 재귀 알고리즘의 비밀이 있습니다 (아래 코드 참조). 이것은 문제를 취해 그것을 더 작고 더 간단하게 분리하여 청크를 푸는 것에 관한 것입니다. - 노트
- 기본 숫자가 0 일 때 0을 0으로 반환해야한다는 것을 알아야합니다. 0 = 0 x 0 x 0
- 지수가 0 일 때 수학적 규칙이므로 항상 1을 반환해야한다는 것을 알아야합니다.
코드 예제 :
public int CalcPowerOf(int b, int e) {
if (b == 0) { return 0; } // when base is 0, it doesn't matter, it will always return 0
if (e == 0) { return 1; } // math rule, exponent 0 always returns 1
return b * CalcPowerOf(b, e - 1); // actual recursive logic, where we split the problem, aka: 2³ = 2 * 2² etc..
}
xUnit에서 논리를 검증하기위한 테스트 :
이것이 반드시 필요한 것은 아니지만 논리를 검증하기위한 테스트를 작성하는 것이 좋습니다. 여기에는 xUnit 프레임 워크로 작성된 것들이 포함됩니다.
[Theory]
[MemberData(nameof(PowerOfTestData))]
public void PowerOfTest(int @base, int exponent, int expected) {
Assert.Equal(expected, CalcPowerOf(@base, exponent));
}
public static IEnumerable<object[]> PowerOfTestData() {
yield return new object[] { 0, 0, 0 };
yield return new object[] { 0, 1, 0 };
yield return new object[] { 2, 0, 1 };
yield return new object[] { 2, 1, 2 };
yield return new object[] { 2, 2, 4 };
yield return new object[] { 5, 2, 25 };
yield return new object[] { 5, 3, 125 };
yield return new object[] { 5, 4, 625 };
}