Базовые навыки, сложность

Углубимся в язык с++ и посмотрим, что такое сложность алгоритма, а также порешаем задачки!

In [1]:
#include <iostream>

using namespace std;

cout << "Hello, world!" << endl;
Hello, world!

Условные операторы

Показать как работают условные операторы. Показать, что у нас они вычисляются только до того момента, пока не будет понятно значение выражения.

if (x) {
       //  Если x - истина
   } else {
      // Если x - ложь   
   }
In [2]:
{
    int x = (float)rand() / RAND_MAX*30;
    if (x > 25)
        cout << "Идём на пляж! Температура: " << x << endl;
    else
        cout << "Сидим дома! Температура: " << x << endl;
}
Идём на пляж! Температура: 27

Тернарный оператор: переменная = условия ? значение_если_истино : значение_если_ложь

In [3]:
{
    int x = (float)rand() / RAND_MAX*30;
    string s = x > 25 ? "Идём на пляж!" : "Сидим дома!"; 
    cout << s << " Температура: " << x << endl;
}
Сидим дома! Температура: 1

Логические операторы

Операторы сравнения:

Операция (выражение) Оператор Синтаксис выражения
Равенство == a == b
Неравенство != a != b
Больше > a > b
Меньше < a < b
Больше или равно >= a >= b
Меньше или равно <= a <= b

Логические операторы:

Операция (выражение) Оператор Синтаксис выражения
Логическое отрицание, НЕ ! !a
Логическое умножение, И && a && b
Логическое сложение, ИЛИ || a || b
In [4]:
{
    bool a = true, b = false;
    cout << ((a && b) ? "true" : "false") << '\n';
}
false
In [5]:
{
    bool a = true, b = false;
    cout << ((a || b) ? "true" : "false") << '\n';
}
true
In [6]:
{
    bool a = true, b = false;
    cout << ((!a) ? "true" : "false") << '\n';
}
false

Циклы

Циклы -- стандартная конструкция большинства языков программирования, которая позволяет выполнять операцию несколько раз, проходить по массивам элементов.

