You are on page 1of 73

Translated from Turkish to English - www.onlinedoctranslator.

com

RECYCLE
(RECURSION)

one
2nd

RECURSION
- Recursionis finding the result of an operation depending on the results
of one or two previous operations.

- Functions that call themselves directly or indirectlyrecursive


called functions.

- Recursion is a technique for solving a problem by dividing it into


simpler subproblems of similar shape.

- Sub-problems can also be divided into other sub-problems within themselves.

- Division stops when the subproblems are small enough to be solved.

- Recursion can be used as an alternative to iterations.


- Assist.Prof.Dr. M. Ali Akcayol
3

RECYCLE
-A problem has a simple non-recursive solution. Other cases
of the problem can be reduced to a stopping case
recursively.

- The recursion process terminates when the stop condition is met.


- Recursion is a powerful problem-solving mechanism.
- Most algorithms can be easily solved recursively.
- But be careful not to make an infinite loop.

- General syntax - rough code:


- do if (stop condition provided)
- solution
- else
- reduce the problem using recursion
4

Recursion - Divide & Conquer Strategy


- It has an important place in computer units:
- Divide the problem into small parts
- Solve each piece independently
- Reach the solution of the main problem by combining the parts

P1
P2 ......................... pn

P11 P12 . . . P1n P21 Pn1 Pn2


P22 ... p2n . . . pnn

.........................................................................................................................................
Situation
Basis

P P P P ..........
P P P P P P P P
5

Recursion - Divide & Conquer Strategy


/* Solve the P problem */
Solve(P){
/* Base case(s) */if If the problem P is in
the ground state
return solution

/* For (n>=2), divide P into P1, P2, ..Pn */ /* Solve problems recursively */

S1 = Solve(P1); /* Solve P1 problem for S1 */ S2 =Solve(P2); /*


Solve P2 problem for S2*/

sec = Solve(Pn); /* Solve Pn problem for sec */

/* Combine parts for solution. */


S = Merge(S1, S2, …, Sn);

/* Return the solution */return


S;
} //done-Solve
6

RECYCLE
- The factorial function is a classic example of recursion:
- n! =1*2*3*…*(n–1)*n
- Todefine the factorial operation recursively, it is necessary to define it as
the factorial of small numbers.
- n! =n*(n–1)!
- Stop status 0 ! = is taken as 1.
- At each call, the n value is decreased by one to reach the stop state.
- Recursive definition:
- n = 1, if n = 0
- f(n) =
- n * f( n – 1 ) , if n > 0
7

RECYCLE
- When the code below runs, it calculates the factorial value
of n.

- int recursiveFactorial(int n)
-{
- if ( n == 1 ) return( 1 );
- else return ( n * recursiveFactorial( n - 1 ));
-}

- n! calculates its value and returns the value it finds.


8

RECYCLE

- Recursion tracking
- One box for each
recursion call
- An arrow from every summoner to the

summoned

- from every call


The arrow drawn to the caller
indicates the return value.
9

RECYCLE
NS
10

RECYCLE
- Generally, iterative functions are more efficient in terms of
time and space.
- Iterative algorithm uses loop structure.
- The recursion algorithm uses the branching algorithm.

- Each time the function is called recursively, memory is allocated


for local variables and parameters.
- Recursion can simplify the solution of the problem, the result is usually
short and the code is easily understandable.
- It is possible to switch to an iterative solution of any
recursively defined problem.
11th

RECYCLE
- Recursive - iterative

- int recFact(int n) - int iteFact(int n)


-{ - {
- if(n ==1) return(1); - int seekValue = 1;

- else - for (int i = n; i > 0; i- -)

- return( n * recFact( n - 1 )); - intermediateValue * = i;

-} - return callValue;
- }
12

FactorialExample.java
import java.io.*;
class FactorialExample
{ static int number;
public static void main(String args[]) throws IOException
{
System.out.print("Please give number:"); System.out.flush();
number=getInt(); int result = factorial(number);
System.out.println(number+"!="+result);
}
public static int factorial(int n) public static String getString() throws IOException
{ {
if(n==0) return 1; InputStreamReader isr = new InputStreamReader(System.in);
else return(n*factorial(n-1)); BufferedReader br = new BufferedReader(isr);
String s = br.readLine();
}
return s;
}
public static int getInt() throws IOException
{ String s = getString();
return Integer.parseInt(s);
}
}
13

