Классические алгоритмы сортировки и поиска на примере С++

Обычно, если необходимо найти элемент в контейнере или провести сортировку, есть смысл использовать уже имеющийся функционал стандартной библиотеки: функции find(), find_if() и sort() хорошо справляются со своей задачей.

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

Сортировка выбором. Квадратическая сложность

  1. Находим самый маленький элемент (если сортируем в порядке возрастания);
  2. Меняем его местами с первым элементом.
  3. Сдвигаем область поиска на 1 элемент вперед
  4. Повторяем шаги 1-2 для новой области.

Перед реализацией обратим внимание на несколько моментов: во-первых, сокращаем внешний цикл на 1 элемент. Т.к. предпоследний элемент будет проверен во вложенном цикле, то нет никакой необходимости проверять его еще раз.

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

Реализуем на С++ (будем честными, работа с «голыми» массивами больше подходит С, чем С++, но нас интересует рабочий пример, а не контейнеры):

template <typename T>
void sort(T *arr, int arr_size) //принимаем на вход массив элементов типа Т
{
   T min; //значение минимального элемента
   int min_index; //индекс минимального
   bool was_min = false;
     for (int i = 0; i < arr_size - 1; ++i)
      {
	min = arr[i];//предполагаем, что текущий элемент - минимальный
	min_index = i;
	was_min = false;
	for (int j = i+1; j < arr_size; ++j)
	{
	   if (arr[j] < min)
	   {
		min = arr[j];
		min_index = j;
		was_min = true;
	    }
	}
	if (was_min)
	{
	    T tmp = arr[i];
	    arr[i] = arr[min_index];
	    arr[min_index] = tmp;
	}
     }
}

Сортировка вставкой. Квадратичная сложности в худшем варианте, O(n)- в лучшем.

  1. Считаем, что первый элемент отсортирован.
  2. Для всех последующих элементов, проверяем идем вложенным циклом назад:
  3. Если предыдущий больше текущего, меняем их местами.
  4. Повторяем, пока не достигнем начала списка.
  5. Повторяем шаги 2-4 для следующего элемента.
template <typename T>
void sort (T arr[], int arr_size)
{
  for(int i = 1; i< arr_size; ++i)
  {
    for(int j = i; j>0; --j)
    {
      if(arr[j] < arr[j-1])
      {
        T tmp = arr[j];
        arr[j] = arr[j-1];
        arr[j-1] = tmp;
      }        
    }
  }
}

Сортировка «пузырьком». Сложность квадратическая для лучшего и худшего случая.

Пузырьковая сортировка по природе своей очень похожа на сортировку выбором. Но в том случае мы сортировали, «опуская» меньшие элементы в самое начало массива. Пузырьковая сортировка, наоборот, поднимает бОльшие элементы в конец, как бы на верх массива.

Но делает это немного по-другому:

  1. Берем первый элемент.
  2. Сравниваем со следующим.
  3. Если текущий больше следующего — меняем их местами.
  4. Повторяем пп. 1-3 до предпоследнего элемента массива.
  5. Повторяем пункты 1-4 для элемента «текущий +1».

Для экономии времени и ресурсов внутренний цикл можем сократить до arr_size 1-i. То есть, каждую итерацию мы будем доходить не до самого конца, а только до последнего не отсортированного элемента.

template <typename T>
void sort(T arr[], int arr_size)
{
  for (int i = 0; i < arr_size-1; ++i)
  {
    for(int j = 0; j <arr_size-1-i; ++j)
   {
     if(arr[j] > arr[j+1])
     {
      T tmp = arr[j];
      arr[j] = arr[j+1];
      arr[j+1] = tmp;
     }
   }
  }
}

Сортировка слиянием. Log n при худшем и лучшем варианте.

Сортировка слиянием по скорости является явным победителем из ранее представленных алгоритмов. Но подобная сортировка предполагает использование дополнительной памяти и постоянного копирования элементов, что может быть не выгодным при использовании пользовательских типов.

Итак, как это работает:

  1. Делим массив на две половины до тех пор, пока у нас не останется массив размером 1 или 2 элемента.
  2. Сортируем этот отрезок массива.
  3. Сохраняем отсортированный отрезок массив в новый массив.
  4. Повторяем для предыдущего отрезка.
  5. Проводим слияние двух отрезков.

Выглядит немного запутанно, не так ли?

