import pyperclip
def n0():
    s = r'''
n001 ооп вопросы
n002 функциональное вопросы
n003 структуры данных вопросы
n004 сортировки вопросы
n005 дерево,куча,хэш таблица
n1 классметод список объектов
n2 декоратор
n3 декоратор с доп параметрами
n4 стек связный список
n5 стек лист
n6 стек(любая структура) с условием
n7 однонаправленный список
n8 кольцевой список
n9 двунаправленный список
n10 быстрая сортировка
n11 дерево
n12 сортировка выбором
n13 cортировка вставками
n14 сортировка слияние
n15 сортировка шелла
'''
    return pyperclip.copy(s)
def n1():
    s=r'''
__objects__=[]
def __init__(self,brand,model,year):
        self.__class__.__objects__.append(self)
@classmethod
    def get_objects(cls):
        for i in cls.__objects__:
            print(i.brand)
    '''
    return pyperclip.copy(s)
def n2():
    s=r'''
import functools
def only5(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        kwargs={k:v for k,v in kwargs.items() if v[1]>=4.5}
        #result=function(*args, **kwargs)
        #return result**2
        function(*args, **kwargs)
    return wrapper
    '''
    return pyperclip.copy(s)
def n3():
    s=r'''
import functools
def tol(dlina,fill):
    def w1(func):
        @functools.wraps(func)
        def w2(*args,**kwargs):
            res=func(*args,**kwargs)
            ma=max(len(res),dlina)
            dres={k:v for k,v in zip(range(ma),res+(ma-len(res))*[fill])}
            return dres
        return w2
    return w1
    '''
    return pyperclip.copy(s)
def n4():
    s=r'''
class Node():
    def __init__(self,data):
        self.data=data
        self.next=None
class Stack:
    def __init__(self):
        self.head = None

    def is_empty(self):
        return self.head is None
    def push(self, item):
        new_node = Node(item)
        new_node.next = self.head
        self.head = new_node

    def pop(self):
        if self.is_empty():
            return None
        else:
            popped_item = self.head.data
            self.head = self.head.next
            return popped_item

    def top(self):
        if self.is_empty():
            return None
        else:
            return self.head.data

    def __str__(self):
        current = self.head
        stack_str = ""
        while current:
            stack_str += str(current.data) + " → "
            current = current.next
        return stack_str.rstrip(" → ")
    '''
    return pyperclip.copy(s)
def n5():
    s=r'''
class Stack():
    def __init__(self):
        self.vals=[]
    def add(self,x):
        self.vals.insert(0,x)
    def is_empty(self):
        return not bool(self.vals)
    def pop(self):
        return self.vals.pop(0)
    def top(self):
        return self.vals[0]
    def __str__(self):
        return ' '.join([str(x) for x in self.vals])
    '''
    return pyperclip.copy(s)
def n6():
    s=r'''
class Node():
    def __init__(self,data):
        self.data=data
        self.next=None
class Stack:
    def __init__(self,check):
        self.check=check
        self.head = None
    def push(self, x):
        if eval(self.check):
            new_node = Node(x)
            new_node.next = self.head
            self.head = new_node
    '''
    return pyperclip.copy(s)
def n7():
    s=r'''
class Node():
    def __init__(self,obj=None,nxt=None):
        self.obj=obj
        self.nxt=nxt
    def __str__(self):
        return f'{self.obj}'

class SingleLinkedList():
    def __init__(self):
        self.head=None
        self.tail=None
    def append_r(self,obj):
        if self.head==None and self.tail==None:
            new_obj=Node(obj)
            self.head=new_obj
            self.tail=new_obj
            print(new_obj.__dict__)
        elif self.head==self.tail:
            new_obj=Node(obj)
            self.head.nxt=new_obj
            self.tail=new_obj
            print(new_obj.__dict__)
        else:
            new_obj=Node(obj)
            self.tail.nxt=new_obj
            self.tail=new_obj
            print(new_obj.__dict__)
    def append_l(self,obj):
        if self.head==None and self.tail==None:
            new_obj=Node(obj)
            self.head=new_obj
            self.tail=new_obj
            print(new_obj.__dict__)
        elif self.head==self.tail:
            new_obj=Node(obj)
            a=self.head
            self.head=new_obj
            self.head.nxt=a
            print(new_obj.__dict__)
        else:
            new_obj=Node(obj)
            a=self.head
            self.head=new_obj
            self.head.nxt=a
            print(new_obj.__dict__)
    def append_m(self,obj,start):
        if self.head!=self.tail:
            temp=self.head
            for i in range(start):
                temp=temp.nxt
            new_obj=Node(obj)
            a=temp.nxt
            temp.nxt=new_obj
            new_obj.nxt=a
        else:
            print('В связном списке меньше двух значений')
    def pop_sll(self,n):
        if n==0:
            self.head=self.head.nxt
        elif len(self)!=1:
            temp=self.head
            for i in range(n-1):
                temp=temp.nxt
            a=temp.nxt
            temp.nxt=a.nxt
            if n==len(self):
                self.tail=temp
        else:
            self.head=None
            self.tail=None
            return 'Список пуст'
    def remove_sll(self,n):
        f=0
        temp=self.head
        last=None
        while temp!=None:
            if temp.obj==n:
                if f==0:
                    self.head=self.head.nxt
                    del(temp)
                    break
                last.nxt=temp.nxt
                del(temp)
                break
            last=temp
            temp=temp.nxt
            f=1
    def __str__(self):
        if self.head==None:
            return 'Список пуст'
        else:
            temp=self.head
            while temp!=None:
                print(temp,end=' ')
                temp=temp.nxt
            return 'конец списка'
    def shw(self):
        if self.head==None:
            return 'Список пуст'
        else:
            temp=self.head
            while temp!=None:
                print(temp,end='')
                temp=temp.nxt
    def __len__(self):
        cur=self.head
        c=0
        while cur is not None:
            cur=cur.nxt
            c+=1
        return c
    '''
    return pyperclip.copy(s)
def n8():
    s=r'''
class Node():
    def __init__(self,obj=None,nxt=None):
        self.obj=obj
        self.nxt=nxt
    def __str__(self):
        return f'{self.obj}'

class CycledLinkedList():
    def __init__(self):
        self.head=None
        self.tail=None
    def append_r(self,obj):
        if self.head==None and self.tail==None:
            new_obj=Node(obj)
            self.head=new_obj
            self.tail=new_obj
            self.tail.nxt=self.head
        elif self.head==self.tail:
            new_obj=Node(obj)
            self.head.nxt=new_obj
            self.tail=new_obj
            self.tail.nxt=self.head
        else:
            new_obj=Node(obj)
            self.tail.nxt=new_obj
            self.tail=new_obj
            self.tail.nxt=self.head
    def append_l(self,obj):
        if self.head==None and self.tail==None:
            new_obj=Node(obj)
            self.head=new_obj
            self.tail=new_obj
            self.tail.nxt=self.head
        elif self.head==self.tail:
            new_obj=Node(obj)
            a=self.head
            self.head=new_obj
            self.head.nxt=a
            self.tail.nxt=self.head
        else:
            new_obj=Node(obj)
            a=self.head
            self.head=new_obj
            self.head.nxt=a
            self.tail.nxt=self.head
    def __str__(self):
        if self.head==None:
            return 'Список пуст'
        else:
            temp=self.head
            while temp!=self.tail:
                print(temp,end=' ')
                temp=temp.nxt
            print(temp,end=' ')
            return 'конец списка'
    def append_m(self,obj,start):
        temp=self.head
        for i in range(start):
            temp=temp.nxt
        new_obj=Node(obj)
        a=temp.nxt
        temp.nxt=new_obj
        new_obj.nxt=a
        if self.tail==temp:
            self.tail=new_obj
    def printt(self,t=10):
        if self.head==None:
            print('Список пуст')
        else:
            temp=self.head
            for i in range(t):
                print(temp.obj,end=' ')
                temp=temp.nxt
            print('конец вывода')
    def __len__(self):
        if self.head==None:
            return 0
        else:
            cur=self.head
            c=1
            while cur!=self.tail:
                cur=cur.nxt
                c+=1
            return c
    def pop(self,elem_ind=0):
        le=len(self)
        if le==1:
            self.head=None
            self.tail=None
        elif le==elem_ind+1:
            temp=self.head
            for i in range(elem_ind-1):
                temp=temp.nxt
            self.tail=temp
            self.tail.nxt=self.head
        elif elem_ind==0 or le%(elem_ind)==0 or (elem_ind)%le==0:
            self.head=self.head.nxt
            self.tail.nxt=self.head
        else:
            temp=self.head
            for i in range(elem_ind):
                if i==elem_ind-1:
                    break
                temp=temp.nxt
            temp.nxt=temp.nxt.nxt
    '''
    return pyperclip.copy(s)