Sum of Numbers Up to N
- Suppose our problem is the sum of numbers from 1 to
n.

- Here's how to think about this problem recursively:


- To calculate sum(n) = 1+2+..+n
- Calculate sum(n-1) = 1+2+..+n-1 (a smaller problem of the
same type)
- Add n to Sum(n-1) to calculate Sum(n).
- Sum(n) = Sum(n-1) + n;
- We need to determine the base case.
- The base case (sub-problem) is the problem that can be easily solved
without the need to split the problem.
- If n = 1, Sum(1) = 1;
14

Recursion Tree for Sum(4)


main
/* Sum 1+2+3+…+n */int x=Sum(4) =10
Sum(int n){
int searchSum = 0;
return 6+4
collect(4)
/* Base Case */ SearchTotal=Sum(3) =6
if (n == 1) return 1;
return 3+3
/* Divide and Conquer */ Collect(3)
SearchTotal = Sum(n-1);
SearchTotal=Sum(2) =3
/* Merge */
return searchSum + n; return 1+2
Collect(2)
} /* done-Sum */
SearchTotal=Sum(1) =1
Public ... main(...){
print(“Sum: ”+ Sum(4));} /* done-
main */ Collect(1)

return 1
15

Runtime of Sum(n)

/* Sum 1+2+3+…+n */int


Sum(int n){
int searchSum = 0;
n =1 - 1 (Base case)
T(n) =
/* Base case */
n > 1 - T(n-1) + 1
if (n == 1) return 1;

/* Divide and conquer */


SearchTotal = Sum(n-1);

/* Merge */
return searchSum + n;
} /* done-searchTotal */
16

an Calculate Expression-Pow(a,n)

/* calculate a^n */
- Divide, rule & join
double Ust(double a, int n){
operations can be done
double searchResult;
with one expression.
/* Base case */
if (n == 0) return 1; else if (n == 1)
return a; /* Calculate a^n */
double Ust(double a, int n){
/* searchResult = a^(n-1) */ /* Base case */
searchResult = top(a, n-1); if (n == 0) return 1; else if (n == 1)
return a;
/* Merge */
return searchResult*a; return Upper(a, n-1)*a;
} /* done-Ust */ } /* done-Ust */
17

Recursion tree for Ust(3, 4)


main
x=Upper(3,4) =81

Top(3,4) return 81
/* Calculate a^n */
double Ust(double a, int n){ return 3*Ut(3,3) =81
/* Base case */
if (n == 0) return 1; else if (n == 1) return 27
Top(3,3)
return a;
return 3*Ut(3,2) =27
return a * Upper(a, n-1);
} /* done-Ust */ return 9
Top(3,2)
Public ... main(...){ return 3*Ut(3,1) =9
double x;

x = Upper(3, 4); Over(3.1)


} /* done-main */ return 3
18

Runtime of Ust(a, n)

/* Calculate a^n */
double Ust(double a, int n){
/* base case */
if (n == 0) return 1; else if (n == 1)
return a;

return a * Upper(a, n-1);


} /* done-Ust */

n <= 1 - 1 (Base case) N >


T(n) =
1 -T(n-1) + 1
19

Runtime of Pow(x, n)

- Thisfunction runs in O(n) time (n


recursive calls are made)
- Is there a better solution?
- We can write a more efficient linear recursive
algorithm by squaring the intermediate results:
20

Power(2, 8)
- 2nd8=2*2*2*2*2*2*2*2

- However, we can express this solution by dividing it into two equal


parts:

- 2nd8=24*2nd4

- and 24=22nd*2nd2nd

- and 22nd=2one*2ndone

- Any number to the 1st power is itself.


- Advantage...

- Since both are the same, we do not calculate both!


and we only do 3 multiplications.
21

If the force value is an odd number

- Here's how we do the only ones:


- 2ndsingle = 2 * 2 (single-1)

- Well then, 221 Let's calculate

- As you can see, instead of multiplying

20 times, the result is reached only in 6

times.
22

The logic of recursion


- If the top is double, we divide it into two parts and process it.

- If the top is odd, we subtract 1, then divide and process the


remainder. But don't forget to do 1 more multiplication at the
end since we subtract one from the top.

- Let's start formulating the process:


-e == 0 , Pow(x, e) = 1
-e == 1 , Pow(x, e) = x
- If e is even , Pow(x, e) = Pow(x, e/2) * Pow(x,e/2)
- If e>1 and odd , Pow(x, e) = x * Pow(x, e‐1)
23

Runtime of Power(x, n)
24

Recursion or Iteration?
- If you prefer the iterative solution, the complexity of your
algorithm Front) It is possible.

- If you use this method in the real world, you will be fired.
- If you use recursion with the dynamic programming design
method, the complexity of your algorithm is O(log)2ndn)
happens.
- You can even get a promotion for it.
25

Fibonacci Numbers

- If we define the Fibonacci numbers:


-1 1 2 3 5 8 13…..
- F(0) =0
- F(1) =1
- F(n) = F(n-1) + F(n-2)

/* n. Calculating the Fibonacci number*/int


Fibonacci(int n){
/* Base case */
if (n == 0) return 0; if (n ==
1) return 1;

return Fibonacci(n-1) + Fibonacci(n-2);


} /* done-Fibonacci */
26

Fibonacci Numbers
- The definition of Fibonacci numbers is recursive.
- For example, let's try to find the 40th fibonacci value.
F(40)

F(39) F(38)

F(38) F(37) F(37) F(36)

F(37) F(36) F(36) F(35) F(36) F(35) F(35) F(34)


..............................................................................................

- How many recursive calls are made to F(40) in total.


- Reply: More than 300 000 000 procedures are called.
27

Fibonacci Numbers

- Recursive algorithms are not used for problems


that can be solved with a simple "for".

/* n. Calculating the Fibonacci number*/public


static int fibonacci(int n){
if(n == 1 || n == 2) return one;
int s1=1,s2=1,result=0;
for(int i=0; i<n; i++){
result = s1 + s2; s1
= s2;
s2 = conclusion;
}
return conclusion;

}
28

Towers of Hanoi
- Given: three needles

-A cluster of discs of different sizes placed on the


first needle with the smallest disc on top.
- Aim: move disks from far left to far right.
- Conditions: Only one disc can be moved at a time.
-A disc can be moved to an empty pin or onto
a larger disc.
29

Towers of Hanoi– Recursive


Solution-Java
- package hanoicles;
- import java.util.*;
- public class Hanoikule {

- public static void main(String[] args) {


-
- System.out.print("Enter the value of n : ");Scanner = keyboard new
- Scanner(System.of); int n = keyboard.nextInt(); move,'A', 'B', 'NS');
-
- }
- public static void Move(int n, char A, char B, char NS) {if(n==1)
- System.out.println(A + " --> " + B);else
-
- {
- stone(n-1, A, C, B); stone(1, A, B, C); stone(n-1, C, B, A); }
- return;
- }
30
31

Tail Recursion

- In the recursion method, this occurs if the recursive call is the last step.
The iteration call is made at the end of the method.
- int recFact(int n)
-{
- if (n<=1) return 1;
- else return n * recFact(n-1);
-}
- void tail() {
- …..
- …..
- tail(); }
32

NonTail Recursion
- In a recursive method, this occurs if the recursive call is not the
last step. Other operations are done after the iteration call
(printing etc) or it could be binary recursion.

- Binary recursion occurs if two recursive calls are made except in


the base case.

- int nontail(int n)
-{
- if (n > 0) {
- …….
- nontail(n-1);
- printf(n);
- …….
- nontail(n-1); }
-}
33

Indirect Recursion
- The iteration call is made from inside another function.
- void A(int n)
-{
- if (n <= 0) return 1;
- n- -;
- B(n);

-}
- void B(int n)
-{
- if (n <= 0) return 1;
- n- -;
- Moment);

-}
34

Nested Recursion

- The iteration call is made within the recursion call.

- int A(int n, int m)


-{
- if (n <= 0) return 1;
- return A(n-1, A(n-1, m-1));
-}
35

SUMMARY

- Recursion is present in all nature.


- Formula is easier than memorizing. Not every problem may have a formula,
but every problem can be expressed as a series of small, repetitive steps.

- Each step in a recursive process should be small and computable.


- It should definitely have a terminating condition.

- Recursion is most useful for algorithms written for use in data