Попробуем реализовать это в коде. Для начала создадим функцию, которая будет проводить слияние двух отрезков. Её задача получить на вход массив для сортировки, индекс первого элемента (начало первого отрезка), середину — она же начало второго отрезка, и конец второго отрезка.

Далее наша функция пошагово сравнивает элементы, выбирая на каждом шагу минимальный и записывая его в новый массив.

template <typename T>
void merge( T arr[],int first_index, int mid_index, int last_index)
{
 //если левая границы подмассивов накладываются друг на друга, выходим из функции
 if(first_index >= last_index || mid_index < first_index || mid_index > last_index) return;
 
 //если размер подмассива для обмена составляет два элемента и первый больше второго - меняем их местами
//и выходим из функции
 if(last_index == first_index +1)
{ 
  if(arr[first_index] > arr[last_index])
  {
   T tmp = arr[first_index];
   arr[first_index] = arr[last_index];
   arr[last_index] = tmp;
  }
  return;  
} 
 
//в противном случае нам нужно провести слияние двух частей массива:
// от arr[first_index] до arr[mid_index]
// и от arr[mid_index+1] до arr[last_index]
   
 //создаем временный массив-буфер
 // т.к. размер буфера будет зависеть от ходя программы, используем динамическое выделение памяти
// в противном случае можно создать буфер такого же размера, как и входящий массив (если его размер задан глобальной константой)
 T * tmp_arr = new T [last_index - first_index +1];

  //копируем в него все элементы соответствующих подмассивов
  for(int i = 0, tmp_first = first_index; tmp_first<= last_index;++i)
  {
   tmp_arr[i] = arr[tmp_first++];
  }

 //счетчики j и k - для отделения отрезков подмассивов в нашем новом буферном массива. Соответственно j - для левой половины, k - для правой
 for(int i = first_index, j = 0, k = mid_index-first_index+1;
     i <=last_index; ++i)
 {
   if(j > mid_index - first_index)
   {
     arr[i] = tmp_arr[k++];
   }
   else if (k > last_index - first_index)
   {
     arr[i] = tmp_arr[j++];
   }
   else
   {
    arr[i] = (tmp_arr[j] < tmp_arr[k]) ? tmp_arr[j++] : tmp_arr[k++];
   }
 }

delete [] tmp_arr;
}

Самое сложное позади. Итак еще раз, что только что произошло:

  1. Функция получила на вход массив, и фактически два его отрезка начало, середину и конец.
  2. Если рекурсивная функция, которая вызывает merge уже «переступила» границы подмассивов, мы просто выходим из функции.
  3. Если диапазон элементов в массиве, который мы получили, составляет всего два элемента — расставляем их в нужном порядке и выходим из функции.
  4. Создаем временный массив и копируем в него элементы в текущем, не отсортированном, порядке.
  5. Далее проверяем на граничные элементы. Если это первый или последний элементы отрезков — без зазрения совести добавляем их в начало или конец нашего изначального массива.
  6. Для всех остальных сравниваем элементы из двух отрезков и добавляем меньший из них обратно в наш массив.

Теперь нам нужна только рекурсивная функция сортировки, которая и будет проводить деление на отрезки и вызов функции merge.

Как для любой рекурсии важно определить условие выхода их функции. В нашем случаем мы постоянно разделяем массив на две части — правую и левую. Если в какой-то момент первый индекс левой стороны станет больше, чем первый индекс правой — это будет значить, что мы обошли массив полностью.

template <typename T>
void sort(T arr[], int first_index, int last_index)
{
  //условие выхода
  if(first_index >= last_index) return;
  
//делим массив пополам
int mid_index = first_index +(last_index - first_index) /2;

sort(arr, first_index, mid_index);
sort(arr, mid_index+1, last_index);
merge(arr, first_index, mid_index, last_index);
}

Теперь у нас есть отсортированный массив и самое время заняться бинарным поиском.

Бинарный поиск. Сложность log n в худшем случае, и O(n) — в лучшем.

Бинарный поиск — крайне быстрый способ поиска. Вот только работает он исключительно в отсортированном массиве (контейнере).

Бинарный поиск работает по принципу «разделяй и властвуй»:

  1. Взять массив и найти средний элемент (length /2).
  2. Проверить, является ли средний элемент искомым?
  3. Если нет, проводим еще две проверки.
  4. Средний элемент меньше искомого? Повторяем шаги 1-3 для элементов от средний+1 до length-1
  5. Средний элемент больше искомого? Повторяем шаги 1-3 для элементов от нуля до средний -1
  6. Если размер массива для поиска нулевой, значит элемент отсутствует в массиве.

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

Для упрощения будем возвращать номер элемента или arr_size +1, если элемент отсутствует в массиве.

template <typename T>
int find (T arr[], int arr_size, T elem)
{
  int mid_elem_index = arr_size/2;
  int first_elem_index = 0, last_elem_index = arr_size-1; 

  if(elem < arr[first_elem_index] || elem > arr[last_elem_index])
 {
  return arr_size+1;
 }  
 
  while (last_elem_index - first_elem_index >= 0)
{
  if(elem == arr[mid_elem_index])
  {
   return mid_elem_index;
  }
  if(elem > arr[mid_elem_index])
 {
   first_elem_index = mid_elem_index+1;
   mid_elem_index = (mid_elem_index+1 + last_elem_index)/2;
 }
if(elem < arr[mid_elem_index])
 { 
   last_elem_index = mid_elem_index-1;
   mid_elem_index = mid_elem_index /2;
 }
}

return arr_size+1;
}

Первое приложение с QT: процесс, ошибки, лайф-хаки

Итак, начинаем работу с QT. Здесь и ниже использованы видео-уроки ->  отсюда и личный опыт их применения. 

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

Сегодня мы создадим первое приложение с текстом, кнопкой и вторым окном, которое откроется по нажатию на кнопку.

Основные моменты для понимания приложений с GUI на Qt

Во-первых, чтобы создать с помощью Qt приложение с графическим интерфейсом нам нужно при создании проекта выбирать пункт Приложение Qt Widgets.

Это изображение имеет пустой атрибут alt; его имя файла - image-5.png
Создание приложения Qt с графическим интерфейсом

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

И последнее. Для работы с кодом нужно понять, как устроен этот фреймворк. Все объекты графического интерфейса — окна, кнопки, текстовые поля и т.д. — это классы. Исходя из этого мы и будем строить с ними свои взаимоотношения. У них, как у всех классов, есть h.- файлы с перечнем переменных и функционала, есть cpp-файлы с описанием этого функционала. Какие-то объекты являются самостоятельными, какие-то — часть других объектов. С этим разберемся позже. Сейчас важно понимать, что работать с ними мы будем как и привыкли в С++: создавая переменные и обращаясь к их функционалу из-под указателей или из-под объектов.

Например, мы можем создать текстовое поле. В Qt такой объект называется label.

QLabel *label = new QLabel ("Hello word!");
label-> resize(400);
label->show();

Как видите, мы динамически создали указатель на объект типа QLabel,и далее из-под этого указателя обратились к его методам — изменили размер и заставили вывести на экран. Аналогично происходит работа со всеми остальными объектами.

Создание первого приложения и первые ошибки

  1. Создаем новый проект Qt Widhets. Сделать это можно классическим Файл->Создать файл или проект…, через горячие клавиши Ctrl+N или нажатием на стартовой странице Qt кнопки +Новый проект .
  2. Теперь у нас имеется «заготовка» проекта. Её уже можно собрать и запустить, и мы увидим пустое окно со стандартными панелями состояния и меню. Для сборки и запуска нажимаем Ctrl+B, ждем, пока закончится сборка и запускаем проект с помощью Ctrl+R.
Это изображение имеет пустой атрибут alt; его имя файла - image-7.png
Запуск «заготовки» программы

Добавим свои элементы на это окошко. Для этого в редакторе нужно найти папку Формы и открыть файл mainwidow.ui.

Это изображение имеет пустой атрибут alt; его имя файла - image-9.png

Теперь мы попадаем на вкладку Дизайн, где собственно и будем проводить манипуляции с окном. Справа находятся все доступные элементы, слева — их свойства и параметры, а по центру, собственно, само пользовательское окно. Добавление элементов происходит банальным «перетаскиванием» нужного объекта на нашу форму. Для начала добавим текстовое поле (объект называется Label) и кнопку (объект Push Button).

Редактируем текст внутри объектов двойным щелчком левой кнопкой мышки на нем. По задумке далее мы создадим еще одно окно, поэтому на текстовом поле пишем «Открытие второго окна», а на кнопке «Открыть окно».

Это изображение имеет пустой атрибут alt; его имя файла - image-10.png
Пользовательское окно в режиме графического редактора

Теперь создадим еще одно окно, которое должно будет открыться по нажатию кнопки. Для этого возвращаемся в редактор, на нашем проекте нажимаем правой кнопкой мыши, выбираем пункт Добавить новый…  В новом окне нужно выбрать Qt и Класс формы Qt Designer

