<Estructura de datos>Complejidad de tiempo y complejidad de espacio

contenido

1. Concepto

       1.1 Eficiencia del algoritmo

       1.2, complejidad del tiempo

       1.3 Complejidad espacial

2. Cálculo

        2.1 Representación asintótica de la gran O

        2.2, cálculo de la complejidad del tiempo

                 ejemplo:

        2.3, cálculo de la complejidad del espacio

                 ejemplo

3. Ejercicios con requisitos de complejidad


1. Concepto

1.1 Eficiencia del algoritmo

¿Cómo medir la calidad de un algoritmo?, por ejemplo, para la siguiente sucesión de Fibonacci:

long long Fib(int N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

La implementación recursiva de la sucesión de Fibonacci es muy concisa, pero ¿es bueno ser conciso? Entonces, ¿cómo medir lo bueno y lo malo? La complejidad del tiempo se le revelará al final del estudio.

Hay dos tipos de análisis de eficiencia de algoritmos: el primero es la eficiencia del tiempo y el segundo es la eficiencia del espacio. La eficiencia del tiempo se denomina complejidad del tiempo, mientras que la eficiencia del espacio se denomina complejidad del espacio. La complejidad del tiempo mide principalmente la velocidad de ejecución de un algoritmo, mientras que la complejidad del espacio mide principalmente el espacio adicional requerido por un algoritmo En los primeros días del desarrollo de computadoras, la capacidad de almacenamiento de las computadoras era muy pequeña. Así que está muy preocupado por la complejidad del espacio. Pero después del rápido desarrollo de la industria informática, la capacidad de almacenamiento de la computadora ha alcanzado un nivel muy alto. Así que ya no necesitamos prestar especial atención a la complejidad espacial de un algoritmo.

1.2, complejidad del tiempo

El tiempo empleado por un algoritmo es proporcional al número de ejecuciones de las declaraciones en él, y el número de ejecuciones de las operaciones básicas en el algoritmo es la complejidad del tiempo del algoritmo.

1.3 Complejidad espacial

La complejidad del espacio es una medida de la cantidad de espacio de almacenamiento ocupado temporalmente por un algoritmo durante su ejecución. La complejidad del espacio no es cuantos bytes ocupa el programa, porque esto no tiene mucho sentido, entonces la complejidad del espacio es el numero de variables. Las reglas de cálculo de la complejidad del espacio son básicamente similares a la complejidad práctica y también utilizan la notación asintótica de O grande.

2. Cálculo

2.1 Representación asintótica de la gran O

Primero mira una cadena de código:

// 请计算一下Func1基本操作执行了多少次?
void Func1(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++j)
		{
			++count;
		}
	}
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

El número de ejecuciones de las operaciones básicas en el algoritmo es la complejidad temporal del algoritmo. Obviamente, el número más preciso de operaciones realizadas por Func1 aquí: F(N)=N*N+2*N+10

Por ejemplo, F(10)=130, F(100)=10210, F(1000)=1002010

Es lógico que la complejidad temporal de este problema sea la fórmula anterior, pero no lo es. La complejidad del tiempo es una estimación, observando el elemento que tiene el mayor impacto en la expresión. A medida que N aumenta en esta pregunta, N^2 en esta expresión tiene el mayor impacto en el resultado

En la práctica, cuando calculamos la complejidad del tiempo, no necesariamente necesitamos calcular el número exacto de ejecuciones, sino solo el
número aproximado de ejecuciones, por lo que aquí usamos la representación asintótica de O grande. , por lo que la complejidad temporal de la pregunta anterior es O(N^2)

Notación Big O: es una notación matemática utilizada para describir el comportamiento asintótico de una función.

  • Derive el método de la O grande:
  1. Reemplace todas las constantes aditivas en tiempo de ejecución con la constante 1.
  2. En la función de tiempos de ejecución modificados, solo se conservan los términos de mayor orden.
  3. Si el término de mayor orden existe y no es 1, elimine la constante multiplicada por este término. El resultado es una orden O grande.

A través de lo anterior, encontraremos que la representación asintótica de O grande elimina aquellos ítems que tienen poco efecto en el resultado, y muestra el número de ejecuciones de manera sucinta y clara. Además, la complejidad temporal de algunos algoritmos tiene un mejor, promedio y peor de los casos:

  1. Peor caso: número máximo de ejecuciones para cualquier tamaño de entrada (límite superior)
  2. Caso promedio: número deseado de ejecuciones para cualquier tamaño de entrada
  3. Mejor caso: número mínimo de ejecuciones para cualquier tamaño de entrada (límite inferior)
  • Por ejemplo: busque un dato x en una matriz de longitud N
  1. Mejor caso: 1 hallazgo
  2. Peor caso: N veces encontrado
  3. Caso promedio: N/2 encontrados