structures that we call a recursively defined binary tree. It is
much easier than the normal iteration.
- It is great for recursion, divide & conquer, and dynamic
program type algorithms.
36

Homework

- 1- Make the program that finds the standard deviation (?) for n x
values with iteration and recursive.

- xm=(1/n)

-? =standard deviation
-V = variance value
- xm = mean value
37

Homework

- 2-The Ackerman function is defined as follows. Perform


this function recursively.

A(m,n)=
Hashing

38
39

HASHING (Crop or Whisk )

- Hashing is the process of obtaining as many unique


integers as possible using the data we have.

- This obtained integer is used as the index of the data held in the form
of a string, allowing us to access the data at once.

- If we divide the subject of Hasing into sub-headings;


- Hashing Function
- Hash Table
- Collision
40

HASHING
- Hash Functions
- Selecting Digits
- Folding (shift folding, boundary folding)
-
- Mid-Square-Mid-Square
- Extraction
- Radix Transformation- Base Transformation

- Collision and solutions


- Linear Probing - Linear Filling Double
- Hashing - Double Clipping Quadratic
- Probing - Quadratic Filling Chaining
-
41

HASHING

- Thebasic operation in search methods is to compare keys.


The search process continues until a key reaches the
position in the table.

- With the hash function, the searched key element can be accessed
directly. The hash function calculates the index where a key
information is located in the table.

- Open hashing: Uses potentially unlimited space.


- Closed hashing: It uses fixed space for information recording.
42

HASHING (Crop or Whisk )


- In a table of size N, the hash function (h(x)) maps a key x to a
value between 0 and N-1.
- Sample:

- For a table with N=15, h(x) = x 15% (partition with - > mode).
If,
- x 25 129 35 2501 47 36 10 9 5
h(x) 11 2 6
- The positions of the keys in the table are as follows:

01 2nd 3 4 5 6 7 8 9 10 11 12 13 14
-- 47 - - 35 36 - - 129 25 2501 - --
43

HASHING (Crop or Whisk )


Hash Table
0
one
Records 2nd
Mehmet 2500 3 Mehmet 2500
key Hash
Ripeness 31250 4 Kemal 31250
function
Selin 27500 5
Mary 28200 6 Selin 27500

7 Mary 28200
key 8
9
44

Hash functions
- Hash functions return the index order in which a key is found in the table.

- Perfect hash function:


-A function that maps only one position to each key is called.
- Simple perfect hash function:
-A function that maps only one position to each key is called when the table size
is equal to the total number of keys (there are no empty spaces in the table).

-A good hash function:


- It should be easy and quick to calculate.

- should assign only one key for each position in the table.
45

Hash functions

- Hash functions operate on integer numbers.


- Non-integer keys are converted to
integer values.
- For example, if the person's health number is 9635-8904, the
dash in between is removed and it is taken as 96358904.
- If the key consists of characters, the ASCII codes of the
characters are used.
46

Hash functions
(Selecting Digits)

- Theposition in the table is found by selecting and


combining certain digits on the key.
- The value created by the selection of the 2nd and 5th digits is as
follows.
- h(033475678) = 37
- h(023455678) = 25
- Pros and Cons
- Its structure is simple.

- It cannot properly distribute the keys over the entire table.


- Conflict can be very frequent.
47

Hash functions (Folding-folding)


- Thekey is divided into several parts and these parts are gathered
together to find the position in the table.
- Shift
folding In the method, each part of the key is collected by mod
without changing the table size.
- Example: SSN = 123-45-6789. When the SSN number is divided into
three parts as 123, 456, 789 and collected, the position number is
obtained as 123+456+789 = 1368.
- Boundary folding In the method, the order of the parts of the key is changed
and the table is collected by mode according to the size.
- Example: SSN number is divided into three parts as 123, 456, 789. The first piece
remains in the same order and the second piece is reversed. Then the third piece
is taken in the same order and collected by mod according to the table size.
(123+654+789 = 1566)
48

Hash functions (Division)


- The key value is divided by mode based on the table size.
- Example: SSN = 123456789. If the table size is
1000, the hash function finds the result as
follows;
- hash(h) = 123456789 1000% = 789
- The structure is simple, but there is overlap.
49

Hash functions (Mid-square, Mid-square)