def n9():
    s=r'''
class Node():
    def __init__(self, obj = None, nxt = None, prv = None):
        self.obj = obj
        self.nxt = nxt
        self.prv = prv
    def __str__(self):
        return f"{self.obj}"
class DoubleLinkedList():
    def __init__(self):
        self.head = None
        self.tail = None
    def append(self, obj):
        if self.head:
            if self.tail:
                a = Node(obj)
                a.prv = self.tail
                self.tail.nxt = a
                self.tail = a
            else:
                self.tail = Node(obj)
                self.head.nxt = self.tail
                self.tail.prv = self.head
        else:
            self.head = Node(obj)
    def append_l(self, obj):
        if self.head:
            if self.tail:
                a = Node(obj)
                a.nxt = self.head
                self.head.prv = a
                self.head = a
            else:
                self.tail = self.head
                self.head = Node(obj)
                self.head.nxt = self.tail
                self.tail.prv = self.head
        else:
            self.head = Node(obj)
    def append_m(self, obj, n):
        assert n < len(self), 'Слишком большое n'
        node = self.head
        for i in range(n):
            node = node.nxt 
        if n == 0:
            self.append_l(obj)
        elif n == len(self) - 1:
            self.append(obj)
        else:
            a = Node(obj)
            a.prv = node.prv
            a.nxt = node
            node.prv.nxt = a
            node.prv = a   
    def delete(self, n):
        node = self.head
        last = self.head
        assert n < len(self), 'Слишком большое n'
        for i in range(n):
            last = node
            node = node.nxt
        if node == self.head:
            self.head = node.nxt
            self.head.prv = None
            del node
        elif node == self.tail:
            last.nxt = None
            self.tail = last
            del node
        else:
            last.nxt = node.nxt
            node.nxt.prv = last
            del node   
    def __str__(self):
        res = []
        if self.head == None:
            return None
        if self.tail == None:
            return str(self.head.obj)
        node = self.head
        while node != self.tail:
            res.append(node.obj)
            node = node.nxt
        res.append(node.obj)
        return ' '.join(map(str, res))
    def __len__(self):
        res = 0
        if self.head == None:
            return 0
        if self.tail == None:
            return 1
        node = self.head
        while node != self.tail:
            res += 1
            node = node.nxt
        return res + 1
    '''
    return pyperclip.copy(s)
def n10():
    s=r'''
def swap(A, i, j):
    A[i],A[j] = A[j],A[i]
def partition(a, start, end):
    pivot = a[end]
    pIndex = start
    for i in range(start, end):
        if a[i] <= pivot:
            swap(a, i, pIndex)
            pIndex = pIndex + 1
    swap(a, end, pIndex)
    return pIndex
def quicksort(a, start, end):
    if start >= end:
        return
    pivot = partition(a, start, end)
    quicksort(a, start, pivot - 1)
    quicksort(a, pivot + 1, end)
#quicksort(a, 0, len(a) - 1)
    '''
    return pyperclip.copy(s)
def n11():
    s=r'''
class Node:
    def __init__(self, data):
        self.data = data
        self.left = self.right = None
class Tree:
    def __init__(self):
        self.root = None
    def __find(self, node, parent, value):
        if node is None:
            return None, parent, False
        if value == node.data:
            return node, parent, True
        if value < node.data:
            if node.left:
                return self.__find(node.left, node, value)
        if value > node.data:
            if node.right:
                return self.__find(node.right, node, value)
        return node, parent, False
    def append(self, obj):
        if self.root is None:
            self.root = obj
            return obj
        s, p, fl_find = self.__find(self.root, None, obj.data)
        if not fl_find and s:
            if obj.data < s.data:
                s.left = obj
            else:
                s.right = obj
        return obj
    def show_tree(self, node):
        if node is None:
            return
        self.show_tree(node.right)
        print(node.data)
        self.show_tree(node.left)
    def show_wide_tree(self, node):
        if node is None:
            return
        v = [node]
        while v:
            vn = []
            for x in v:
                print(x.data, end=" ")
                if x.left:
                    vn += [x.left]
                if x.right:
                    vn += [x.right]
            print()
            v = vn
    def __del_leaf(self, s, p):
        if p.left == s:
            p.left = None
        elif p.right == s:
            p.right = None
    def __del_one_child(self, s, p):
        if p.left == s:
            if s.left is None:
                p.left = s.right
            elif s.right is None:
                p.left = s.left
        elif p.right == s:
            if s.left is None:
                p.right = s.right
            elif s.right is None:
                p.right = s.left
    def __find_min(self, node, parent):
        if node.left:
            return self.__find_min(node.left, node)
        return node, parent
    def del_node(self, key):
        s, p, fl_find = self.__find(self.root, None, key)
        if not fl_find:
            return None
        if s.left is None and s.right is None:
            self.__del_leaf(s, p)
        elif s.left is None or s.right is None:
            self.__del_one_child(s, p)
        else:
            sr, pr = self.__find_min(s.right, s)
            s.data = sr.data
            self.__del_one_child(sr, pr)
    '''
    return pyperclip.copy(s)
def n12():
    s=r'''
def selection_sort(a_list):
    for fill_slot in range(len(a_list) - 1, 0, -1):
        pos_of_max = 0
        for location in range(1, fill_slot + 1):
            if a_list[location] > a_list[pos_of_max]:
                pos_of_max = location
        temp = a_list[fill_slot]
        a_list[fill_slot] = a_list[pos_of_max]
        a_list[pos_of_max] = temp
    return a_list
    '''
    return pyperclip.copy(s)
def n13():
    s=r'''
def insertion_sort(a_list):
    for index in range(1, len(a_list)):
        current_value = a_list[index]
        position = index
        while position > 0 and a_list[position - 1] > current_value:
            a_list[position] = a_list[position - 1]
            position -= 1
        a_list[position] = current_value
        print(a_list)
    return a_list
    '''
    return pyperclip.copy(s)
def n14():
    s=r'''
def merge_sort(a_list):
    if len(a_list) > 1:
        mid = len(a_list) // 2
        left_half = a_list[:mid]
        right_half = a_list[mid:]
        merge_sort(left_half)
        merge_sort(right_half)
        i = 0
        j = 0
        k = 0
        while i < len(left_half) and j < len(right_half):
            if left_half[i] < right_half[j]:
                a_list[k] = left_half[i]
                i += 1
            else:
                a_list[k] = right_half[j]
                j += 1
            k += 1
        while i < len(left_half):
            a_list[k] = left_half[i]
            i += 1
            k += 1
        while j < len(right_half):
            a_list[k] = right_half[j]
            j += 1
            k += 1
    return a_list
    '''
    return pyperclip.copy(s)
