Телефонная книга на миллион имен. Если проверять их по одному, можно совершить миллион сравнений. Но вы уже знаете секрет: откройте её посередине…
English narration · English + 中文 subtitles burned in · Английское озвучивание · Английский + китайские субтитры (встроенные)
19.1
Searching algorithms · Алгоритмы поиска
Syllabus · Программа
English
Candidates should be able to:
Notes and guidance
Show understanding of linear search and binary search methods
Write an algorithm to implement a linear search Write an algorithm to implement a binary search The conditions necessary for the use of a binary search How the performance of a binary search varies according to the number of data items
Show understanding of insertion sort and bubble sort methods
Write an algorithm to implement an insertion sort Write an algorithm to implement a bubble sort Performance of a sorting routine may depend on the initial order of the data and the number of data items
Show understanding of and use Abstract Data Types (ADT)
Write algorithms to find an item in each of the following: linked list, binary tree Write algorithms to insert an item into each of the following: stack, queue, linked list, binary tree Write algorithms to delete an item from each of the following: stack, queue, linked list Show understanding that a graph is an example of an ADT. Describe the key features of a graph and justify its use for a given situation. Candidates will not be required to write code for a graph structure
Show how it is possible for ADTs to be implemented from another ADT
Describe the following ADTs and demonstrate how they can be implemented from appropriate built-in types or other ADTs: stack, queue, linked list, dictionary, binary tree
Show understanding that different algorithms which perform the same task can be compared by using criteria (e.g. time taken to complete the task and memory used)
Including use of Big O notation to specify time and space complexity
Русский
Кандидаты должны уметь:
Примечания и рекомендации
Показать понимание методов линейного поиска и бинарного поиска
Написать алгоритм для реализации линейного поиска. Написать алгоритм для реализации бинарного поиска. Условия, необходимые для использования бинарного поиска. Как производительность бинарного поиска зависит от количества элементов данных.
Показать понимание методов сортировки вставками (insertion sort) и пузырьковой сортировки (bubble sort)
Написать алгоритм для реализации сортировки вставками. Написать алгоритм для реализации пузырьковой сортировки. Производительность процедуры сортировки может зависеть от исходного порядка данных и количества элементов.
Показать понимание и умение использовать абстрактные типы данных (Abstract Data Types — ADT)
Написать алгоритмы для поиска элемента в следующих структурах: связный список, двоичное дерево. Написать алгоритмы для вставки элемента в следующие структуры: стек, очередь, связный список, двоичное дерево. Написать алгоритмы для удаления элемента из следующих структур: стек, очередь, связный список. Показать понимание того, что граф является примером ADT. Описать ключевые особенности графа и обосновать его применение для конкретной ситуации. От кандидатов не требуется писать код для структуры графа.
Показать, как возможно реализовать ADT на основе другого ADT
Описать следующие ADT и продемонстрировать, как их можно реализовать с помощью встроенных типов или других ADT: стек, очередь, связный список, словарь (dictionary), двоичное дерево.
Показать понимание того, что различные алгоритмы, выполняющие одну и ту же задачу, можно сравнивать по критериям (например, времени выполнения задачи и используемой памяти)
Включая использование нотации Big O для определения временной и пространственной сложности.
Source: Cambridge International syllabus · Источник: Программа Cambridge International
English
Big O: how algorithms scaleInsertion sort: slide each card into placeBubble sort, pass by passBinary search: halve and conquer
A search finds a target value in a collection (often an array 数组) and returns its position, or "not found".
Linear search
A linear search 线性查找 walks from start to end, comparing each element with the target:
No preparation is needed, so it works on any list. Worst case O($n$) (target at the end or absent); best case 1 comparison. Use it on unsorted data or small lists. (The returned -1 is a sentinel value — an impossible position that means "not found"; the caller tests IF result = -1.)
The exam's version. Paper 3 asks you to complete a linear search written with a flag and a WHILE loop, and Paper 4 to write a function that returns the index or a count. Both look like this:
To stop at the first match instead, use a WHILE Index <= 100 AND NOT Found loop that sets Found ← TRUE and remembers the index. The marks are for the loop over every element, the comparison, and what is returned when the value is absent.
Binary search
A binary search 二分查找 needs the data sorted. Look at the middle element; if it is the target, done; if the target is smaller, search the left half, else the right half — halving the range each time:
Worst case O($\log_{2} n$) — for a million items, about 20 comparisons. Much faster than linear search on large sorted arrays, but you must sort first (a one-off O($n \log n$) cost), worth it if you search many times.
"State the condition necessary for a binary search."The data must be in order (sorted, ascending or descending, on the key being searched). "Describe how to perform a binary search" (three marks): (1) find the middle item of the list (or of the current range) and compare it with the target; (2) if it matches, the search ends; if the target is smaller, repeat on the lower half, if larger, on the upper half; (3) keep halving the range until the item is found or the range is empty, which means it is not present.
The exam's version, with the bounds and a flag, is the one to reproduce when asked to complete the algorithm:
"Explain how the performance varies with the number of items." Each comparison halves the number of items left, so the maximum number of comparisons is about $\log_{2} n$: doubling the size of the list adds only one more comparison. This is O($\log n$). "Compare linear and binary search": a linear search needs up to $n$ comparisons (O($n$)) and, on average, half that, but works on unsorted data; a binary search needs at most $\log_{2} n$ (O($\log n$)) and is far faster for large lists, but the data must first be sorted and it must allow direct access to the middle item (an array, not a linked list). For $1000$ items: $1000$ against $10$ comparisons.
Русский
Big O: как масштабируются алгоритмыСортировка вставками: переместите каждую карточку на местоСортировка пузырьком, проход за проходомБинарный поиск: делите пополам и побеждайте
Поиск находит целевое значение в наборе данных (часто в массиве) и возвращает его позицию или сообщение «не найдено».
Поиск в отсортированном списке, например в телефонной книге, происходит гораздо быстрее, чем проверка каждой записи по очереди
Линейный поиск
Линейный поиск проходит от начала до конца, сравнивая каждый элемент с целевым:
FOR i ← 1 TO n
IF A[i] = target THEN
RETURN i
ENDIF
NEXT i
RETURN -1 // not found
Подготовка не требуется, поэтому он работает с любым списком. Худший случай O($n$) (цель в конце или отсутствует); лучший случай 1 сравнение. Используйте его для неотсортированных данных или малых списков. (Возвращаемое -1 является сигнальным значением — невозможной позицией, означающей «не найдено»; вызывающая программа проверяет IF result = -1.)
Версия для экзамена. В задании Paper 3 требуется дополнить линейный поиск, написанный с использованием флага и цикла WHILE, а в Paper 4 — написать функцию, возвращающую индекс или счетчик. Оба варианта выглядят так:
FUNCTION LinearSearch(Data : ARRAY OF INTEGER, Target : INTEGER) RETURNS INTEGER
DECLARE Index, Count : INTEGER
Count ← 0
FOR Index ← 1 TO 100
IF Data[Index] = Target THEN
Count ← Count + 1
ENDIF
NEXT Index
RETURN Count // how many times Target occurs; 0 means not found
ENDFUNCTION
Чтобы остановиться на первом совпадении, используйте цикл WHILE Index <= 100 AND NOT Found, который устанавливает Found ← TRUE и запоминает индекс. Баллы начисляются за проход по каждому элементу, сравнение и то, что возвращается при отсутствии значения.
Линейный поиск проверяет каждую букву по очереди — 23 сравнения, чтобы найти W
Бинарный поиск
Бинарный поиск требует, чтобы данные были отсортированы. Посмотрите на средний элемент; если он целевой — готово; если цель меньше, ищите в левой половине, иначе в правой — уменьшая диапазон вдвое на каждом шаге:
low ← 1
high ← n
WHILE low <= high DO
mid ← (low + high) DIV 2
IF A[mid] = target THEN
RETURN mid
ENDIF
IF A[mid] < target THEN
low ← mid + 1
ELSE
high ← mid - 1
ENDIF
ENDWHILE
RETURN -1
Худший случай O($\log_{2} n$) — для миллиона элементов около 20 сравнений. Гораздо быстрее линейного поиска на больших отсортированных массивах, но сначала нужно отсортировать (однократная стоимость O($n \log n$)), что оправдано, если поиск выполняется много раз.
«Укажите условие, необходимое для бинарного поиска.»Данные должны быть упорядочены (отсортированы по возрастанию или убыванию по ключу поиска). «Опишите, как выполнить бинарный поиск» (три балла): (1) найдите средний элемент списка (или текущего диапазона) и сравните его с целевым; (2) если совпадает, поиск завершается; если цель меньше, повторите для нижней половины, если больше — для верхней половины; (3) продолжайте делить пополам диапазон, пока элемент не будет найден или диапазон не опустеет, что означает отсутствие элемента.
Версия для экзамена, с границами и флагом, — это та, которую следует воспроизвести, когда просят дополнить алгоритм:
DECLARE Lower, Upper, Mid : INTEGER
DECLARE Found : BOOLEAN
Lower ← 0
Upper ← 99
Found ← FALSE
WHILE Lower <= Upper AND NOT Found
Mid ← (Lower + Upper) DIV 2
IF Names[Mid] = Target THEN
Found ← TRUE
ELSE
IF Names[Mid] < Target THEN
Lower ← Mid + 1
ELSE
Upper ← Mid - 1
ENDIF
ENDIF
ENDWHILE
IF Found THEN
OUTPUT Mid
ELSE
OUTPUT "Not found"
ENDIF
"Объясните, как производительность меняется с количеством элементов." Каждое сравнение сокращает количество оставшихся элементов вдвое, поэтому максимальное число сравнений составляет около $\log_{2} n$: удвоение размера списка добавляет лишь одно дополнительное сравнение. Это O($\log n$). «Сравните линейный и бинарный поиск»: линейный поиск требует до $n$ сравнений (O($n$)) и в среднем половину этого, но работает на неотсортированных данных; бинарный поиск требует максимум $\log_{2} n$ (O($\log n$)) и значительно быстрее для больших списков, но данные должны быть предварительно отсортированы, и необходимо произвольное обращение к среднему элементу (массив, а не связный список). Для $1000$ элементов: $1000$ против $10$ сравнений.
Бинарный поиск делит диапазон пополам на каждом шаге (low / mid / high) — всего 3 сравнения, чтобы найти WКарточный каталог: отсортированные записи позволяют проводить бинарный поиск — делите пополам, смотрите, делите снова
Explore · Исследовать
Linear vs binary search · Линейный против бинарного поиска
Search for a value. Binary search halves the list each step (only on sorted data); linear search checks one by one. · Поиск значения. Бинарный поиск делит список пополам на каждом шаге (только для отсортированных данных); линейный поиск проверяет элементы по одному.
A bubble sort 冒泡排序 repeatedly walks the array, swapping adjacent pairs that are out of order, so the largest "bubbles" to the end each pass:
Best case O($n$) (already sorted, with the early exit); average/worst O($n^{2}$). Simple but slow for large $n$.
Insertion sort
An insertion sort 插入排序 builds a sorted prefix from the left, inserting each new element into place by shifting larger ones right:
Best case O($n$) (already sorted); worst O($n^{2}$). Good for small or nearly-sorted arrays. It sorts in place 原地 and is stable 稳定 (keeps the order of equal elements).
Tracing a sort
A common task is to show the array after each outer pass. For [D, T, H, R] with insertion sort: pass 1 (key T) no change; pass 2 (key H) → [D, H, T, R]; pass 3 (key R) → [D, H, R, T].
Writing a sort from scratch. "Write pseudocode to sort DataArray[1:1000] into ascending order" is answered by a complete bubble sort with the early-exit flag, or an insertion sort, declared and indented; either scores full marks if it works for every input:
For descending order change > to <; to sort records or a 2D array by one field, compare that field but swap the whole record (or every column). Asked to write an insertion sort "that performs the same task" as a given bubble sort, keep the same array name and direction and reproduce the insertion sort above with the comparison reversed if the order is descending.
"Describe two ways the performance of a sort is affected by the data" (two marks). (1) The number of items: an $O(n^{2})$ sort takes four times as long for twice as many items. (2) How far the data is already in order: a bubble sort with a flag, or an insertion sort, finishes in one pass over already-sorted data ($O(n)$) and does the most work on data in reverse order; the number of swaps depends on how many pairs are out of order. (Also accepted: the range or number of duplicate values, and whether the items are large records that are expensive to move.) Bubble and insertion sort are both O($n^{2}$) in the worst and average cases and O($n$) at best; quicksort and merge sort are O($n \log n$), which is why they are used for large data.
Русский
Пузырьковая сортировка
Пузырьковая сортировка многократно проходит по массиву, меняя местами соседние пары, стоящие в неправильном порядке, так что наибольшие элементы «всплывают» к концу при каждом проходе:
FOR pass ← 1 TO n - 1
swapped ← FALSE
FOR i ← 1 TO n - pass
IF A[i] > A[i + 1] THEN
temp ← A[i]
A[i] ← A[i + 1]
A[i + 1] ← temp
swapped ← TRUE
ENDIF
NEXT i
IF swapped = FALSE THEN // already sorted
EXIT FOR
ENDIF
NEXT pass
Лучший случай O($n$) (уже отсортировано, с ранним выходом); средний/худший O($n^{2}$). Проста, но медленна для больших $n$.
Сортировка вставками
Сортировка вставками формирует отсортированный префикс слева, вставляя каждый новый элемент на место путем сдвига бо́льших элементов вправо:
FOR i ← 2 TO n
key ← A[i]
j ← i - 1
WHILE j >= 1 AND A[j] > key DO
A[j + 1] ← A[j]
j ← j - 1
ENDWHILE
A[j + 1] ← key
NEXT i
Лучший случай O($n$) (уже отсортировано); худший O($n^{2}$). Хороша для малых или почти отсортированных массивов. Сортирует на месте и является стабильной (сохраняет порядок равных элементов).
Отслеживание сортировки
Типичная задача — показать массив после каждого внешнего прохода. Для [D, T, H, R] со сортировкой вставками: проход 1 (ключ T) без изменений; проход 2 (ключ H) → [D, H, T, R]; проход 3 (ключ R) → [D, H, R, T].
Написание сортировки с нуля. Задание «Запишите псевдокод для сортировки DataArray[1:1000] по возрастанию» решается полной сортировкой пузырьком с флагом раннего выхода или сортировкой вставками, объявленной и отступленной; оба варианта получают полный балл, если работают для любого входа:
DECLARE Pass, Index, Temp : INTEGER
DECLARE Swapped : BOOLEAN
Pass ← 1
REPEAT
Swapped ← FALSE
FOR Index ← 1 TO 1000 - Pass
IF DataArray[Index] > DataArray[Index + 1] THEN
Temp ← DataArray[Index]
DataArray[Index] ← DataArray[Index + 1]
DataArray[Index + 1] ← Temp
Swapped ← TRUE
ENDIF
NEXT Index
Pass ← Pass + 1
UNTIL Swapped = FALSE OR Pass = 1000
Для сортировки по убыванию замените > на <; для сортировки записей или двумерного массива 2D по одному полю сравнивайте это поле, но меняйте местами всю запись (или каждый столбец). Если просят написать сортировку вставками «выполняющую ту же задачу», что и заданная сортировка пузырьком, сохраните то же имя массива и направление, воспроизведите описанную выше сортировку вставками, изменив знак сравнения на обратный, если порядок убывающий.
"Опишите два способа, которыми данные влияют на производительность сортировки" (два балла). (1) Количество элементов: алгоритм $O(n^{2})$ требует в четыре раза больше времени для вдвое большего количества элементов. (2) Насколько данные уже упорядочены: пузырьковая сортировка с флагом или сортировка вставками завершается за один проход по уже отсортированным данным ($O(n)$) и выполняет наибольшую работу на данных, расположенных в обратном порядке; количество обменов зависит от количества неупорядоченных пар. (Также принимается: диапазон или количество дублирующихся значений, а также то, являются ли элементы крупными записями, перемещение которых обходится дорого.) Пузырьковая сортировка и сортировка вставками имеют сложность O($n^{2}$) в худшем и среднем случаях и O($n$) в лучшем; быстрая сортировка и слиянием имеют O($n \log n$), именно поэтому они используются для больших объемов данных.
Сортировка вставками [D, T, H, R], размещающая каждый ключ на своем месте последовательно
Explore · Исследовать
Watch a sort run · Просмотр работы сортировки
Step through a sort and watch the bars settle into order — how a sorting algorithm works pass by pass. · Проходите через процесс сортировки и наблюдайте, как столбцы выстраиваются в порядке — как работает алгоритм сортировки шаг за шагом.
19.1
ADTs in algorithms · Абстрактные типы данных (ADT) в алгоритмах
English
The Abstract Data Types (ADTs) from Topic 10 appear inside many algorithms: a stack 栈 drives depth-first traversal and undo; a queue 队列 drives breadth-first traversal and print ordering; a linked list 链表 lets data grow and shrink.
ADTs can be built from other ADTs, not just from arrays: a queue from two stacks; a stack from a linked list (push = prepend a head node 节点); a queue from a linked list with head and tail pointers 指针; a binary tree 二叉树 from nodes with two child pointers; a dictionary 字典 stores key→value pairs (often on a hash table). Layering this way separates concerns — the algorithm using the ADT need not know how it is built.
The ADTs the exam asks you to describe and implement
Stack (last in, first out): items are added (pushed) and removed (popped) at the same end, the top; a pointer TopOfStack holds the index of the top item. Implemented with an array and that one pointer: push checks the stack is not full, increments the pointer and stores the item; pop checks it is not empty, returns the top item and decrements the pointer.
Queue (first in, first out): items join at the rear (enqueue) and leave from the front (dequeue); two pointers and a count. In a linear queue the front pointer creeps along the array until the space at the start is wasted; a circular queue 循环队列 wraps both pointers round with MOD, so every cell is reused.
Linked list: a sequence of nodes, each holding a data item and a pointer to the next node; a start pointer gives the first node and a null pointer (0 or $-1$) ends the list. In an array implementation two parallel arrays hold the data and the pointers, and unused cells are chained into a free list 空闲列表 so that an insertion knows where to put the new node.
To insert into an ordered list: take the first free cell (NewNode ← FreeList, FreeList ← Pointer[FreeList]), store the item, then walk the list with a Previous and Current pointer until Data[Current] > Item or the end; set Pointer[NewNode] ← Current and Pointer[Previous] ← NewNode (or Start ← NewNode if it goes first). To delete, re-link the previous node past the deleted one and return the cell to the free list.
Binary tree: a root node, each node holding data, a left pointer to a subtree of smaller values and a right pointer to a subtree of larger values. Implemented as a 2D array (or three 1D arrays) Tree[Index, 0..2] for left pointer, data, right pointer, with a root pointer and a next-free pointer.
To insert: store the item in the next free node with both pointers $-1$; if the tree is empty make it the root; otherwise walk down from the root, going left or right by comparison, until the pointer you would follow is $-1$, and set that pointer to the new node. An ADT from another ADT: a stack is a linked list where push and pop both work at the start; a queue is a linked list with a start and an end pointer; a queue can be made from two stacks (push onto one, pop from the other, moving everything across when the second is empty); a binary tree's nodes are records or objects linked by pointers, so it is built from a linked structure of nodes. Say which operations of the new ADT map onto which operations of the old one.
Русский
Абстрактные типы данных (ADT) из Темы 10 используются во многих алгоритмах: стек обеспечивает обход в глубину и функцию отмены; очередь обеспечивает обход в ширину и порядок печати; связный список позволяет данным динамически расширяться и уменьшаться.
ADT могут строиться на основе других ADT, а не только массивов: очередь из двух стеков; стек из связного списка (push = добавить узел в начало узла); очередь из связного списка с указателями на голову и хвост; бинарное дерево из узлов с двумя указателями на потомков; словарь хранит пары ключ→значение (часто на хеш-таблице). Такое построение分层 (layering) разделяет ответственность — алгоритму, использующему ADT, не нужно знать, как он реализован.
ADT, которые требуется описать и реализовать на экзамене
Стек (last in, first out): элементы добавляются (push) и удаляются (pop) с одного конца — вершины; указатель TopOfStack хранит индекс верхнего элемента. Реализуется с помощью массива и одного указателя: push проверяет, что стек не полон, увеличивает указатель и сохраняет элемент; pop проверяет, что стек не пуст, возвращает верхний элемент и уменьшает указатель.
FUNCTION Push(Item : INTEGER) RETURNS BOOLEAN
IF TopOfStack = 9 THEN // full (array 0 to 9)
RETURN FALSE
ENDIF
TopOfStack ← TopOfStack + 1
StackData[TopOfStack] ← Item
RETURN TRUE
ENDFUNCTION
FUNCTION Pop() RETURNS INTEGER
IF TopOfStack = -1 THEN // empty
RETURN -1
ENDIF
TopOfStack ← TopOfStack - 1
RETURN StackData[TopOfStack + 1]
ENDFUNCTION
Очередь (first in, first out): элементы добавляются в хвост (enqueue) и покидают из головы (dequeue); два указателя и счетчик. В линейной очереди указатель головы смещается по массиву до тех пор, пока пространство в начале не будет потеряно; циклическая очередь оборачивает оба указателя через MOD, благодаря чему каждая ячейка используется повторно.
Циклическая очередь: указатели хвоста и головы двигаются вперед с использованием MOD, поэтому первые ячейки массива используются повторно после того, как их элементы покинут очередь
FUNCTION Enqueue(Item : STRING) RETURNS BOOLEAN
IF Count = 6 THEN // full
RETURN FALSE
ENDIF
Rear ← (Rear + 1) MOD 6
QueueArray[Rear] ← Item
Count ← Count + 1
RETURN TRUE
ENDFUNCTION
FUNCTION Dequeue() RETURNS STRING
IF Count = 0 THEN // empty
RETURN ""
ENDIF
DECLARE Item : STRING
Item ← QueueArray[Front]
Front ← (Front + 1) MOD 6
Count ← Count - 1
RETURN Item
ENDFUNCTION
Связный список: последовательность узлов, каждый из которых содержит элемент данных и указатель на следующий узел; указатель начала указывает на первый узел, а нулевой указатель (0 или $-1$) обозначает конец списка. При реализации на массиве два параллельных массива хранят данные и указатели, а неиспользованные ячейки соединяются в свободный список, чтобы при вставке можно было определить место для нового узла.
Связный список в двух массивах: порядок элементов определяется указателями, а не позициями; вставка имени означает взятие ячейки из свободного списка и перезамену двух указателей
FUNCTION FindInList(Target : STRING) RETURNS INTEGER // index, or 0 if absent
DECLARE Current : INTEGER
Current ← Start
WHILE Current <> 0
IF Data[Current] = Target THEN
RETURN Current
ENDIF
Current ← Pointer[Current]
ENDWHILE
RETURN 0
ENDFUNCTION
Для вставки в упорядоченный список: возьмите первую свободную ячейку (NewNode ← FreeList, FreeList ← Pointer[FreeList]), сохраните элемент, затем пройдите по списку со значениями Previous и Current, пока не встретите Data[Current] > Item или конец; установите Pointer[NewNode] ← Current и Pointer[Previous] ← NewNode (или Start ← NewNode, если вставка происходит в начало). Для удаления пересоедините предыдущий узел, минуя удаленный, и верните ячейку в свободный список.
Двоичное дерево: узел корня, каждый узел содержит данные, левый указатель на поддерево меньших значений и правый указатель на поддерево бо́льших значений. Реализуется как 2-мерный массив (или три 1-мерных массива) Tree[Index, 0..2] для левого указателя, данных, правого указателя, с указателем корня и указателем следующего свободного элемента.
FUNCTION FindInTree(Target : INTEGER) RETURNS INTEGER // index, or -1
DECLARE Current : INTEGER
Current ← Root
WHILE Current <> -1
IF Tree[Current, 1] = Target THEN
RETURN Current
ENDIF
IF Target < Tree[Current, 1] THEN
Current ← Tree[Current, 0] // go left
ELSE
Current ← Tree[Current, 2] // go right
ENDIF
ENDWHILE
RETURN -1
ENDFUNCTION
Для вставки: сохраните элемент в следующую свободную ячейку, установив оба указателя равными $-1$; если дерево пусто, сделайте его корнем; иначе спускайтесь от корня, выбирая левый или правый путь в зависимости от сравнения, пока указатель, по которому следовало бы перейти, не станет $-1$, и установите этот указатель на новый узел. ADT на основе другого ADT: стек — это связный список, где push и pop работают с начала; очередь — это связный список с указателями начала и конца; очередь можно построить из двух стеков (push в один, pop из другого, перемещая все элементы, когда второй становится пустым); узлы бинарного дерева представляют собой записи или объекты, связанные указателями, поэтому оно строится на основе связанной структуры узлов. Укажите, какие операции нового ADT соответствуют операциям старого.
Бинарное дерево: каждый узел имеет до двух дочерних узловТри вида обхода в глубину бинарного дерева: pre-order, in-order (порядок сортировки) и post-order
Time complexity 时间复杂度 is how the running time grows with input size $n$, written in Big-O notation 大O表示法 (the dominant term): O(1) constant, O($\log n$) binary search, O($n$) linear search, O($n \log n$) good sorts, O($n^{2}$) bubble/insertion sort. A smaller order is better at scale, even if another algorithm is faster for small $n$.
To make that concrete: to sort a million items, an $O(n \log n)$ sort finishes in a fraction of a second, while an $O(n^{2})$ sort can take minutes.
Worked example. A sorted list holds $1000$ items. How many comparisons does each search need in the worst case?
A linear search checks items one at a time, so it may need up to $1000$ comparisons — this is $O(n)$. A binary search halves the list each step, so it needs at most $\lceil \log_2 1000 \rceil = 10$ comparisons — this is $O(\log n)$. Doubling the list to $2000$ items adds only one comparison to the binary search, but up to another $1000$ to the linear search — which is why the order of growth, not raw speed, decides the winner at scale.
Describing an order.O(1): the time is constant, independent of the number of items (pushing onto a stack, reading an array element). O($\log n$): the time grows with the logarithm of the number of items, so doubling the data adds only a fixed extra step (binary search). O($n$): the time grows in proportion to the number of items (linear search, one pass through a list). O($n \log n$): a little worse than linear (efficient sorts). O($n^{2}$): the time grows with the square of the number of items, so doubling the data quadruples the time (bubble and insertion sort). "State the Big O of a binary search of Names[0:99]" is answered $O(\log n)$, and "describe its meaning" as above; Big O measures how the time or memory scales, not the actual time.
Space complexity
Space complexity 空间复杂度 is the extra memory needed. Bubble and insertion sort use O(1) extra (in place); merge sort uses O($n$); recursion uses stack memory proportional to its depth. There is often a time–memory trade-off.
Other criteria
Simplicity (easier to code and maintain), stability, and adaptiveness (faster on nearly-sorted data). The right algorithm depends on the data and the constraints.
Русский
Сложность по времени
Временная сложность показывает, как время выполнения растет с увеличением размера входных данных $n$. Записывается в нотации Big-O (доминирующий член): O(1) — константная, O($\log n$) — бинарный поиск, O($n$) — линейный поиск, O($n \log n$) — эффективные сортировки, O($n^{2}$) — пузырьковая и сортировка вставками. Меньший порядок роста предпочтительнее на больших объемах данных, даже если другой алгоритм быстрее для малых $n$.
Чтобы это стало понятнее: при сортировке миллиона элементов алгоритм со сложностью $O(n \log n)$ завершится за долю секунды, тогда как алгоритм со сложностью $O(n^{2})$ может занять минуты.
Разобранный пример. В отсортированном списке находится ⟨⟩$1000$ элементов. Сколько сравнений потребуется каждому поиску в худшем случае?
Линейный поиск проверяет элементы по одному, поэтому ему может потребоваться до ⟨⟩$1000$ сравнений — это $O(n)$. Бинарный поиск на каждом шаге уменьшает список вдвое, поэтому ему нужно не более ⟨⟩$\lceil \log_2 1000 \rceil = 10$ сравнений — это $O(\log n)$. Удвоение списка до ⟨⟩$2000$ элементов добавляет к бинарному поиску всего одно сравнение, но до ⟨⟩$1000$ к линейному поиску — именно поэтому на больших объемах данных победителем становится порядок роста, а не абсолютная скорость.
Описание порядка.O(1): время постоянно и не зависит от количества элементов (добавление элемента в стек, чтение из массива). O($\log n$): время растет пропорционально логарифму количества элементов, поэтому удвоение данных добавляет лишь фиксированный дополнительный шаг (бинарный поиск). O($n$): время растет пропорционально количеству элементов (линейный поиск, один проход по списку). O($n \log n$): немного хуже линейного (эффективные сортировки). O($n^{2}$): время растет пропорционально квадрату количества элементов, поэтому удвоение данных увеличивает время в четыре раза (пузырьковая и сортировка вставками). Вопрос «Укажите Big O для бинарного поиска по ⟨⟩Names[0:99]» имеет ответ $O(\log n)$, а «опишите его значение» — см. выше; Big O измеряет масштабирование времени или памяти, а не фактическое время работы.
Сравнение распространенных порядков роста: меньший порядок выигрывает на больших объемахКак время сортировки растет с количеством элементов ⟨⟩$n$: алгоритмы со сложностью $O(n^2)$ уходят вверх относительно алгоритма со сложностью $O(n\log n)$
Пространственная сложность
Пространственная сложность — это дополнительная所需 памяти. Пузырьковая и сортировка вставках используют O(1) дополнительной памяти (in-place); слияние требует O($n$); рекурсия использует память стека, пропорциональную глубине. Часто существует компромисс между временем и памятью.
Другие критерии
Простота (легче кодить и поддерживать), стабильность и адаптивность (быстрее работает на почти отсортированных данных). Правильный выбор алгоритма зависит от данных и ограничений.
Explore · Исследовать
How running time grows with n · Как время выполнения растет вместе с n
Slide n upward and compare the curves: O(1) and O(log n) stay almost flat, O(n) rises steadily, O(n²) explodes. This is why Big-O — not a stopwatch — is how we compare algorithms on large inputs. · Наклоните график n вверх и сравните кривые: O(1) и O(log n) остаются почти горизонтальными, O(n) растет плавно, O(n²) резко возрастает. Вот почему Big-O — а не секундомер — используется для сравнения алгоритмов на больших объемах входных данных.
Explore · Исследовать
Big-O growth · Рост Big-O
Change the input size n and compare how fast each algorithm's work grows — the idea behind time complexity. · Изменяйте размер входных данных n и сравнивайте, насколько быстро растет объем работы каждого алгоритма — это суть временной сложности.
Essential features of recursion How recursion is expressed in a programming language Write and trace recursive algorithms When the use of recursion is beneficial
Show awareness of what a compiler has to do to translate recursive programming code
Use of stacks and unwinding
Русский
Кандидаты должны уметь:
Примечания и рекомендации
Показать понимание рекурсии
Сущностные особенности рекурсии. Как рекурсия выражается в языке программирования. Написание и трассировка рекурсивных алгоритмов. Когда применение рекурсии целесообразно.
Проявлять осведомленность о том, что должен сделать компилятор для трансляции кода на рекурсивных языках программирования
Использование стеков и разворачивания (unwinding).
Source: Cambridge International syllabus · Источник: Программа Cambridge International
English
Recursion: the call stack winds up and unwinds
Recursive algorithms use recursion 递归: the routine calls itself with a smaller version of the same problem, until a base case 基本情形 ends the chain. It has two parts: the base case (small enough to solve directly — without it the recursion never stops) and the recursive case 递归情形 (reduce the input and call itself).
Factorial 阶乘:
Recursion is natural for self-similar problems: trees, divide-and-conquer 分治 (binary search, merge sort), and nested data. When it is a poor fit, a loop is usually cleaner.
"Describe what is meant by recursion" (two marks).A function or procedure that is defined in terms of itself: it calls itself from within its own body, with a smaller version of the problem each time, until a base case is reached."State three essential features of recursion": (1) a base case (stopping condition) that returns a value without a further call; (2) a general case 一般情形 in which the routine calls itself; (3) each call moves the problem closer to the base case (the parameter is reduced), so that the recursion terminates. Some schemes add: values are returned as the calls unwind.
"Describe when the use of recursion is beneficial, and give an example." When the problem is naturally defined in terms of smaller versions of itself, so that the recursive solution is shorter, clearer and closer to the mathematical definition than a loop would be: a factorial or Fibonacci number, a binary search, traversing a binary tree, merge sort or quicksort, and processing nested structures such as folders within folders. It is a poor choice when the depth is large (the stack may overflow) or when the same sub-problem is computed many times (naive Fibonacci).
Tracing a recursive call
For Factorial(4): the calls go down to Factorial(1)=1, then unwinding multiplies back up: 2*1=2, 3*2=6, 4*6=24. Final result 24. Track each pending call on a stack.
Worked example. The function below is given without an explanation. Trace Unknown(3, 5) and state its output and return value.
Call 1: $X = 3, Y = 5$: $3 < 5$, output 8, call Unknown(4, 4). Call 2: $4 < 4$ is false, return 0. Unwinding: call 1 returns $0 + 1 = 1$. Output 8, return value 1. Write the trace as a table with a row per call (parameters, condition, output, what it returns), and do the returns from the deepest call upwards: that is the unwinding the mark scheme looks for.
Worked example (Fibonacci).Fib(n) returns n when n < 2, otherwise Fib(n - 1) + Fib(n - 2). Find Fib(5).
Fib(5) = Fib(4) + Fib(3); Fib(4) = Fib(3) + Fib(2); Fib(3) = Fib(2) + Fib(1); Fib(2) = Fib(1) + Fib(0) = 1 + 0 = 1. So Fib(3) = 1 + 1 = 2, Fib(4) = 2 + 1 = 3, Fib(5) = 3 + 2 = 5. The base case is reached many times (Fib(2) is computed three times), which is why this version is slow: it makes 15 calls for $n = 5$ and roughly doubles the calls for every increase in $n$.
Converting recursion to iteration. Every recursive routine can be rewritten with a loop, which uses less memory and is faster: keep a running result and loop from the base case upwards. Factorial as a loop:
Asked to change a recursive insertion sort or search into an iterative one, replace the self-call with a loop over the index that the recursion was stepping through, and turn the base case into the loop's exit condition.
Risks
infinite recursion if the base case is missed — crashes with a stack overflow 栈溢出.
high memory use for deep recursion.
slow if it repeats work (naive Fibonacci is exponential — use a loop or memoisation 记忆化).
Русский
Рекурсия: стек вызовов заполняется и опустожается
Рекурсивные алгоритмы используют рекурсию: процедура вызывает саму себя с уменьшенной версией той же задачи, пока не будет достигнут базовый случай, завершающий цепочку. Она состоит из двух частей: базового случая (достаточно малого для прямого решения — без него рекурсия никогда не остановится) и рекурсивного случая (уменьшение входных данных и вызов самой себя).
Факториал:
FUNCTION Factorial(n : INTEGER) RETURNS INTEGER
IF n = 0 OR n = 1 THEN
RETURN 1
ELSE
RETURN n * Factorial(n - 1)
ENDIF
ENDFUNCTION
Рекурсия естественна для самоподобных задач: деревьев, разделяй и властвуй (бинарный поиск, слияние), и вложенных данных. Когда она плохо подходит, цикл обычно чище.
«Опишите, что понимается под рекурсией» (два балла).Функция или процедура, определенная через саму себя: она вызывает саму себя внутри собственного тела, передавая каждый раз меньшую версию задачи, пока не будет достигнут базовый случай.«Назовите три основных признака рекурсии»: (1) базовый случай (условие остановки), возвращающий значение без дальнейшего вызова; (2) общий случай, в котором процедура вызывает саму себя; (3) каждый вызов приближает задачу к базовому случаю (параметр уменьшается), так что рекурсия завершается. Некоторые схемы добавляют: значения возвращаются по мере того, как вызовы опустошают стек.
«Опишите, когда использование рекурсии полезно, и приведите пример». Когда задача естественно определяется через меньшие версии самой себя, так что рекурсивное решение короче, понятнее и ближе к математическому определению, чем циклическое: факториал или числа Фибоначчи, бинарный поиск, обход двоичного дерева, слияние или быстрая сортировка, обработка вложенных структур, таких как папки внутри папок. Это плохой выбор, когда глубина велика (стек может переполниться) или когда одна и та же подзадача вычисляется многократно (наивный алгоритм Фибоначчи).
Отслеживание рекурсивного вызова
Для ⟨⟩Factorial(4): вызовы спускаются до ⟨⟩Factorial(1)=1, затем опустошение умножает обратно вверх: ⟨⟩2*1=2, ⟨⟩3*2=6, ⟨⟩4*6=24. Итоговый результат ⟨⟩24. Отслеживайте каждый ожидающий вызов в стеке.
Разобранный пример. Ниже приведена функция без объяснения. Отследите ⟨⟩Unknown(3, 5) и укажите её вывод и возвращаемое значение.
FUNCTION Unknown(BYVAL X, BYVAL Y : INTEGER) RETURNS INTEGER
IF X < Y THEN
OUTPUT X + Y
RETURN Unknown(X + 1, Y - 1) + 1
ELSE
RETURN 0
ENDIF
ENDFUNCTION
Вызов 1: ⟨⟩$X = 3, Y = 5$: ⟨⟩$3 < 5$, вывод 8, вызов ⟨⟩Unknown(4, 4). Вызов 2: ⟨⟩$4 < 4$ является ложным, возврат 0. Опустошение: вызов 1 возвращает ⟨⟩$0 + 1 = 1$. Вывод 8, возвращаемое значение 1. Запишите отслеживание в виде таблицы со строкой на каждый вызов (параметры, условие, вывод, что он возвращает), и выполняйте возвраты от самого глубокого вызова вверх: именно такое опустошение ожидает ключ к оцениванию.
Разобранный пример (числа Фибоначчи).Fib(n) возвращает n при n < 2, иначе Fib(n - 1) + Fib(n - 2). Найдите Fib(5).
Fib(5) = Fib(4) + Fib(3); Fib(4) = Fib(3) + Fib(2); Fib(3) = Fib(2) + Fib(1); Fib(2) = Fib(1) + Fib(0) = 1 + 0 = 1. Таким образом, Fib(3) = 1 + 1 = 2, Fib(4) = 2 + 1 = 3, Fib(5) = 3 + 2 = 5. Базовый случай достигается множественно (Fib(2) вычисляется три раза), поэтому эта версия работает медленно: она выполняет 15 вызовов для $n = 5$ и примерно удваивает количество вызовов с каждым увеличением $n$.
Преобразование рекурсии в итерацию. Любую рекурсивную процедуру можно переписать с использованием цикла, который требует меньше памяти и работает быстрее: храните текущий результат и выполняйте цикл от базового случая вверх. Факториал как цикл:
FUNCTION Factorial(N : INTEGER) RETURNS INTEGER
DECLARE Result, Count : INTEGER
Result ← 1
FOR Count ← 2 TO N
Result ← Result * Count
NEXT Count
RETURN Result
ENDFUNCTION
Если требуется преобразовать рекурсивную сортировку вставками или поиск в итеративный, замените самовызов циклом по индексу, через который проходила рекурсия, а базовый случай превратите в условие выхода из цикла.
Рекурсия использует стек вызовов: вызовы создают фреймы, уходящие вниз до базового случая, затем возвраты разматываются обратно вверх
Риски
бесконечная рекурсия при отсутствии базового случая — завершается аварийно с ошибкой переполнения стека.
высокое потребление памяти при глубокой рекурсии.
медленная работа при повторении вычислений (наивное число Фибоначчи имеет экспоненциальную сложность — используйте цикл или мемоизацию).
Explore · Исследовать
Recursion unwinds from the leaves up · Рекурсия раскручивается от листьев вверх
Step through fib(4) in the order the calls actually finish: the leaves (base cases) resolve first, then each parent combines its children. Notice fib(2) is computed twice — that repeated work is why naive recursion is slow. · Пройдите вычисление fib(4) в порядке фактического завершения вызовов: сначала разрешаются листья (базовые случаи), затем каждый родитель объединяет своих детей. Заметьте, что fib(2) вычисляется дважды — эта повторяющаяся работа объясняет медленную работу наивной рекурсии.
What the compiler does for recursive code · Что делает компилятор с рекурсивным кодом
English
Recursion needs each call to have its own copy of its parameters 参数 and local variables 局部变量. The compiler keeps these on the call stack 调用栈. For each call it pushes a stack frame 栈帧 holding the parameters, the local variables, and the return address 返回地址 (where to resume in the caller). When the function returns, the return value is handed back, the frame is popped, and control resumes at the return address.
Because each call has its own frame, recursive calls don't trample each other's variables. The stack can grow large for deep recursion, which is why very deep recursion may overflow it. This is the same call-and-return mechanism used for ordinary (non-recursive) calls — there is no special "recursion mechanism".
"Explain why a stack is suitable for implementing recursion" (three marks). Each recursive call must save its return address, its parameters and its local variables, and the calls are completed in the reverse order to that in which they were made (the last call made is the first to finish), which is exactly the last in, first out behaviour of a stack: each new call pushes a frame, and each return pops the most recent frame, restoring the caller's state and telling it where to continue. This is the compiler's job when it translates recursive code: it generates the push of a stack frame on every call and the pop on every return, and the frames are unwound as the results come back.
Русский
Для рекурсии необходимо, чтобы каждый вызов имел собственную копию своих параметров и локальных переменных. Компилятор хранит эти данные на стеке вызовов. Для каждого вызова он помещает фрейм стека, содержащий параметры, локальные переменные и адрес возврата (место, откуда нужно продолжить выполнение в вызывающей функции). Когда функция завершает работу, значение результата передается обратно, фрейм удаляется из стека, и управление возобновляется по адресу возврата.
Поскольку у каждого вызова есть свой фрейм, рекурсивные вызовы не затирают друг друга. Стек может вырасти большим при глубокой рекурсии, именно поэтому очень глубокая рекурсия может привести к его переполнению. Это тот же механизм вызова и возврата, что используется для обычных (нерекурсивных) вызовов; здесь нет никакого специального «рекурсивного механизма».
"Объясните, почему стек подходит для реализации рекурсии (три балла).** Каждый рекурсивный вызов должен сохранить свой адрес возврата, параметры и локальные переменные, а вызовы завершаются в обратном порядке по отношению к тому, в котором они были сделаны (последний вызванный вызов является первым завершенным), что точно соответствует поведению стека «последним вошел — первым вышел» (LIFO): каждый новый вызов создает (push) фрейм, а каждый возврат удаляет (pop) самый последний фрейм, восстанавливая состояние вызывающей функции и указывая ей, где продолжать. Это задача компилятора при трансляции рекурсивного кода: он генерирует создание фрейма стека при каждом вызове и удаление при каждом возврате, а фреймы разматываются по мере поступления результатов.
19.2
Definitions the examiner accepts · Определения, принимаемые экзаменатором
English
A definition question is marked against fixed wording. Learn these exactly, and give one answer only.
Term
Definition
linear search
checking each item in turn from the start until the target is found or the end is reached
binary search
repeatedly comparing the target with the middle item of a sorted list and discarding the half that cannot contain it
bubble sort
repeatedly passing through the list, swapping adjacent items that are in the wrong order, until a pass makes no swaps
insertion sort
taking each item in turn and inserting it into its correct place among the items already sorted
abstract data type
a collection of data and the operations that can be performed on it, defined independently of how it is stored
stack
a last-in-first-out structure with push and pop at the top
queue
a first-in-first-out structure with items added at the rear and removed from the front
linked list
a sequence of nodes, each holding data and a pointer to the next node, with a start pointer
binary tree
nodes each holding data and pointers to a left subtree of smaller values and a right subtree of larger values
Big O notation
a way of classifying the time (or memory) an algorithm needs by how it grows with the size of the input
recursion
a routine that calls itself with a smaller version of the problem until a base case stops the calls
base case
the condition under which a recursive routine returns without calling itself
unwinding
the returns of a chain of recursive calls, from the deepest call back to the first, as the stack frames are popped
Русский
Вопросы на определение оцениваются по фиксированной формулировке. Выучите их точно и дайте только один ответ.
Термин
Определение
линейный поиск
последовательная проверка каждого элемента от начала до тех пор, пока целевой элемент не будет найден или не будет достигнут конец списка
бинарный поиск
многократное сравнение целевого значения со средним элементом отсортированного списка и отбрасывание половины, которая не может его содержать
пузырьковая сортировка
многократное прохождение по списку с обменом соседних элементов, стоящих в неправильном порядке, пока одно прохождение не пройдет без обменов
сортировка вставками
последовательная обработка каждого элемента и его вставка на правильное место среди уже отсортированных элементов
абстрактный тип данных
совокупность данных и операций, которые могут быть над ними выполнены, определенная независимо от способа их хранения
стек
структура типа «последним вошел — первым вышел» (LIFO) с операциями добавления (push) и удаления (pop) сверху
очередь
структура типа «первым вошел — первым вышел» (FIFO) с элементами, добавляемыми сзади и удаляемыми спереди
связный список
последовательность узлов, каждый из которых содержит данные и указатель на следующий узел, с указателем начала
двоичное дерево
узлы, содержащие данные и указатели на левое поддерево с меньшими значениями и правое поддерево с бо́льшими значениями
нотация Big O
способ классификации времени (или памяти), необходимого алгоритму, в зависимости от того, как оно растет вместе с размером входных данных
рекурсия
процедура, которая вызывает сама себя с уменьшенной версией задачи до тех пор, пока базовый случай не остановит вызовы
базовый случай
условие, при котором рекурсивная процедура завершает работу без собственного вызова
разматывание стека
последовательность возвратов цепочки рекурсивных вызовов, от самого глубокого вызова к первому, по мере удаления фреймов из стека
19.2
Exam tips · Советы для экзамена
English
Searches: linear needs no order and O($n$); binary needs a sorted array, halves each time and is O($\log n$). Know both algorithms by heart, including the bounds and the flag.
Sorts: bubble with a swapped flag, insertion with a key that shifts larger items right; both O($n^{2}$) worst, O($n$) on sorted data. Performance depends on the number of items and how ordered they are.
ADT implementations are pointer bookkeeping: a top pointer; front, rear and count with MOD; start, pointers and a free list; root with left and right pointers. Always check for full and empty.
Big O is about scaling: constant, logarithmic, linear, square. Say "doubling the data adds one comparison" for a binary search.
Recursion: base case, general case, progress towards the base case; beneficial when the problem is defined in terms of itself; a stack holds the return addresses and variables because calls return in reverse order. Trace with a table and unwind from the deepest call.
Common mistakes
Using a binary search on unsorted data, or on a linked list; and setting Lower ← Mid instead of Mid + 1, which loops for ever.
A bubble sort inner loop that runs to the end of the array every pass, or a swap without a temporary variable.
A push or enqueue that does not test for full, or a pop or dequeue that does not test for empty.
Moving the queue's front pointer without MOD in a circular queue, or treating front = rear as always meaning empty.
Inserting into a linked list by shifting the array contents; only the pointers change.
A recursive function with no base case, or one whose recursive call does not make the problem smaller.
Tracing a recursive call but forgetting to add the pending work on the way back up.
Answering "why a stack" with "because it is fast"; the reason is the last-in-first-out order of the returns.
Русский
Поиски: линейный поиск не требует упорядоченности и имеет сложность O($n$); бинарный поиск требует отсортированного массива, делит задачу пополам на каждом шаге и имеет сложность O($\log n$). Выучите оба алгоритма наизусть, включая границы и флаги.
Сортировки: пузырьковая с флагом обмена, вставками с ключом, сдвигающим бо́льшие элементы вправо; обе имеют худшую сложность O($n^{2}$) и лучшую O($n$) на отсортированных данных. Производительность зависит от количества элементов и степени их упорядоченности.
Реализации АТД требуют управления указателями: указатель верха; front, rear и count с операцией MOD; start, указатели и свободный список; корень с указателями на левое и правое поддеревья. Всегда проверяйте условия полноты и пустоты.
Big O касается масштабируемости: константная, логарифмическая, линейная, квадратичная. Для бинарного поиска говорите: «удвоение объема данных добавляет одну операцию сравнения».
Рекурсия: базовый случай, общий случай, прогресс к базовому случаю; полезна, когда задача определена через саму себя; стек хранит адреса возврата и переменные, так как вызовы завершаются в обратном порядке. Отслеживайте процесс с помощью таблицы и разматывайте стек от самого глубокого вызова.
Распространенные ошибки
Использование бинарного поиска на неотсортированных данных или на связном списке; а также установка Lower ← Mid вместо Mid + 1, что приводит к бесконечному циклу.
Вложенный цикл пузырьковой сортировки, проходящий до конца массива при каждом проходе, или обмен без использования временной переменной.
Операция push или enqueue без проверки на переполнение, или pop/dequeue без проверки на пустоту.
Перемещение указателя front очереди без применения операции MOD в кольцевой очереди или трактовка front = rear как всегда означающего пустую очередь.
Вставка в связный список путем сдвига содержимого массива; должны меняться только указатели.
Рекурсивная функция без базового случая или у которой рекурсивный вызов не уменьшает размер задачи.
Отслеживание рекурсивного вызова, но забывание добавить ожидаемую работу на обратном пути вверх.
Ответ на вопрос «почему стек» — «потому что это быстро»; истинная причина заключается в порядке возврата элементов LIFO (последним вошедшим — первым вышедшим).
Interactive lessons on this topic · Интерактивные уроки по этой теме
Work through it step by step, with instant-check exercises. · Пройдите его шаг за шагом с упражнениями мгновенной проверки.
Pick one and the site follows you — notes, papers, videos and practice all open on it. · Выберите один, и сайт будет вести вас — конспекты, работы, видео и практика откроются там.
Type to search notes, lessons, code, vocabulary and past-paper questions across every subject. · Введите запрос для поиска заметок, уроков, кода, словаря и вопросов с реальных экзаменов по всем предметам.