- Linear search
- Binary search
- Lower bound
- Upper bound
- Binary search по отговора
- Ternary search
- Jump search
- работи за сортирани и несортирани данни
- O(N) сложност по време
- подходящ за малък брой заявки при несортирани данни
Всеки елемент от масива се сравнява последователно с търсения.
C++
int linear_search(const vector<int>& arr, int x) {
for (int i = 0; i < arr.size(); i++) {
if (arr[i] == x) {
return i;
}
}
return -1;
}Python
def linear_search(arr, X):
for i in range(len(arr)):
if arr[i] == X:
return i
return -1
arr = [9, 4, 3, 2, 6, 7, 1, 8, 5]
X = 6
print(linear_search(arr, X)) # 4- работи само за сортирани данни
- O(logN) сложност по време
- подходящ за много наброй заявки
- нуждае се от броя на данните (N)
Сравнява се средния елемент с търсения. Ако търсеният елемент е по-малък, разглежда се масива отляво на средния елемент. Ако търсеният е по-голям, разглежда се масива отдясно на средния. Стъпките се повтарят до намиране на елемента.
Въпрос: Колко заявки трябва да имам, че да си заслужава да сортирам масивът и после да използвам binary search вместо вместо linear?
Искаме да търсим многократно в несортиран масив от n елемента:
- Всяко query с linear search = O(n)
- Ако сортираме веднъж (O(n log n)), после всяко търсене е binary search: O(log n)
- Ако НЕ сортираш, всяка заявка:
- O(n)
- Общо време: T₁ = k * n
- Ако сортираш веднъж, после binary search:
- O(n log n) (сортиране) + O(log n) (всяко търсене)
- Общо време: T₂ = n log n + k * log n
Искаме T₂ < T₁: т.е. n log n + k log n < k n
Определяме при кое k това е изпълнено:
n * log n + k * log n < k * n
n * log n < k * (n - log n)
k > n * log n / (n - log n)
// За голямо `n`, може да пренебрегнем `log n` спрямо `n`:
// следователно
k > log n
- Ако търсиш броя на запитвания
k > log n, си струва да сортираш масива и да използваш двоично търсене. - Ако са по-малко от log n, линейното търсене често е по-изгодно.
C++
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left <= right) {
int mid = left + (right - left) / 2; // !!!
if(nums[mid] == target)
return mid;
else if(nums[mid] > target)
right = mid - 1;
else
left = mid + 1;
}
return -1;
}Python
def binary_search(arr, X):
left = 0
right = len(arr) - 1
Xi = 0
while left <= right:
mid = left + (right - left) // 2
if X <= arr[mid]:
right = mid - 1
Xi = mid
else:
left = mid + 1
return Xi
arr = [10, 20, 30, 40, 50, 60, 70, 80, 90]
X = 60
Xi = binary_search(arr, X)
print(Xi) # 5На C++, при пресмятане на средния индекс трябва да се внимава за overflow, ако бъде пресмятан като (left + right) / 2.
- O(logN) сложност по памет на рекурсивната имплементация
C++
int binarySearchRecursive(const vector<int> &arr, int l, int r, int x) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] > x)
return binarySearchRecursive(arr, l, mid - 1, x);
return binarySearchRecursive(arr, mid + 1, r, x);
}
return -1;
}Python
def binary_search_recursive(arr, X, left, right):
if left <= right:
mid = left + (right - left) // 2
if X == arr[mid]:
return mid
elif X < arr[mid]:
return binary_search_recursive(arr, X, left, mid - 1)
return binary_search_recursive(arr, X, mid + 1, right)
return -1- връща първият елемент в масива не по-малък от x.
C++
int lowerBound(const std::vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left; // If left == nums.size(), target is greater than all elements
}Python
Аналогично поведение като bisect.bisect_left()
def binary_search_lower_bound(arr, X):
left = 0
right = len(arr) - 1
Xi = len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if X <= arr[mid]:
right = mid - 1
Xi = mid
else:
left = mid + 1
return Xi- връща първият елемент в масива, който е по-голям от x.
C++
int upperBound(const std::vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}Python
Аналогично поведение като bisect.bisect_right()
def binary_search_upper_bound(arr, X):
left = 0
right = len(arr) - 1
Xi = len(arr)
while left <= right:
mid = left + (right - left) // 2
if X < arr[mid]:
right = mid - 1
Xi = mid
else:
left = mid + 1
return XiВ следния snippet са използвани имплементации от стандартната библиотека на binary_search, lower_bound, upper_bound.
Изпробвайте следния code snippet с target - 0, 5, 6, 11. При target 11 връща v.end() на C++, а на Python - len(v).
C++
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = { 1, 2, 3, 4, 6, 7, 8, 9 };
int target = 0;
// https://en.cppreference.com/w/cpp/algorithm/binary_search.html
std::cout << (std::binary_search(v.begin(), v.end(), target)) << std::endl;
// https://en.cppreference.com/w/cpp/algorithm/lower_bound.html
auto lbIter = std::lower_bound(v.begin(), v.end(), target);
if (lbIter != v.end()) {
std::cout << *lbIter << std::endl;
}
// https://en.cppreference.com/w/cpp/algorithm/upper_bound.html
auto ubIter = std::upper_bound(v.begin(), v.end(), target);
if (ubIter != v.end()) {
std::cout << *ubIter << std::endl;
}
}Python
import bisect
v = [1, 2, 3, 4, 6, 7, 8, 9]
targets = [0, 5, 6, 11]
for target in targets:
print(bisect.bisect_left(v, target))
print(bisect.bisect_right(v, target))
print()Намерете броя срещания на едно число в сортиран масив
Binary Search on Answer е техника за решаване на задачи, в които търсим оптимална стойност (най-малка или най-голяма), която отговаря на дадено условие. Тя се използва, когато можем да проверим дали дадена стойност е валидно решение и отговорът се намира в определен диапазон. В този случай, вместо да търсим конкретен елемент, търсим минимална или максимална граница на възможното решение.
Критерии дали мога да си реша задачата с тази техника?
- Можем да проверим дали дадена стойност е решение на задачата
- Възможните стойности са ограничени отгоре и отдолу
Sqrt(X) и Puncakes
- работи за сортирани и параболични данни
- O(log3N) сложност по време
- подходящ за намиране на единствен максимум/ минимум на функция
Сравняват се двата средни елементи с търсения. Ако търсеният елемент е по-малък от първия среден, разглежда се масива отляво на първия среден елемент. Ако търсеният е между двата средни, разглежда се втората третина от масива. Ако търсеният е по-голям от втория среден елемент - разглежда се масива отдясно на втория среден елемент. Отново стъпките се повтарят до намиране на елемента.
C++
int ternarySearch(const vector<int> &arr, int l, int r, int x) {
while (r >= l) {
int mid1 = l + (r - l) / 3;
int mid2 = r - (r - l) / 3;
if (arr[mid1] == x)
return mid1;
if (arr[mid2] == x)
return mid2;
if (x < arr[mid1]) {
r = mid1 - 1;
}
else if (x > arr[mid2]) {
l = mid2 + 1;
}
else {
l = mid1 + 1;
r = mid2 - 1;
}
}
return -1;
}Python
def ternarySearch(arr, key, l, r):
if r >= l:
mid1 = l + (r - l) //3
mid2 = r - (r - l) //3
if arr[mid1] == key:
return mid1
if arr[mid2] == key:
return mid2
if key < arr[mid1]:
return ternarySearch(arr, key, l, mid1 - 1)
elif key > arr[mid2]:
return ternarySearch(arr, key, mid2 + 1, r)
else:
return ternarySearch(arr, key, mid1 + 1, mid2 - 1)
return -1
arr = [10, 20, 30, 40, 50, 60, 70, 80, 90]
X = 60
Xi = ternarySearch(arr, X, 0, len(arr) - 1)
print(Xi) # 5Въпрос: Кой алгоритъм е по-оптимален в общия случай BinarySearch или TernarySearch?
Отговор: BinarySearch, понеже прави по-малко сравнения.
https://www.geeksforgeeks.org/binary-search-preferred-ternary-search/
Бонус: Защо Merge sort дели масива на две части? Защо не на повече?
- работи само за сортирани данни
- оптималният скок е sqrt(N) (за минимален общ брой сравнения)
- O(sqrt(N)) сложност по време
- по-бавен от Binary search (O(log(N)) < O(sqrt(N)) < O(N))
- подходящ когато стъпките назад в масив ни струват скъпо
Търсеният елемент се сравнява последователно с елементи на позиции, кратни на стъпката (корен от N). Когато търсеният елемент стане по-малък от елемента на K-та стъпка, се извършва линейно търсене в интервала (K-1)-ва стъпка до K-та стъпка.
Jump search прави 1 връщане назад в масива, докато при Binary search има най-много logN връщания. При нужда от ограничаване на връщанията Jump search е предпочитан пред Binary search въпреки по-голямата сложност по време. Точно поради тази причина не се извършва двоично търсене в намерения интервал.
C++
int jump_search(const vector<int> &arr, int x) {
int len = arr.size();
int jump = sqrt(len);
int start = 0, end = start + jump;
while (end < len && arr[end] <= x) {
start = end;
end += jump;
if (end > len - 1)
end = len;
}
// we don't do binary here because it requires back steps
for (int i = start; i < end; i++) {
if (arr[i] == x)
return i;
}
return -1;
}Python
import math
def jumpSearch(arr, X):
N = len(arr)
block_size = int(math.sqrt(N))
step = block_size
prev = 0
while arr[min(step, N) - 1] < X:
prev = step
step += block_size
if prev >= N:
return -1
while arr[prev] < X:
prev += 1
if prev == min(step, N):
return -1
if arr[prev] == X:
return prev
return -1
arr = [10, 20, 30, 40, 50, 60, 70, 80, 90]
X = 50
Xi = jumpSearch(arr, X)
print(Xi) # 4Математиката зад оптималния скок
- Масивът има
nелемента. - Размерът на скока е
m.
- Скокове: Минаваме масива през всеки
m-ти елемент: индекси 0, m, 2m, 3m, ...- Брой скокове:
n / m
- Брой скокове:
- Линейно търсене: След последния скок — търсим линейно в блока (до
mелемента).
T(m) = брой скокове + брой проверки в блока
T(m) = n/m + m
Търсим минимум на T(m) спрямо m:
-
Вземаме производната и я приравняваме на 0:
dT/dm = -n/m^2 + 1 = 0 -
Решаваме за
m:n/m^2 = 1 m^2 = n m = √n
Заместваме m = √n във формулата:
T(√n) = n/√n + √n = √n + √n = 2√n
Jump Search е най-ефикасен при скок m = √n и има сложност O(√n).
- Puncakes
- Perfect Printer
- Peak Index in a Mountain Array
- Search a 2D Matrix
- 3 решения
- Find Minimum in rotated sorted array
...
- работи само за сортирани данни
- O(logi) сложност по време, където i е позицията на числото
- подходящ при търсене на елементи, намиращи се в началото на масива (i < N)
- подходяш, когато не знаем големината на входните данни, в които търсим (например поток)
Търсеният елемент се сравнява последователно с елементи на позиции, степени на двойката (2K). Когато търсеният елемент стане по-малък от елемента на К-та степен, се извършва двоично търсене в интервала 2K-1-ва позиция до 2K-та позиция.
C++
int exponentialSearch(const vector<int>& arr, int X) {
int N = arr.size();
if (arr[0] == X)
return 0;
int i = 1;
while (i < N && arr[i] <= X)
i *= 2;
int left = i / 2;
int right = min(i, N - 1);
return binarySearch(arr, left, right, X);
}Python
def exponential_search(arr, X):
N = len(arr)
i = 1
while i < N and arr[i] <= X:
i = i * 2
left = i // 2
right = min(i, N-1)
return binary_search(arr, X, left, right)
arr = [10, 20, 30, 40, 50, 60, 70, 80, 90]
X = 60
Xi = exponential_search(arr, X)
print(Xi) # 5- Eytzinger binary search - https://algorithmica.org/en/eytzinger
- Как може да бъде оптимизиран binary search при много заявки в големи масиви?
- Branch predictor - https://en.wikipedia.org/wiki/Branch_predictor