def n15():
    s=r'''
def shell_sort(arr):
    gap = len(arr) // 2
    while gap > 0:
        for i in range(gap, len(arr)):
            temp = arr[i]
            j = i
            while j >= gap and arr[j - gap] > temp:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = temp
        gap //= 2
    return arr
    '''
    return pyperclip.copy(s)
def n001():
    s=r'''
1.Концепция класса и объекта. Принципы и механизмы ООП. 
1.Классы: Класс является шаблоном или определением, описывающим состояние и поведение объектов. Он определяет атрибуты (переменные) и методы (функции) объекта.
2.Объекты: Объекты являются экземплярами класса. Они создаются на основе определения класса и имеют своё состояние и поведение.
3.Инкапсуляция: Инкапсуляция позволяет объединить данные и методы внутри класса и скрыть детали реализации от внешнего мира. Доступ к данным и методам осуществляется через интерфейс класса.
4.Наследование: Наследование позволяет создавать новые классы на основе существующих. Подкласс (наследник) может наследовать атрибуты и методы от родительского класса (родителя) и расширять или изменять их.
5.Полиморфизм: Полиморфизм позволяет использовать объекты разных классов с одинаковым интерфейсом. Это означает, что разные объекты могут реагировать на одни и те же методы, но вести себя по-разному.

2.Объявление класса, конструктор, создание объектов и одиночное наследование в Python. Управление доступом к атрибутам класса в Python. Полиморфизм и утиная типизация и проверка принадлежности объекта к классу в языке Python.
Объявление класса: Для объявления класса в Python используется ключевое слово class, за которым следует имя класса. Внутри класса определяются атрибуты и методы.
Конструктор: Конструктор - это специальный метод, который вызывается при создании объекта класса. В Python конструктор имеет имя __init__() и используется для инициализации атрибутов объекта.
Создание объектов: Для создания объекта класса вызывается имя класса, как если бы это была функция. При вызове класса создается новый экземпляр (объект) класса.
Одиночное наследование: Одиночное наследование позволяет классу наследовать атрибуты и методы от одного родительского класса. Для наследования используется круглые скобки после имени класса, указывающие родительский класс.
Управление доступом к атрибутам класса: В Python есть три типа доступа к атрибутам класса: публичный, защищенный и приватный. Публичные атрибуты доступны из любого места в программе. Защищенные атрибуты начинаются с одного подчеркивания (_) и могут быть доступны только внутри класса и его подклассов. Приватные атрибуты начинаются с двух подчеркиваний (__) и имеют самый ограниченный доступ - они могут быть доступны только внутри класса.
Полиморфизм и утиная типизация: Полиморфизм в Python позволяет объектам разных классов реагировать на одни и те же методы, что облегчает использование различных классов с общим интерфейсом. Python поддерживает утиную типизацию, которая не проверяет явно указанный тип объекта, а вместо этого проверяет наличие определенных методов или атрибутов. Если объект может выполнять необходимые операции, то он считается соответствующим типу.
Проверка принадлежности объекта к классу: Для проверки принадлежности объекта к определенному классу в Python используется функция isinstance().

3.Методы классов и статические переменные и методы в Python. Специальные методы для использования пользовательских классов со стандартными операторами и функциями.
Методы классов в Python: Методы классов - это функции, определенные внутри класса, которые могут выполнять определенные операции с атрибутами объекта класса. Методы классов объявляются так же, как обычные функции, но с одним дополнительным аргументом self, который ссылается на сам объект.
Статические переменные и методы в Python: Статические переменные и методы принадлежат классу, а не объектам класса. Они общие для всех экземпляров класса и могут быть доступны без создания объекта. Для объявления статических переменных и методов используется декоратор @staticmethod.
Методы экземпляра нуждаются в экземпляре класса
и могут иметь доступ к нему через self.
Методы класса не нуждаются в экземпляре класса. Они не могут получить доступ к такому экземпляру (self), но у них есть доступ к самому классу через cls.
Статические методы не имеют доступа к cls или self. Они работают как обычные функции, но принадлежат к пространству имен класса.
Специальные методы для использования пользовательских классов со стандартными операторами и функциями: В Python есть специальные методы, которые позволяют пользовательским классам использовать стандартные операторы и функции. Эти методы имеют специальные имена, начинающиеся и заканчивающиеся двумя символами подчеркивания. Некоторые из распространенных специальных методов включают __init__() (конструктор), __str__() (преобразование объекта в строку), __add__() (сложение объектов) и __len__() (определение длины объекта).
    '''
    return pyperclip.copy(s)
def n002():
    s=r'''
4.Основные возможности, поддерживаемые функциональными языками программирования. Поддержка элементов функционального программирования в Python.
Основные возможности, поддерживаемые функциональными языками программирования, включают:
1.Функции первого класса: Функции могут быть присвоены переменным, переданы как аргументы другим функциям и возвращены как результаты из функций.
2.Замыкания: Функции могут захватывать и сохранять состояние окружающей их области видимости, даже после завершения выполнения этой области видимости.
3.Рекурсия: Функции могут вызывать сами себя для решения задачи.
4.Неизменяемость данных: Функциональные языки часто поддерживают неизменяемость данных, что означает, что данные не могут быть изменены после их создания. Вместо этого создаются новые данные.
5.Функции высших порядков: Функции могут принимать другие функции в качестве аргументов или возвращать их как результаты.
6.Рекурсивные структуры данных: Функциональные языки часто поддерживают рекурсивные структуры данных, такие как списки или деревья, для обработки данных.
Хотя Python является преимущественно объектно-ориентированным языком программирования, он также поддерживает некоторые элементы функционального программирования. Вот несколько способов поддержки функционального программирования в Python:
1.Функции первого класса: В Python функции являются объектами первого класса, что означает, что их можно присваивать переменным, передавать в качестве аргументов и возвращать как результаты из других функций.
2.Анонимные функции (lambda-функции): В Python есть возможность создавать анонимные функции с помощью ключевого слова lambda. Они обычно используются для создания простых функций, которые не требуют именования.
3.Функции высших порядков: В Python можно передавать функции в качестве аргументов другим функциям и возвращать их как результаты.
4.Функции map(), filter(), reduce(): Python предоставляет встроенные функции map(), filter() и reduce(), которые позволяют применять функции к элементам последовательности, фильтровать элементы или сводить их к единственному значению.
5.Генераторы: Генераторы

5.Концепция «функции – граждане первого класса» в языке программирования, поддержка этой концепции в Python. Специфика лямбда-функций в Python их возможности и ограничения. Типичные сценарии использования лямбда-функций в Python.
Концепция "функции - граждане первого класса" в языке программирования означает, что функции рассматриваются как значения, которые могут быть переданы, присвоены переменным, сохранены в структурах данных и возвращены из других функций. Это позволяет использовать функции в качестве гибких и мощных строительных блоков для решения задач.
В Python функции являются объектами первого класса, что означает, что они полностью поддерживают концепцию "функции - граждане первого класса". Вот некоторые способы поддержки этой концепции в Python:
1.Функции могут быть присвоены переменным
2.Функции могут передаваться в качестве аргументов
3.Функции могут быть возвращены из других функций
Лямбда-функции в Python представляют собой анонимные функции, которые можно создавать без использования ключевого слова def. Они имеют следующий синтаксис: lambda arguments: expression. Лямбда-функции полезны, когда требуется определить простую функцию, которая будет использована один раз.
Однако лямбда-функции имеют некоторые ограничения:
•Они могут содержать только одно выражение, которое должно быть возвращено.
•Они не могут содержать несколько операторов или блоки кода.
Типичные сценарии использования лямбда-функций в Python включают:
•Передача функции в качестве аргумента другой функции.
•Использование в функциях высшего порядка, таких как sorted(), map() и filter().

6.Глобальные и локальные переменные в функциях на примере Python. Побочные эффекты вызова функций и их последствия.
В Python переменные могут быть объявлены как глобальные или локальные в рамках функций. Это влияет на область видимости переменной и ее доступность в разных частях программы.
Глобальные переменные:
1.Глобальные переменные объявляются вне функций и доступны из любой части программы.
2.Они могут быть использованы внутри функций без необходимости объявления или передачи в аргументах.
3.Глобальные переменные могут быть изменены и прочитаны в любой части программы.
Локальные переменные:
•Локальные переменные объявляются внутри функций и доступны только внутри этой функции.
•Они существуют только во время выполнения функции и уничтожаются после ее завершения.
•Локальные переменные не могут быть использованы вне функции.
Побочные эффекты вызова функций относятся к изменениям, которые функция может внести в состояние программы, помимо возвращаемого значения. Побочные эффекты могут включать изменение глобальных переменных, ввод-вывод операции, изменение состояния объектов и другие воздействия на программу.

7.Вложенные функции и замыкания, специфика реализации в Python.
В Python вложенные функции и замыкания предоставляют механизм для создания функций, которые могут получать доступ к переменным, определенным внутри внешней функции. Это может быть полезно, когда требуется сохранить состояние или передать данные между вызовами функции. Рассмотрим специфику реализации в Python.
1.Вложенные функции: В Python можно определить функцию внутри другой функции. Эта вложенная функция будет доступна только внутри внешней функции, и ее область видимости ограничена этой функцией. Вложенная функция может получать доступ к переменным из внешней функции, но не может изменять их значения.
2.Замыкания: Замыкание в Python - это функция, которая запоминает значения переменных из своей внешней области видимости, даже если эти переменные больше не существуют. Замыкание сохраняет состояние этих переменных, чтобы иметь к ним доступ позднее.

8.Функции высшего порядка и декораторы в Python.
Функции высшего порядка и декораторы - это два мощных инструмента в Python, которые позволяют работать с функциями как с данными и модифицировать их поведение. Давайте рассмотрим каждый из них подробнее:
1.Функции высшего порядка: Функции высшего порядка (higher-order functions) в Python - это функции, которые могут принимать другие функции в качестве аргументов и/или возвращать функции в качестве результата. Они позволяют абстрагироваться от конкретных функций и создавать более гибкие и универсальные решения. Пример: map,filter,reduce
2.Декораторы: Декораторы в Python позволяют модифицировать поведение функций путем обертывания (или декорирования) их в другие функции. Они представляют собой специальный синтаксис, который облегчает добавление дополнительной функциональности к существующим функциям, без необходимости изменения их кода.

9.Концепция map/filter/reduce. Реализация map/filter/reduce в Python и пример их использования.
Концепция map, filter и reduce относится к функциональному программированию и представляет собой способы обработки коллекций данных.
1.map: Функция map применяет заданную функцию ко всем элементам итерируемого объекта и возвращает итератор с результатами. Она позволяет преобразовывать каждый элемент входного списка, создавая новый список с преобразованными значениями.
2.filter: Функция filter применяет заданную функцию к каждому элементу итерируемого объекта и возвращает итератор, содержащий только те элементы, для которых функция возвращает True. Она позволяет фильтровать элементы исходного списка по определенному критерию.
3.reduce: Функция reduce применяет заданную функцию к элементам итерируемого объекта последовательно, сводя их к одному значению. Она использует результат предыдущей операции в качестве одного из аргументов следующей операции. reduce находится в модуле functools.

10.Итераторы в Python: встроенные итераторы, создание собственных итераторов, типичные способы обхода итераторов и принцип их работы. Встроенные функции для работы с итераторами и возможности модуля itertools. Функции генераторы и выражения генераторы: создание и применение в Python.
Итераторы в Python представляют собой объекты, которые позволяют обходить элементы коллекции или последовательности. Они предоставляют единый интерфейс для получения следующего элемента и проверки окончания последовательности. Давайте рассмотрим различные аспекты итераторов в Python.
1.Встроенные итераторы: В Python множество встроенных типов данных и объектов предоставляют встроенные итераторы, которые позволяют обходить их элементы. 
2.Создание собственных итераторов: В Python можно создать собственные итераторы, реализуя два метода: __iter__() и __next__(). Метод __iter__() возвращает сам итератор, а метод __next__() возвращает следующий элемент или вызывает исключение StopIteration, если последовательность закончена. Для создания легче использовать функция yield(ласт пункт)
3.Обход итераторов и принцип их работы: Обход итератора осуществляется с помощью цикла for, который автоматически вызывает функцию next() для получения следующего элемента и проверяет исключение StopIteration для определения окончания последовательности. Цикл for продолжает итерацию до тех пор, пока не будет вызвано исключение StopIteration.
Встроенные функции для работы с итераторами: В Python существуют несколько встроенных функций, которые упрощают работу с итераторами:
4.iter(iterable): Возвращает итератор для заданного итерируемого объекта.
5.next(iterator[, default]): Возвращает следующий элемент итератора. Если итератор исчерпан, возвращает default (если указано) или вызывает исключение StopIteration.
6.list(iterator): Преобразует итератор в список, содержащий все его элементы.
Модуль itertools: Модуль itertools предоставляет различные функции для работы с итераторами и создания новых итераторов на основе существующих. Он содержит функции, такие как chain(), cycle(), islice(), product(), permutations(), combinations() и другие, которые позволяют выполнять различные операции с итераторами и комбинировать их.
Функции-генераторы и выражения-генераторы: Функции-генераторы и выражения-генераторы позволяют создавать итераторы с использованием ключевых слов yield или выражения yield. Они генерируют значения по мере необходимости и сохраняют свое состояние между вызовами. Функции-генераторы представляют собой функции, которые возвращают объект-генератор, а выражения-генераторы создаются с использованием синтаксиса генераторных выражений.
'''
    return pyperclip.copy(s)