Это изображение имеет пустой атрибут alt; его имя файла - image-11.png

Далее нам дадут выбор вида окна: с кнопками, без кнопок, виджет. Рядом будет предпросмотр, так что вы сможете сразу посмотреть, какое окно хотели бы создать в итоге. Мы выберем окно без кнопок Dialog without Buttons, т.к. сейчас задача просто открыть второе окошко.

Задаем окну осознанное имя (я назову его SecondWindow) и у нас теперь имеется новое пустое окно в режиме редактора. Добавим сюда текстовое поле с классическим «Hello word!» и перейдем к коду.

Это изображение имеет пустой атрибут alt; его имя файла - image-14.png
Второе окно в режиме редактора

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

Возвращаемся в редактор и снова выбираем файл mainwindow.ui. Для задания действий правой кнопкой мыши нажимаем на нашей кнопке (уж простите за тавтологию) и выбираем пункт «Перейти к слоту». Мы увидим окно выбора действия. Т.к. действие должно происходить по нажатию, мы выбираем пункт clicked(). И попадаем в файл mainwindow.cpp, где для нас уже создана функция on_pushButton_clicked().

Это изображение имеет пустой атрибут alt; его имя файла - image-15.png

Также автоматически были созданы файлы для второго окна. У меня они называются secondwindow.h,secondwindow. cpp  и secondwindow.ui. Для работы со вторым окном нам понадобится переменная типа «второе окно». Создадим же ее и добавим немного действий:

void MainWindow::on_pushButton_clicked()
{
    SecondWindow wind;
    wind.setModal(true);
    wind.exec();
}

Обратите внимание! Для использования второго окна, его необходимо «подключить» к основному. Используем директиву #include «secondwindow.h» в файле mainwindow.cpp

Выше мы создали переменную для второго окна и из-под нее уже вызвали некоторый функционал: сделали окно модальным (второстепенным), и запустили его.

Возможные проблемы

В ходе написания кода или сборки может оказаться, что Qt все равно считает SecondWindow неизвестным форматом. Или при сборке мы встречаем ошибки Error C1083 и LNK2019: Unresolved External Symbol. Не спешите переходить в документацию и искать, что вы сделали не так.

 
В первую очередь проверьте отсутствие опечаток в именах (например,
SecondWidow вместо SecondWindow). Если уверены, что сделали всё
правильно, пересоберите проект насильно: Сборка -> Очистить проект ->
Запустить qmake.

Теперь мы можем запустить наш проект через Ctrl+R и получить окошко с кнопкой, по нажатию которой наше приложение наконец-то поздоровается с миром:

Это изображение имеет пустой атрибут alt; его имя файла - image-12.png
Это изображение имеет пустой атрибут alt; его имя файла - image-13.png

На этом пока все. Более подробное видео по ссылке в начале поста, а примеры кода можно посмотреть здесь.

Qt — первые шаги с нуля

На данный момент Qt — очень востребованный, многофункциональный фреймворк, который имеет невероятно обширный функционал для создания мощных кросплатформенных приложений.

В общем, первый камень преткновения для всех, кто начинает знакомиться с Qt — его установка и настройка. Как ни странно, этот процесс не ограничивается привычными «Далее-далее-готово».

Вы можете найти в интернете много мануалов и рекомендаций, но все равно столкнуться с дополнительными сложностями.

Оговорюсь: все публикации будут касаться работы с Qt под Windows.

Для меня установка и настройка выглядели следующим образом:

  1. Скачиваем Qt online installer с официального сайта https://www.qt.io/ . Собственно, на сайте автоматически определяется ваша операционная система и рекомендуются определенные способы установки. Если не планируем писать ничего секретного, можем выбирать Open source версию и не заморачиваться временно-бесплатной версией.
  2. В процессе установки действуем по привычном плану — далее, далее, готово. Учтите, сам по себе Qt с компиляторами и прочим необходимым весит не мало. Но must have галочка на последней (или нужной вам) версии Qt и на подпункте Tools. Все нужные галочки и скрины можно посмотреть здесь, но все равно скопипастим для себя скрины:
Обязательные отметки для установки

Продолжаем.

На вышеупомянутом cppstudio мануал по установке заканчивается после нажатия кнопки Готово и уже через пару несложных манипуляций с кодом мы получаем первое приложение на Qt.

На деле же многие (и я, естественно, стала одной из них) сталкиваются с проблемами. Большинство из них — проблемы линковщика.