- The key value is squared and the middle part of the result is
selected to find the position value in the table.
- Example: Let key = 3121. The hash function finds
the result as follows;
- 31212nd = 9740641
- hash(3121) = 406
- The square of the key can be represented as binary.

- 31212nd = 100101001010000101100001
- hash(3121) = 0101000010 = 322
50

Hash functions (Extraction)

- Only some parts of the key value are selected to


find the position value in the table.
- Example: Let the key = 123-45-6789. The hash
function can be any of the following;
- hash(123-45-6789) = 123456789 = 1234
- hash(123-45-6789) = 123456789 = 6789
- hash(123-45-6789) = 123456789 = 1289
51

Hash functions (Radix Transformation)

- The key value is converted to another radix.


- Example: Let the key = 1238. Taking the table
size as 1000,
- 123810 = 23268
- The position value is found by dividing the calculated value with
the table size by the mode.

- Hash(1238) = 2326 1000% = 326


52

Hash functions -Conflict

- Let me add the value x = 65 to the table


below.

- x= 65
- h(x) =65 mod 15 = 5
- Conflict occurs if more than one record
arrives at the same position.
53

Hash functions
Conflict elimination (Chaining)
- Records arriving at the same position are

displayed with linked lists.

- Add: Add to the top of the list


- Delete/Access: Search the
appropriate list
54

Hash functions
Conflict elimination (Chaining)
- Sample:

- Addition of the numbers 29, 16,


14, 99, 127
- Other keys in the same position are added
to the beginning of the linked list.

- Disadvantages of the chaining method

- Some parts of the table are


not used at all.
- Time required for search and delete
operations as linked lists get longer
is lengthening.
55

Hash functions
Conflict elimination (Linear Probing)
- The second record coming to the same

position is from the related position.

placed in the next first


vacant position.

- Adding: It is done by
finding an empty space.

- Delete/Access: Continue until


