kopia lustrzana https://github.com/animator/learn-python
Merge branch 'main' into main
commit
48fac96b97
|
@ -12,6 +12,7 @@
|
|||
- [Protocols](protocols.md)
|
||||
- [Exception Handling in Python](exception-handling.md)
|
||||
- [Generators](generators.md)
|
||||
- [Match Case Statement](match-case.md)
|
||||
- [Closures](closures.md)
|
||||
- [Filter](filter-function.md)
|
||||
- [Reduce](reduce-function.md)
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
# Match Case Statements
|
||||
## Introduction
|
||||
Match and case statements are introduced in Python 3.10 for structural pattern matching of patterns with associated actions. It offers more readible and
|
||||
cleaniness to the code as opposed to the traditional `if-else` statements. They also have destructuring, pattern matching and checks for specific properties in
|
||||
addition to the traditional `switch-case` statements in other languages, which makes them more versatile.
|
||||
|
||||
## Syntax
|
||||
```
|
||||
match <statement>:
|
||||
case <pattern_1>:
|
||||
<do_task_1>
|
||||
case <pattern_2>:
|
||||
<do_task_2>
|
||||
case _:
|
||||
<do_task_wildcard>
|
||||
```
|
||||
A match statement takes a statement which compares it to the various cases and their patterns. If any of the pattern is matched successively, the task is performed accordingly. If an exact match is not confirmed, the last case, a wildcard `_`, if provided, will be used as the matching case.
|
||||
|
||||
## Pattern Matching
|
||||
As discussed earlier, match case statements use pattern matching where the patterns consist of sequences, mappings, primitive data types as well as class instances. The structural pattern matching uses declarative approach and it nexplicitly states the conditions for the patterns to match with the data.
|
||||
|
||||
### Patterns with a Literal
|
||||
#### Generic Case
|
||||
`sample text` is passed as a literal in the `match` block. There are two cases and a wildcard case mentioned.
|
||||
```python
|
||||
match 'sample text':
|
||||
case 'sample text':
|
||||
print('sample text')
|
||||
case 'sample':
|
||||
print('sample')
|
||||
case _:
|
||||
print('None found')
|
||||
```
|
||||
The `sample text` case is satisfied as it matches with the literal `sample text` described in the `match` block.
|
||||
|
||||
O/P:
|
||||
```
|
||||
sample text
|
||||
```
|
||||
|
||||
#### Using OR
|
||||
Taking another example, `|` can be used as OR to include multiple patterns in a single case statement where the multiple patterns all lead to a similar task.
|
||||
|
||||
The below code snippets can be used interchangebly and generate the similar output. The latter is more consive and readible.
|
||||
```python
|
||||
match 'e':
|
||||
case 'a':
|
||||
print('vowel')
|
||||
case 'e':
|
||||
print('vowel')
|
||||
case 'i':
|
||||
print('vowel')
|
||||
case 'o':
|
||||
print('vowel')
|
||||
case 'u':
|
||||
print('vowel')
|
||||
case _:
|
||||
print('consonant')
|
||||
```
|
||||
```python
|
||||
match 'e':
|
||||
case 'a' | 'e' | 'i' | 'o' | 'u':
|
||||
print('vowel')
|
||||
case _:
|
||||
print('consonant')
|
||||
```
|
||||
O/P:
|
||||
```
|
||||
vowel
|
||||
```
|
||||
|
||||
#### Without wildcard
|
||||
When in a `match` block, there is no wildcard case present there are be two cases of match being present or not. If the match doesn't exist, the behaviour is a no-op.
|
||||
```python
|
||||
match 'c':
|
||||
case 'a' | 'e' | 'i' | 'o' | 'u':
|
||||
print('vowel')
|
||||
```
|
||||
The output will be blank as a no-op occurs.
|
||||
|
||||
### Patterns with a Literal and a Variable
|
||||
Pattern matching can be done by unpacking the assignments and also bind variables with it.
|
||||
```python
|
||||
def get_names(names: str) -> None:
|
||||
match names:
|
||||
case ('Bob', y):
|
||||
print(f'Hello {y}')
|
||||
case (x, 'John'):
|
||||
print(f'Hello {x}')
|
||||
case (x, y):
|
||||
print(f'Hello {x} and {y}')
|
||||
case _:
|
||||
print('Invalid')
|
||||
```
|
||||
Here, the `names` is a tuple that contains two names. The `match` block unpacks the tuple and binds `x` and `y` based on the patterns. A wildcard case prints `Invalid` if the condition is not satisfied.
|
||||
|
||||
O/P:
|
||||
|
||||
In this example, the above code snippet with the parameter `names` as below and the respective output.
|
||||
```
|
||||
>>> get_names(('Bob', 'Max'))
|
||||
Hello Max
|
||||
|
||||
>>> get_names(('Rob', 'John'))
|
||||
Hello Rob
|
||||
|
||||
>>> get_names(('Rob', 'Max'))
|
||||
Hello Rob and Max
|
||||
|
||||
>>> get_names(('Rob', 'Max', 'Bob'))
|
||||
Invalid
|
||||
```
|
||||
|
||||
### Patterns with Classes
|
||||
Class structures can be used in `match` block for pattern matching. The class members can also be binded with a variable to perform certain operations. For the class structure:
|
||||
```python
|
||||
class Person:
|
||||
def __init__(self, name, age):
|
||||
self.name = name
|
||||
self.age = age
|
||||
```
|
||||
The match case example illustrates the generic working as well as the binding of variables with the class members.
|
||||
```python
|
||||
def get_class(cls: Person) -> None:
|
||||
match cls:
|
||||
case Person(name='Bob', age=18):
|
||||
print('Hello Bob with age 18')
|
||||
case Person(name='Max', age=y):
|
||||
print(f'Age is {y}')
|
||||
case Person(name=x, age=18):
|
||||
print(f'Name is {x}')
|
||||
case Person(name=x, age=y):
|
||||
print(f'Name and age is {x} and {y}')
|
||||
case _:
|
||||
print('Invalid')
|
||||
```
|
||||
O/P:
|
||||
```
|
||||
>>> get_class(Person('Bob', 18))
|
||||
Hello Bob with age 18
|
||||
|
||||
>>> get_class(Person('Max', 21))
|
||||
Age is 21
|
||||
|
||||
>>> get_class(Person('Rob', 18))
|
||||
Name is Rob
|
||||
|
||||
>>> get_class(Person('Rob', 21))
|
||||
Name and age is Rob and 21
|
||||
```
|
||||
Now, if a new class is introduced in the above code snippet like below.
|
||||
```python
|
||||
class Pet:
|
||||
def __init__(self, name, animal):
|
||||
self.name = name
|
||||
self.animal = animal
|
||||
```
|
||||
The patterns will not match the cases and will trigger the wildcard case for the original code snippet above with `get_class` function.
|
||||
```
|
||||
>>> get_class(Pet('Tommy', 'Dog'))
|
||||
Invalid
|
||||
```
|
||||
|
||||
### Nested Patterns
|
||||
The patterns can be nested via various means. It can include the mix of the patterns mentioned earlier or can be symmetrical across. A basic of the nested pattern of a list with Patterns with a Literal and Variable is taken. Classes and Iterables can laso be included.
|
||||
```python
|
||||
def get_points(points: list) -> None:
|
||||
match points:
|
||||
case []:
|
||||
print('Empty')
|
||||
case [x]:
|
||||
print(f'One point {x}')
|
||||
case [x, y]:
|
||||
print(f'Two points {x} and {y}')
|
||||
case _:
|
||||
print('More than two points')
|
||||
```
|
||||
O/P:
|
||||
```
|
||||
>>> get_points([])
|
||||
Empty
|
||||
|
||||
>>> get_points([1])
|
||||
One point 1
|
||||
|
||||
>>> get_points([1, 2])
|
||||
Two points 1 and 2
|
||||
|
||||
>>> get_points([1, 2, 3])
|
||||
More than two points
|
||||
```
|
||||
|
||||
### Complex Patterns
|
||||
Complex patterns are also supported in the pattern matching sequence. The complex does not mean complex numbers but rather the structure which makes the readibility to seem complex.
|
||||
|
||||
#### Wildcard
|
||||
The wildcard used till now are in the form of `case _` where the wildcard case is used if no match is found. Furthermore, the wildcard `_` can also be used as a placeholder in complex patterns.
|
||||
|
||||
```python
|
||||
def wildcard(value: tuple) -> None:
|
||||
match value:
|
||||
case ('Bob', age, 'Mechanic'):
|
||||
print(f'Bob is mechanic of age {age}')
|
||||
case ('Bob', age, _):
|
||||
print(f'Bob is not a mechanic of age {age}')
|
||||
```
|
||||
O/P:
|
||||
|
||||
The value in the above snippet is a tuple with `(Name, Age, Job)`. If the job is Mechanic and the name is Bob, the first case is triggered. But if the job is different and not a mechanic, then the other case is triggered with the wildcard.
|
||||
```
|
||||
>>> wildcard(('Bob', 18, 'Mechanic'))
|
||||
Bob is mechanic of age 18
|
||||
|
||||
>>> wildcard(('Bob', 21, 'Engineer'))
|
||||
Bob is not a mechanic of age 21
|
||||
```
|
||||
|
||||
#### Guard
|
||||
A `guard` is when an `if` is added to a pattern. The evaluation depends on the truth value of the guard.
|
||||
|
||||
`nums` is the tuple which contains two integers. A guard is the first case where it checks whether the first number is greater or equal to the second number in the tuple. If it is false, then it moves to the second case, where it concludes that the first number is smaller than the second number.
|
||||
```python
|
||||
def guard(nums: tuple) -> None:
|
||||
match nums:
|
||||
case (x, y) if x >= y:
|
||||
print(f'{x} is greater or equal than {y}')
|
||||
case (x, y):
|
||||
print(f'{x} is smaller than {y}')
|
||||
case _:
|
||||
print('Invalid')
|
||||
```
|
||||
O/P:
|
||||
```
|
||||
>>> guard((1, 2))
|
||||
1 is smaller than 2
|
||||
|
||||
>>> guard((2, 1))
|
||||
2 is greater or equal than 1
|
||||
|
||||
>>> guard((1, 1))
|
||||
1 is greater or equal than 1
|
||||
```
|
||||
|
||||
## Summary
|
||||
The match case statements provide an elegant and readible format to perform operations on pattern matching as compared to `if-else` statements. They are also more versatile as they provide additional functionalities on the pattern matching operations like unpacking, class matching, iterables and iterators. It can also use positional arguments for checking the patterns. They provide a powerful and concise way to handle multiple conditions and perform pattern matching
|
||||
|
||||
## Further Reading
|
||||
This article provides a brief introduction to the match case statements and the overview on the pattern matching operations. To know more, the below articles can be used for in-depth understanding of the topic.
|
||||
|
||||
- [PEP 634 – Structural Pattern Matching: Specification](https://peps.python.org/pep-0634/)
|
||||
- [PEP 636 – Structural Pattern Matching: Tutorial](https://peps.python.org/pep-0636/)
|
|
@ -0,0 +1,185 @@
|
|||
# AVL Tree
|
||||
|
||||
In Data Structures and Algorithms, an **AVL Tree** is a self-balancing binary search tree (BST) where the difference between heights of left and right subtrees cannot be more than one for all nodes. It ensures that the tree remains balanced, providing efficient search, insertion, and deletion operations.
|
||||
|
||||
## Points to be Remembered
|
||||
|
||||
- **Balance Factor**: The difference in heights between the left and right subtrees of a node. It should be -1, 0, or +1 for all nodes in an AVL tree.
|
||||
- **Rotations**: Tree rotations (left, right, left-right, right-left) are used to maintain the balance factor within the allowed range.
|
||||
|
||||
## Real Life Examples of AVL Trees
|
||||
|
||||
- **Databases**: AVL trees can be used to maintain large indexes for database tables, ensuring quick data retrieval.
|
||||
- **File Systems**: Some file systems use AVL trees to keep track of free and used memory blocks.
|
||||
|
||||
## Applications of AVL Trees
|
||||
|
||||
AVL trees are used in various applications in Computer Science:
|
||||
|
||||
- **Database Indexing**
|
||||
- **Memory Allocation**
|
||||
- **Network Routing Algorithms**
|
||||
|
||||
Understanding these applications is essential for Software Development.
|
||||
|
||||
## Operations in AVL Tree
|
||||
|
||||
Key operations include:
|
||||
|
||||
- **INSERT**: Insert a new element into the AVL tree.
|
||||
- **SEARCH**: Find the position of an element in the AVL tree.
|
||||
- **DELETE**: Remove an element from the AVL tree.
|
||||
|
||||
## Implementing AVL Tree in Python
|
||||
|
||||
```python
|
||||
class AVLTreeNode:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.height = 1
|
||||
|
||||
class AVLTree:
|
||||
def insert(self, root, key):
|
||||
if not root:
|
||||
return AVLTreeNode(key)
|
||||
|
||||
if key < root.key:
|
||||
root.left = self.insert(root.left, key)
|
||||
else:
|
||||
root.right = self.insert(root.right, key)
|
||||
|
||||
root.height = 1 + max(self.getHeight(root.left), self.getHeight(root.right))
|
||||
balance = self.getBalance(root)
|
||||
|
||||
if balance > 1 and key < root.left.key:
|
||||
return self.rotateRight(root)
|
||||
if balance < -1 and key > root.right.key:
|
||||
return self.rotateLeft(root)
|
||||
if balance > 1 and key > root.left.key:
|
||||
root.left = self.rotateLeft(root.left)
|
||||
return self.rotateRight(root)
|
||||
if balance < -1 and key < root.right.key:
|
||||
root.right = self.rotateRight(root.right)
|
||||
return self.rotateLeft(root)
|
||||
|
||||
return root
|
||||
|
||||
def search(self, root, key):
|
||||
if not root or root.key == key:
|
||||
return root
|
||||
|
||||
if key < root.key:
|
||||
return self.search(root.left, key)
|
||||
|
||||
return self.search(root.right, key)
|
||||
|
||||
def delete(self, root, key):
|
||||
if not root:
|
||||
return root
|
||||
|
||||
if key < root.key:
|
||||
root.left = self.delete(root.left, key)
|
||||
elif key > root.key:
|
||||
root.right = self.delete(root.right, key)
|
||||
else:
|
||||
if root.left is None:
|
||||
temp = root.right
|
||||
root = None
|
||||
return temp
|
||||
elif root.right is None:
|
||||
temp = root.left
|
||||
root = None
|
||||
return temp
|
||||
|
||||
temp = self.getMinValueNode(root.right)
|
||||
root.key = temp.key
|
||||
root.right = self.delete(root.right, temp.key)
|
||||
|
||||
if root is None:
|
||||
return root
|
||||
|
||||
root.height = 1 + max(self.getHeight(root.left), self.getHeight(root.right))
|
||||
balance = self.getBalance(root)
|
||||
|
||||
if balance > 1 and self.getBalance(root.left) >= 0:
|
||||
return self.rotateRight(root)
|
||||
if balance < -1 and self.getBalance(root.right) <= 0:
|
||||
return self.rotateLeft(root)
|
||||
if balance > 1 and self.getBalance(root.left) < 0:
|
||||
root.left = self.rotateLeft(root.left)
|
||||
return self.rotateRight(root)
|
||||
if balance < -1 and self.getBalance(root.right) > 0:
|
||||
root.right = self.rotateRight(root.right)
|
||||
return self.rotateLeft(root)
|
||||
|
||||
return root
|
||||
|
||||
def rotateLeft(self, z):
|
||||
y = z.right
|
||||
T2 = y.left
|
||||
y.left = z
|
||||
z.right = T2
|
||||
z.height = 1 + max(self.getHeight(z.left), self.getHeight(z.right))
|
||||
y.height = 1 + max(self.getHeight(y.left), self.getHeight(y.right))
|
||||
return y
|
||||
|
||||
def rotateRight(self, z):
|
||||
y = z.left
|
||||
T3 = y.right
|
||||
y.right = z
|
||||
z.left = T3
|
||||
z.height = 1 + max(self.getHeight(z.left), self.getHeight(z.right))
|
||||
y.height = 1 + max(self.getHeight(y.left), self.getHeight(y.right))
|
||||
return y
|
||||
|
||||
def getHeight(self, root):
|
||||
if not root:
|
||||
return 0
|
||||
return root.height
|
||||
|
||||
def getBalance(self, root):
|
||||
if not root:
|
||||
return 0
|
||||
return self.getHeight(root.left) - self.getHeight(root.right)
|
||||
|
||||
def getMinValueNode(self, root):
|
||||
if root is None or root.left is None:
|
||||
return root
|
||||
return self.getMinValueNode(root.left)
|
||||
|
||||
def preOrder(self, root):
|
||||
if not root:
|
||||
return
|
||||
print(root.key, end=' ')
|
||||
self.preOrder(root.left)
|
||||
self.preOrder(root.right)
|
||||
|
||||
#Example usage
|
||||
avl_tree = AVLTree()
|
||||
root = None
|
||||
|
||||
root = avl_tree.insert(root, 10)
|
||||
root = avl_tree.insert(root, 20)
|
||||
root = avl_tree.insert(root, 30)
|
||||
root = avl_tree.insert(root, 40)
|
||||
root = avl_tree.insert(root, 50)
|
||||
root = avl_tree.insert(root, 25)
|
||||
|
||||
print("Preorder traversal of the AVL tree is:")
|
||||
avl_tree.preOrder(root)
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```markdown
|
||||
Preorder traversal of the AVL tree is:
|
||||
30 20 10 25 40 50
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
|
||||
- **Insertion**: O(logn). Inserting a node involves traversing the height of the tree, which is logarithmic due to the balancing property.
|
||||
- **Search**: O(logn). Searching for a node involves traversing the height of the tree.
|
||||
- **Deletion**: O(logn). Deleting a node involves traversing and potentially rebalancing the tree, maintaining the logarithmic height.
|
|
@ -51,10 +51,6 @@ print(f"The {n}th Fibonacci number is: {fibonacci(n)}.")
|
|||
- **Time Complexity**: O(n) for both approaches
|
||||
- **Space Complexity**: O(n) for the top-down approach (due to memoization), O(1) for the bottom-up approach
|
||||
|
||||
</br>
|
||||
<hr>
|
||||
</br>
|
||||
|
||||
# 2. Longest Common Subsequence
|
||||
|
||||
The longest common subsequence (LCS) problem is to find the longest subsequence common to two sequences. A subsequence is a sequence that appears in the same relative order but not necessarily contiguous.
|
||||
|
@ -84,13 +80,33 @@ Y = "GXTXAYB"
|
|||
print("Length of Longest Common Subsequence:", longest_common_subsequence(X, Y, len(X), len(Y)))
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
- **Time Complexity**: O(m * n) for the top-down approach, where m and n are the lengths of the input sequences
|
||||
- **Space Complexity**: O(m * n) for the memoization table
|
||||
## Longest Common Subsequence Code in Python (Bottom-Up Approach)
|
||||
|
||||
</br>
|
||||
<hr>
|
||||
</br>
|
||||
```python
|
||||
|
||||
def longestCommonSubsequence(X, Y, m, n):
|
||||
L = [[None]*(n+1) for i in range(m+1)]
|
||||
for i in range(m+1):
|
||||
for j in range(n+1):
|
||||
if i == 0 or j == 0:
|
||||
L[i][j] = 0
|
||||
elif X[i-1] == Y[j-1]:
|
||||
L[i][j] = L[i-1][j-1]+1
|
||||
else:
|
||||
L[i][j] = max(L[i-1][j], L[i][j-1])
|
||||
return L[m][n]
|
||||
|
||||
|
||||
S1 = "AGGTAB"
|
||||
S2 = "GXTXAYB"
|
||||
m = len(S1)
|
||||
n = len(S2)
|
||||
print("Length of LCS is", longestCommonSubsequence(S1, S2, m, n))
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
- **Time Complexity**: O(m * n) for both approaches, where m and n are the lengths of the input sequences
|
||||
- **Space Complexity**: O(m * n) for the memoization table
|
||||
|
||||
# 3. 0-1 Knapsack Problem
|
||||
|
||||
|
@ -123,10 +139,98 @@ n = len(weights)
|
|||
print("Maximum value that can be obtained:", knapsack(weights, values, capacity, n))
|
||||
```
|
||||
|
||||
## 0-1 Knapsack Problem Code in Python (Bottom-up Approach)
|
||||
|
||||
```python
|
||||
def knapSack(capacity, weights, values, n):
|
||||
K = [[0 for x in range(capacity + 1)] for x in range(n + 1)]
|
||||
for i in range(n + 1):
|
||||
for w in range(capacity + 1):
|
||||
if i == 0 or w == 0:
|
||||
K[i][w] = 0
|
||||
elif weights[i-1] <= w:
|
||||
K[i][w] = max(values[i-1]
|
||||
+ K[i-1][w-weights[i-1]],
|
||||
K[i-1][w])
|
||||
else:
|
||||
K[i][w] = K[i-1][w]
|
||||
|
||||
return K[n][capacity]
|
||||
|
||||
values = [60, 100, 120]
|
||||
weights = [10, 20, 30]
|
||||
capacity = 50
|
||||
n = len(weights)
|
||||
print(knapSack(capacity, weights, values, n))
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
- **Time Complexity**: O(n * W) for the top-down approach, where n is the number of items and W is the capacity of the knapsack
|
||||
- **Time Complexity**: O(n * W) for both approaches, where n is the number of items and W is the capacity of the knapsack
|
||||
- **Space Complexity**: O(n * W) for the memoization table
|
||||
|
||||
</br>
|
||||
<hr>
|
||||
</br>
|
||||
# 4. Longest Increasing Subsequence
|
||||
|
||||
The Longest Increasing Subsequence (LIS) is a task is to find the longest subsequence that is strictly increasing, meaning each element in the subsequence is greater than the one before it. This subsequence must maintain the order of elements as they appear in the original sequence but does not need to be contiguous. The goal is to identify the subsequence with the maximum possible length.
|
||||
|
||||
**Algorithm Overview:**
|
||||
- **Base cases:** If the sequence is empty, the LIS length is 0.
|
||||
- **Memoization:** Store the results of previously computed subproblems to avoid redundant computations.
|
||||
- **Recurrence relation:** Compute the LIS length by comparing characters of the sequences and making decisions based on their values.
|
||||
|
||||
## Longest Increasing Subsequence Code in Python (Top-Down Approach using Memoization)
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
def f(idx, prev_idx, n, a, dp):
|
||||
if (idx == n):
|
||||
return 0
|
||||
|
||||
if (dp[idx][prev_idx + 1] != -1):
|
||||
return dp[idx][prev_idx + 1]
|
||||
|
||||
notTake = 0 + f(idx + 1, prev_idx, n, a, dp)
|
||||
take = -sys.maxsize - 1
|
||||
if (prev_idx == -1 or a[idx] > a[prev_idx]):
|
||||
take = 1 + f(idx + 1, idx, n, a, dp)
|
||||
|
||||
dp[idx][prev_idx + 1] = max(take, notTake)
|
||||
return dp[idx][prev_idx + 1]
|
||||
|
||||
def longestSubsequence(n, a):
|
||||
|
||||
dp = [[-1 for i in range(n + 1)]for j in range(n + 1)]
|
||||
return f(0, -1, n, a, dp)
|
||||
|
||||
a = [3, 10, 2, 1, 20]
|
||||
n = len(a)
|
||||
|
||||
print("Length of lis is", longestSubsequence(n, a))
|
||||
|
||||
```
|
||||
|
||||
## Longest Increasing Subsequence Code in Python (Bottom-Up Approach)
|
||||
|
||||
```python
|
||||
def lis(arr):
|
||||
n = len(arr)
|
||||
lis = [1]*n
|
||||
|
||||
for i in range(1, n):
|
||||
for j in range(0, i):
|
||||
if arr[i] > arr[j] and lis[i] < lis[j] + 1:
|
||||
lis[i] = lis[j]+1
|
||||
|
||||
maximum = 0
|
||||
for i in range(n):
|
||||
maximum = max(maximum, lis[i])
|
||||
|
||||
return maximum
|
||||
|
||||
arr = [10, 22, 9, 33, 21, 50, 41, 60]
|
||||
print("Length of lis is", lis(arr))
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
- **Time Complexity**: O(n * n) for both approaches, where n is the length of the array.
|
||||
- **Space Complexity**: O(n * n) for the memoization table in Top-Down Approach, O(n) in Bottom-Up Approach.
|
||||
|
|
|
@ -16,4 +16,6 @@
|
|||
- [Two Pointer Technique](two-pointer-technique.md)
|
||||
- [Hashing through Linear Probing](hashing-linear-probing.md)
|
||||
- [Hashing through Chaining](hashing-chaining.md)
|
||||
- [Hash Tables, Sets, Maps ](hash-tables.md)
|
||||
- [Hash Tables, Sets, Maps ](hash-tables.md)
|
||||
- [AVL Trees](avl-trees.md)
|
||||
- [Splay Trees](splay-trees.md)
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
# Splay Tree
|
||||
|
||||
In Data Structures and Algorithms, a **Splay Tree** is a self-adjusting binary search tree with the additional property that recently accessed elements are quick to access again. It performs basic operations such as insertion, search, and deletion in O(log n) amortized time. This is achieved by a process called **splaying**, where the accessed node is moved to the root through a series of tree rotations.
|
||||
|
||||
## Points to be Remembered
|
||||
|
||||
- **Splaying**: Moving the accessed node to the root using rotations.
|
||||
- **Rotations**: Tree rotations (left and right) are used to balance the tree during splaying.
|
||||
- **Self-adjusting**: The tree adjusts itself with each access, keeping frequently accessed nodes near the root.
|
||||
|
||||
## Real Life Examples of Splay Trees
|
||||
|
||||
- **Cache Implementation**: Frequently accessed data is kept near the top of the tree, making repeated accesses faster.
|
||||
- **Networking**: Routing tables in network switches can use splay trees to prioritize frequently accessed routes.
|
||||
|
||||
## Applications of Splay Trees
|
||||
|
||||
Splay trees are used in various applications in Computer Science:
|
||||
|
||||
- **Cache Implementations**
|
||||
- **Garbage Collection Algorithms**
|
||||
- **Data Compression Algorithms (e.g., LZ78)**
|
||||
|
||||
Understanding these applications is essential for Software Development.
|
||||
|
||||
## Operations in Splay Tree
|
||||
|
||||
Key operations include:
|
||||
|
||||
- **INSERT**: Insert a new element into the splay tree.
|
||||
- **SEARCH**: Find the position of an element in the splay tree.
|
||||
- **DELETE**: Remove an element from the splay tree.
|
||||
|
||||
## Implementing Splay Tree in Python
|
||||
|
||||
```python
|
||||
class SplayTreeNode:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.left = None
|
||||
self.right = None
|
||||
|
||||
class SplayTree:
|
||||
def __init__(self):
|
||||
self.root = None
|
||||
|
||||
def insert(self, key):
|
||||
self.root = self.splay_insert(self.root, key)
|
||||
|
||||
def search(self, key):
|
||||
self.root = self.splay_search(self.root, key)
|
||||
return self.root
|
||||
|
||||
def splay(self, root, key):
|
||||
if not root or root.key == key:
|
||||
return root
|
||||
|
||||
if root.key > key:
|
||||
if not root.left:
|
||||
return root
|
||||
if root.left.key > key:
|
||||
root.left.left = self.splay(root.left.left, key)
|
||||
root = self.rotateRight(root)
|
||||
elif root.left.key < key:
|
||||
root.left.right = self.splay(root.left.right, key)
|
||||
if root.left.right:
|
||||
root.left = self.rotateLeft(root.left)
|
||||
return root if not root.left else self.rotateRight(root)
|
||||
|
||||
else:
|
||||
if not root.right:
|
||||
return root
|
||||
if root.right.key > key:
|
||||
root.right.left = self.splay(root.right.left, key)
|
||||
if root.right.left:
|
||||
root.right = self.rotateRight(root.right)
|
||||
elif root.right.key < key:
|
||||
root.right.right = self.splay(root.right.right, key)
|
||||
root = self.rotateLeft(root)
|
||||
return root if not root.right else self.rotateLeft(root)
|
||||
|
||||
def splay_insert(self, root, key):
|
||||
if not root:
|
||||
return SplayTreeNode(key)
|
||||
|
||||
root = self.splay(root, key)
|
||||
|
||||
if root.key == key:
|
||||
return root
|
||||
|
||||
new_node = SplayTreeNode(key)
|
||||
|
||||
if root.key > key:
|
||||
new_node.right = root
|
||||
new_node.left = root.left
|
||||
root.left = None
|
||||
else:
|
||||
new_node.left = root
|
||||
new_node.right = root.right
|
||||
root.right = None
|
||||
|
||||
return new_node
|
||||
|
||||
def splay_search(self, root, key):
|
||||
return self.splay(root, key)
|
||||
|
||||
def rotateRight(self, node):
|
||||
temp = node.left
|
||||
node.left = temp.right
|
||||
temp.right = node
|
||||
return temp
|
||||
|
||||
def rotateLeft(self, node):
|
||||
temp = node.right
|
||||
node.right = temp.left
|
||||
temp.left = node
|
||||
return temp
|
||||
|
||||
def preOrder(self, root):
|
||||
if root:
|
||||
print(root.key, end=' ')
|
||||
self.preOrder(root.left)
|
||||
self.preOrder(root.right)
|
||||
|
||||
#Example usage:
|
||||
splay_tree = SplayTree()
|
||||
splay_tree.insert(50)
|
||||
splay_tree.insert(30)
|
||||
splay_tree.insert(20)
|
||||
splay_tree.insert(40)
|
||||
splay_tree.insert(70)
|
||||
splay_tree.insert(60)
|
||||
splay_tree.insert(80)
|
||||
|
||||
print("Preorder traversal of the Splay tree is:")
|
||||
splay_tree.preOrder(splay_tree.root)
|
||||
|
||||
splay_tree.search(60)
|
||||
|
||||
print("\nSplay tree after search operation for key 60:")
|
||||
splay_tree.preOrder(splay_tree.root)
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```markdown
|
||||
Preorder traversal of the Splay tree is:
|
||||
50 30 20 40 70 60 80
|
||||
|
||||
Splay tree after search operation for key 60:
|
||||
60 50 30 20 40 70 80
|
||||
```
|
||||
|
||||
## Complexity Analysis
|
||||
|
||||
The worst-case time complexities of the main operations in a Splay Tree are as follows:
|
||||
|
||||
- **Insertion**: (O(n)). In the worst case, insertion may take linear time if the tree is highly unbalanced.
|
||||
- **Search**: (O(n)). In the worst case, searching for a node may take linear time if the tree is highly unbalanced.
|
||||
- **Deletion**: (O(n)). In the worst case, deleting a node may take linear time if the tree is highly unbalanced.
|
||||
|
||||
While these operations can take linear time in the worst case, the splay operation ensures that the tree remains balanced over a sequence of operations, leading to better average-case performance.
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 541 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 12 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 13 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 6.0 KiB |
|
@ -19,4 +19,5 @@
|
|||
- [Hierarchical Clustering](hierarchical-clustering.md)
|
||||
- [Grid Search](grid-search.md)
|
||||
- [Transformers](transformers.md)
|
||||
- [K-Means](kmeans.md)
|
||||
- [K-nearest neighbor (KNN)](knn.md)
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
# K-Means Clustering
|
||||
Unsupervised Learning Algorithm for Grouping Similar Data.
|
||||
|
||||
## Introduction
|
||||
K-means clustering is a fundamental unsupervised machine learning algorithm that excels at grouping similar data points together. It's a popular choice due to its simplicity and efficiency in uncovering hidden patterns within unlabeled datasets.
|
||||
|
||||
## Unsupervised Learning
|
||||
Unlike supervised learning algorithms that rely on labeled data for training, unsupervised algorithms, like K-means, operate solely on input data (without predefined categories). Their objective is to discover inherent structures or groupings within the data.
|
||||
|
||||
## The K-Means Objective
|
||||
Organize similar data points into clusters to unveil underlying patterns. The main objective is to minimize total intra-cluster variance or the squared function.
|
||||
|
||||

|
||||
## Clusters and Centroids
|
||||
A cluster represents a collection of data points that share similar characteristics. K-means identifies a pre-determined number (k) of clusters within the dataset. Each cluster is represented by a centroid, which acts as its central point (imaginary or real).
|
||||
|
||||
## Minimizing In-Cluster Variation
|
||||
The K-means algorithm strategically assigns each data point to a cluster such that the total variation within each cluster (measured by the sum of squared distances between points and their centroid) is minimized. In simpler terms, K-means strives to create clusters where data points are close to their respective centroids.
|
||||
|
||||
## The Meaning Behind "K-Means"
|
||||
The "means" in K-means refers to the averaging process used to compute the centroid, essentially finding the center of each cluster.
|
||||
|
||||
## K-Means Algorithm in Action
|
||||

|
||||
The K-means algorithm follows an iterative approach to optimize cluster formation:
|
||||
|
||||
1. **Initial Centroid Placement:** The process begins with randomly selecting k centroids to serve as initial reference points for each cluster.
|
||||
2. **Data Point Assignment:** Each data point is assigned to the closest centroid, effectively creating a preliminary clustering.
|
||||
3. **Centroid Repositioning:** Once data points are assigned, the centroids are recalculated by averaging the positions of the points within their respective clusters. These new centroids represent the refined centers of the clusters.
|
||||
4. **Iteration Until Convergence:** Steps 2 and 3 are repeated iteratively until a stopping criterion is met. This criterion can be either:
|
||||
- **Centroid Stability:** No significant change occurs in the centroids' positions, indicating successful clustering.
|
||||
- **Reaching Maximum Iterations:** A predefined number of iterations is completed.
|
||||
|
||||
## Code
|
||||
Following is a simple implementation of K-Means.
|
||||
|
||||
```python
|
||||
# Generate and Visualize Sample Data
|
||||
# import the necessary Libraries
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Create data points for cluster 1 and cluster 2
|
||||
X = -2 * np.random.rand(100, 2)
|
||||
X1 = 1 + 2 * np.random.rand(50, 2)
|
||||
|
||||
# Combine data points from both clusters
|
||||
X[50:100, :] = X1
|
||||
|
||||
# Plot data points and display the plot
|
||||
plt.scatter(X[:, 0], X[:, 1], s=50, c='b')
|
||||
plt.show()
|
||||
|
||||
# K-Means Model Creation and Training
|
||||
from sklearn.cluster import KMeans
|
||||
|
||||
# Create KMeans object with 2 clusters
|
||||
kmeans = KMeans(n_clusters=2)
|
||||
kmeans.fit(X) # Train the model on the data
|
||||
|
||||
# Visualize Data Points with Centroids
|
||||
centroids = kmeans.cluster_centers_ # Get centroids (cluster centers)
|
||||
|
||||
plt.scatter(X[:, 0], X[:, 1], s=50, c='b') # Plot data points again
|
||||
plt.scatter(centroids[0, 0], centroids[0, 1], s=200, c='g', marker='s') # Plot centroid 1
|
||||
plt.scatter(centroids[1, 0], centroids[1, 1], s=200, c='r', marker='s') # Plot centroid 2
|
||||
plt.show() # Display the plot with centroids
|
||||
|
||||
# Predict Cluster Label for New Data Point
|
||||
new_data = np.array([-3.0, -3.0])
|
||||
new_data_reshaped = new_data.reshape(1, -1)
|
||||
predicted_cluster = kmeans.predict(new_data_reshaped)
|
||||
print("Predicted cluster for new data:", predicted_cluster)
|
||||
```
|
||||
|
||||
### Output:
|
||||
Before Implementing K-Means Clustering
|
||||

|
||||
|
||||
After Implementing K-Means Clustering
|
||||

|
||||
|
||||
Predicted cluster for new data: `[0]`
|
||||
|
||||
## Conclusion
|
||||
**K-Means** can be applied to data that has a smaller number of dimensions, is numeric, and is continuous or can be used to find groups that have not been explicitly labeled in the data. As an example, it can be used for Document Classification, Delivery Store Optimization, or Customer Segmentation.
|
||||
|
||||
## References
|
||||
|
||||
- [Survey of Machine Learning and Data Mining Techniques used in Multimedia System](https://www.researchgate.net/publication/333457161_Survey_of_Machine_Learning_and_Data_Mining_Techniques_used_in_Multimedia_System?_tp=eyJjb250ZXh0Ijp7ImZpcnN0UGFnZSI6Il9kaXJlY3QiLCJwYWdlIjoiX2RpcmVjdCJ9fQ)
|
||||
- [A Clustering Approach for Outliers Detection in a Big Point-of-Sales Database](https://www.researchgate.net/publication/339267868_A_Clustering_Approach_for_Outliers_Detection_in_a_Big_Point-of-Sales_Database?_tp=eyJjb250ZXh0Ijp7ImZpcnN0UGFnZSI6Il9kaXJlY3QiLCJwYWdlIjoiX2RpcmVjdCJ9fQ)
|
Ładowanie…
Reference in New Issue