kopia lustrzana https://github.com/animator/learn-python
Merge branch 'main' into binary_tree
commit
f6d9d50c27
|
@ -18,3 +18,4 @@
|
|||
- [Reduce](reduce-function.md)
|
||||
- [List Comprehension](list-comprehension.md)
|
||||
- [Eval Function](eval_function.md)
|
||||
- [Magic Methods](magic-methods.md)
|
||||
|
|
|
@ -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 Python’s 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.
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Ładowanie…
Reference in New Issue