Notas de estudio "Estructura de datos y análisis de algoritmos" - Capítulo 2 - Análisis de algoritmos

Análisis de algoritmos

Si el algoritmo para resolver un problema se determina y se demuestra que es correcto mediante un método de prueba determinado, entonces el tiempo de ejecución del algoritmo y el espacio ocupado por el tiempo de ejecución deben juzgarse a continuación. Este capítulo trata principalmente

  • Tiempo estimado de ejecución del programa
  • Reduce el tiempo de ejecución del programa
  • Riesgo recursivo
  • Un algoritmo efectivo para multiplicar un número para obtener su potencia y calcular el máximo factor común de dos números

2.1 Fundamento matemático

  1. Si hay números normales c y n0 tales que cuando N> = n0, T (N) <= cf (N), entonces se registra como T (N) = 0 (f (N)). Aquí estamos hablando de T (N) La tendencia de crecimiento no excede la tendencia de crecimiento de f (N). A menudo usamos la definición aquí para la complejidad del tiempo, f (N) también se llama límite superior de T (N)
  2. Si hay números normales c y n0 de modo que cuando N> = n0, T (N)> = cg (N), entonces se registra como T (N) = Ω (f (N)). Aquí es T (N) La tendencia de crecimiento de no es menor que la tendencia de crecimiento de g (N). Aquí se dice que g (N) es el límite inferior de T (N)
  3. T (N) = Θ (h (N)) si y solo si T (N) = O (h (N)), y T (N) = Ω (h (N)). Aquí se dice que la tendencia de crecimiento de T (N) yg (N) son las mismas
  4. Si T (N) = O (p (N)), y T (N)! = Θ (p (N)), entonces T (N) = o (p (N)). Aquí se dice que la tendencia de crecimiento de T (N) es siempre menor que p (N). Y no hay igualdad

La declaración anterior es demasiado oscura. Da un ejemplo simple. Cuando g (N) = N ^ 2, g (N) = O (N ^ 3) yg (N) = O (N ^ 4) están bien. g (N) = Ω (N), g (N) = Ω (1) también son correctos. g (N) = Θ (N ^ 2) significa g (N) = O (N ^ 2), g (N) = Ω (N ^ 2). Es decir, el resultado actual es más consistente con la tendencia de crecimiento de g (N) en sí. Como se muestra:

Hay tres reglas importantes para recordar:

  1. Si T1 (N) = O (f (N)) y T2 (N) = O (g (N)), entonces
    • T1 (N) + T2 (N) = max (O (f (N)), O (g (N))),
    • T1 (N) * T2 (N) = 0 (f (N) * g (N))
  2. Si T (N) es un polinomio de grado k, entonces T (N) = Θ (N ^ k)
  3. Para cualquier constante k, log ^ k N = O (N). Nos dice que el logaritmo crece muy lentamente.

Cuando use la notación O grande, mantenga poderes de alto orden, descarte términos constantes y poderes de bajo orden. La función se clasifica por la tasa de crecimiento como se muestra en la figura:

Siempre podemos determinar las tasas de crecimiento relativo de las dos funciones f (N) yg (N) calculando el límite lim f (N) / g (N) (n-> ∞). Se puede calcular utilizando el criterio de Lobita.

  • El límite es 0, entonces f (N) = o (g (N))
  • El límite es c y c! = 0, entonces f (N) = Θ (g (N))
  • El límite es ∞, entonces g (N) = o (f (N))
  • Límite de swing: los dos no tienen nada que hacer

Por ejemplo, la tasa de crecimiento relativa de f (N) = NlogN yg (N) = N ^ 1.5 se puede calcular como f (N) / g (N) = logN / N ^ 0.5 = log ^ 2 N / N. Y porque N crece más rápido que cualquier poder de logN. Entonces el crecimiento de g (N) es más rápido que el crecimiento de f (N)

Criterio de Lupida: si lim f (N) = ∞ (n-> ∞) y lim g (N) = ∞ (n-> ∞). Entonces lim f (N) / g (N) = lim f '( N) / g '(N) (n-> ∞).

2.2 Modelo

Para facilitar el análisis de problemas, asumimos una computadora modelo. Ejecuta cualquier instrucción básica, consume una unidad de tiempo y supone que tiene memoria ilimitada.

2.3 Problemas a analizar

  1. Si es una entrada pequeña, no vale la pena gastar mucho tiempo para diseñar un algoritmo inteligente
  2. La lectura de datos es un cuello de botella. Una vez que se leen los datos, los buenos problemas de algoritmos se resolverán rápidamente. Por lo tanto, es importante hacer que el algoritmo sea lo suficientemente efectivo como para no convertirse en el cuello de botella del problema

2.4 Cálculo del tiempo de ejecución

2.4.1 Ejemplos

  • Si los dos algoritmos toman aproximadamente el mismo tiempo, la mejor manera de determinar qué programa es más rápido es codificarlos y ejecutarlos
  • Para simplificar el análisis, utilizamos la notación Big O para calcular el tiempo de ejecución, Big O es un límite superior. Por lo tanto, el resultado del análisis es proporcionar una garantía de que el programa puede completarse dentro del tiempo especificado en el peor de los casos. El procedimiento puede finalizar temprano, pero no se retrasará.
// 书上例程
// 计算i^3的累加求和
int sum (int N)
{
    int i, PartialSum;
    PartialSum = 0;             /*1*/
    for(i = 1; i <= N; i++)     /*2*/
        PartialSum += i * i * i;/*3*/
    return PartialSum;          /*4*/
}

Aquí para cada línea de análisis:

  1. Pasa 1 unidad de tiempo: 1 tarea
  2. Gasta 1 + N + 1 + N = 2N + 2 unidades de tiempo: 1 asignación, N + 1 juicios, N adiciones
  3. Costo N (2 + 1 + 1) = 4N unidades de tiempo: 2 multiplicaciones, 1 suma, 1 asignación, ejecutar N veces
  4. Toma 1 unidad de tiempo: 1 retorno

El costo total es 1 + 2N + 2 + 4N + 1 = 6N + 4 unidades de tiempo.

Pero, de hecho, no necesitamos hacer este análisis cada vez, porque cuando nos enfrentamos con cientos de miles de líneas de programas, no podemos hacer esto para cada línea. Solo se necesita calcular el orden más alto. Se puede ver que el bucle for toma más tiempo. Entonces la complejidad del tiempo es O (N)

2.4.2 Reglas generales

  1. bucle for: el tiempo de ejecución de un bucle for debe ser como máximo el tiempo de ejecución de la instrucción en el bucle for multiplicado por el número de iteraciones
  2. Anidado para bucles: analice los bucles de adentro hacia afuera . El tiempo de ejecución total de una instrucción dentro de un grupo de bucles anidados es el producto del tiempo de ejecución de la instrucción multiplicado por el tamaño de todos los bucles del grupo.
for (i = 0; i < N; i++)
    for (j=0; j < N; j++)
        k++;    // 1 * N * N = N^2,时间复杂度为O(N^2)
  1. Declaración secuencial: el tiempo de ejecución de cada declaración puede resumirse. Toma el valor máximo.
for (i = 0; i < N; i++)
    A[i] = 0;   // O(N)
for (i = 0; i < N; i++)
    for (j = 0; j < N; j++)
        A[i] += A[j] + i + j;   // O(N^2)
// 总时间为O(N) + O(N^2),因此取最高阶,总时间复杂度为O(N^2)
  1. declaración if-else: tiempo de evaluación más tiempo de ejecución más largo en dos ramas

Queremos evitar hacer trabajos repetitivos en llamadas recursivas.

2.4.3 Subsecuencia máxima y solución al problema

Problema de subsecuencia máxima: dados los enteros A1, A2, ..., AN (pueden tener números negativos), encuentre el valor máximo de la suma de los enteros consecutivos. Si todos los enteros son negativos, la suma máxima de subsecuencias es 0

  1. Opción uno, complejidad temporal O (N ^ 3)
// 书上例程
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, i, j, k;
    
    MaxSum = 0;
    for (i = 0; i < N; i++) {
        for (j = i; j < N; j++) {
            ThisSum = 0;
            for (k = i; k <= j; k++) {
                ThisSum += A[k];
            }
            
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    
    return MaxSum;
}
  1. Solución dos, complejidad temporal O (N ^ 2). En comparación con el esquema uno, el bucle más interno se descarta
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, i, j, k;
    
    MaxSum = 0;
    for (i = 0; i < N; i++) {
        ThisSum = 0;
        for (j = i; j < N; j++) {
            ThisSum += A[k];
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            }
        }
    }
    
    return MaxSum;
}
  1. Esquema tres, complejidad temporal O (NlogN). Usa una estrategia de divide y vencerás. 'Fen' es dividir los datos en dos partes, es decir, dividir el problema en dos subproblemas aproximadamente iguales, y luego resolverlos de forma recursiva; 'Govern' es calcular la suma de las subsecuencias máximas de las dos partes, y luego fusionar los resultados. En este problema, la subsecuencia máxima puede ocurrir en tres casos: mitad izquierda, mitad derecha, que abarca la mitad izquierda y la mitad derecha (incluido el último elemento de la mitad izquierda y el primer elemento de la mitad derecha). La suma máxima de subsecuencia en el tercer caso es la suma de la suma máxima de subsecuencia que incluye el último elemento de la mitad izquierda más la suma máxima de subsecuencia que incluye el primer elemento de la mitad derecha.
// 书上例程
int 
max3(int a, int b, int c)
{
    int x;
    x = a > b? a: b;
    return (x > c? x: c);    
}

int
MaxSubsequenceSum(const int A[], int Left, int Right)
{
    int MaxLeftSum, MaxRightSum;
    int MaxLeftBorderSum, MaxRightBorderSum;
    int MaxLeftThisSum, MaxRightThisSum;
    int Center;
    int cnt;
    
    if (Left == Right) {
        if (A[Left] > 0) {
            return A[Left];
        } else {
            return 0;
        }
    }
    
    Center = (Left + Right) / 2;
    MaxLeftSum = MaxSubsequenceSum(A, Left, Center);
    MaxRightSum = MaxSubsequenceSum(A, Center + 1, Right);
    
    MaxLeftBorderSum = 0;
    MaxLeftThisSum = 0;
    for (cnt = Center; cnt >= Left; cnt--) {
        MaxLeftThisSum += A[cnt];
        if (MaxLeftThisSum > MaxLeftBorderSum) {
            MaxLeftBorderSum = MaxLeftThisSum;
        }
    }
    
    MaxRightBorderSum = 0;
    MaxRightThisSum = 0;
    for (cnt = Center + 1; cnt <= Right; cnt++) {
        MaxRightThisSum += A[cnt];
        if (MaxRightThisSum > MaxRightBorderSum) {
            MaxRightBorderSum = MaxRightThisSum;
        }
    }
    
    return max3(MaxLeftSum, MaxRightSum, MaxRightBorderSum + MaxLeftBorderSum);
}
  1. Solución cuatro, la complejidad del tiempo es O (N). Los datos se escanean solo una vez, y una vez leídos y procesados, no es necesario memorizarlos. Si la matriz se almacena en el disco, puede leerse secuencialmente, sin tener que almacenar ninguna parte de la matriz en la memoria principal. Y en cualquier momento, el algoritmo puede dar la respuesta correcta al problema de subsecuencia para los datos que ha leído . Los algoritmos con esta característica también se denominan algoritmos en línea (algoritmos en línea).Los algoritmos en línea que solo requieren espacio constante y se ejecutan en tiempo lineal son algoritmos casi perfectos
//书上例程
int
MaxSubsequenceSum(const int A[], int N)
{
    int ThisSum, MaxSum, j;
    
    ThisSum = MaxSum = 0;
    for (j = 0; j < N; j++) {
        ThisSum += A[j];
        if (ThisSum > MaxSum) {
            MaxSum = ThisSum;
        } else if (ThisSum < 0) {
            ThisSum = 0;
        }
    }
    return MaxSum;
}

2.4.4 Logaritmo en tiempo de ejecución

Si un algoritmo usa tiempo constante (O (1)) para reducir el tamaño del problema a una parte (generalmente 1/2), entonces el algoritmo es O (logN). Por otro lado, si usar el tiempo constante solo reduce el problema en una constante (como reducir el problema en 1), entonces este algoritmo es O (N)

  1. Búsqueda binaria: la búsqueda binaria proporciona una operación de búsqueda con una complejidad temporal de O (logN). Su premisa es que los datos se han ordenado, y cada vez que se inserta un elemento, la complejidad temporal de la operación de inserción es O (N). Porque la búsqueda binaria es adecuada para el caso donde los elementos son relativamente fijos.
// 书上例程,时间复杂度为O(logN)
#define NotFound -1

int BinarySearch(const ElementType A[], ElementType X, int N)
{
    int low, high, mid;
    low = 0;
    high = N - 1;
    mid = (low + high) / 2;
    
    while (low <= high) {
        if (A[mid] < X) {
            low = mid + 1;
        } else if (A[mid] > X) {
            high = mid - 1;
        } else {
            return mid;
        }
    }
    return NotFound;
}
  1. Algoritmo euclidiano: el nombre del algoritmo euclidiano suena muy alto, de hecho, es lo que llamamos división de tornados. Cuando encuentre el máximo factor común de dos enteros, use uno de los enteros para dividir el otro y obtener el resto. Luego divida el divisor anterior para dividir el resto para obtener el nuevo resto, y así sucesivamente. Cuando el nuevo resto es 0, el divisor en el entero actual es el máximo factor común. Después de dos iteraciones, el resto es como máximo la mitad del valor original. El número máximo de iteraciones es 2logN = 0 (logN)
