[Structure de données et implémentation de l'algorithme C++] 2. Recherche binaire et récursivité simple

La vidéo originale est l'enseignement de la station B de Zuo Chengyun



1 dichotomie

La recherche binaire est un algorithme de recherche permettant de trouver un élément spécifique dans un tableau ordonné . Son idée de base est de diviser le tableau à partir du milieu, puis d'évaluer la relation de taille entre l'élément cible et l'élément du milieu pour déterminer si l'élément cible se trouve dans la moitié gauche ou dans la moitié droite. Continuez ensuite à effectuer la même opération dans le sous-réseau correspondant jusqu'à ce que l'élément cible soit trouvé ou qu'il soit déterminé que l'élément cible n'existe pas.

Les étapes spécifiques sont les suivantes :

  • Définit la limite gauche du tableau à gauche et la limite droite à droite.
  • Calculez la position médiane au milieu, c'est-à-dire au milieu = (gauche + droite) / 2.
  • Comparez la relation de taille entre l'élément cible et l'élément intermédiaire :
  • Si l'élément cible est égal à l'élément du milieu, l'élément cible est trouvé et son index est renvoyé.
  • Si l'élément cible est plus petit que l'élément du milieu, mettez à jour la limite droite droite = milieu - 1 et continuez la recherche binaire sur la moitié gauche.
  • Si l'élément cible est plus grand que l'élément du milieu, mettez à jour la limite gauche left = mid + 1 et continuez la recherche binaire sur la moitié droite.
  • Répétez les étapes 2 et 3 jusqu'à ce que l'élément cible soit trouvé ou que la limite gauche soit plus grande que la limite droite.

Complexité temporelle O(logN) , où n est la longueur du tableau. Étant donné que la plage de recherche est réduite de moitié à chaque fois, l'algorithme est très efficace. Mais le tableau doit être ordonné, sinon la dichotomie ne peut pas être appliquée pour la recherche.

1.1 有序Trouver un élément spécifique dans un tableau

L'idée de base est de réduire de moitié la plage de recherche en comparant la relation de taille entre l'élément intermédiaire et l'élément cible jusqu'à ce que l'élément cible soit trouvé ou que la plage de recherche soit vide.

Comme, par exemple, le nombre de tableaux est N=16, le pire cas est divisé en 4 fois ( [ 8 ∣ 8 ] → [ 4 ∣ 4 ] → [ 2 ∣ 2 ] → [ 1 ∣ 1 ] ) ( [8 |8] \à [4|4] \à [2|2] \à [1|1] )([ 8∣8 ][ 4∣4 ][ 2∣2 ][ 1∣1 ]) , et4 = log 2 16 4 = log_2164=l o g216 . Autrement dit, la complexité temporelle estO (log N) O(logN)O ( log N ) _ _

/* 注意:题目保证数组不为空,且 n 大于等于 1 ,以下问题默认相同 */
int binarySearch(std::vector<int>& arr, int value)
{
    
    
    int left = 0;
    int right = arr.size() - 1;
    // 如果这里是 int right = arr.size() 的话,那么下面有两处地方需要修改,以保证一一对应:
    // 1、下面循环的条件则是 while(left < right)
    // 2、循环内当 array[middle] > value 的时候,right = middle

    while (left <= right)
    {
    
    
        int middle = left + ((right - left) >> 1);  // 不用right+left,避免int溢出,且更快
        if (array[middle] > value)
            right = middle - 1;
        else if (array[middle] < value)
            left = middle + 1;
        else
            return middle;
        // 可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多
        // 如果每次循环都判断一下是否相等,将耗费时间
    }
    return -1;
}

Attention aux résultats gauche + ((droite - gauche) >> 1) etc pour (droite + gauche) / 2, mais plus rapide sans débordement int

1.2 Trouver la position la plus à gauche de >= un certain nombre dans un tableau ordonné

L'idée est toujours la méthode de la dichotomie, qui est différente de la recherche d'une certaine valeur et de l'arrêt de la dichotomie lorsque la valeur cible est trouvée. Le problème de la recherche de la position la plus à gauche/la plus à droite doit être dichotomique jusqu'au bout


int nearLeftSearch(const std::vector<int>& arr, int target)
{
    
    
	int left = 0;
	int right = arr.size() - 1;
	int result = -1;
	
	while (left <= right)
	{
    
    
		int mid = left + ((right - left) >> 1);
		if (target <= arr[mid]){
    
     // 目标值小于等于mid,就要往左继续找
			result = mid;// 暂时记录下这个位置,因为左边可能全都比目标值小了,就已经找到了
			right = mid - 1;
		} else{
    
    		// target > arr[mid]
			left = mid + 1;
		}
	}
	return result;
}

1.3 Trouver la position la plus à droite de <= un certain nombre dans un tableau ordonné

  • Si l'élément du milieu est supérieur à la valeur cible, cela signifie que la valeur cible doit se trouver dans la moitié gauche, nous limitons donc la recherche à la moitié gauche et mettons à jour à droite au milieu - 1.
  • Si l'élément du milieu est inférieur ou égal à la valeur cible, cela signifie que la valeur cible doit être dans la moitié droite ou la position actuelle, nous mettons donc à jour le résultat à l'index du milieu actuel pour enregistrer la position la plus à droite trouvée et réduire le plage de recherche vers la moitié droite, mise à jour de gauche vers le milieu + 1.
int nearRightSearch(const std::vector<int>& arr, int target) 
{
    
    
    int left = 0;
    int right = arr.size() - 1;
    int result = -1;

    while (left <= right) {
    
    
        int mid = left + (right - left) / 2;

        if (target < arr[mid]) {
    
    
            right = mid - 1;
        } else {
    
    	// target >= arr[mid]
            result = mid;
            left = mid + 1;
        }
    }

    return result;
}

1.4 Problème de minimum local (un cas de tableau non ordonné utilisant la dichotomie)

Le tableau arr est non ordonné, deux nombres adjacents ne sont pas égaux , trouvez une position minimale locale (valeur minimale) et la complexité temporelle doit être meilleure que O(N)

Le désordre peut également être dichotomisé , tant que le problème cible doit avoir une solution d'un côté, et que l'autre côté n'a pas d'importance, la dichotomie peut être utilisée

1. Jugez d'abord les deux limites du tableau

  • Trouvé si lié à gauche arr[0] < arr[1]
  • Trouvé s'il existe une borne arr[n-1] < arr[n-2]
  • Si aucune des deux frontières n'est un minimum local, et parce que deux nombres adjacents ne sont pas égaux, la frontière gauche est localement décroissante de manière monotone et la frontière droite est localement croissante de manière monotone . Par conséquent, dans le tableau, il doit y avoir un point de valeur minimum
    insérez la description de l'image ici

2. Effectuez une dichotomie et jugez la relation entre les positions médiane et adjacente, qui sont divisées en 3 situations : (Rappel : deux éléments adjacents dans le tableau ne sont pas égaux !) 3. Répétez le processus 2
insérez la description de l'image ici
jusqu'à ce que la valeur minimale soit trouvée

int LocalMinimumSearch(const std::vector<int>& arr) 
{
    
    
    int n = arr.size();
    // 先判断元素个数为0,1的情况,如果题目给出最少元素个>1数则不需要判断
    if (n == 0) return -1;
    if (n == 1) return 0; // 只有一个元素,则是局部最小值
	
	if (arr[0] < arr[1]) return 0;
	
	int left = 0;
    int right = n - 1;
	// 再次提醒,数组中相邻两个元素是不相等的!
    while (left < right) 
    {
    
    
        int mid = left + ((right - left) >> 1);

        if (arr[mid] < arr[mid - 1] && arr[mid] < arr[mid + 1]) {
    
    
            return mid;  // 找到局部最小值的位置
        } else if (arr[mid - 1] < arr[mid]) {
    
    
            right = mid - 1;  // 局部最小值可能在左侧
        } else {
    
    
            left = mid + 1;  // 局部最小值可能在右侧
        }
    }

    // 数组中只有一个元素,将其视为局部最小值
    return left;
}

2 Pensée récursive simple

Depuis le début de Zuoge P4 , il appartient à la pré-connaissance du tri par fusion. La dichotomie est également utilisée ici. Principalement pour comprendre le processus d'exécution

Exemple : trouver la valeur maximale dans la plage spécifiée du tableau, en utilisant la récursivité pour obtenir

#incldue <vector>
#include <algorithm>
int process(const std::vector<int>& arr, int L, int R)
{
    
    
	if (L == R) return arr[L]; 
	
	int mid = L + ((R - L) >> 1);	// 求中点,右移1位相当于除以2
	int leftMax = process(arr, L, mid);
	int rightMax = process(arr, mid + 1, R);
	return std::max(leftMax, rightMax);
}

Lorsque la fonction process(arr, L, R) est appelée, elle effectue les opérations suivantes :

  1. Tout d'abord, la condition de terminaison de la récursivité est vérifiée. Si L et R sont égaux, cela signifie que l'intervalle minimum du tableau a été récursif et qu'il n'y a qu'un seul élément. A ce moment, l'élément arr[L] est renvoyé directement.
  2. Si la condition de terminaison n'est pas remplie, la récursivité doit continuer. Tout d'abord, calculez le point médian mid et divisez l'intervalle [L, R] en deux sous-intervalles [L, mid] et [mid+1, R].
  3. Ensuite, appelez récursivement process(arr, L, mid) pour traiter le sous-intervalle gauche. Cette étape pousse la fonction actuelle sur la pile, entrant dans un nouveau niveau de récursivité.
  4. Dans le nouveau niveau récursif, réexécutez les étapes 1 à 3 jusqu'à ce que la condition de terminaison soit satisfaite et renvoyez la valeur maximale leftMax du sous-intervalle gauche.
  5. Ensuite, appelez récursivement process(arr, mid+1, R) pour traiter le sous-intervalle droit. Encore une fois, cette étape pousse la fonction actuelle sur la pile, entrant dans un nouveau niveau de récursivité.
  6. Dans le nouveau niveau récursif, réexécutez les étapes 1 à 3 jusqu'à ce que la condition de fin soit satisfaite et renvoyez la valeur maximale rightMax du sous-intervalle droit.
  7. Enfin, comparez les valeurs maximales leftMax et rightMax des sous-intervalles gauche et droit, prenez la valeur la plus grande comme
    valeur maximale de l'intervalle entier [L, R] et renvoyez-la comme résultat.
  8. Lors du retour récursif à la couche précédente, transmettez la valeur maximale obtenue à la couche précédente jusqu'au retour au point d'appel d'origine pour obtenir la valeur maximale de l'ensemble du tableau.

Dans le processus de récursivité, chaque appel récursif créera un nouveau cadre de pile de fonctions pour enregistrer les variables locales et les paramètres de la fonction. Lorsque la condition de terminaison est remplie, la récursivité commence à revenir en arrière et le résultat final est renvoyé couche par couche. En même temps, les variables locales et les paramètres de chaque couche sont également détruits et les cadres de la pile de fonctions sont extraits de la pile. à son tour.

graphique de dépendance

  • Supposons que le tableau cible est [3, 2, 5, 6, 7, 4], appelez process(0,5) pour trouver la valeur maximale, et le paramètre arr est omis
  • Le nombre rouge est le flux d'exécution du processus, qui omet le processus de comparaison et de retour std :: max
    insérez la description de l'image ici

Mots humains : si vous voulez obtenir la valeur de retour du tableau, c'est-à-dire 1la valeur de retour de la première étape, vous devez d'abord obtenir 2la valeur maximale, et sa valeur maximale doit être exécutée en premier selon le code 3, et il 3doit également être exécuté en premier 4Lorsque les deux points atteignent 4cette étape, il n'y a qu'un seul élément L==R, regardez le code, cet élément est la valeur maximale de l'intervalle actuel (0,0), revenez à, 3après 3avoir obtenu leftMax , vous avez besoin de la valeur maximale de la partie droite, exécutez-la 5, obtenez rightMax, et enfin Comparez la valeur maximale des intervalles gauche et droit, obtenez la valeur maximale de l'intervalle (0,1) et revenez-y 2, 2et il est nécessaire de 6revenir pour obtenir la valeur maximale de (0,2) après comparaison 6, et 2d'y revenir 1. Faites ensuite celui de droite. . . . plus tard omis

2.1 Complexité temporelle de l'algorithme récursif (formule maîtresse)

En programmation, la récursivité est un algorithme très courant. Il est largement utilisé en raison de la concision de son code. Cependant, par rapport à l'exécution séquentielle ou aux programmes cycliques, la récursivité est difficile à calculer. La formule maîtresse est utilisée pour calculer la complexité temporelle des programmes récursifs.

Conditions d'utilisation : Tous les sous-problèmes doivent être de même taille . Pour le dire franchement, il s'agit essentiellement de l'algorithme récursif créé par les arbres binaires et binaires.

公式T ( N ) = a T ( N / b ) + O ( N d ) T(N) = aT(N/b) + O(N^d)T ( N )=un T ( N / b )+O ( N )

  • NNN : La taille des données du processus parent est N
  • N/b N/bN / b : Taille des données du sous-processus
  • aaa : le nombre d'appels au sous-programme
  • O ( N d ) O(N^d)O ( Nd ): Complexité temporelle des autres processus sauf l'invocation des sous-problèmes

Après avoir obtenu abd, obtenez la complexité temporelle en fonction des différentes situations suivantes

  • logba > d log_ba > dl o gbun>d : la complexité temporelle estO ( N logba ) O(N^{log_ba})O ( Nl o gbun )
  • log = d log_ba = dl o gbun=d : La complexité temporelle estO ( N d ⋅ log N ) O(N^d · logN)O ( Nlog N ) _ _
  • logba < d log_ba < dl o gbun<d : la complexité temporelle estO ( N d ) O(N^d)O ( N )

Par exemple, l'exemple de recherche de la valeur maximale mentionné ci-dessus peut utiliser cette formule :

N = 2 ⋅ T ( N 2 ) + O ( 1 ) N = 2·T(\frac{N}{2}) + O(1)N=2 T (2N)+O ( 1 ) . où a = 2 ; b = 2 ; d = 0

log 2 2 = 1 > 0 log_22 = 1 > 0l o g22=1>0 Par conséquent, la complexité temporelle est :O ( N ) O(N)O ( N )

Je suppose que tu aimes

Origine blog.csdn.net/Motarookie/article/details/131382340
conseillé
Classement