Merge branch 'main' into binary_tree

pull/1095/head
Ashita Prasad 2024-06-22 15:46:23 +05:30 zatwierdzone przez GitHub
commit f6d9d50c27
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
6 zmienionych plików z 496 dodań i 0 usunięć

Wyświetl plik

@ -18,3 +18,4 @@
- [Reduce](reduce-function.md)
- [List Comprehension](list-comprehension.md)
- [Eval Function](eval_function.md)
- [Magic Methods](magic-methods.md)

Wyświetl plik

@ -0,0 +1,151 @@
# Magic Methods
Magic methods, also known as dunder (double underscore) methods, are special methods in Python that start and end with double underscores (`__`).
These methods allow you to define the behavior of objects for built-in operations and functions, enabling you to customize how your objects interact with the
language's syntax and built-in features. Magic methods make your custom classes integrate seamlessly with Pythons built-in data types and operations.
**Commonly Used Magic Methods**
1. **Initialization and Representation**
- `__init__(self, ...)`: Called when an instance of the class is created. Used for initializing the object's attributes.
- `__repr__(self)`: Returns a string representation of the object, useful for debugging and logging.
- `__str__(self)`: Returns a human-readable string representation of the object.
**Example** :
```python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person({self.name}, {self.age})"
def __str__(self):
return f"{self.name}, {self.age} years old"
p = Person("Alice", 30)
print(repr(p))
print(str(p))
```
**Output** :
```python
Person("Alice",30)
Alice, 30 years old
```
2. **Arithmetic Operations**
- `__add__(self, other)`: Defines behavior for the `+` operator.
- `__sub__(self, other)`: Defines behavior for the `-` operator.
- `__mul__(self, other)`: Defines behavior for the `*` operator.
- `__truediv__(self, other)`: Defines behavior for the `/` operator.
**Example** :
```python
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 1)
v3 = v1 + v2
print(v3)
```
**Output** :
```python
Vector(3, 4)
```
3. **Comparison Operations**
- `__eq__(self, other)`: Defines behavior for the `==` operator.
- `__lt__(self, other)`: Defines behavior for the `<` operator.
- `__le__(self, other)`: Defines behavior for the `<=` operator.
**Example** :
```python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
p1 = Person("Alice", 30)
p2 = Person("Bob", 25)
print(p1 == p2)
print(p1 < p2)
```
**Output** :
```python
False
False
```
5. **Container and Sequence Methods**
- `__len__(self)`: Defines behavior for the `len()` function.
- `__getitem__(self, key)`: Defines behavior for indexing (`self[key]`).
- `__setitem__(self, key, value)`: Defines behavior for item assignment (`self[key] = value`).
- `__delitem__(self, key)`: Defines behavior for item deletion (`del self[key]`).
**Example** :
```python
class CustomList:
def __init__(self, *args):
self.items = list(args)
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __delitem__(self, index):
del self.items[index]
def __repr__(self):
return f"CustomList({self.items})"
cl = CustomList(1, 2, 3)
print(len(cl))
print(cl[1])
cl[1] = 5
print(cl)
del cl[1]
print(cl)
```
**Output** :
```python
3
2
CustomList([1, 5, 3])
CustomList([1, 3])
```
Magic methods provide powerful ways to customize the behavior of your objects and make them work seamlessly with Python's syntax and built-in functions.
Use them judiciously to enhance the functionality and readability of your classes.

Wyświetl plik