def n003():
    s = r'''
11.Специфика массивов, как структур данных. Динамические массивы – специфика работы, сложность операций. Специфика работа с array в Python.
Массивы являются одной из наиболее распространенных структур данных и представляют собой упорядоченную коллекцию элементов одного типа. Они позволяют хранить большие объемы данных в памяти и обеспечивают доступ к этим данным по индексам.
Динамические массивы, или расширяемые массивы, являются разновидностью массивов, которые могут изменять свой размер по мере необходимости. Это означает, что при добавлении новых элементов в динамический массив, он автоматически увеличивает свой размер, чтобы вместить дополнительные элементы. Обратно, при удалении элементов размер массива может быть уменьшен для экономии памяти.
Получение элемента по индексу: O(1)
Вставка элемента в конец массива (при достаточном свободном месте): O(1)
Вставка элемента в начало или середину массива (или в конец при недостаточном свободном месте): O(n)
Удаление элемента: O(n)
В Python существует встроенный тип данных array.array, который реализует массивы. Важно отметить, что в Python array.array является динамическим массивом. Это означает, что он автоматически изменяет свой размер при добавлении или удалении элементов.
Взаимодействие с массивами в Python происходит с использованием индексов. 
Python также предоставляет удобные методы для работы с массивами, такие как append() для добавления элемента в конец списка и pop() для удаления элемента.

12.Абстрактная структура данных стек и очередь: базовые и расширенные операции, их сложность. 
Стек (stack) и очередь (queue) являются двумя базовыми абстрактными структурами данных. Они определяют специфические операции и правила доступа к элементам. Давайте рассмотрим базовые операции и их сложность для каждой из этих структур данных.
Стек:
•push(item): Добавляет элемент на вершину стека. Сложность операции push составляет O(1), так как элемент просто помещается на вершину стека.
•pop(): Удаляет и возвращает элемент с вершины стека. Сложность операции pop также составляет O(1), поскольку доступ к вершине стека выполняется непосредственно.
•peek(): Возвращает элемент с вершины стека, но не удаляет его. Сложность операции peek также составляет O(1), так как доступ к вершине стека выполняется непосредственно.
•isEmpty(): Проверяет, является ли стек пустым. Сложность операции isEmpty составляет O(1), так как проверка выполняется непосредственно.
Очередь:
•enqueue(item): Добавляет элемент в конец очереди. Сложность операции enqueue также составляет O(1), так как элемент просто помещается в конец очереди.
•dequeue(): Удаляет и возвращает элемент из начала очереди. Сложность операции dequeue составляет O(1), поскольку доступ к началу очереди выполняется непосредственно.
•peek(): Возвращает элемент из начала очереди, но не удаляет его. Сложность операции peek также составляет O(1), так как доступ к началу очереди выполняется непосредственно.
•isEmpty(): Проверяет, является ли очередь пустой. Сложность операции isEmpty составляет O(1), так как проверка выполняется непосредственно.

13.Специфика реализации и скорости основных операций в очереди на базе массива и связанного списка.
Все операции из 12 за О(1)
Одна из основных различий между реализациями заключается в расширении контейнера. В случае очереди на базе массива требуется копирование элементов при расширении массива, что может занять O(n) времени в худшем случае. В связанном списке же не требуется копирование элементов, и расширение может быть выполнено за O(1) времени, просто добавив новый узел.
Однако, в случае очереди на базе связанного списка, требуется больше памяти на хранение указателей, чем в случае очереди на базе массива, где память выделяется непосредственно для элементов.

14.Связанные списки: однонаправленные и двунаправленные – принцип реализации. Сравнение скорости выполнения основных операций в связанных списках и в динамическом массиве.
Связанные списки - это структуры данных, состоящие из узлов, каждый из которых содержит элемент данных и ссылку на следующий узел (или предыдущий узел в случае двунаправленного связанного списка). Они могут быть реализованы как однонаправленные или двунаправленные.
Принцип реализации однонаправленного связанного списка: Каждый узел содержит элемент данных и ссылку на следующий узел. Последний узел в списке ссылается на null, указывая на конец списка. Для доступа к элементам списка нужно последовательно проходить через узлы, начиная с головы списка.
Принцип реализации двунаправленного связанного списка: Каждый узел содержит элемент данных, ссылку на следующий узел и ссылку на предыдущий узел. Такая структура позволяет обращаться к элементам как в прямом, так и в обратном направлении. Для доступа к элементам списка можно начать с головы или с хвоста.
Сравнение скорости выполнения основных операций в связанных списках и динамическом массиве:
Операции в однонаправленных и двунаправленных связанных списках:
•Вставка в начало: O(1) - независимо от размера списка, так как требуется только изменить ссылки на следующий или предыдущий узел.
•Вставка в конец: O(1) - для двунаправленного списка, так как имеется ссылка на последний узел. Для однонаправленного списка требуется O(n) времени, где n - размер списка, так как нужно пройти через все узлы до конца списка.
•Удаление из начала: O(1) - независимо от размера списка, так как требуется только изменить ссылки на следующий или предыдущий узел.
•Удаление из конца: O(1) - для двунаправленного списка, так как имеется ссылка на последний узел. Для однонаправленного списка требуется O(n) времени, где n - размер списка, так как нужно пройти через все узлы до конца списка.
•Доступ к элементу по индексу: O(n) - в обоих типах связанных списков, так как нужно пройти через n узлов, чтобы достичь нужного индекса.
Операции в динамическом массиве:
•Вставка в начало: O(n) - требуется сдвинуть все элементы в массиве для освобождения места в начале и вставки нового элемента.
•Вставка в конец (при достижении предела емкости): O(n) - требуется создать новый массив большего размера и скопировать все элементы из старого массива.
•Вставка в конец (при наличии свободного места): O(1) - элемент просто добавляется в конец массива.
•Удаление из начала: O(n) - требуется сдвинуть все элементы в массиве после удаления первого элемента.
•Удаление из конца: O(1) - элемент просто удаляется из конца массива.
•Доступ к элементу по индексу: O(1) - доступ к элементу выполняется непосредственно по индексу без необходимости прохода по всем элементам.
Итак, связанные списки имеют преимущество при вставке и удалении в начало и конец, но они менее эффективны при доступе к элементам по индексу. Динамический массив более эффективен при доступе по индексу, но может потребовать дополнительные ресурсы при вставке и удалении элементов. Выбор между ними зависит от конкретных требований и характеристик приложения или задачи.
'''
    return pyperclip.copy(s)
def n004():
    s = r'''
15.Алгоритм обменной сортировки, сложность сортировки и возможности по ее улучшению.
Алгоритм обменной сортировки, также известный как сортировка пузырьком (bubble sort), представляет собой простой алгоритм сортировки, который сравнивает пары соседних элементов и меняет их местами, если они находятся в неправильном порядке. Этот процесс повторяется до тех пор, пока массив не будет полностью отсортирован.
Принцип работы алгоритма обменной сортировки:
1.Проходим по массиву и сравниваем каждую пару соседних элементов.
2.Если элементы находятся в неправильном порядке, меняем их местами.
3.Повторяем шаги 1 и 2 для каждой пары соседних элементов до тех пор, пока массив не будет полностью отсортирован.
Сложность сортировки обменной сортировкой:
•В худшем случае, когда массив уже отсортирован в обратном порядке, требуется выполнить n-1 проходов для сортировки массива длиной n. Каждый проход требует O(n) операций, поэтому общая сложность в худшем случае составляет O(n^2).
•В лучшем случае, когда массив уже отсортирован, алгоритм все равно выполняет проход по всем элементам, чтобы убедиться в отсутствии необходимости обмена. Поэтому лучший случай также имеет сложность O(n^2).
•В среднем случае, сложность обменной сортировки также составляет O(n^2).
Улучшения алгоритма обменной сортировки:
•Досрочное завершение: Если на каком-то проходе не было выполнено ни одного обмена, то массив уже отсортирован, и алгоритм может быть прерван.
•Флаг упорядоченности: При каждом проходе можно запоминать индекс последнего обмена, и на следующем проходе продолжать сравнения только до этого индекса, так как элементы после него уже отсортированы.
•Оптимизация границы: После каждого прохода, самый большой элемент перемещается в конец массива. Поэтому на каждом следующем проходе можно уменьшать границу сравнения на 1, так как последние элементы уже отсортированы.
Эти улучшения могут немного снизить количество операций, но в худшем и среднем случае сложность обменной сортировки остается O(n^2). 

16.Алгоритм сортировки выбором, сложность сортировки и возможности по ее улучшению.
Алгоритм сортировки выбором (selection sort) - это простой алгоритм сортировки, который на каждом шаге находит минимальный (или максимальный) элемент в неотсортированной части массива и помещает его в начало (или конец) отсортированной части. После каждого шага граница между отсортированной и неотсортированной частью сдвигается.
Принцип работы алгоритма сортировки выбором:
1.Инициализируем границу между отсортированной и неотсортированной частью массива на первом элементе.
2.Находим минимальный (или максимальный) элемент в неотсортированной части массива.
3.Меняем найденный элемент с элементом на границе отсортированной и неотсортированной частей.
4.Сдвигаем границу между отсортированной и неотсортированной частью на один элемент вправо (или влево).
5.Повторяем шаги 2-4 до тех пор, пока неотсортированная часть не будет пустой.
Сложность сортировки выбором:
•В худшем, лучшем и среднем случае сложность сортировки выбором составляет O(n^2), где n - размер массива.
•На каждом шаге требуется выполнить поиск минимального (или максимального) элемента в неотсортированной части массива, что требует O(n) операций. После этого элемент меняется местами с элементом на границе отсортированной и неотсортированной частей, что также требует O(1) операций. Таким образом, общая сложность сортировки выбором составляет O(n^2).
Улучшения алгоритма сортировки выбором:
•Улучшение выбора границы: Вместо одного элемента на границе можно выбрать k наименьших (или наибольших) элементов и их упорядочить. Затем можно использовать этот упорядоченный набор элементов для поиска следующего наименьшего (или наибольшего) элемента в неотсортированной части массива. Это может снизить количество операций сравнения и обмена, особенно при больших значениях k.
•Прекращение работы: Если после каждого прохода не было выполнено ни одного обмена элементов, значит массив уже отсортирован. Алгоритм может быть прерван, и не нужно выполнять лишние проходы.

17.Алгоритм сортировки вставками, его сложность. Алгоритм быстрого поиска в отсортированном массиве. Сложность поиска в отсортированном и не отсортированном массиве.
Алгоритм сортировки вставками (insertion sort) - это простой алгоритм сортировки, который постепенно строит отсортированную последовательность путем вставки каждого элемента на его правильное место. На каждом шаге алгоритм берет очередной элемент из неотсортированной части и вставляет его в правильную позицию в отсортированной части массива.
Принцип работы алгоритма сортировки вставками:
1.Инициализируем границу между отсортированной и неотсортированной частями массива на втором элементе.
2.Берем очередной элемент из неотсортированной части и вставляем его на правильное место в отсортированной части, двигая большие элементы вправо.
3.Сдвигаем границу между отсортированной и неотсортированной частями на один элемент вправо.
4.Повторяем шаги 2-3 до тех пор, пока неотсортированная часть не будет пустой.
Сложность сортировки вставками:
•В худшем, лучшем и среднем случае сложность сортировки вставками составляет O(n^2), где n - размер массива.
•На каждом шаге алгоритма требуется сравнить текущий элемент с элементами в отсортированной части массива, что требует в худшем случае O(n) операций. При этом может быть выполнено O(n) перестановок элементов. Таким образом, общая сложность сортировки вставками составляет O(n^2).
Алгоритм быстрого поиска в отсортированном массиве (binary search) - это эффективный алгоритм поиска элемента в отсортированном массиве. Алгоритм быстрого поиска сокращает область поиска пополам на каждом шаге, сравнивая искомый элемент с элементом в середине текущей области.
Принцип работы алгоритма быстрого поиска в отсортированном массиве:
1.Инициализируем начальную и конечную границы области поиска на первый и последний элементы массива соответственно.
2.Находим элемент в середине текущей области.
3.Сравниваем искомый элемент с элементом в середине:
oЕсли они совпадают, то элемент найден, возвращаем его индекс.
oЕсли искомый элемент меньше элемента в середине, сужаем область поиска до левой половины массива и переходим к шагу 2.
oЕсли искомый элемент больше элемента в середине, сужаем область поиска до правой половины массива и переходим к шагу 2.
4.Повторяем шаги 2-3, пока область поиска не сократится до одного элемента.
Сложность поиска в отсортированном и неотсортированном массиве:
•В отсортированном массиве сложность поиска быстрым поиском составляет O(log n), где n - размер массива.
•На каждом шаге алгоритма область поиска сокращается пополам, что приводит к логарифмическому времени выполнения.
•В неотсортированном массиве сложность поиска быстрым поиском составляет O(n), так как нет возможности использовать свойства отсортированного массива для сокращения области поиска.
Алгоритм быстрого поиска позволяет эффективно находить элемент в отсортированном массиве, в то время как в неотсортированном массиве лучше использовать другие алгоритмы поиска, например, линейный поиск, который имеет сложность O(n).

18.Алгоритм сортировки Шелла, сложность сортировки и возможности по ее улучшению.
Алгоритм сортировки Шелла (Shell sort) - это усовершенствованный вариант сортировки вставками. Он использует идею инкрементных последовательностей (gap sequence), чтобы сравнивать и перемещать элементы, находящиеся на большем удалении друг от друга, и постепенно уменьшает размер этой последовательности, чтобы сортировать элементы ближе друг к другу.
Принцип работы алгоритма сортировки Шелла:
1.Выбираем последовательность инкрементов, определяющих расстояние между сравниваемыми элементами.
2.Пока инкремент больше 0, выполняем сортировку вставками с этим инкрементом:
oСравниваем элементы, находящиеся на расстоянии инкремента, и меняем их местами, если необходимо.
oПосле сравнения и обмена элементов на текущем инкременте, сдвигаемся к следующему инкременту и повторяем сортировку вставками.
oПродолжаем уменьшать инкремент, пока не достигнем значения 1.
3.Завершаем сортировку вставками с инкрементом 1, чтобы окончательно отсортировать массив.
Сложность сортировки Шелла:
•Сложность сортировки Шелла зависит от выбора последовательности инкрементов.
•В лучшем случае сложность может быть близкой к O(n log n), но это редкое явление.
•В худшем случае сложность составляет примерно O(n^2), хотя она обычно лучше, чем у сортировки пузырьком и сортировки вставками.
•Сложность сортировки Шелла может быть улучшена выбором оптимальной последовательности инкрементов, которая обеспечивает более эффективное сравнение и перемещение элементов. Однако поиск оптимальной последовательности инкрементов является сложной задачей, и на практике часто используются предварительно определенные последовательности инкрементов, такие как последовательность Шелла или последовательность Седжвика.
В целом, сортировка Шелла является улучшением сортировки вставками и может быть эффективной для некоторых типов данных и размеров массивов.

19.Алгоритм быстрой сортировки, сложность сортировки и возможности по ее улучшению.
Алгоритм быстрой сортировки (или quicksort) является одним из наиболее эффективных алгоритмов сортировки, основанных на сравнении элементов. Он работает по принципу "разделяй и властвуй" и применяет стратегию разбиения массива на подмассивы, сортируя их отдельно.
Шаги алгоритма быстрой сортировки:
1.Выберите опорный элемент из массива (обычно это средний элемент).
2.Разделите массив на две подгруппы: элементы, меньшие опорного, и элементы, большие опорного.
3.Рекурсивно примените быструю сортировку к обеим подгруппам.
4.Объедините отсортированные подгруппы и опорный элемент в один отсортированный массив.
Сложность быстрой сортировки зависит от выбора опорного элемента и структуры входного массива. В среднем случае и в лучшем случае (когда опорный элемент каждый раз делит массив на две примерно равные части) сложность быстрой сортировки составляет O(n log n), где n - количество элементов в массиве.
Однако в худшем случае (когда опорный элемент является минимальным или максимальным элементом массива и разбиение происходит на две группы с одним элементом и n-1 элементов соответственно), сложность быстрой сортировки может достигать O(n^2). Чтобы избежать худшего случая, можно использовать различные методы выбора опорного элемента, такие как выбор случайного элемента, медиана из трёх элементов и другие.
Существуют также различные методы для улучшения быстрой сортировки:
1.Оптимизация выбора опорного элемента: Выбор опорного элемента может существенно повлиять на производительность. Использование случайного выбора, медианы из трёх элементов или медианы из пяти элементов помогает снизить вероятность худшего случая.
2.Оптимизация для малых подмассивов: Когда размер подмассива становится достаточно малым, можно использовать другой алгоритм сортировки, такой как сортировка вставками или сортировка пузырьком, чтобы избежать лишних рекурсивных вызовов.

20.Алгоритм сортировки слиянием, сложность сортировки.
Алгоритм сортировки слиянием (или mergesort) также относится к эффективным алгоритмам сортировки. Он использует стратегию "разделяй и властвуй", разбивая массив на меньшие подмассивы, сортируя их отдельно и затем объединяя отсортированные подмассивы.
Шаги алгоритма сортировки слиянием:
1.Разделите входной массив на две равные части.
2.Рекурсивно примените сортировку слиянием к каждой половине массива.
3.Объедините две отсортированные половины, выполнив операцию слияния.
Сложность сортировки слиянием в худшем, лучшем и среднем случаях составляет O(n log n), где n - количество элементов в массиве. Это делает сортировку слиянием стабильным алгоритмом сортировки с хорошей производительностью.
Возможности улучшения сортировки слиянием включают:
1.Оптимизация использования памяти: Стандартная реализация сортировки слиянием требует дополнительной памяти для хранения временных подмассивов при каждом слиянии. Это может быть проблемой для больших массивов. Один из способов снизить использование памяти - использовать сортировку слиянием на месте (in-place mergesort), которая выполняет сортировку без использования дополнительной памяти, но с более сложным алгоритмом.
2.Оптимизация для малых подмассивов: Для очень малых подмассивов, сортировка слиянием может оказаться излишне сложной и затратной. В таких случаях можно переключиться на более простые алгоритмы сортировки, такие как сортировка вставками или сортировка пузырьком.
'''
    return pyperclip.copy(s)