first free space is found
can.
- h(k)=x% m
- h(k,i)=(h(k)'+i)%m

- x:key, m:table size


56

Hash functions
Conflict elimination (Linear Probing)
- Example: h(x) = x mod 13
- Enter the values 18, 41, 22, 44, 59, 32, 31, 73 in the given order.

0 1 2 3 4 5 6 7 8 9 10 11 12

41 18 44 59 32 22 31 735 6 7 8 9 10 11
012 3 4 12
57

Hash functions
Conflict elimination (Linear Probing)
- Advantages / disadvantages of the Linear Probing method

- There is no need for a separate data structure such as linked lists.

- Causes records to be stacked.

- The time required for delete and search operations increases as the number of

same hashes increases.


58

Hash functions
Conflict elimination (Quadratic Probing)
- The second record that comes to the same position is placed

with the Quadratic Function.

- Most used function


- x:key, m:table size
- h(x)=x% m
- h(x,i) = (h(k)'+i2nd)%m
- For the new position, respectively (t+12nd), (t+22nd), ..., (t+n2nd

The positions corresponding to the ) values are looked up

and placed in the first empty one.


59

Hash functions
Conflict elimination (Quadratic Probing)

- Example: Put the values 29, 16, 14, 99, 127 in


the hash table in order with the quadratic
probing method.
- h(x) = x mode m
- h(29) = 29 % 15=14
- h(29.1)=(h(29)'+1)15%=(14+1)%15=0
60

Hash functions
Conflict elimination (Quadratic Probing)

- Example: Put the values 29, 16, 14, 99, 127 in


the hash table in order with the quadratic
probing method.
- h(16) = 16% 15=1
61

Hash functions
Conflict elimination (Quadratic Probing)

- Example: Put the values 29, 16, 14, 99, 127 in


the hash table in order with the quadratic
probing method.
- h(14) = 14% 15=14
- h(14,1)=(14+1)%15=0

- h(14,2)=(14+22nd)%15=18%15=3
62

Hash functions
Conflict elimination (Quadratic Probing)

- Example: Put the values 29, 16, 14, 99, 127 in


the hash table in order with the quadratic
probing method.
- h(99) = 99 % 15=9
- h(99.1)=(9+1)%15=0

- h(99.2)=(9+22nd)%15=13%15=13
63

Hash functions
Conflict elimination (Quadratic Probing)

- Example: Put the values 29, 16, 14, 99, 127 in


the hash table in order with the quadratic
probing method.
- h(127) = 127 % 15= 7
64

Hash functions
Conflict elimination (Quadratic Probing)
- Advantages / disadvantages of the Quadratic Probing method

- It distributes the key values more smoothly than the linear probing
method.

- If the table size is not taken into consideration while adding a new element, there is
a risk of working forever.

- Example: In a table with size 16 (0-15), when positions 0, 1, 4


and 9 are filled, when we try to add the value 16, it enters an
infinite loop.
65

Hash functions
Conflict removal (Double Hashing)
-A second hash function is used for the second record that comes to the same
position.

- The second hash function cannot take the value 0.

- Most used function:


- hash(x) = (hashone(x) + i * hash2nd(x)) % m

- hash2nd(x)= R − ( x % R ), R < m, R: prime (or prime between m)


- hash(x) = hashone(x)
- hash(x) = (hashone(x) + 1 * hash2nd(x)) % m

- hash(x) = (hashone(x) + 2 * hash2nd(x)) % m

- hash(x) = (hashone(x) + 3 * hash2nd(x)) % m


66

Hash functions
Conflict removal (Double Hashing)
- Example: Adding the value 65
- hashone(x)= x 15%
- hash2nd(x)=11 − ( x 11% )
- hash(65) = hashone(65)
- hash(65.0) =5 full
- hash(65.1) = (hashone(5) + 1 * hash2nd(65))15%
- hash(65.1) =(5 +(11-10))%15=6
- hash(65.2) = (hashone(5) + 2 * hash2nd(65))15%
- hash(65.2) =(5 +2)%15=7
67

Hash functions
Conflict removal (Double Hashing)
- Advantages / disadvantages of the Double Hashing method

- It distributes the key values more smoothly than the linear probing
method and groups do not form.

- It is slower than the Quadratic probing method because a second hash


function is calculated.
68

Hash functions

- Performance

to do
number of probing:
1/(1-α)
α:table fill rate
α=n/m
n: element entered
number
m: table size
69

Homework

- Writea program that randomly generates 100 key values


and places them in a hash table of size 100.
- In the program, use the division method as a hash function,
chaining, linear probing and quadratic probing methods
separately for conflict resolution.
- Get key values as integers.
- Use the array structure for the hash table.
70

Chaining java kd
- public class LinkedHashEntry
- { private int key;
- private int value;
- private LinkedHashEntry next;
- LinkedHashEntry (int key, int value)
- { this.key = key;
- this.value = value;
- this.next = null;
- }
- public int getValue() { return value; }
- public void setValue(int value) { this.value = value; }
- public int getKey() { return key; }
- public LinkedHashEntry getNext() { return next; }
- public void setNext(LinkedHashEntry next) { this.next = next; }
71

Chaining java kd
- public class HashMap
- { private static int TABLE_SIZE = 128;
- LinkedHashEntry[] table;
- HashMap()
- { table = new LinkedHashEntry[TABLE_SIZE];
- for (int i = 0; i < TABLE_SIZE; i++) table[i] = null;
- }
- public int get(int key) {
- int hash = (key % TABLE_SIZE); if (table[hash] ==
- null) return -1; else { LinkedHashEntry entry =
- table[hash];
- while (entry != null && entry.getKey() != key) entry = entry.getNext();
- if (entry == null) return -1;
- else return entry.getValue();
- }
- }
-
72

Chaining java kd

- public void put(int key, int value) {


- int hash = (key % TABLE_SIZE);
- if (table[hash] == null) table[hash] = new LinkedHashEntry(key, value); else
- { LinkedHashEntry entry = table[hash];
- while (entry.getNext() != null && entry.getKey() != key)
- entry = entry.getNext(); if (entry.getKey() == key) else
- entry.setValue(value);
entry.setNext(new LinkedHashEntry(key, value));
-
- }
- }
-
73

Chaining java kd
- public void remove(int key)
- { int hash = (key % TABLE_SIZE);
- if (table[hash] != null) {
- LinkedHashEntry prevEntry = null;
- LinkedHashEntry entry = table[hash];
- while (entry.getNext() != null && entry.getKey() != key)
- { prevEntry = entry; entry = entry.getNext(); }
- if (entry.getKey() == key) {
- if (prevEntry == null) else table[hash] = entry.getNext();
- prevEntry.setNext(entry.getNext());
- }
- }
- }
- }
- }

You might also like