// 书上例程:辗转相除法,时间复杂度O(logN)
int test(unsigned int M, ungisned int N)
{
    unsigned int Rem;
    
    while (N > 0) {
        Rem = M % N;
        M = N;
        N = Rem;
    }
    return M;
}
  • Teorema 2.1: Si M> N, entonces M mod N <M / 2.
    Prueba: si N <= M / 2, el resto debe ser menor que N, entonces M mod N <M / 2; si N> M / 2, entonces M-N <M / 2, es decir, M mod N <M / 2. El teorema está probado
  1. Operación de potencia: encuentre la potencia de un número entero. Eso es X ^ N. El número de multiplicaciones requeridas es como máximo 2logN, por lo que dividir el problema a la mitad requiere hasta dos multiplicaciones (N es un número impar)
// 书上例程,时间复杂度O(logN)
long int Pow(long int X, unsigned int N)
{
    if (N == 0) {
        return 1;
    } else if (N == 1) {
        return X;
    }
    
    if (isEven(N)) {
        return Pow(X * X, N / 2);
    } else {
        return Pow(X * X, N / 2) * X;
    }
}

2.4.5 Pon a prueba tu análisis

  1. Método uno: programación real, observe si el resultado del tiempo de ejecución coincide con el tiempo de ejecución previsto por el análisis. Cuando N se duplica, el tiempo de ejecución de un programa lineal se multiplica por un factor de 2, el tiempo de ejecución de un programa secundario se multiplica por un factor de 4 y el tiempo de ejecución de un tercer programa se multiplica por un factor de 8. Para un programa que se ejecuta en tiempo logarítmico, cuando N aumenta en A veces, el tiempo de ejecución solo aumenta en una constante. El programa que se ejecuta con O (NlogN) es un poco más del doble del tiempo de ejecución original. (NX, 2N (X + 1)). Si el coeficiente del término de orden inferior es relativamente grande y N no es lo suficientemente grande, entonces el tiempo de ejecución es difícil de observar con claridad. Es difícil distinguir O (N) y O (NlogN) basado solo en la práctica
  2. Método 2: Calcule la relación T (N) / f (N) para un rango de N (generalmente separado por un múltiplo de 2), donde T (N) es el tiempo de ejecución observado yf (N) es la derivación teórica Fuera de tiempo de ejecución. Si el valor calculado converge a un número normal, significa que f (N) es una aproximación ideal del tiempo de ejecución; si converge a 0, significa que f (N) está sobreestimado; si el resultado diverge (cada vez más), entonces Se estima que el representante f (N) es demasiado pequeño.
//书上例程,时间复杂度O(N^2)
void test(int N)
{
    int Rel = 0, Tot = 0;
    int i, j;
    
    for( i = 1; i <= N; i++) {
        for ( j = i + 1, j <= N; j++) {
            Tot++;
            
            if (Gcd(i,j) == 1) {
                Rel++;
            }
        }
    }
    
    printf("%f", (double)Rel / Tot);
}

2.4.6 Precisión de los resultados del análisis.

A veces el análisis se estimará demasiado grande. Entonces, o el análisis debe ser más detallado, o el tiempo de ejecución promedio es significativamente menor que el peor de los casos y no hay forma de mejorar el límite resultante. En muchos algoritmos, el peor límite se logra con alguna entrada incorrecta, pero en la práctica generalmente se sobreestima. Para la mayoría de estos problemas, el análisis de la situación promedio es extremadamente complejo o no resuelto. El peor de los casos es un poco pesimista, pero es el resultado analítico más conocido.

  • Los programas simples pueden no tener un análisis simple
  • El análisis de límite inferior no solo es aplicable a un cierto algoritmo sino a un cierto tipo de algoritmo
  • El algoritmo GCD y el algoritmo de exponenciación se usan ampliamente en criptografía

Consejo:

Este artículo es original, bienvenido a aprender a reimprimir _

Reimpresión por favor indique en una posición destacada:

ID de blogger: CrazyCatJack

Dirección original del enlace del blog: https://www.cnblogs.com/CrazyCatJack/p/12688582.html


Esto concluye el Capítulo 2 y luego el Capítulo 3, donde comenzamos a explicar la implementación de estructuras de datos y algoritmos específicos. Si se siente bien, haga clic en & recomendar, para que puedan aprender juntos más tarde. ¡Gracias a todos por su apoyo!

CrazyCatJack

Supongo que te gusta

Origin www.cnblogs.com/CrazyCatJack/p/12688582.html
Recomendado
Clasificación