Первая проблема, с которой столкнулась я, звучала так:

error LNK1158: не удается запустить «rc.exe»

Несложный поиск в гугле выдал несколько советов. В первую очередь, заходим к самому авторитетному в этом плане источнику — документации. Что мы видим?

Ок, значит нам нужно добавить пусть в переменную среды PATH. Возьмем описание отсюда и применим на практике:

Сделайте это добавлением адреса расположение папки /bin вашего QT (например, c:\Qt\4.4.3\bin) к переменной PATH.

Для более новых версий Windows, PATH может быть расширен через меню Control Panel|System|Advanced|Environment variables.

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

Ок, кажется проблема решена. Пробуем запустить первую программу и (барабанная дробь): снова линкер не находит тот самый rc.exe.

Одна из самых популярных рекомендаций на форумах по этой проблеме — переустановите IDE. Не спорю, отличный совет. Если не брать во внимание, что наш Qt еще девственно-чист и только-только установлен.

Значит, нужно ему помочь другим способом. Лично мне помогла вот эта публикация . Первую часть статьи можно спокойно пропускать, если вы ставите или уже установили версию Qt 5 и выше.

Итак, создаем свой профиль Qt с нужными настройками.

  1. Заходим в меню Инструменты -> Параметры…
  2. В открывшемся окне выбираем подпункт Комплекты и находим там вкладку Профили Qt.

Наша задача — создать свой «особый» профиль с нужными настройками. Нажимаем кнопку «Добавить», задаем имя профиля (на свое усмотрение, но лучше сохранить номер версии) и указываем расположение файла qmake.exe.  Он находится в подкаталоге /bin каталога, куда был установлен Qt. Я устанавливала для себя несколько вариантов, поэтому в этот конкретный профиль выбрала одну из папок с версией winrt_x86_msvc2017.

ВАЖНО! Необходимо выбрать непосредственно exe-файл, а не просто папку расположения.

Нажимаем кнопку «Подробнее» и сохраняем для себя ABI, которое использует эта сборка. Нам нужно будет повторить его на следующей вкладке.

АBI, используемое текущей сборкой

Далее нам нужно выбрать компилятор. Переходим на соседнюю вкладку «Компиляторы«. По аналогии нажимаем кнопку «Добавить», вводим название компилятора и указываем место расположение файла g++.

ВНИМАНИЕ! Необходимо выбрать непосредственно файл g++.exe (именно G, не С).

Проверяем нижнюю строчку текущего окна. Если ABI в этом окне не совпадает с тем, что мы видели на вкладке «Профили Qt», создаем его вручную: Особое -> и нужные параметры по каждому пункту.

Поздравляю, мы на финишной! Теперь вы создаете проект любым удобным способом (Файл->Создать файл или проект… или через кнопку на панели Новый проект). Далее перед сборкой проекта заходите в меню Проекты и нажимаете кнопку Управление:

В открывшемся окне выбираем наш новосозданный профиль Qt в подпункте Особые и нажимаем «Применить».

Пробуем собрать наш проект. Вуа-ля: мой собрался! Надеюсь, вас постигнет та же участь и вы увидите вожделенное диалоговое окно или консоль.

Итак, итоги. Для установки и настройки необходимо:

  • скачать и «стандартно» установить Qt;
  • прописать путь в переменную среды Path;
  • сформировать свой профиль Qt непосредственно в свойствах фреймворка.
  • Выбирать этот профиль при сознании новых проектов.

В статье также использованы материалы Википедии.

Let’s talk

В современном мире для того, чтобы оставаться «на волне» каждому человеку приходиться учиться. Благо, интернет дает много информации — бери да учись.

И в процессе изучения чего-то нового мы перелопачиваем эти горы информации, переходим по ссылкам, которые потом не сможем найти, находим ответы на текущий вопрос и уже через месяц не можем вспомнить, как же его решили.

Для решения этой проблемы у меня имеется блокнотик. Точнее, несколько. Плюс миллион тематических папок в закладках. Плюс папки на личном компьютере с нужной информацией. Плюс несколько документов в Google-doc для систематизации.

Пришло время свести весь прошлый и будущий опыт в единое место. С полезными ссылками, с проблемами, которые не стоили выеденного яйца, но забрали драгоценное время.

Таким местом и станет этот блог. Для начала будем осваивать Qt по урокам Гоши Дударя и параллельно совершенствуем базовые знания С++ по переводам уроков от Юрия Ворона.