def n005():
    s = r'''
21.Реализация двоичных деревьев в виде связанных объектов. Различные реализации рекурсивного обхода двоичных деревьев.
Реализация двоичных деревьев в виде связанных объектов включает создание узлов дерева и установку связей между ними. Каждый узел содержит значение и указатели на его левого и правого потомков.
В этой реализации каждый узел содержит значение (value), а также указатели на левого (left) и правого (right) потомков.
Что касается реализации различных рекурсивных обходов двоичных деревьев, основные методы включают прямой (preorder), центрированный (inorder) и обратный (postorder) обходы. Вот примеры реализаций этих обходов:
def preorder_traversal(node):
    if node is None:
        return
    print(node.value) #прямой обход
    preorder_traversal(node.left)  
    print(node.value) #центрированный обход
    preorder_traversal(node.right)
    print(node.value) #обратный обход

22.Двоичное дерево поиска – принципы реализации и логика реализации основных операций.
Двоичное дерево поиска (Binary Search Tree, BST) - это структура данных, в которой каждый узел содержит значение и удовлетворяет следующим условиям:
1.Значение любого узла в левом поддереве меньше его собственного значения.
2.Значение любого узла в правом поддереве больше или равно его собственному значению.
Принципы реализации двоичного дерева поиска включают создание узлов дерева и правильную вставку значений в соответствии с условиями BST.
Каждый узел содержит значение (value), а также указатели на левого (left) и правого (right) потомков.
Основные операции, которые можно выполнять в двоичном дереве поиска, включают:
1.Вставка (Insertion):
oЕсли дерево пустое, создайте новый узел и сделайте его корнем.
oЕсли дерево не пустое, начните с корня и рекурсивно ищите место для вставки нового узла, сравнивая его значение с текущим узлом. Если значение меньше, переходите к левому поддереву, иначе - к правому поддереву.
oПовторяйте этот процесс до тех пор, пока не найдете подходящее место для вставки нового узла. Создайте новый узел и установите его как левого или правого потомка текущего узла.
2.Поиск (Search):
oНачните с корня и сравните искомое значение с текущим узлом.
oЕсли значения равны, верните текущий узел.
oЕсли искомое значение меньше текущего узла, перейдите к левому поддереву и продолжайте поиск.
oЕсли искомое значение больше текущего узла, перейдите к правому поддереву и продолжайте поиск.
oПовторяйте этот процесс до тех пор, пока не найдете искомый узел или не достигнете конца дерева (текущий узел равен None).
3.Удаление (Deletion):
oПоиск узла, который нужно удалить.
oРассмотрите разные случаи:
Если удаляемый узел является листовым (не имеет потомков), просто удалите его.
Если удаляемый узел имеет только одного потомка, замените его на потомка.
Если удаляемый узел имеет двух потомков, найдите его преемника (например, наименьший узел в правом поддереве), замените его значение удаляемым узлом и затем рекурсивно удалите преемника из правого поддерева.
Во всех случаях обновляйте связи между узлами и поддеревьями, чтобы сохранить структуру дерева.
Это основные операции, которые можно выполнять в двоичном дереве поиска. Они обеспечивают эффективный поиск, вставку и удаление элементов на основе значения.

23.Двоичная куча – принципы реализации и логика реализации основных операций.
Двоичная куча (Binary Heap) - это структура данных, представляющая собой полное бинарное дерево, удовлетворяющее свойству кучи. Куча может быть реализована в виде массива или связанных объектов.
Принципы реализации двоичной кучи:
1.Представление в виде массива: Элементы двоичной кучи хранятся в массиве, где каждый индекс соответствует узлу, а значение в ячейке массива представляет сам элемент. Для узла с индексом i, его левым потомком будет элемент с индексом 2*i, а правым потомком - элемент с индексом 2*i + 1.
2.Свойство кучи: Двоичная куча имеет два важных свойства:
oСвойство упорядоченности: Для кучи типа "максимум" (max heap), значение каждого узла больше или равно значений его потомков. Для кучи типа "минимум" (min heap), значение каждого узла меньше или равно значению его потомков.
oСвойство полноты: Все уровни кучи, кроме, возможно, последнего, должны быть полностью заполнены, и на последнем уровне элементы должны заполняться слева направо без пропусков.
Логика реализации основных операций двоичной кучи:
1.Вставка (Insertion):
oНовый элемент вставляется в следующую доступную позицию в массиве (на следующий свободный узел в дереве).
oЗатем выполняется операция "просеивания вверх" (up-heapify), чтобы восстановить свойство кучи. Сравнивайте добавленный элемент с его родителем и, если необходимо, меняйте их местами, чтобы элемент восходил вверх по дереву до тех пор, пока он не достигнет своей правильной позиции.
2.Извлечение максимума/минимума (Extract-Max/Extract-Min):
oКорневой элемент (максимум или минимум) извлекается из кучи.
oЗатем последний элемент в массиве перемещается на место корневого элемента.
oВыполняется операция "просеивания вниз" (down-heapify), чтобы восстановить свойство кучи. Сравнивайте элемент с его потомками и, если необходимо, меняйте их местами, чтобы элемент спускался вниз по дереву до тех пор, пока он не достигнет своей правильной позиции.
3.Построение кучи (Heapify):
oПроцесс преобразования неупорядоченного массива элементов в двоичную кучу.
oНачиная с последнего уровня кучи и двигаясь вверх, применяйте операцию "просеивания вниз" к каждому узлу, чтобы восстановить свойство кучи.
Двоичная куча эффективна для выполнения операций извлечения максимума/минимума и вставки элементов с временной сложностью O(log n), где n - количество элементов в куче.

24.Абстрактный тип данных - ассоциативный массив и принцип его реализации на основе хэш-таблиц и хэш-функций.
Абстрактный тип данных (АТД) e- это математическая модель данных, которая определяет набор операций, которые можно выполнить с этими данными, а также ограничения на эти операции. Ассоциативный массив (также известный как словарь, карта, хеш-таблица и др.) - это один из АТД, который представляет собой коллекцию пар ключ-значение, где каждый ключ уникален и связан с определенным значением.
Принцип реализации ассоциативного массива на основе хэш-таблицы и хэш-функций включает следующие шаги:
1.Хэш-таблица: Хэш-таблица - это структура данных, которая использует хэш-функцию для преобразования ключей в индексы массива, где значения хранятся. Размер хэш-таблицы обычно является простым числом, чтобы уменьшить коллизии.
2.Хэш-функция: Хэш-функция принимает ключ и вычисляет хэш-код (индекс) для этого ключа в хэш-таблице. Хорошая хэш-функция должна равномерно распределять ключи по индексам и минимизировать коллизии (когда два разных ключа имеют одинаковый хэш-код).
3.Вставка элемента:
oПримените хэш-функцию к ключу, чтобы получить хэш-код.
oИспользуйте хэш-код для вычисления индекса в хэш-таблице.
oЕсли в этом индексе уже есть элемент, возникает коллизия. В таком случае может быть применен метод разрешения коллизий, например, метод цепочек (создание связанного списка для хранения элементов с одним и тем же хэш-кодом) или метод открытой адресации (поиск пустой ячейки вблизи хэш-кода).
oЕсли индекс свободен, вставьте пару ключ-значение в хэш-таблицу.
4.Получение значения по ключу:
oПримените хэш-функцию к ключу, чтобы получить хэш-код.
oИспользуйте хэш-код для вычисления индекса в хэш-таблице.
oЕсли в этом индексе есть элементы, проверьте каждый элемент, чтобы найти соответствующий ключ.
oЕсли ключ найден, верните соответствующее значение. Если ключ не найден, выполните соответствующую обработку отсутствия значения.
5.Удаление элемента:
oПримените хэш-функцию к ключу, чтобы получить хэш-код.
oИспользуйте хэш-код для вычисления индекса в хэш-таблице.
oЕсли в этом индексе есть элементы, проверьте каждый элемент, чтобы найти соответствующий ключ.
oЕсли ключ найден, удалите элемент из хэш-таблицы. Если ключ не найден, выполните соответствующую обработку отсутствия значения.
Реализация ассоциативного массива на основе хэш-таблиц и хэш-функций обеспечивает эффективный доступ к данным по ключу средней временной сложностью O(1). Однако, при коллизиях или плохо выбранных хэш-функциях, временная сложность может возрасти до O(n), где n - количество элементов в хэш-таблице.

25.Общая схема построения хэш-функции и возможная роль в этой схеме хэш-функции multiply-add-and-divide. Принцип работы хэш-функции multiply-add-and-divide.
Общая схема построения хэш-функции включает следующие шаги:
1.Инициализация: Выберите начальное значение хэша (например, ноль) и определите размер выходного хэш-кода (обычно это размерность целочисленного типа данных).
2.Обработка каждого символа/байта ключа: Примените определенные операции к каждому символу/байту ключа и обновите текущий хэш-код.
3.Финализация: Если необходимо, выполните окончательные операции над хэш-кодом, чтобы получить окончательное значение хэш-функции.
Хэш-функция "multiply-add-and-divide" является одним из примеров хэш-функций, которые могут быть использованы в общей схеме. Она работает следующим образом:
1.Инициализация: Установите начальное значение хэша в ноль и выберите простое число как модуль (например, 31 или 31_337).
2.Обработка каждого символа/байта ключа:
oУмножьте текущее значение хэша на определенную константу (например, 31 или 2654435761) и прибавьте значение символа/байта ключа.
oМожно применить эту операцию к каждому символу/байту ключа в цикле для набора значений.
3.Финализация: Выполните операцию деления хэш-кода на простое число модуля, чтобы уменьшить результат до ограниченного диапазона значений (например, от 0 до модуля - 1).
Роль хэш-функции "multiply-add-and-divide" в общей схеме заключается в том, чтобы преобразовать входные данные (например, ключи) в хэш-коды с использованием операций умножения, сложения и деления. Эта хэш-функция обеспечивает хорошее распределение хэш-кодов и минимизирует коллизии, что важно для эффективной работы хэш-таблиц и других структур данных, которые зависят от хэширования.

26.Полиномиальная хэш-функция – принцип работы, специфика эффективной реализации и специфика применения хэш-функции.
pref(a,p,m)=(a0+a1*p+a2*p^2+a3*p^3+...+ak-1*p^k-1) mod m
p входное значение, m кол-во ячеек в хэш-таблице, a какой-то полином
Полиномиальная хэш-функция - это хэш-функция, которая использует полиномиальную формулу для вычисления хэш-кода на основе символов или байтов входных данных. Принцип работы полиномиальной хэш-функции следующий:
1.Инициализация: Выберите начальное значение хэша (например, ноль) и определите размер выходного хэш-кода (обычно это размерность целочисленного типа данных).
2.Обработка каждого символа/байта входных данных: Примените полиномиальную формулу к каждому символу/байту и обновите текущий хэш-код.
3.Финализация: Если необходимо, выполните окончательные операции над хэш-кодом, чтобы получить окончательное значение хэш-функции.
Специфика эффективной реализации полиномиальной хэш-функции включает следующие аспекты:
1.Выбор полинома: Выбор подходящего полинома имеет важное значение для равномерного распределения хэш-кодов и минимизации коллизий. Хорошо известным примером является полиномиальная хэш-функция с использованием полинома Мюррая-Дэвиса (MurmurHash), который демонстрирует хорошую производительность и статистические свойства.
2.Учет размера хэш-кода: При выборе размера хэш-кода необходимо учесть требования вашей системы. Если размер хэш-кода слишком маленький, может возникнуть большое количество коллизий. Если размер слишком большой, это может повлиять на производительность и потребление памяти.
3.Эффективная обработка символов/байтов: Для обработки каждого символа/байта входных данных можно использовать эффективные операции, такие как сдвиги битов, побитовые операции и арифметические операции. Это позволяет достичь хорошей производительности хэш-функции.
Специфика применения полиномиальной хэш-функции включает следующие аспекты:
1.Хэширование данных: Полиномиальная хэш-функция может быть использована для хэширования различных типов данных, включая строки, числа и байтовые массивы. Она позволяет преобразовать входные данные в уникальные хэш-коды, которые могут быть использованы для идентификации и сравнения данных.
2.Применение в структурах данных: Полиномиальная хэш-функция широко применяется в структурах данных, таких как хэш-таблицы, ассоциативные массивы, множества и другие. Она обеспечивает эффективное распределение данных и быстрый доступ к значениям по ключу.
3.Контроль целостности данных: Полиномиальная хэш-функция может использоваться для контроля целостности данных, особенно при передаче данных по ненадежным каналам связи. Путем сравнения хэш-кодов можно проверить, были ли данные повреждены или изменены.
Общим свойством полиномиальной хэш-функции является ее способность генерировать уникальные хэш-коды для различных входных данных, что делает ее полезной во многих приложениях, требующих быстрого и надежного хэширования.

27.Различные методы разрешения коллизий в хэш-таблицах.
При использовании хэш-таблиц для хранения данных возникают коллизии, когда два или более ключа сопоставляются с одним и тем же хэш-кодом. Разрешение коллизий - это процесс обработки этих ситуаций. Вот несколько распространенных методов разрешения коллизий в хэш-таблицах:
1.Метод цепочек (Chaining): При использовании метода цепочек каждая ячейка хэш-таблицы содержит список (связанный список, массив или другую структуру данных), в который добавляются элементы с одинаковым хэш-кодом. При вставке элемента с конфликтующим хэш-кодом он просто добавляется в соответствующий список. При поиске значения по ключу необходимо просмотреть список элементов с соответствующим хэш-кодом.
2.Метод открытой адресации (Open Addressing): При использовании метода открытой адресации элементы с конфликтующим хэш-кодом вставляются в другие доступные ячейки хэш-таблицы. Существует несколько стратегий для определения новой позиции элемента, например, линейное пробирование (переход к следующей ячейке в случае коллизии), квадратичное пробирование (увеличение шага пробирования на квадратичное значение), двойное хэширование (использование второй хэш-функции для определения шага пробирования), и другие. При поиске значения по ключу нужно последовательно проверять ячейки, пока не будет найден элемент с нужным ключом или пустая ячейка.
3.Метод псевдослучайных чисел: В этом методе используется генерация псевдослучайных чисел для определения новой позиции элемента при коллизии. Каждый элемент, имеющий коллизию, снова хэшируется с использованием другой хэш-функции, которая может быть зависима от текущей позиции элемента и других параметров. Этот процесс повторяется до тех пор, пока не будет найдено свободное место для элемента.

'''
    return pyperclip.copy(s)