Recursion: Must Knows

PART II

Recursion: Must Knows

PREREQUISITE

RECURSION IN ONE LINE

The manifestation of complex phenomena through the repetition of simpler, self-referential elements, allows for deeper understanding and abstraction in problem-solving and conceptualization.

PROCESS OF SOLVING RECURSION IN ONE LINE

"It's a mental voyage, unravelling intricate problems into progressively simpler components, ultimately encoding them into a programmatic form."

IN OTHER WORDS: "It is a cognitive journey where one visualizes and dissects intricate problems into progressively simpler instances. And finally able to imprint it onto a programmable format"

BEHAVIOUR OF ARGUMENTS IN FUNCTION AS WELL AS RECURSIVE FUNCTION

The behaviour of arguments passed in recursive function parameters being reference variables to the same object depends on the programming language in use.

  • MOST PROGRAMMING LANGUAGE: including Python, Java, and C++

    • Arguments passed to a function are typically passed by reference.

    • This means that the function receives a reference (memory address) to the original object, and any modifications made to the object within the function will affect the original object outside the function. This behaviour holds true for recursive functions as well.

Are javascript object variables just reference type? - Stack Overflow

class EgString5
{
  public static void main ( String[] args )
  {
    String strA;  // reference to the object
    String strB;  // another reference to the object

    strA = new String( "The Gingham Dog" );    // Create the only object.  
                                               // Save its reference in strA.
    System.out.println( strA ); 

    strB = strA;     // Copy the reference into strB.

    System.out.println( strB );

    if ( strA == strB )
      System.out.println( "Same info in each reference variable." );  
   }
}

OUTPUT:

The Gingham Dog
The Gingham Dog
Same info in each reference variable.
  • However, in some languages like C,

    • Arguments are passed by value by default.

    • In such languages, modifications made to arguments within a function do not affect the original objects.

PASSING NUMBER IN PARAMETERS

In programming, "n--" and "--n" are both decrement operators, but they are used in different ways and have different effects:

  1. "n--" (Post-decrement): This operator is used to decrement the value of "n" after its current value is used in an expression. It effectively subtracts 1 from "n" but returns the original value of "n" for use in the expression. Here's an example:
int n = 5;
int result = n--;  // result will be 5, n will become 4 after this line

In this example, "result" gets the original value of "n" (5), and then "n" is decremented to 4.

  1. "--n" (Pre-decrement): This operator is used to decrement the value of "n" before its current value is used in an expression. It subtracts 1 from "n" and returns the updated value of "n" for use in the expression. Here's an example:
int n = 5;
int result = --n;  // result will be 4, n is already 4 after this line

In this example, "result" gets the updated value of "n" (4) because "n" was decremented before its value was used in the expression.

So, the key difference between "n--" and "--n" is when the decrement operation occurs in relation to the use of the variable's value in an expression.

HOW IT CAN LEAD TO STACK OVERFLOW ERROR

public static void recursiveDecrement(int n) {
    if (n == 0) {
        return; // Termination condition
    }

    System.out.println(n);
    recursiveDecrement(n--); // Recursive call
}

public static void main(String[] args) {
    int n = 10000;
    recursiveDecrement(n); // This can lead to a stack overflow error for large values of n
}

OUTPUT:

1000
1000
1000
...
STACKOVERFLOW ERROR

WAYS TO REDUCE COMPLEXITY

HELPS IN:

  1. Complexity Reduction: If your recursive function has become too complex due to multiple recursive calls, conditionals, or other logic, creating helper functions can break down the problem into smaller, more manageable pieces. Each helper function can handle a specific part of the problem.

  2. Modularization: If the recursive algorithm can be logically divided into subproblems, creating helper functions for each subproblem can make the code more modular and easier to understand. Each helper function can focus on solving one aspect of the problem.

  3. Readability: Recursive functions can quickly become hard to read and understand, especially when dealing with complex data structures or algorithms. Creating well-named helper functions with clear purposes can significantly improve the readability of your code.

  4. Code Reuse: If there are parts of your recursive algorithm that are reused within the algorithm itself, those parts can be extracted into helper functions. This promotes code reuse and prevents duplication of code.

  5. Testing: Helper functions can be individually tested, which can simplify the debugging and testing process. It's often easier to write test cases for smaller, more focused functions than for a single large recursive function.

  6. Abstraction: Helper functions can abstract away implementation details, making the recursive function's main logic more high-level and understandable. This is particularly useful when the recursive algorithm involves low-level details that don't need to be exposed in the main function.

  7. Maintainability: Code maintenance is often easier when you have well-structured helper functions. If you need to make changes or fix bugs in your recursive algorithm, you can focus on one function at a time, rather than having to navigate through a complex recursive function.

  8. Encapsulation: In object-oriented programming, you can create helper methods within a class that encapsulate recursive behaviour related to that class. This promotes encapsulation and can make your code more object-oriented.