In [7]:
{
    for (int i = 0; i < 100; i++)
        cout << i << " ";
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 
In [8]:
{
    int j = 0;
    for (; j < 100; ) {
        cout << j << " ";
        j++;
    }
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 
In [9]:
{
    int i = 0;
    while (i < 100) {
        cout << i << " ";
        i++;
    }
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 
In [10]:
{
    int k = 0;
    do {
        cout << k << " ";
        k++;
    } while (k < 100);
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

Итерируемся через массивы объектов

In [11]:
{
    vector<int> a(10, 0);
    
    for (int i = 0; i < 10; i++) {
        a[i] = i;
    }
    
    for (auto a_elem : a) {
        cout << a_elem << " ";
    }    
}
0 1 2 3 4 5 6 7 8 9 

что такое auto???

auto -- ключевое слово, использующеся для вывода типа.

Например, у нас дан такой код:

double x = 4.0;

Если C++ и так знает, что 4.0 является литералом типа double, то зачем нам дополнительно указывать, что переменная x должна быть типа double?

Начиная с C++11, ключевое слово auto при инициализации переменной может использоваться вместо типа переменной, чтобы сообщить компилятору, что он должен присвоить тип переменной исходя из инициализируемого значения. Это называется выводом типа (или ещё «автоматическим определением типа данных компилятором»).

In [12]:
{
    #include <typeinfo> // библиотека, чтобы узнать тип переменной

    auto x = 4.0; // 4.0 - это литерал типа double, поэтому и x должен быть типа double
    auto y = 3 + 4; // выражение 3 + 4 обрабатывается как целочисленное, поэтому и переменная y должна быть типа int

    // проверим:
    cout << typeid(x).name() << endl; // d значит double
    cout << typeid(y).name() << endl; // i значит integer
}
d
i

Каким типом лучше определить итератор в "обычном" цикле?

size_t

size_t это базовый беззнаковый целочисленный тип языка Си/Си++. Является типом результата, возвращаемого оператором sizeof (возвращает размер типа переменной-аргумента в байтах).

Размер типа выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива любого типа. На 32-битной системе size_t будет занимать 32-бита, на 64-битной - 64-бита.

Другими словами в тип size_t может быть безопасно помещен указатель. Тип size_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики. В ряде случаев использование типа size_t безопаснее и эффективнее, чем использование более привычного программисту типа unsigned.

In [13]:
{
    int i = 0;
    cout << sizeof(i) << endl; // размер типа int - 4 байта
}
4
In [14]:
{
    int N = 10;
    int* a = new int[N];
    for(size_t n = 0; n < N; ++n) {
        a[n] = n;
        cout << a[n] << " ";
    }
}
0 1 2 3 4 5 6 7 8 9 

Векторы

Для хранения массивов данных в C++ есть стандартный контейнер vector. Он может хранить элементы только одного типа, к примеру vector<int>.

In [15]:
#include <vector>

using namespace std;
In [16]:
{
    // Вектор из 10 неопределённых элементов
    vector<int> v1(10);
    // Пустой вектор
    vector<int> v2;
    // Вектор из 10 нулей
    vector<int> v3(10, 0);
}
In [17]:
template<typename T>
void printVector(vector<T>& vec) {
    for (auto a: vec)
        cout << a << " ";
    cout << endl;
}
In [18]:
{
    vector<int> a(10, 10);
    printVector<int>(a);
}
10 10 10 10 10 10 10 10 10 10 
In [19]:
{
    vector<int> a(10);
    printVector<int>(a);
}
0 0 0 0 0 0 0 0 0 0 
In [20]:
{
    vector<int> a;
    printVector<int>(a);
}

In [21]:
{
    vector<int> a;
    a.push_back(10);
    printVector<int>(a);
    a.push_back(9);
    printVector<int>(a);
    
    cout << "Vector size: " << a.size();
}
10 
10 9 
Vector size: 2

По вектору можно пройти несколькими способами.

In [22]:
{
    vector<int> a;
    for (int i = 0; i < 10; i++)
        a.push_back(i);
    printVector<int>(a);
    
    for (size_t i = 0; i < a.size(); i++)
        cout << a[i] << " ";
    cout << endl;
    
    for (auto e: a) {
        cout << e << " ";
    }
    
}
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 

Сложность алгоритмов

Вычислительная сложность алгоритма это зависимость между вычислительными ресурсами, потребляемыми алгоритмом и размером входных данных. Вычислительные ресурсы включают время (измеряется в элементарных шагах алгоритма) и память (байты).

Для упрощения рассчётов временной сложности можно считать что за 1 сек вы успеваете совершить $4.5 ∗ 10^8$ простейших операций.

Сложность алгоритма - оценка затрат ресурсов алгоритмом в зависимости от входных данных.

  • По времени - как растёт время работы
  • По памяти - как растёт объём памяти

$O(f)$ - ($O$-нотация, $Big-O Notation$) - означает функцию порядка не более чем $f$. Если мы говорим, что у нас для алгоритма сложность $O(f)$, значит затраты на этот алгоритм в худшем случае растут как $f$.

Пусть $f(x)$ и $g(x)$ - бесконечно большие функции при $x \to a$.

$$ \lambda = \lim_{x \to a} \frac{f(x)}{g(x)} $$

Если $0 < |\lambda| < \infty$, то говорят, что $f(x)$ и $g(x)$ - бесконечно большие функции одного порядка. Т.е. $O(f) \sim O(g)$.

1) $О(1)$ - константная сложность

Пример: определение четности числа

In [23]:
{
    int a;
    cin >> a;
    if (a % 2 == 0)
        cout << "even";
    else
        cout << "odd";
}
7
odd

2) $O(n)$ - линейная сложность

Пример: вывод всех значений массива, поиск наибольшего и наименьшего элемента в неотсортирвоанном массиве

In [24]:
{
    int n = 10;
    // инициализируем и заполняем массив
    vector<int> a(10);
    for(int i = 0; i < n; i++) {
        a[i] = (float)rand() / RAND_MAX*30;
        cout << a[i] << " ";
    }
    
    // ищем максимум в неотсортированном массиве:
    int max = a[0];
    for(int i = 1; i < n; i++) {
        if (max < a[i]) 
            max = a [i] ;
    }
    cout << endl << "max element : " << max << endl;
}
19 16 1 0 3 11 16 7 21 2 
max element : 21

3) $O(\log n)$ - логарифмическая сложность

Пример: вывести все цифры положительного целого числа в обратном порядке

In [25]:
{
    int a = (float)rand() / RAND_MAX*100000;
    cout << a << endl;
    for(; a > 0; a = a / 10) {
        cout << a%10 << " ";
    }
}
64112
2 1 1 4 6 

4) $O(n*m)$

пример: поиск максимума или минимума в матрице $n$ на $m$

In [26]:
{
    // инициализируем и заполним матрицу
    int n = 8, m = 5;
    vector<vector<int>> matrix(n);
    for (int i = 0; i < matrix.size(); i++) { // заполняем строку
        for (int j = 0; j < m; j++) { 
            // заполняем значения в строке (столбцы)
            matrix[i].push_back((float)rand() / RAND_MAX*100); 
            cout << matrix[i][j] << "\t ";
        }
        cout << endl; // конец строки
    }

    // ищем минимум
    int min = 200;
    for (int i = 0; i < matrix.size(); i++) { 
        // в каждой строке...
        for (int j = 0; j < matrix[i].size(); j++) { 
            // ... просматриваем каждое значение
            if (matrix[i][j] < min) {
                min = matrix[i][j];
            }
        }
    }

    cout << endl << "min elem: " << min << endl;
}
76	 54	 60	 88	 68	 
72	 43	 75	 31	 10	 
88	 70	 85	 67	 12	 
56	 30	 73	 63	 66	 
25	 55	 16	 77	 2	 
31	 3	 79	 55	 29	 
4	 92	 43	 33	 14	 
39	 14	 38	 50	 95	 

min elem: 2

Ссылка на презентацию про сложности:

https://docs.google.com/presentation/d/1gRB3tWk-LlG69_8LR6T5_SW-msnM6LUtHXAMKmuK1bE/edit?usp=sharing

Сортировка пузырьком

данный алгоритм меняет местами два соседних элемента, если первый элемент массива больше второго. Так происходит до тех пор, пока алгоритм не обменяет местами все неотсортированные элементы.

for(int i = 0; i < n-1; i++) {
        for(int j = 0; j < n-i-1; j++) {
            //текущее значение больше следующего?
            if(arr[j]>arr[j+1]) {
                // меняем их местами
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }