Estructura de datos y algoritmo (1) - Complejidad del tiempo

¿Por qué es necesario el análisis de la complejidad del tiempo?

A través de estadísticas y monitoreo se puede obtener el tiempo de ejecución del algoritmo y el tamaño de memoria ocupado, sin embargo, este método estadístico
tiene muchas deficiencias, tales como:

  1. Los resultados de la prueba dependen del entorno de prueba. Por ejemplo, si el chip de la PC de prueba se cambia de i7 a i5, el tiempo de ejecución aumentará.
  2. Los resultados de las pruebas dependen del tamaño de los datos de prueba, como la clasificación de datos a pequeña escala, la clasificación por inserción es más rápida que la clasificación rápida

Representación de la Complejidad del Tiempo

Notación Big O (importante)

Definición: Si y solo si hay dos parámetros c > 0, n0 > 0, para todo n >= n0, f(n) <= cg(n), entonces f(n) = O(g( n ))
, como se muestra en la figura:

inserte la descripción de la imagen aquí

Ejemplo de complejidad temporal en notación Big O:

 int cal(int n) {
    
    
 int sum = 0;
 int i = 1;
 int j = 1;
 for (; i <= n; ++i) {
    
    
       j = 1;
       for (; j <= n; ++j) {
    
    
       sum = sum + i * j;
     }
 }
 return sum;
 }

Asumiendo el código anterior, el tiempo para ejecutar una línea de código es t, y el tiempo total empleado es (2*n^2+2*n+4)*t. Cuando n es muy grande, el tiempo empleado por el código anterior
depende solo de n^2, es decir, T(n) = O(n 2), la complejidad temporal del código anterior es O(n 2)

En pocas palabras: la notación O grande es ignorar las constantes, los órdenes bajos y los coeficientes en la fórmula, y solo necesita registrar la magnitud del orden más grande

Gran notación Ω (solo entienda)

Definición: Si hay números positivos c y n0, de modo que para todo n >= n0,
f(n) >= cg(n), entonces f(n) = Ω(g(n)), como se muestra en la figura :

inserte la descripción de la imagen aquí

Gran notación θ (solo entienda)

Definición: Una función se llama θ(g(n)) si está tanto en el conjunto O(g(n)) como en el conjunto Ω(g(n)
). Es decir, cuando los límites superior e inferior son iguales, se puede utilizar la representación de θ grande, como se muestra en la figura:

inserte la descripción de la imagen aquí

Escala de complejidad de tiempo de uso común

  1. Orden constante O(1)

O (1) es solo una representación de la complejidad de tiempo de nivel constante, no que solo se ejecute una línea de código. Por ejemplo, este código, aunque tenga 3 líneas, su complejidad temporal es O(1),
no O(3).

 int i = 8;
 int j = 6;
 int sum = i + j;

Siempre que el tiempo de ejecución del código no aumente con el aumento de n, la complejidad temporal del código se registra como O(1). O, en general, siempre que no haya sentencias de bucle o sentencias recursivas en el algoritmo
, incluso si hay decenas de miles de líneas de código, su complejidad temporal es Ο(1).

  1. Orden logarítmico O(logn)
  2. Orden lineal O(n)
  3. Orden logarítmico lineal O(n * logn)
  4. Orden al cuadrado O(n 2), orden legislativo O(n 3), k orden al cuadrado O(n^k), etc.
  5. Orden exponencial O(2^n)
  6. Orden factorial O(n!)

Análisis de la complejidad del tiempo

Calcule el número de corridas para determinar

La complejidad temporal de algunos algoritmos se puede determinar simplemente contando el número de veces que se ejecutan.

Ejemplo 1, un algoritmo para calcular la suma de dos matrices:

int count(int[] array1,int m,int[] array2,int n){
    
    
  int sum = 0;
  for(int i = 0;i < m;i++){
    
    //运行 m 次
      sum += array1[i];
  }
  for(int i = 0;i < n;i++){
    
    //运行 n 次
      sum += array2[i];
  }
  return sum; 
}

Como m y n no son iguales, el número de ejecuciones es m+n, por lo que la complejidad temporal es O(m+n)

Ejemplo 2, algoritmo de clasificación de burbujas:

  private static void sort(int[] data){
    
    
        if(data.length <= 1)return;
        int n = data.length;
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 0; j+1 < n - i; j++) {
    
    
                if(data[j] > data[j+1]){
    
    
                    int cache = data[j];
                    data[j] = data[j+1];
                    data[j+1] = cache;
                }
            }
        }
    }

La cantidad de veces que se ejecuta el código anterior es n * (n + 1) / 2. Debido a la gran notación O, las constantes, los órdenes bajos y los coeficientes deben eliminarse, por lo que la complejidad del tiempo es O (n ^ 2)

Ejemplo 3, se necesitan algunas matemáticas para obtener la complejidad del tiempo:

 i=1;
 while (i <= n) {
    
    
 i = i * 2;
 }

Como se muestra en el código, el tiempo de ejecución x de este programa satisface n = 2^0 * 2^1 * 2^2 * ... *2^x, es decir, x = logn, por lo que la complejidad del tiempo es O( iniciar sesión)

árbol de ejecución

Para las funciones recursivas, la cantidad de llamadas recursivas rara vez se escala linealmente con el tamaño de la entrada. En este caso, es mejor usar un árbol de ejecución, que es un árbol que se usa para representar el flujo de ejecución de una función recursiva. Cada nodo del árbol representa una llamada a una función recursiva. Así, el número total de nodos en el árbol corresponde al número de llamadas recursivas durante la ejecución.

El siguiente es el problema de programación del número de Fibonacci en leetcode .

斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。
该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。

El código de respuesta es el siguiente (tenga en cuenta que el siguiente código se puede optimizar mediante memorización)

class Solution {
    
    
    public int fib(int N) {
    
    
        if(N == 0) return 1;
        return fib(N-1)+fib(N-2);
    }
}

El siguiente diagrama (leetcode fuente de la imagen) muestra el árbol de ejecución utilizado para calcular el número de Fibonacci f(4).

inserte la descripción de la imagen aquí

En un árbol binario completo con n niveles, el número total de nodos es 2^n -1. Entonces, el límite superior (aunque no estricto) del número de recurrencias en f(n) también es 2^n -1. Entonces podemos estimar la complejidad temporal del algoritmo como O(2^n)

otro

Mejor, peor, complejidad de tiempo de caso promedio

Ejemplo, para encontrar la posición de x en la matriz:

// n表示数组array的长度
int find(int[] array, int n, int x) {
    
    
 int i = 0;
 int pos = -1;
 for (; i < n; ++i) {
    
    
     if (array[i] == x) {
    
    
         pos = i;
         break;
     }
 }
 return pos;
}

Cuando x está en la posición i=0 de la matriz, el programa solo necesita ejecutarse una vez y la complejidad de tiempo es O(1);

Cuando x no está en el arreglo, el programa necesita atravesar el arreglo y la complejidad del tiempo es O(n);

Para expresar la diferente complejidad temporal del código en diferentes situaciones, necesitamos introducir tres conceptos: complejidad temporal del mejor caso , complejidad temporal del peor caso y complejidad temporal del caso promedio . Pero la probabilidad de la mejor y la peor complejidad temporal es pequeña Para representar mejor la complejidad del caso promedio, necesitamos la complejidad temporal del caso promedio.

Tomando el código anterior como ejemplo, la probabilidad de que x exista o no es 1/2, entonces la probabilidad de que x exista y aparezca en la posición 0 ~ n-1 es (1+2+...+n)/2n , y la probabilidad de que x no exista es n/2, es decir, (3*n+1)/4, utilizando la notación O grande, la complejidad temporal es O(n)

Complejidad del tiempo amortizado

La mayoría de los casos de complejidad de ejecución de código son de bajo nivel de complejidad. Cuando los casos individuales son de alto nivel de complejidad y tienen una relación de tiempo, la complejidad individual de alto nivel puede distribuirse uniformemente a la complejidad de bajo nivel
. Básicamente, el resultado amortizado equivale a una complejidad de bajo nivel

为了更全面,更准确的描述代码的时间复杂度,才要引入最好、最坏、平均情况时间复杂度以及均摊时间复杂度。但是只有代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。

Complejidad temporal de algoritmos comunes

Durante la entrevista, a menudo se le pide que escriba el código a mano. Por cierto, cuál es la complejidad temporal del algoritmo, por lo que es muy necesario recordar la complejidad temporal de los algoritmos comunes.

Algoritmo de clasificación

Algoritmo de clasificación complejidad del tiempo
Ordenamiento de burbuja O(n^2)
tipo de inserción O(n^2)
clasificación de selección O(n^2)
ordenación rápida O (iniciar sesión)
ordenar por fusión O (iniciar sesión)
tipo de cubeta En)
tipo de conteo En)
ordenamiento radix En)

Algoritmo de cruce de árbol binario

Algoritmo de cruce de árbol binario complejidad del tiempo
Preorder transversal (implementación recursiva) En)
Recorrido de preorden (implementación de iteración) En)
Recorrido en orden (implementación recursiva) En)
Recorrido en orden (implementación iterativa) En)
Recorrido posterior al pedido (implementación recursiva) En)
Recorrido posterior al pedido (implementación de iteración) En)

referencia

Supongo que te gusta

Origin blog.csdn.net/lichukuan/article/details/126964403
Recomendado
Clasificación