En la práctica, la preocupación general es la operación del algoritmo en el peor de los casos, por lo que la complejidad temporal de la búsqueda de datos en la matriz es O(N)

Nota: cálculo de la complejidad del tiempo del algoritmo recursivo

  1. Cada llamada de función es O (1), luego depende del número de recursividad
  2. Cada llamada de función no es O(1), entonces depende de la acumulación del número de llamadas recursivas.

2.2, cálculo de la complejidad del tiempo

ejemplo:

  • Ejemplo 1:
// 计算Func2的时间复杂度?
void Func2(int N)
{
	int count = 0;
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}
  • Respuesta: O(N)

Análisis : el número de veces más preciso en esta pregunta es 2*N+10, y el más influyente es N. Algunas personas pueden pensar que es 2*N, pero como N continúa aumentando, 2 tiene poco impacto en el resultado. ., y para cumplir con la tercera regla anterior: si el término de mayor orden existe y no es 1, entonces elimina la constante multiplicada por este término. El resultado es una orden O grande. Entonces quita 2, entonces la complejidad del tiempo es O(N)

  • Ejemplo 2:
// 计算Func3的时间复杂度?
void Func3(int N, int M)
{
	int count = 0;
	for (int k = 0; k < M; ++k)
	{
		++count;
	}
	for (int k = 0; k < N; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}
  • Respuesta: O(M+N)

Análisis : Debido a que tanto M como N son incógnitas, tanto N como M deben transportarse, pero si está claro que M es mucho mayor que N, entonces la complejidad temporal es O(M). Si M y N tienen aproximadamente el mismo tamaño , entonces la complejidad del tiempo es O(M) u O(N)

  • Ejemplo tres:
// 计算Func4的时间复杂度?
void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}
  • Respuesta : O(1)

Explicación : el número más preciso aquí es 100, pero para cumplir con las reglas de la notación asintótica de O grande, reemplace todas las constantes aditivas en tiempo de ejecución con la constante 1. Entonces la complejidad del tiempo es O(1)

  • Ejemplo 4:
// 计算strchr的时间复杂度?
const char* strchr(const char* str, char character)
{
	while (*str != '\0')
	{
		if (*str == character)
			return str;
		++str;
	}
	return NULL;
}
  • Respuesta : O(N)

Análisis : esta pregunta se dividirá en situaciones. Aquí, se supone que la cadena es abcdefghijklmn. Si se encuentra que el carácter de destino es g, debe ejecutarse N/2 veces. Si se encuentra que es a, necesita ejecutarse una vez. Si se encuentra que es n, entonces N veces, por lo que es necesario dividir la situación. Aquí, hay algunos algoritmos cuya complejidad de tiempo tiene el mejor O(1), promedio O(N/2 ) y peores casos O(N), pero en la práctica, la preocupación general es El peor caso de ejecución del algoritmo, por lo que la complejidad temporal de este problema es O(N)

  • Ejemplo 5:
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}
  • Respuesta : O(N^2)

Análisis : este código examina el tipo de burbuja. La especie de burbuja del primer viaje va N veces, el segundo viaje N-1 veces, el tercero N-2, ... El último es 1, la regularidad de los tiempos coincide con la sucesión aritmética, y la suma es (N+ 1)*N/2, por supuesto, este es el más preciso, aquí necesitamos encontrar el elemento que tiene el mayor impacto en el resultado, es decir, N^2, por lo que la complejidad del tiempo es O(N^2)

  • Ejemplo 6:
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n;
	while (begin < end)
	{
		int mid = begin + ((end - begin) >> 1);
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = mid;
		else
			return mid;
	}
	return -1;
}
  • Respuesta : O(logN)

Análisis : Esta pregunta es obviamente una búsqueda binaria. Suponiendo que la longitud de la matriz es N, y se encuentran X veces, entonces 1*2*2*2*2*...*2=N, es decir, 2^X=N, entonces X es igual a la logaritmo de base logarítmica 2 N, y el cálculo de la complejidad del algoritmo, me gusta omitir la abreviatura como logN, porque no es fácil escribir la base en muchos lugares, por lo que la complejidad temporal de este problema es O(logN)

  • Ejemplo siete:
// 计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
  • Respuesta : O(N)

Análisis : si N es 10

  • Ejemplo 8:
long long Fib(int N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

Esta cadena de código es el código presentado al principio. El estilo del código es muy simple. El cálculo de la secuencia de Fibonacci se puede completar en unas pocas líneas. Pero, ¿el código aparentemente simple es realmente "bueno"? Primero calculemos la complejidad del tiempo:

  • Respuesta : O(2^N)

Analizar gramaticalmente:

 Como se puede ver en la figura anterior, la primera fila se ejecuta una vez, la segunda fila se ejecuta 2^1 veces, la tercera fila se ejecuta 2^2 veces y así sucesivamente, es una secuencia proporcional, que se acumula y luego se expresa de acuerdo con el gran orden O. De acuerdo con las reglas de la ley, la complejidad temporal de esta secuencia de Fibonacci es O(2^N).

Sin embargo, de acuerdo con la complejidad temporal de 2^N, que es un número muy grande, cuando n=10, la respuesta se obtiene rápida y fácilmente en el entorno VS, pero cuando n es ligeramente mayor, como 50, tomará mucho tiempo de espera Se tarda un tiempo en calcular los resultados, lo que demuestra que el código conciso no es necesariamente el mejor código.

 Complejidad de tiempo común: O(N^2), O(N), O(logN), O(1)

  • Comparación de complejidad:

2.3, cálculo de la complejidad del espacio

  • La complejidad del espacio también es una expresión matemática, que es una medida de la cantidad de espacio de almacenamiento ocupado temporalmente por un algoritmo durante su funcionamiento. La complejidad del espacio no es cuantos bytes ocupa el programa, porque esto no tiene mucho sentido, entonces la complejidad del espacio es el numero de variables .
  • Las reglas de cálculo de la complejidad espacial son básicamente similares a la complejidad práctica, y también utilizan la notación asintótica de O grande .
  • Nota: El espacio de pila (parámetros de almacenamiento, variables locales, cierta información de registro, etc.) requerido por la función para ejecutarse ha sido determinado durante la compilación, por lo que la complejidad del espacio está determinada principalmente por el espacio adicional solicitado explícitamente por la función en tiempo de ejecución .

ejemplo

  • Ejemplo 1:
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}
  • Respuesta : O(1)

Análisis: En realidad, hay tres espacios abiertos aquí, a saber, final, intercambio e i. Dado que es una variable constante, la complejidad del espacio es O (1). int*a no tiene nada que ver con int n. Algunas personas pueden pensar que se trata de un bucle for y que el intercambio debe abrirse n veces. De hecho, cada vez que entra el bucle, el intercambio se volverá a abrir y se destruirá una vez que finalice el bucle. Y así sucesivamente, el intercambio es siempre el mismo espacio.

¿Y cuándo aparecerá O(n)?

  • 1. malloc una matriz
int *a = (int*)malloc(sizeof(int)*numsSize); //O(N)

La premisa de esta situación es que numsSize debe ser un número desconocido, si es un número específico, entonces la complejidad del espacio sigue siendo O(1)

  • 2. Matriz de longitud variable
int a[numsSize]; //numsSize未知,O(N)
  • Ejemplo 2:
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
	if (n == 0)
		return NULL;
	long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
	fibArray[0] = 0;
	fibArray[1] = 1;
	for (int i = 2; i <= n; ++i)
	{
		fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
	}
	return fibArray;
}
  • Respuesta: O(N+1)

Análisis: Aquí vemos que malloc ha abierto n+1 arrays de tipo long long. Veo que no hay necesidad de calcular demasiado y crear varias variables después, porque la complejidad del espacio es una estimación, por lo que es directamente O( norte)

  • Ejemplo tres:
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
	if (N == 0)
		return 1;
	return Fac(N - 1) * N;
}
  • Respuesta : O(1)

Análisis: la función recursiva aquí es crear un marco de pila, y el número de marcos de pila que se crearán es N, y las variables de cada marco de pila son constantes, y la complejidad espacial de N es O (N).

  • Ejemplo 4:
// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}
  • Respuesta : O(N)

Análisis : el tiempo se ha ido para siempre, se acumula y el espacio se puede reutilizar después de la recuperación. Al recurrir a Fib (3), llame a Fib (2) y Fib (1) en este momento, y llame a Fib (2) para regresar. En este momento, el marco de pila de Fib (2) se destruye. El llamado Fib ( 1) y Fib (2) usan el mismo espacio. De manera similar, Fib (N-1) crea un total de N-1 marcos de pila. De manera similar, llame a Fib (N-2) y solo Fib (N-1) El mismo espacio se usa, lo que muestra completamente que el tiempo se ha ido para siempre, se acumula y el espacio se puede reutilizar después de la recuperación.

