Семинар 8. Параллельное программирование на MPI (часть 1)
Лекция 3: Бинарный поиск. Связные списки
1. Лекция 3
Бинарный поиск
Связные списки
Курносов Михаил Георгиевич
к.т.н. доцент Кафедры вычислительных систем
Сибирский государственный университет
телекоммуникаций и информатики
http://www.mkurnosov.net/teaching
2. Контроль
Свойства O, Θ, Ω
1.
2.
3.
4.
5.
O(3n3 + 1000n2) = …. ?
Θ(n2 + logn) = ….
O(nlogn + n2 ) = …. ?
O(2n + n6 – 1000n3) = …. ?
Ω(n6 + n!) = …. ?
Алгоритмы сортировки
1. Алгоритмы сортировки со сложностью O(nlogn)
в худшем случае?
2. Сортировка за линейное время в худшем случае?
3. Что такое сортировка “на месте” (in place)?
4. QuickSort vs. MergeSort
2
3. Задача поиска элемента по ключу
Имеется последовательность ключей
𝑎1 , 𝑎2 , … , 𝑎 𝑖 , … , 𝑎 𝑛
Требуется найти индекс (номер) ключа, который совпадает
с заданным ключом key
Пример
Дана последовательность из 10 ключей
Требуется найти элемент с ключом key = 181
Index
1
2
3
4
5
6
7
8
9
10
Key
178
150
190
177
155
181
179
167
204
175
Data
Решение – искомый элемент с индексом 6
3
4. Линейный поиск (Linear search)
function Search(v[1:n], n, value)
for i = 1 to n do
if v[i] = value then
return i
end if
end for
return -1
end function
TLinearSearch = O(n)
Просматриваем элементы с начиная с первого
и сравниваем ключи
В худшем случае искомый элемент находится в конце
массива или отсутствует
Количество операций в худшем случае (worst case)
T(n) = 2n = O(n)
4
5. Бинарный поиск (Binary Search)
Имеется упорядоченная последовательность ключей
𝑎1 ≤ 𝑎2 ≤ ⋯ ≤ 𝑎 𝑖 ≤ ⋯ ≤ 𝑎 𝑛
Требуется найти позицию элемента, ключ которого
совпадает с заданным ключом key
Бинарный поиск (Binary search)
1. Если центральный элемент равен искомому, конец
2. Если центральный меньше, делаем текущей правую половину массива
3. Если центральный больше, делаем текущей левую половину массива
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
3
5
7
9
13
23
25
29
31
33
37
41
42
46
49
50
52
67
73
81
94
Поиск key = 42
5
6. Бинарный поиск (Binary Search)
function BinarySearch(v[1:n], n, key)
l = 1
// Левая граница массива (low)
h = n
// Правая граница массива (high)
while l <= h do
mid = (l + h) / 2
// Возможно переполнение mid,
// Решение: mid = l + ((h - l) / 2)
if v[mid] = key then
return mid
else if key > v[mid] then
l = mid + 1
else
h = mid - 1
end if
end while
return -1
end function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
3
5
7
9
13
23
25
29
31
33
37
41
42
46
49
50
52
67
73
81
94
key = 100 (worst case)
6
7. Эффективность бинарного поиска
function BinarySearch(v[1:n], n, key)
l = 1
// Левая граница массива (low)
h = n
// Правая граница массива (high)
while l <= h do
mid = (l + h) / 2
// 2 операции
if v[mid] = key then // 2 оп.
return mid
else if key > v[mid] then // 2 оп.
l = mid + 1
// 1 оп.
else
h = mid - 1
end if
end while
Количество операций в худшем случае
return -1
end function
T(n) = 2 + kTwhile + 1 = 7k + 3
k – количество итераций цикла while
Twhile – количество операций в теле цикла while
Twhile = 2 + 2 + 2 + 1 = 7
7
8. Эффективность бинарного поиска
function BinarySearch(v[1:n], n, Количество k разбиений массива
key)
l = 1
// Левая граница массива (low)
h = n
// Правая граница массива длины n
Массив (high)
while l <= h do
Массив длины n / 2
mid = (l + h) / 2
// 2 операции
Массив длины n / 4
if v[mid] = key then // 2 оп.
...
return mid
k
else if key > v[mid] then Массивоп.
// 2 длины n / 2 = 1
𝒏
l = mid + 1
// 1 оп.
= 𝟏, 𝒏 = 𝟐 𝒌 ,
𝟐𝒌
else
h = mid - 1
𝐥𝐨𝐠 𝟐 𝒏 = 𝐥𝐨𝐠 𝟐 𝟐 𝒌
end if
end while
𝒌 = 𝐥𝐨𝐠 𝟐 𝒏
return -1
𝑻 𝒏 = 𝟕𝐥𝐨𝐠 𝟐 𝒏 + 𝟑 = 𝑶(𝐥𝐨𝐠 𝒏)
end function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
3
5
7
9
13
23
25
29
31
33
37
41
42
46
49
50
52
67
73
81
94
key = 100
(worst case)
8
9. Эффективность бинарного поиска
function BinarySearch(v[1:n], n, Количество k разбиений массива
key)
l = 1
// Левая граница массива (low)
h = n
// Правая граница массива длины n
Массив (high)
while l <= h do
Массив длины n / 2
mid = (l + h) / 2 неэффективно использует
операции
Бинарный поиск // 2Массив длины n / 4
if v[mid] = key then // 2 оп.
...
return кеш-память процессора –
mid
k
else if key > v[mid] then Массивоп.
// 2 длины n / 2 = 1
𝒏
l = mid + 1 к элементам 1 оп.
// массива
доступ
= 𝟏, 𝒏 = 𝟐 𝒌 ,
𝟐𝒌
else
h = mid - 1
непоследовательный (прыжки по массиву)
𝐥𝐨𝐠 𝟐 𝒏 = 𝐥𝐨𝐠 𝟐 𝟐 𝒌
end if
end while
𝒌 = 𝐥𝐨𝐠 𝟐 𝒏
return -1
𝑻 𝒏 = 𝟕𝐥𝐨𝐠 𝟐 𝒏 + 𝟑 = 𝑶(𝐥𝐨𝐠 𝒏)
end function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
3
5
7
9
13
23
25
29
31
33
37
41
42
46
49
50
52
67
73
81
94
key = 100
(worst case)
9
10. Galloping search (“Поиск от края”)
Задан отсортированный массив A[n]
Алгоритм Galloping проверяет ключи с индексами
1, 3, 7, 15, … , 2 𝑖 − 1, …
Проверка идет то тех пор, пока не будет найден элемент
𝐴 2 𝑖 − 1 > 𝑘𝑒𝑦
Далее выполняется бинарный поиск в интервале
2 𝑖−1 − 1, … , 2 𝑖 − 1
TGalloping(n) = O(logn)
Поиск key = 31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
3
5
7
9
13
23
25
29
31
33
37
41
42
46
49
50
52
67
73
81
94
Galloping search = One sided binary search, exponential search, doubling search
J. L. Bentley and A. C.-C. Yao. An almost optimal algorithm for unbounded searching //
Information processing letters, 5(3):82–87, 1976
10
11. Поиск в массиве
Задан неупорядоченный массив ключей
(новые элементы добавляться крайне редко)
Требуется периодически осуществлять поиск в массиве
Решение 1 – “в лоб” за O(n)
Каждый раз при поиске использовать линейный поиск за O(n)
Решение 2 – в среднем за O(logn)
1. Один раз отсортировать массив за O(nlogn) или за O(n + k)
2. Использовать экспоненциальный поиск (Galloping) – O(logn)
function Search(v[1:n], n, key)
if issorted = false then
Sort(v, n) // Устойчивая сортировка, вызывается редко
issorted = true
end if
return GallopSearch(v, n, key) // Эксп. поиск O(logn)
11
end function
12. Связные списки (Linked lists)
Связный список (Linked list) – динамическая структура
данных для хранения информации, в которой каждый
элемент хранит указатели на один или несколько других
элементов
Операция
Описание
Вычислительная
сложность
Сложность
по памяти
AddFront(L, x)
Добавляет элемент x
в начало списка L
O(1)
O(1)
AddEnd(L, x)
Добавляет элемент x
в конец списка L
O(n)
O(1)
Lookup(L, x)
Отыскивает элемент x
в списке L
O(n)
O(1)
Size(L)
Возвращает
количество элементов
в списке L
O(1) или O(n)
O(1)
12
13. Односвязный список (Singly linked list)
Размер списка заранее не известен –
элементы добавляются во время работы программы
(динамически)
Память под элементы выделяется динамически
(функции: malloc, calloc, free)
Head
Данные
Next
Данные
Next
Данные
Next
NULL
13
14. Односвязный список (Singly linked list)
#include <stdio.h>
#include <stdlib.h>
struct listnode {
char *data;
int value;
struct listnode *next;
};
/* Data */
/* Data */
/* Next node */
14
15. Создание элемента (выделение памяти)
struct listnode *list_createnode(char *data,
int value)
{
struct listnode *p;
p = malloc(sizeof(*p));
// Выделяем память
if (p != NULL) {
p->data = data;
p->value = value;
p->next = NULL;
}
Сложность создания элемента
return p;
}
TCreateNode = O(1)
15
16. Создание элемента (выделение памяти)
int main()
{
struct listnode *node;
/* Список из одного элемента */
node = list_createnode(“Ivanov Ivan”, 178);
return 0;
}
0x22340AFF
node =
0x22340AFF
data: “Ivanov Ivan”
value: 178
next: NULL
16
17. Добавление элемента в начало списка
head = 0xAF22
0xAF22
Data
Value
Next
Data
Value
NULL
Next
1. Создаем новый узел newnode в памяти
newnode = 0x12A2
head = 0xAF22
0x12A2
Data
Value
Next
NULL
0xAF22
Data
Value
Next
Data
Value
Next
NULL
17
18. Добавление элемента в начало списка
head = 0xAF22
0xAF22
Data
Value
Next
Data
Value
NULL
Next
2. Устанавливаем указатель next узла newnode на head
newnode = 0x12A2
head = 0xAF22
0x12A2
Data
Value
Next
0xAF22
0xAF22
Data
Value
Next
Data
Value
Next
NULL
18
19. Добавление элемента в начало списка
head = 0xAF22
Data
Value
Next
Data
Value
NULL
Next
3. Делаем головой списка узел newnode
newnode = 0x12A2
head = 0x12A2
0x12A2
Data
Value
Next
0xAF22
0xAF22
Data
Value
Next
Data
Value
Next
NULL
19
21. Добавление элемента в начало списка
int main()
{
struct listnode *head;
head = list_addfront(NULL, "Ivanov Ivan", 178);
head = list_addfront(head, "Petrov Petr", 182);
return 0;
}
head =
0x6600AA
0x6600AA
0x2233FF
data: “Petrov Petr”
value: 182
data: “Ivanov Ivan”
value: 178
next: 0x2233FF
next: NULL
21
22. Поиск элемента в списке (Lookup)
Начиная с головы списка просматриваем все узлы
и сравниваем ключи
В худшем случае требуется просмотреть все узлы,
это требует O(n) операций
head
data: “Mars”
value: 182
data: “Pluton”
value: 178
data: “Saturn”
value: 169
next: 0xdeadbeaf
next: 0x3434ff99
next: NULL
22
23. Поиск элемента в списке (Lookup)
struct listnode *list_lookup(
struct listnode *list,
char *data, int value)
{
for ( ; list != NULL; list = list->next) {
if (strcmp(list->data, data) == 0 &&
list->value == value)
{
return list;
}
}
return NULL; // Не нашли
TLookup = O(n)
}
23
24. Поиск элемента в списке (Lookup)
int main()
{
struct listnode *node, *p;
node = list_addfront(NULL, “Mars", 178);
node = list_addfront(node, “Pluton", 182);
node = list_addfront(node, “Saturn", 169);
p = list_lookup(node, “Pluton", 182);
if (p != NULL) {
printf("Data: %sn", p->data);
}
return 0;
}
24
25. Удаление элемента (Delete)
1. Находим элемент в списке (за время O(n))
2. Корректируем указатели (за O(1))
3. Удаляем элемент из памяти (за O(1))
head
Удаляемый элемент
data: “Mars”
value: 182
data: “Pluton”
value: 178
data: “Saturn”
value: 169
next: 0xdeadbeaf
next: 0x3434ff99
next: NULL
Три возможных ситуации:
удаляемый узел – в начале списка
удаляемый узел – внутренний узел (есть элементы слава и справа)
удаляемый узел – в конце списка
25
26. Удаление элемента (Delete)
struct listnode *list_delete(struct listnode *list,
char *data, int value)
{
struct listnode *p, *prev = NULL;
}
for (p = list; p != NULL; p = p->next) {
if (strcmp(p->data, data) == 0 && p->value == value) {
if (prev == NULL)
list = p->next;
// Удаляем голову
else
prev->next = p->next; // Есть элемент слева
free(p);
// Освобождаем память
return list;
// Указатель на новую голову
}
prev = p; // Запоминаем предыдущий элемент (левый)
}
return NULL;
// Не нашли
TDelete = O(n)
26
27. Удаление элемента
int main()
{
struct listnode *node, *p;
node = list_addfront(NULL, “Mars", 178);
node = list_addfront(node, “Pluton", 182);
node = list_addfront(node, “Saturn", 169);
p = list_delete(node, “Pluton", 182);
if (p != NULL) {
node = p; // Указатель на новую голову
printf(“Item deletedn");
}
return 0;
}
27
28. Домашнее чтение
Прочитать о реализации связных списков
в “практике программирования” [Kernighan2011, С. 61-66]
28