@ -0,0 +1,212 @@
# Data Structures: Hash Tables, Hash Sets, and Hash Maps
## Table of Contents
- [Introduction](#introduction)
- [Hash Tables](#hash-tables)
- [Overview](#overview)
- [Operations](#operations)
- [Hash Sets](#hash-sets)
- [Overview](#overview-1)
- [Operations](#operations-1)
- [Hash Maps](#hash-maps)
- [Overview](#overview-2)
- [Operations](#operations-2)
- [Conclusion](#conclusion)
## Introduction
This document provides an overview of three fundamental data structures in computer science: hash tables, hash sets, and hash maps. These structures are widely used for efficient data storage and retrieval operations.
## Hash Tables
### Overview
A **hash table** is a data structure that stores key-value pairs. It uses a hash function to compute an index into an array of buckets or slots, from which the desired value can be found.
### Operations
1. **Insertion**: Add a new key-value pair to the hash table.
2. **Deletion**: Remove a key-value pair from the hash table.
3. **Search**: Find the value associated with a given key.
4. **Update**: Modify the value associated with a given key.
**Example Code (Python):**
```python
class Node:
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
def __init__(self, capacity):
self.capacity = capacity
self.size = 0
self.table = [None] * capacity
def _hash(self, key):
return hash(key) % self.capacity
def insert(self, key, value):
index = self._hash(key)
if self.table[index] is None:
self.table[index] = Node(key, value)
self.size += 1
else:
current = self.table[index]
while current:
if current.key == key:
current.value = value
return
current = current.next
new_node = Node(key, value)
new_node.next = self.table[index]
self.table[index] = new_node
self.size += 1
def search(self, key):
index = self._hash(key)
current = self.table[index]
while current:
if current.key == key:
return current.value
current = current.next
raise KeyError(key)
def remove(self, key):
index = self._hash(key)
previous = None
current = self.table[index]
while current:
if current.key == key:
if previous:
previous.next = current.next
else:
self.table[index] = current.next
self.size -= 1
return
previous = current
current = current.next
raise KeyError(key)
def __len__(self):
return self.size
def __contains__(self, key):
try:
self.search(key)
return True
except KeyError:
return False
# Driver code
if __name__ == '__main__':
ht = HashTable(5)
ht.insert("apple", 3)
ht.insert("banana", 2)
ht.insert("cherry", 5)
print("apple" in ht)
print("durian" in ht)
print(ht.search("banana"))
ht.insert("banana", 4)
print(ht.search("banana")) # 4
ht.remove("apple")
print(len(ht)) # 3
```
# Insert elements
hash_table["key1"] = "value1"
hash_table["key2"] = "value2"
# Search for an element
value = hash_table.get("key1")
# Delete an element
del hash_table["key2"]
# Update an element
hash_table["key1"] = "new_value1"
## Hash Sets
### Overview
A **hash set** is a collection of unique elements. It is implemented using a hash table where each bucket can store only one element.
### Operations
1. **Insertion**: Add a new element to the set.
2. **Deletion**: Remove an element from the set.
3. **Search**: Check if an element exists in the set.
4. **Union**: Combine two sets to form a new set with elements from both.
5. **Intersection**: Find common elements between two sets.
6. **Difference**: Find elements present in one set but not in the other.
**Example Code (Python):**
```python
# Create a hash set
hash_set = set()
# Insert elements
hash_set.add("element1")
hash_set.add("element2")
# Search for an element
exists = "element1" in hash_set
# Delete an element
hash_set.remove("element2")
# Union of sets
another_set = {"element3", "element4"}
union_set = hash_set.union(another_set)
# Intersection of sets
intersection_set = hash_set.intersection(another_set)
# Difference of sets
difference_set = hash_set.difference(another_set)
```
## Hash Maps
### Overview
A **hash map** is similar to a hash table but often provides additional functionalities and more user-friendly interfaces for developers. It is a collection of key-value pairs where each key is unique.
### Operations
1. **Insertion**: Add a new key-value pair to the hash map.
2. **Deletion**: Remove a key-value pair from the hash map.
3. **Search**: Retrieve the value associated with a given key.
4. **Update**: Change the value associated with a given key.
**Example Code (Python):**
```python
# Create a hash map
hash_map = {}
# Insert elements
hash_map["key1"] = "value1"
hash_map["key2"] = "value2"
# Search for an element
value = hash_map.get("key1")
# Delete an element
del hash_map["key2"]
# Update an element
hash_map["key1"] = "new_value1"
```
## Conclusion
Hash tables, hash sets, and hash maps are powerful data structures that provide efficient means of storing and retrieving data. Understanding these structures and their operations is crucial for developing optimized algorithms and applications.

Wyświetl plik

@ -16,6 +16,7 @@
- [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)
- [Binary Tree](binary-tree.md)
- [AVL Trees](avl-trees.md)
- [Splay Trees](splay-trees.md)

Wyświetl plik

@ -11,3 +11,4 @@
- [Sorting NumPy Arrays](sorting-array.md)
- [NumPy Array Iteration](array-iteration.md)
- [Concatenation of Arrays](concatenation-of-arrays.md)
- [Universal Functions (Ufunc)](universal-functions.md)

Wyświetl plik

@ -0,0 +1,130 @@
# Universal functions (ufunc)
---
A `ufunc`, short for "`universal function`," is a fundamental concept in NumPy, a powerful library for numerical computing in Python. Universal functions are highly optimized, element-wise functions designed to perform operations on data stored in NumPy arrays.
## Uses of Ufuncs in NumPy
Universal functions (ufuncs) in NumPy provide a wide range of functionalities for efficient and powerful numerical computations. Below is a detailed explanation of their uses:
### 1. **Element-wise Operations**
Ufuncs perform operations on each element of the arrays independently.
```python
import numpy as np
A = np.array([1, 2, 3, 4])
B = np.array([5, 6, 7, 8])
# Element-wise addition
np.add(A, B) # Output: array([ 6, 8, 10, 12])
```
### 2. **Broadcasting**
Ufuncs support broadcasting, allowing operations on arrays with different shapes, making it possible to perform operations without explicitly reshaping arrays.
```python
C = np.array([1, 2, 3])
D = np.array([[1], [2], [3]])
# Broadcasting addition
np.add(C, D) # Output: array([[2, 3, 4], [3, 4, 5], [4, 5, 6]])
```
### 3. **Vectorization**
Ufuncs are vectorized, meaning they are implemented in low-level C code, allowing for fast execution and avoiding the overhead of Python loops.
```python
# Vectorized square root
np.sqrt(A) # Output: array([1., 1.41421356, 1.73205081, 2.])
```
### 4. **Type Flexibility**
Ufuncs handle various data types and perform automatic type casting as needed.
```python
E = np.array([1.0, 2.0, 3.0])
F = np.array([4, 5, 6])
# Addition with type casting
np.add(E, F) # Output: array([5., 7., 9.])
```
### 5. **Reduction Operations**
Ufuncs support reduction operations, such as summing all elements of an array or finding the product of all elements.
```python
# Summing all elements
np.add.reduce(A) # Output: 10
# Product of all elements
np.multiply.reduce(A) # Output: 24
```
### 6. **Accumulation Operations**
Ufuncs can perform accumulation operations, which keep a running tally of the computation.
```python
# Cumulative sum
np.add.accumulate(A) # Output: array([ 1, 3, 6, 10])
```
### 7. **Reduceat Operations**
Ufuncs can perform segmented reductions using the `reduceat` method, which applies the ufunc at specified intervals.
```python
G = np.array([0, 1, 2, 3, 4, 5, 6, 7])
indices = [0, 2, 5]
np.add.reduceat(G, indices) # Output: array([ 1, 9, 18])
```
### 8. **Outer Product**
Ufuncs can compute the outer product of two arrays, producing a matrix where each element is the result of applying the ufunc to each pair of elements from the input arrays.
```python
# Outer product
np.multiply.outer([1, 2, 3], [4, 5, 6])
# Output: array([[ 4, 5, 6],
# [ 8, 10, 12],
# [12, 15, 18]])
```
### 9. **Out Parameter**
Ufuncs can use the `out` parameter to store results in a pre-allocated array, saving memory and improving performance.
```python
result = np.empty_like(A)
np.multiply(A, B, out=result) # Output: array([ 5, 12, 21, 32])
```
# Create Your Own Ufunc
You can create custom ufuncs for specific needs using np.frompyfunc or np.vectorize, allowing Python functions to behave like ufuncs.
Here, we are using `frompyfunc()` which takes three argument:
1. function - the name of the function.
2. inputs - the number of input (arrays).
3. outputs - the number of output arrays.
```python
def my_add(x, y):
return x + y
my_add_ufunc = np.frompyfunc(my_add, 2, 1)
my_add_ufunc(A, B) # Output: array([ 6, 8, 10, 12], dtype=object)
```
# Some Common Ufunc are
Here are some commonly used ufuncs in NumPy:
- **Arithmetic**: `np.add`, `np.subtract`, `np.multiply`, `np.divide`
- **Trigonometric**: `np.sin`, `np.cos`, `np.tan`
- **Exponential and Logarithmic**: `np.exp`, `np.log`, `np.log10`
- **Comparison**: `np.maximum`, `np.minimum`, `np.greater`, `np.less`
- **Logical**: `np.logical_and`, `np.logical_or`, `np.logical_not`
For more such Ufunc, address to [Universal functions (ufunc) — NumPy](https://numpy.org/doc/stable/reference/ufuncs.html)