EXAMPLE: count no. of zeros -> in repo

EXAMPLE: reverse a number

solution 1: for example REVERSE A NUMBER

public class ReverseNumber {
    public static void main(String[] args) {
        System.out.println(revNum(56321));
    }
    public static int revNum(int n){
        if(n%10 == n){
            return n;
        }
        return (n%10 * (int)Math.pow(10, (int)Math.log10(n))) + revNum(n/10);
    }
}

AUXILIARY VARIABLE

simplified solution 2: for example REVERSE A NUMBER

public class ReverseNumber {
    public static void main(String[] args) {
        revNum(56321);
        System.out.println(sum);
    }

    public static int sum;

    public static void revNum(int n){
        if(n == 0){
            return;
        }
        sum = (sum * 10 )+ (n%10);
        revNum(n/10);
    }
}

HELPER FUNCTION

To prevent the number of parameters from increasing, in situations where we need auxiliary variables to solve the problem.

When creating helper functions in recursive code, it's essential to carefully design the interface and relationships between the main recursive function and its helpers. Ensure that the recursive calls and data passing between functions are correctly handled to achieve the desired recursive behaviour. Helper functions should aim to simplify and clarify the overall recursive algorithm, not add unnecessary complexity.

simplified solution 3: for example REVERSE A NUMBER

public class ReverseNumber {
    public static void main(String[] args) {
        System.out.println(revNum(56321)); // OUTPUT: 12365
    }

    // HELPER FUNCTION
    public static int getLen(int n){
        return (int)Math.log10(n);
    }

    public static int revNum(int n){
        if(n%10 == n){
            return n;
        }
        return (n%10 * (int)Math.pow(10, getLen(n))) + revNum(n/10);
    }
}

EXTRA PARAMETER

simplified solution 4: for example REVERSE A NUMBER

public class ReverseNumber {
    public static void main(String[] args) {
        revNum(56321, 0);
    }

    public static void revNum(int n, int ans){
        if(n%10 == n){
            ans = (ans * 10 )+ (n%10);
            System.out.println(ans);
            return;
        }
        ans = (ans * 10 )+ (n%10);
        revNum(n/10, ans);
    }
}

WAYS OF WORKING WITH ARRAY-LIST-BASED RECURSIVE FUNCTION

EXAMPLE: Linear Search using recursion + Find all occurrence

USING AUXILIARY ARRAYLIST VARIABLE

SOLUTION 1: for Linear Search using recursion + Find all occurrence

  • VARIABLE AVAILABLE TO ALL RECURSIVE FUNCTION CALL
import java.util.ArrayList;

public class LinearSearchGetAllIndex {
    public static void main(String[] args) {
        linearSearch(new int[]{1,8,3,8, 39, 18}, 8, 0);
        System.out.println(list);
    }

    static ArrayList<Integer> list = new ArrayList<>();
    public static void linearSearch(int[] arr, int target, int idx){
        if(idx == arr.length){
            return;
        }
        if(arr[idx] == target){
            list.add(idx);
        }
        linearSearch(arr, target, idx+1);
    }
}

USING VARIABLE IN PARAMETER

SOLUTION 2: for Linear Search using recursion + Find all occurrence

  • VARIABLE AVAILABLE TO ALL RECURSIVE FUNCTION CALL
import java.util.ArrayList;

public class LinearSearchGetAllIndex {
    public static void main(String[] args) {
        System.out.println(linearSearch(new int[]{1,8,3,8, 39, 18}, 8, 0, new ArrayList<>()));

    }

    public static ArrayList<Integer> linearSearch(int[] arr, int target, int idx, ArrayList<Integer> output){
        if(idx == arr.length){
            return output; // when end: return same output to all recursive function
        }
        if(arr[idx] == target){
            output.add(idx); // always refers to common obj. var.
        }
        return linearSearch(arr, target, idx+1, output);
    }
}

SOLUTION 3: for Linear Search using recursion + Find all occurrence

  • VARIABLE ONLY AVAILABLE TO THAT FUNCTION CALL

  • VARIABLE NOT AVAILABLE TO ALL RECURSIVE FUNCTION CALL

import java.util.ArrayList;

public class LinearSearchGetAllIndex {
    public static void main(String[] args) {
        System.out.println(linearSearch(new int[]{1,8,3,8, 39, 18}, 8, 0));

    }

    //    OPTION 3
    public static ArrayList<Integer> linearSearch(int[] arr, int target, int idx){
        ArrayList<Integer> temp = new ArrayList<>();
        if(idx == arr.length){
            return temp;
        }
        if(arr[idx] == target){
            temp.add(idx);
        }
        temp.addAll(linearSearch(arr, target, idx+1));
        // parth which runs after recursive call over
        return temp;
    }

VIDEO REFERENCE