DAA Unit 2 Notes
DAA Unit 2 Notes
DAA Unit 2 Notes
UNIT II
Recursion
What is Recursion?
The process in which a function calls itself directly or indirectly is called
recursion and the corresponding function is called a recursive function. Using a
recursive algorithm, certain problems can be solved quite easily. Examples of
such problems are Towers of Hanoi (TOH), Inorder/Preorder/Postorder Tree
Traversals, DFS of Graph, etc. A recursive function solves a particular problem
by calling a copy of itself and solving smaller sub problems of the original
problems. Many more recursive calls can be generated as and when required. It
is essential to know that we should provide a certain case in order to terminate
this recursion process. So we can say that every time the function calls itself
with a simpler version of the original problem.
Need of Recursion
Recursion is an amazing technique with the help of which we can reduce the
length of our code and make it easier to read and write. It has certain advantages
over the iteration technique which will be discussed later. A task that can be
defined with its similar subtask, recursion is one of the best solutions for it. For
example; The Factorial of a number.
Properties of Recursion:
Performing the same operations multiple times with different inputs.
Base condition is needed to stop the recursion otherwise infinite loop will
occur.
In Python, we know that a function can call other functions. It is even possible
for the function to call itself. These types of construct are termed as recursive
functions.
The following image shows the working of a recursive function called recurse.
if x == 1:
return 1
else:
return (x * factorial(x-1))
num = 3
print("The factorial of", num, "is", factorial(num))
Run Code
Output
The factorial of 3 is 6
In the above example, factorial () is a recursive function as it calls itself.
When we call this function with a positive integer, it will recursively call itself by
decreasing the number.
Each function multiplies the number with the factorial of the number below it
until it is equal to one. This recursive call can be explained in the following steps.
Let's look at an image that shows a step-by-step process of what is going on:
Our recursion ends when the number reduces to 1. This is called the base
condition.
Every recursive function must have a base condition that stops the recursion or
else the function calls itself infinitely.
The Python interpreter limits the depths of recursion to help avoid infinite
recursions, resulting in stack overflows.
By default, the maximum depth of recursion is 1000. If the limit is crossed, it
results in RecursionError. Let's look at one such condition.
def recursor():
recursor()
recursor()
Output
Traceback (most recent call last):
File "<string>", line 3, in <module>
File "<string>", line 2, in a
File "<string>", line 2, in a
File "<string>", line 2, in a
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
Advantages of Recursion
1. Recursive functions make the code look clean and elegant.
2. A complex task can be broken down into simpler sub-problems using recursion.
3. Sequence generation is easier with recursion than using some nested iteration.
Disadvantages of Recursion
1. Sometimes the logic behind recursion is hard to follow through.
2. Recursive calls are expensive (inefficient) as they take up a lot of memory and
time.
3. Recursive functions are hard to debug.
Iteration
The process of doing something again and again, usually to improve it.
Iterators are methods that iterate collections like lists, tuples, etc. Using an
iterator method, we can loop through an object and return its elements.
Technically, a Python iterator object must implement two special
methods, __iter__() and __next__(), collectively called the iterator protocol.
Other folktale states, when they would solve this puzzle, the temple would smash
into dust, and the world would end. But it would take a lot of time because to
solve this problem 264 - 1 moves are necessary i.e., 18,446,744,073,709,551,615
per second that is equal to 5, 84,942,417,355 years according to the rules.
Rules of the game
The rules of "Tower of Hanoi" are quite simple, but the solution is slightly hard.
There are three rods. The disks are stacked in the descending order; the largest
disk stacked at the bottom and the smallest one on top.
The task is to transfer the disks from one source rod to another target rod.
The following rule must not be violated
o Only one disk can be moved at a time.
o The most upper disk from one of the rod can be stimulated in move.
o The smaller disk cannot be placed at the lower of the largest disk.
The number of moves can be calculated as 2n - 1.
Solution:
At the beginning of this tutorial, we have mentioned that we will use the
recursive function to find the solution.
Suppose we have three disks on the first rod; we need total 7 moves from the
above formula. The most left rod is called SOURCE, and the rightmost rod is
called TARGET. The middle rod is referred to as an AUX.
The AUX is needed to deposit disks temporarily.
Problem Approach
1. Create a tower_of_hanoi recursive function and pass two arguments: the
number of disks n and the name of the rods such as source, aux, and target.
2. We can define the base case when the number of disks is 1. In this case,
simply move the one disk from the source to target and return.
3. Now, move remaining n-1 disks from source to auxiliary using the target as
the auxiliary.
4. Then, the remaining 1 disk move on the source to target.
5. Move the n-1 disks on the auxiliary to the target using the source as the
auxiliary.
Python Program/ Source Code
1. # Creating a recursive function
2. def tower_of_hanoi(disks, source, auxiliary, target):
3. if(disks == 1):
4. print('Move disk 1 from rod {} to rod {}.'.format(source, target))
5. return
6. # function call itself
7. tower_of_hanoi(disks - 1, source, target, auxiliary)
8. print('Move disk {} from rod {} to rod {}.'.format(disks, source, target))
9. tower_of_hanoi(disks - 1, auxiliary, source, target)
10.
11.
12. disks = int(input('Enter the number of disks: '))
13. # We are referring source as A, auxiliary as B, and target as C
14. tower_of_hanoi(disks, 'A', 'B', 'C') # Calling the function
Output:
Enter the number of disks: 3
Move disk 1 from rod A to rod C.
Move disk 2 from rod A to rod B.
Move disk 1 from rod C to rod B.
Move disk 3 from rod A to rod C.
Move disk 1 from rod B to rod A.
Move disk 2 from rod B to rod C.
Move disk 1 from rod A to rod C.
Case - 2:
1. Enter number of disks: 2
2. Move disk 1 from rod A to rod B.
3. Move disk 2 from rod A to rod C.
4. Move disk 1 from rod B to rod C.
According to the formula, the moves can be happened 2n - 1, so n = 3 took 7
moves and, n = 2 took 3 moves.
What is Sorting?
Bubble sort is a sorting algorithm that compares two adjacent elements and
swaps them until they are in the intended order.
Just like the movement of air bubbles in the water that rise up to the surface, each
element of the array move to the end in each iteration. Therefore, it is called a
bubble sort.
bubbleSort(array)
for i <- 1 to indexOfLastUnsortedElement-1
if leftElement > rightElement
swap leftElement and rightElement
end bubbleSort
# Bubble sort in Python
def bubbleSort(array):
bubbleSort(data)
Selection Sort
Selection sort is a simple and efficient sorting algorithm that works by
repeatedly selecting the smallest (or largest) element from the unsorted portion
of the list and moving it to the sorted portion of the list. The algorithm
repeatedly selects the smallest (or largest) element from the unsorted portion of
the list and swaps it with the first element of the unsorted portion. This process
is repeated for the remaining unsorted portion of the list until the entire list is
sorted. One variation of selection sort is called “Bidirectional selection sort”
that goes through the list of elements by alternating between the smallest and
largest element, this way the algorithm can be faster in some cases.
The algorithm maintains two sub arrays in a given array.
Selection sort is a sorting algorithm that selects the smallest element from an
unsorted list in each iteration and places that element at the beginning of the
unsorted list.
Working of Selection Sort
Compare minimum with the third element. Again, if the third element is smaller,
then assign minimum to the third element otherwise do nothing. The process
selectionSort(array, size)
repeat (size - 1) times
set the first unsorted element as the minimum
for each of the unsorted elements
if element < currentMinimum
set element as new minimum
swap minimum with first unsorted position
end selectionSort
Insertion Sort
Insertion sort is a sorting algorithm that places an unsorted element at its suitable
place in each iteration.
Insertion sort works similarly as we sort cards in our hand in a card game.
We assume that the first card is already sorted then, we select an unsorted card. If
the unsorted card is greater than the card in hand, it is placed on the right
otherwise, to the left. In the same way, other unsorted cards are taken and put in
their right place.
A similar approach is used by insertion sort.
Initial array
1. The first element in the array is assumed to be sorted. Take the second
element and store it separately in key.
Compare key with the first element. If the first element is greater than key,
then key is placed in front of the first element.
If the first element is greater than key, then
key is placed in front of the first element.
2. Now, the first two elements are sorted.
Take the third element and compare it with the elements on the left of it.
Placed it just behind the element smaller than it. If there is no element
smaller than it, then place it at the beginning of the array.
Place 4 behind 1
Insertion Sort Algorithm
insertionSort(array)
mark first element as sorted
for each unsorted element X
'extract' the element X
for j <- lastSortedIndex down to 0
if current element j > X
move sorted element to the right by 1
break loop and insert X here
end insertionSort
def insertionSort(array):
data = [9, 5, 1, 4, 3]
insertionSort(data)
print('Sorted Array in Ascending Order:')
print(data)
Searching Techniques
Searching is an operation or a technique that helps finds the place of a given
element or value in the list. Any search is said to be successful or unsuccessful
depending upon whether the element that is being searched is found or not. Some
of the standard searching technique that is being followed in the data structure is
listed below:
Linear Search or Sequential Search
Binary Search
Linear Search Algorithm
Linear search is the simplest method for searching.
In Linear search technique of searching; the element to be found in searching
the elements to
be found is searched sequentially in the list.
This method can be performed on a sorted or an unsorted list (usually arrays).
In case of a sorted list searching starts from 0th element and continues until the
element is
found from the list or the element whose value is greater than (assuming the list
is sorted in
ascending order), the value being searched is reached.
As against this, searching in case of unsorted list also begins from the 0th
element and
continues until the element or the end of the list is reached.
The linear search algorithm searches all elements in the array sequentially.
Its best execution time is 1, whereas the worst execution time is n, where n is
the total
number of items in the search array.
It is the most simple search algorithm in data structure and checks each item in
the set of
elements until it matches the search element until the end of data collection.
When data is unsorted, a linear search algorithm is preferred.
Linear Search is defined as a sequential search algorithm that starts at one end
and goes through each element of a list until the desired element is found,
otherwise the search continues till the end of the data set. It is the easiest
searching algorithm
Linear Search Algorithm
Step 1: First, read the search element (Target element) in the array.
Step 2: In the second step compare the search element with the first element in
the array.
Step 3: If both are matched, display “Target element is found” and terminate the
Linear Search
function.
Step 4: If both are not matched, compare the search element with the next
element in the array.
Step 5: In this step, repeat steps 3 and 4 until the search (Target) element is
compared with the
last element of the array.
Step 6 – If the last element in the list does not match, the Linear Search Function
will be
terminated, and the message “Element is not found” will be displayed.
Given an array arr[] of N elements, the task is to write a function to search a
given element x in arr[].
Examples:
Input: arr[] = {10, 20, 80, 30, 60, 50,110, 100, 130, 170}, x = 110;
Output: 6
Explanation: Element x is present at index 6
Input: arr[] = {10, 20, 80, 30, 60, 50,110, 100, 130, 170}, x = 175;
Output: -1
Explanation: Element x is not present in arr[].
Binary Search
Binary search is a fast search algorithm with run-time complexity of Ο(log n).
This search algorithm works on the principle of divide and conquer. For this
algorithm to work properly, the data collection should be in the sorted form.
Binary search looks for a particular item by comparing the middle most item of
the collection. If a match occurs, then the index of item is returned. If the middle
item is greater than the item, then the item is searched in the sub-array to the left
of the middle item. Otherwise, the item is searched for in the sub-array to the
right of the middle item. This process continues on the sub-array as well until the
size of the subarray reduces to zero.
How Binary Search Works?
For a binary search to work, it is mandatory for the target array to be sorted. We
shall learn the process of binary search with a pictorial example. The following is
our sorted array and let us assume that we need to search the location of value 31
using binary search.
Now we compare the value stored at location 4, with the value being searched,
i.e. 31. We find that the value at location 4 is 27, which is not a match. As the
value is greater than 27 and we have a sorted array, so we also know that the
target value must be in the upper portion of the array.
We change our low to mid + 1 and find the new mid value again.
low = mid + 1
mid = low + (high - low) / 2
Our new mid is 7 now. We compare the value stored at location 7 with our target
value 31.
The value stored at location 7 is not a match, rather it is more than what we are
looking for. So, the value must be in the lower part from this location.
We compare the value stored at location 5 with our target value. We find that it is
a match.
Selection Sort
Selection sort is a simple sorting algorithm. This sorting algorithm is an in-place
comparison-based algorithm in which the list is divided into two parts, the sorted
part at the left end and the unsorted part at the right end. Initially, the sorted part
is empty and the unsorted part is the entire list.
The smallest element is selected from the unsorted array and swapped with the
leftmost element, and that element becomes a part of the sorted array. This
process continues moving unsorted array boundary by one element to the right.
This algorithm is not suitable for large data sets as its average and worst case
complexities are of Ο(n2), where n is the number of items.
How Selection Sort Works?
Consider the following depicted array as an example.
For the first position in the sorted list, the whole list is scanned sequentially. The
first position where 14 is stored presently, we search the whole list and find that
10 is the lowest value.
So we replace 14 with 10. After one iteration 10, which happens to be the
minimum value in the list, appears in the first position of the sorted list.
For the second position, where 33 is residing, we start scanning the rest of the list
in a linear manner.
We find that 14 is the second lowest value in the list and it should appear at the
second place. We swap these values.
After two iterations, two least values are positioned at the beginning in a sorted
manner.
The same process is applied to the rest of the items in the array.
Following is a pictorial depiction of the entire sorting process −
Algorithm
Step 1 − Set MIN to location 0
Step 2 − Search the minimum element in the list
Step 3 − Swap with value at location MIN
Step 4 − Increment MIN to point to next element
Step 5 − Repeat until list is sorted
String matching algorithms have greatly influenced computer science and play
an essential role in various real-world problems. It helps in performing time-
efficient tasks in multiple domains. These algorithms are useful in the case of
searching a string within another string. String matching is also used in
the Database schema, Network systems.
Let us look at a few string matching algorithms before proceeding to their
applications in real world. String Matching Algorithms can broadly be classified
into two types of algorithms –
1. Exact String Matching Algorithms
2. Approximate String Matching Algorithms
Exact String Matching Algorithms:
Exact string matching algorithms is to find one, several, or all occurrences of a
defined string (pattern) in a large string (text or sequences) such that each
matching is perfect. All alphabets of patterns must be matched to corresponding
matched subsequence. These are further classified into four categories:
1. Algorithms based on character comparison:
Naive Algorithm: It slides the pattern over text one by one and
Naive and KMP algorithm and starts matching from the last
character of the pattern.
Using the Trie data structure: It is used as an efficient information
the hash value of current substring of text, and if the hash values
match then only it starts matching individual characters.
Approximate String Matching Algorithms:
Approximate String Matching Algorithms (also known as Fuzzy String
Searching) searches for substrings of the input string. More specifically, the
approximate string matching approach is stated as follows: Suppose that we are
given two strings, text T[1…n] and pattern P[1…m]. The task is to find all the
occurrences of patterns in the text whose edit distance to the pattern is at most k.
Some well-known edit distances are – Levenshtein edit distance and Hamming
edit distance.
These techniques are used when the quality of the text is low, there are spelling
errors in the pattern or text, finding DNA subsequences after mutation,
heterogeneous databases, etc. Some approximate string matching algorithms are:
Naive Approach: It slides the pattern over text one by one and check for
approximate matches. If they are found, then slides by 1 again to check for
subsequent approximate matches.
Sellers Algorithm (Dynamic Programming)
set.
Digital Forensics: String matching algorithms are used to locate specific
text strings of interest in the digital forensic text, which are useful for the
investigation.
Spelling Checker: Trie is built based on a predefined set of patterns.
Then, this trie is used for string matching. The text is taken as input, and if
any such pattern occurs, it is shown by reaching the acceptance
state.
Spam filters: Spam filters use string matching to discard the spam. For
example, to categorize an email as spam or not, suspected spam keywords
are searched in the content of the email by string matching algorithms.
Hence, the content is classified as spam or
not.