3. Ejercicios con requisitos de complejidad

  • Pregunta 1: (números que desaparecen)

Enlace: https://leetcode-cn.com/problems/missing-number-lcci/

 Esta pregunta aclara un requisito: encuentre la manera de completarlo en tiempo O(n), esta pregunta proporcionará dos métodos efectivos y factibles, el texto comienza:

Método 1: Suma - Suma

  • Pensamiento:

Este problema es que falta un número en una serie de enteros consecutivos, entonces sumamos el número de enteros que debe haber a su vez y restamos la suma de todos los elementos del número que falta en el arreglo original, que es el número que queremos .

el código se muestra a continuación:

int missingNumber(int* nums, int numsSize){
    int sum1=0;
    int sum2=0;
    for(int i=0;i<numsSize+1;i++)
    {
        sum1+=i;
    }
    for(int i=0;i<numsSize;i++)
    {
        sum2+=nums[i];
    }
    return sum1-sum2;
}

Método 2: XOR

  • Pensamiento:

Como en el ejemplo 2, aquí asumimos un total de 10 números, luego la matriz de números aquí es [0 - 9], pero falta un número, ya conocemos las reglas de operación XOR (lo mismo es 0, la diferencia es 1) y Dos conclusiones importantes: 1. El XOR de dos números idénticos es igual a 0. 2. El XOR de 0 con cualquier número es igual a ese número arbitrario. Por lo tanto, primero podemos XOR todos los elementos de la matriz original, y luego XOR todos los elementos que teóricamente aumentan secuencialmente de 0 a n, y luego XOR las dos piezas nuevamente para obtener el número que falta.

  • Pantalla de dibujo:

 el código se muestra a continuación:

int missingNumber(int* nums, int numsSize){
    int n=0;
    for(int i=0;i<numsSize;i++)
    {
        n^=nums[i];
    }
    for(int i=0;i<numsSize+1;i++)
    {
        n^=i;
    }
    return n;
}

Nota : el número de bucles en el segundo bucle for debe basarse en numsSize más 1, porque falta un número, por lo que teóricamente la longitud de la matriz aumenta en 1 en la base original.

  • Pregunta 2: (rotar matriz)

Enlace: https://leetcode-cn.com/problems/rotate-array/

 En la idea avanzada de esta pregunta, es claro que se utiliza un algoritmo con complejidad espacial de O(1) para resolver este problema.El texto comienza

Método 1: girar a la derecha K veces, mover uno a la vez

  • Pensamiento:

Primero, defina una variable tmp para almacenar el último elemento de la matriz. En segundo lugar, mueva los primeros valores N-1 hacia atrás. Finalmente, coloque el valor de tmp en la primera posición. como muestra la imagen:

La complejidad temporal de este método es: O(N*K), y la complejidad espacial es O(1). La complejidad espacial de este método cumple con el significado de la pregunta, pero existe el riesgo de que la complejidad temporal sea cuando K %N=N-1 Si es demasiado grande, es O(N^2), así que veamos si hay una mejor manera:

Método 2: Matriz extra abierta

  • Pensamiento:

 Cree una nueva matriz adicional, coloque los últimos elementos K delante de la nueva matriz y luego copie los elementos NK de la matriz original en la parte posterior de la nueva matriz. Sin embargo, la complejidad temporal de este método es O(N), y la complejidad espacial también es O(N), lo que no cumple con el significado del título, y luego cambia:

Método 3: inversión de tres veces

  • Pensamiento:

El primer paso invierte sus primeros NK elementos, el segundo paso invierte sus últimos K elementos y finalmente se invierte el todo. como muestra la imagen:

Este método es muy inteligente, la complejidad del tiempo es O(N) y la complejidad del espacio es O(N), lo cual está en línea con el significado del título.

el código se muestra a continuación:

void reverse(int*nums,int left,int right)
{
    while(left<right)
    {
        int tmp=nums[left];
        nums[left]=nums[right];
        nums[right]=tmp;
        left++;
        right--;
    }
}
void rotate(int* nums, int numsSize, int k){
    k%=numsSize;
    reverse(nums,0,numsSize-k-1);
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-1);
}

 Nota: Cuando k=7 aquí, es equivalente a completar la inversión una vez, es decir, vuelve al estado original, hay reglas a seguir, por lo que el número de inversiones reales es k%=numsSize;

Supongo que te gusta

Origin blog.csdn.net/bit_zyx/article/details/123266353
Recomendado
Clasificación