6.  Сортиране. Търсене. Хеширане

Сортиране - общи положения; сортиране чрез сравнение

** Класификации на алгоритмите за сортиране [Глава 3, стр.187]

* В зависимост от местонахождението на данните:
 - вътрешно (директен достъп), например бързо сортиране и
 - външно (последователен достъп), например сливане.
* В зависимост от операцията:
 - чрез сравнение (<, > и ==) на двойки елементи и
 - чрез трансформация, напр. сортиране чрез броене.
* Свойство на алгоритъма за сортиране:
 - устойчиви - относителният ред на елементите с равни ключове остава непроменен и
 - неустойчиви - разместване на елементи с равни ключове (сортиране с 2 ключа)
* Ефективност на алгоритмите за сортиране - брой на извършени сравнения и размени (присвоявания).
* Използване на допълнителна памет.

** Дърво на сравненията; сортиране на 3 числа.

** Класически универсални "елементарни" методи за сортиране чрез сравнение O(n2):
 - пряко вмъкване - намираме елемент, който "не е сортиран" и го поставяме на мястото му в сортираната част;
 - пряка селекция (избор) - намираме най-малкия елемент и го поставяме на мястото му в окончателно сортираната част;
 - мехурчето - последователно се разглеждат двойки елементи и евентуално се разменят.

** Бързо сортиране на Хоор - O(n log2n) средно и O(n2) в най-лошия случай.

** Пирамидално сортиране, сортиране чрез сливане: O(n log2n) и тази оценка не може да се подобри при сортиране чрез сравняване.

** Сортиране чрез трансформация [3.2]

** Сортиране чрез множество [3.2.1]
    Дадено е множество M от числа в затворения интервал [a, b] и инективна функция за нареждане f: M -> [a, b], т.е. ако x1 и x2 са различни, то са различни и f (x1) и f (x2).
    Построяваме нулев масив S с индекси от a до b и с едно минаване през множеството M поставяме стойности 1 на S[f (x)] за всяко x от M.  След това минаваме през масива S за да подредим елементите на M.
    
Сложност O(m+n), където n е броят на елементите на M, а m = b - a + 1.
** Сортиране чрез броене [3.2.2]
** Побитово сортиране [3.2.3]
** Метод на бройните системи [3.2.4]
** Сортиране чрез пермутация [3.2.5]

   Дадено е множество M от n елементи. Означаваме с S множеството {1, 2, 3, ..., n}.  Функцията за нареждане е f: S -> S е сюрективна, т.е. ако x1 и x2 са различни, то са различни и f (x1) и f (x2) и за всяко y от S съществува x от S такова, че y = f (x).
    Разменяме m[1] с m[m[1]] докато на 1-во място не дойде 1. После по същия начин с втория елемент и т.н.
позиции 1234567
        4375612
        5374612
        6374512
        1374562
        1734562
        1234567
Броят на размените не недвишава n, а броят на сравненията - 2n.

Бързо сортиране

 А. Разделяне на дялове:
 1. Избираме случаен елемент
x от масива a
 2. Преглеждаме масива отляво (от началото), докато достигнем до елемент
> x
 3. Преглеждаме масива отдясно (от края), докато достигнем до елемент
< x
 4. Разменяме местата на двата елемента
vector<int> a(n);
void partition(int x)
{
 int i=1, j=n;
 do
 {
  while (a[i] < x) i++;
  while (a[j] > x) j--;
  if (i<=j) { swap(a[i], a[j]);  i++; j--; }
 }
 while (i<=j);
}

Б. Сортиране - след като масивът се раздели, двата му дяла се подлагат на същата обработка и това продължава, докато се получат дялове само с по един елемент.

// qsort.cpp
void quicksort(int left, int right)

{
 int i=left, j=right;
 int x=a[(i + j)/2];
 do
 {
  while (a[i] < x) i++;
  while (a[j] > x) j--;
  if (i<=j)
  { swap(a[i], a[j]); i++; j--; }
 }
 while (i<=j);
 if (left<j) quicksort(left, j);
 if (i<right) quicksort(i, right);
}

qsort от STL

Сортиране чрез сливане

mergesort.cpp


Последователно търсене [4, стр. 231]

** Човешката дейност търсене

** Опростен модел на търсене

** Фундаментални операции над елемнтите на множество (правилна оценка на ефективността на алгоритмите за търсене)
-- инициализиране
-- търсене
-- вмъкване
-- изтриване
-- обединяване на множества
--  сортиране.

** Ключ, повтарящи се ключове
-- има ли елемент с даден ключ? [bool exists(unsigned key)]
-- индекс на елемент (обект) с даден ключ? [unsigned find_one(unsigned key)]
-- брой елементи с даден ключ? [unsigned count(unsigned key)]
-- индекси на всички елементи (обекти) с даден ключ? [vector<unsigned> find_all(unsigned key)]

** Последователно (линейно) търсене
Проверяваме последователно елементите на множеството (което е линейно наредено), докато или намерим търсения елемент или стигнем до края на редицата.
Ефективност на алгоритъма за линейно търсене:
Броят на обръщенията към елементите на масива зависи от търсеното число, но в най-лошия случай, когато числото не се среща в масива, е равен на дължината на масива. Следователно сложността на алгоритъма е O(n).
// lsearch.cpp

int linear_search(vector<int> v, int a)
{
   int i;
   for (i = 0; i < v.size(); i++)
       if (v[i] == a) return i;
   return -1;
}

** Последователно търсене в сортиран списък с поддържане на сортирания списък при вмъкване на нов елемент.

** Последователно търсене с преподреждане

Двоично търсене (разделай и владей) [4.3, стр. 239]

// bsearch.cpp

int binary_search(vector<int> v, int from, int to, int a)
{
   if (from > to)  return -1;
   int mid = (from + to) / 2;
   int diff = v[mid] - a;
   if (diff == 0) return mid;          /* v[mid] == a */
   else if (diff < 0)                  /* v[mid] < a */
      return binary_search(v, mid + 1, to, a);
   else
      return binary_search(v, from, mid - 1, a);
}

Търсене и сортиране на реални данни
// esearch.cpp

int binary_search(vector<Employee> v, int from, int to, string n)
{
   if (from > to) return -1;
   int mid = (from + to) / 2;
   if (v[mid].get_name() == n)  return mid;
   else if (v[mid].get_name() < n)
      return binary_search(v, mid + 1, to, n);
   else
      return binary_search(v, from, mid - 1, n);
}

Хеширане

** Структура от данни речник (dictionary): ключ-елемент

** Търсене в речник

** Хеш-функция h: key -> [0, N-1], пример
h(x) = x mod N