[Estructura de datos] Complejidad temporal y espacial del algoritmo

Tabla de contenido

1. ¿Qué es un algoritmo?

1.1 Complejidad del algoritmo

2. La complejidad temporal del algoritmo

2.1 El concepto de complejidad temporal

Calcule cuántas veces se ha ejecutado en total la instrucción ++count en Func1

2.2 Representación asintótica de la gran O

2.3 Ejemplos de cálculos de complejidad de tiempo comunes 

Ejemplo 1: Ejecutar 2N+10 veces

Ejemplo 2: Ejecutar M+N veces

Ejemplo 3: Ejecutado 100000000 veces

Ejemplo 4: Calcular la complejidad temporal de strchr

Ejemplo 5: Calcular la complejidad temporal de BubbleSort

Ejemplo 6: Calcular la complejidad temporal de BinarySearch

Ejemplo 7: Complejidad temporal de la computación Factorial Recursive Fac

Ejemplo 8: Calcular la complejidad temporal de la recursividad de Fibonacci Fib

3. Complejidad espacial del algoritmo

Ejemplo 1: Calcular la complejidad espacial de BubbleSort

Ejemplo 2: Calcular la complejidad espacial de Fibonacci

Ejemplo 3: Calcular la complejidad espacial de Fac recursiva factorial

4. Comparación de complejidad común


1. ¿Qué es un algoritmo?

algoritmo:

Algoritmo : Es un proceso de cálculo bien definido que toma uno o un conjunto de valores como entrada y produce uno o un conjunto de valores como salida. En pocas palabras, un algoritmo es una serie de pasos computacionales utilizados para transformar datos de entrada en resultados de salida.
Aplicaciones comunes para clasificación / búsqueda binaria
Características del algoritmo:

1. Infinito. Un algoritmo debe contener pasos finitos de operaciones, no infinitos. De hecho, "finito" a menudo significa "dentro de límites razonables". Si a una computadora se le permite ejecutar un algoritmo que tarda 1000 años en completarse, aunque es finito, excede un límite razonable y la gente no lo considera un algoritmo efectivo.

2. Certeza. Cada paso en el algoritmo debe ser definido , no vago y ambiguo. Cada paso del algoritmo no debe interpretarse con un significado diferente, pero debe ser muy claro. Es decir, el significado del algoritmo debe ser único y no debe producir "ambigüedad".

3. Hay cero o más entradas .La llamada entrada se refiere a la información necesaria que se obtiene del mundo exterior cuando se ejecuta el algoritmo.

4. Tiene una o más salidas. El propósito de un algoritmo es resolverlo, y un algoritmo sin salida no tiene sentido .

5. Eficacia. Cada paso en el algoritmo debe ser capaz de funcionar de manera eficiente. y obtener resultados definitivos.

1.1 Complejidad del algoritmo

Una vez que el algoritmo se compila en un programa ejecutable, necesita consumir recursos de tiempo y recursos de espacio ( memoria ) cuando se ejecuta. Por lo tanto, para medir la calidad de un algoritmo, generalmente se mide desde dos dimensiones de tiempo y espacio , es decir, complejidad de tiempo y complejidad de espacio.
La complejidad del tiempo mide principalmente qué tan rápido se ejecuta un algoritmo , mientras que la complejidad del espacio mide principalmente el espacio adicional (cuánta memoria) necesita un algoritmo para ejecutarse . En los primeros días del desarrollo de las computadoras, las computadoras tenían muy poca capacidad de almacenamiento. Así que estoy muy preocupado por la complejidad del espacio. Sin embargo, con el rápido desarrollo de la industria informática, la capacidad de almacenamiento de las computadoras ha alcanzado un nivel muy alto. Así que ya no necesitamos prestar especial atención a la complejidad espacial de un algoritmo.

2. La complejidad temporal del algoritmo

2.1 El concepto de complejidad temporal

Definición de complejidad temporal: en informática, la complejidad temporal de un algoritmo es una función ( fórmula funcional matemática, no esas funciones anidadas en lenguaje C ) que describe cuantitativamente el tiempo de ejecución del algoritmo. Teóricamente hablando, el tiempo que tarda en ejecutarse un algoritmo no se puede calcular, solo cuando pones tu programa en la máquina y lo ejecutas puedes saberlo. Pero, ¿necesitamos probar cada algoritmo en la computadora? Es posible probarlos todos en la computadora, pero es muy problemático, por lo que existe un método de análisis de la 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 .

Calcule cuántas veces se ha ejecutado en total la instrucción ++count en Func1

Probemos:
// 请计算一下Func1中++count语句总共执行了多少次?
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);
}

La fórmula funcional de la complejidad del tiempo (es decir, el número de operaciones básicas realizadas por Func1 ):

                                               F(N)=N^2+2N+10

Pero esta expresión es demasiado precisa, demasiado detallada y demasiado engorrosa. La complejidad del tiempo no es calcular con precisión el número de ejecuciones de esta función matemática, sino asignarle un nivel , qué magnitud es .

Por ejemplo: al igual que Ma Yun y Ma Huateng, no necesita preocuparse por la cantidad específica de sus cuentas, solo necesita saber que son ricos.
Valor exacto F(N)=N^2+2N+10 Valor estimado O(N^2)
norte = 10
F(N) = 130 100
norte = 100
F(N) = 10210
10000
norte = 1000
F(N) = 1002010
1000000
Conclusión 1:
Cuanto mayor sea N, menor será el efecto del último elemento en el resultado, es decir, el elemento con el orden más alto (N^2) es el elemento más influyente y se conserva el elemento de orden más alto.
Conclusión 2:
A través de lo anterior, encontraremos que la representación progresiva de la gran O elimina aquellos elementos que tienen poca influencia en el resultado , y expresa el número de ejecuciones de manera concisa y clara.
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 notación asintótica de O grande.
Siempre que esté representado por la gran O, significa que es un valor estimado.

2.2 Representación asintótica de la gran O

Notación Big O ( Big O notation ): es una notación matemática utilizada para describir el comportamiento asintótico de una función.
Derivación del método de orden Big O :
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 mantiene el término de mayor orden .
3. Si existe el elemento de mayor orden y no es 1 , elimine la constante multiplicada por este elemento . El resultado es un gran pedido O.
Hay complejidades de tiempo en el mejor, promedio y peor de los casos para algunos algoritmos:
Peor caso: número máximo de ejecuciones para cualquier tamaño de entrada ( límite superior )
Caso promedio: número esperado de ejecuciones para cualquier tamaño de entrada
Mejor caso: número mínimo de ejecuciones ( límite inferior ) para cualquier tamaño de entrada
Por ejemplo: para buscar un dato x en una matriz de longitud N :
Mejor caso: 1 encontrado
Peor caso: N veces encontrado
Caso promedio: N/2 encontrados
En la práctica, la situación general se refiere a la peor operación del algoritmo , por lo que la complejidad temporal de la búsqueda de datos en el arreglo es O(N)

2.3 Ejemplos de cálculos de complejidad de tiempo comunes 

Ejemplo 1: Ejecutar 2N+10 veces

// 计算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);
}
La operación básica se realiza 2N+10 veces, y la complejidad de tiempo es O(N) al derivar el método de orden O grande.

Ejemplo 2: Ejecutar M+N veces

// 计算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);
}
En el Ejemplo 2 , la operación básica se realiza M+N veces, hay dos incógnitas M y N , y la complejidad temporal es O(N+M)
No se puede decir que N es infinitamente grande y M no es importante. A menos que diga daría una relación:
N es mucho mayor que M, la complejidad temporal es O(N)
M es mucho mayor que N, entonces la complejidad del tiempo es O(M)
Cuando M es igual a N o la diferencia entre los dos no es grande, la complejidad temporal es O(M+N)

Ejemplo 3: Ejecutado 100000000 veces

void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100000000; ++k)
	{
		++count;
	}
	printf("%d\n", count + N);
}

int main()
{
	Func4(100000);
	Func4(1);

	return 0;
}

Ejecución: de hecho, la velocidad de la CPU es muy rápida y se puede ignorar la diferencia en el número de ejecuciones, por lo que la complejidad del tiempo sigue siendo O (1)

O(1) no representa 1 tiempo, sino un tiempo constante, incluso si k<1 billón, también es O(1)

La constante más grande que normalmente podemos escribir es de aproximadamente 4 mil millones (el rango que puede representarse con un número entero), y la CPU puede soportarlo.

En el Ejemplo 3, la operación básica se realiza 100000000 veces, y la complejidad de tiempo es O(1) al derivar el método de orden O grande

Ejemplo 4: Calcular la complejidad temporal de strchr

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );

Se trata de la implementación simulada de strchr

#include<stdio.h>
#include<assert.h>
char* my_strchr(const char* str, const char ch)
{
	assert(str);
	const char* dest = str;
	while (dest != '\0' && *dest != ch)
	{
		dest++;
	}
	if (*dest == ch)
		return (char*)dest;
	return NULL;
}
int main()
{
	char* ret = my_strchr("hello", 'l');
	if (ret == NULL)
		printf("不存在");
	else
		printf("%s\n", ret);
	return 0;
}

Según tengo entendido, la diferencia entre strchr y strstr: strstr es ingresar una cadena de caracteres y buscar en la cadena principal, mientras que strchr es ingresar un carácter y luego buscar en la cadena principal. Este enlace tiene puntos de conocimiento sobre strstr: http://t.csdn.cn/NEaip

Si la búsqueda falla, devuelve NULL. Si la búsqueda tiene éxito, se devolverá la dirección del primer carácter y luego se imprimirá hasta el final de '\0'

entonces:

La complejidad de tiempo de especificar la longitud de esta matriz y luego encontrarla es O (1). Si la longitud no está clara, la longitud es N, entonces se requiere recursión N veces, y la complejidad de tiempo es O (N)
En el Ejemplo 4 , la operación básica se realiza una vez en el mejor de los casos y en el peor de los casos N veces. La complejidad del tiempo es generalmente la peor y la complejidad del tiempo es O(N)

Ejemplo 5: Calcular la complejidad temporal de BubbleSort

// 计算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;
 }
}
Ilustración:

 Luego, el número de comparaciones constituye una secuencia aritmética: utilice la fórmula de suma de secuencias aritméticas para obtener el número final de ejecuciones F(N)=(N-1)*N/2;

Esta pregunta sobre los bucles no significa que si hay dos capas de anidamiento de bucles, se juzga directamente que su complejidad temporal es O(N^2), porque si se conoce el número de comparaciones (bucle exterior n<10, bucle interior n< 1000000) que es O(1), y habrá una versión optimizada de bubble sorting. En el caso de order, su complejidad temporal es O(N), y solo se utiliza el ciclo exterior.
Ejemplo 5 La operación básica se ejecuta mejor N veces, y la peor se ejecuta (N*(N+1)/2 veces. Al derivar el método de gran orden O + complejidad temporal, generalmente se ve lo peor y la complejidad temporal es O(N^2)

Ejemplo 6: Calcular la complejidad temporal de BinarySearch

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
 assert(a);
 int begin = 0;
 int end = n-1;
 // [begin, end]:begin和end是左闭右闭区间,因此有=号
 while (begin <= end)
 {
 int mid = begin + ((end-begin)>>1);
 if (a[mid] < x)
 begin = mid+1;
 else if (a[mid] > x)
 end = mid-1;
 else
 return mid;
 }
 return -1;

}

Ilustración:

Suponiendo que se encuentran x veces, entonces excepto x 2

2^x =N --> x = log2N   

Por lo tanto, se puede multiplicar por 2 desde la última búsqueda hasta la longitud de la matriz original

Ejemplo 6 La operación básica se realiza una vez en el mejor de los casos, O(log2N) veces en el peor, y la complejidad del tiempo es O(log2N) ps: logN significa que la base es 2 y el logaritmo es N en el análisis de algoritmos. En algunos lugares se escribirá como lgN.

Ejemplo 7: Complejidad temporal de la computación Factorial Recursive Fac

Calcule la complejidad temporal de los siguientes dos fragmentos de código
//实例7:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

long long Fac(size_t N)
{
	if (0 == N)
		return 1;

	for (size_t i = 0; i < N; i++)
	{

	}

	return Fac(N - 1) * N;
}

Ilustración:

El número de ejecuciones de la declaración de bucle for (si hay otras declaraciones de bucle) en cada llamada de función a la izquierda, el 1 a la izquierda significa que es un número constante de veces, no 1 vez (es decir, si hay no hay declaración de bucle en la función, hay varias declaraciones if, la complejidad del tiempo también es O (1)).

El de la derecha simplemente significa que hay N+1 llamadas de función, y las declaraciones de bucle en cada llamada de función se ejecutan N+1 veces, por lo que las declaraciones de bucle en cada función llamada recursivamente deben sumarse.

Puntos adicionales:

El tiempo se suma, no se multiplica Por ejemplo, el retorno Fac(N-1)*N en la figura anterior: Significa que el último resultado se multiplica por N, pero el número de ejecuciones también es una vez, porque el * en este lugar es para la computadora Es solo un comando. La complejidad temporal se calcula como el número de ejecuciones de esta instrucción durante el proceso del programa.

Ejemplo 8: Calcular la complejidad temporal de la recursividad de Fibonacci Fib

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
	if (N < 3)
		return 1;

	return Fib(N - 1) + Fib(N - 2);

}

Ilustración:

 La suma del número de ejecuciones se ajusta a la secuencia geométrica: use el método de resta fuera de lugar

Esta pregunta puede entenderse de la siguiente manera:

Con respecto al triángulo, el área blanca será mucho más grande que el área negra cuando N es más grande, y la complejidad del tiempo se usa para calcular la magnitud de una determinada función matemática y asignarle un nivel, por lo que puede considerarse como un cálculo en el estado de los elementos completos, y luego la suma de los tiempos de ejecución constituye una secuencia geométrica, y la complejidad temporal del cálculo es O (2 ^ N) mediante el uso de la notación asintótica O grande.

3. Complejidad espacial del algoritmo

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 la operación .
La complejidad del espacio no es cuántos bytes ocupa el programa , porque esto no es muy significativo, por lo que la complejidad del espacio se calcula por el número de variables .
Las reglas de cálculo de la complejidad espacial son básicamente similares a las de la complejidad práctica, y también se utiliza la notación asintótica O grande .
Nota: El espacio de pila ( parámetros de almacenamiento, variables locales, alguna información de registro, etc. ) requerido por la función en tiempo de ejecución se ha determinado durante la compilación, por lo que la complejidad del espacio está determinada principalmente por el espacio extra solicitado explícitamente por la función en tiempo de ejecución .

Ejemplo 1: Calcular la complejidad espacial de BubbleSort

// 计算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;
	}
}

Debido a que aquí solo se crean las variables end, exchange e i, solo se calcula la cantidad de variables, independientemente del tipo de variable, no se cuenta como la cantidad específica de bytes en el espacio, y todas se crean en un bucle. , por lo que la complejidad del espacio es O ( 1 ) .

En cuanto a los parámetros formales int *a, e int n, no serán contados en la complejidad del espacio.

Ejemplo 2: Calcular la complejidad espacial de Fibonacci

// 计算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;
}

Complejidad del espacio, calcula cuánto espacio adicional ha abierto dentro de esta función. Si es un número constante , es O1. Si el tamaño de la abertura es incierto , generalmente es O (N).

Entonces la complejidad del espacio es O(N).

Ejemplo 3: Calcular la complejidad espacial de Fac recursiva factorial

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

Ilustración:

Se llaman recursivamente N capas. Cada llamada crea un marco de pila. Cada marco de pila usa un espacio constante O(1).
Dado que aquí se llaman N funciones y no hay retorno, el total es O(N)

Ejemplo 4: Calcular la complejidad espacial de la recursión de Fibonacci Fib (dos recursiones)

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

Prefacio:

La destrucción del espacio no significa que el espacio se destruya por completo, sino que se devuelve el derecho a utilizarlo al sistema operativo .

Debido a que el espacio de memoria pertenece al proceso del sistema operativo, por ejemplo, le permite malloc una pieza de espacio, obtendrá el derecho a usar este espacio y Free devolverá el derecho a usar el espacio al sistema operativo.

El tiempo se ha ido para siempre El tiempo es un cálculo acumulativo, el espacio se puede reutilizar sin un cálculo acumulativo
En pocas palabras, la función de la derecha y la función de la izquierda comparten un marco de pila.

Código en ejecución: la pila crece hacia abajo, y llamar a Func1 y Func2 es equivalente a compartir un espacio, porque después de que se destruye Func1, cuando se crea Func2, la ubicación sigue siendo la misma y la dirección también es la misma.

Debido a que una de la función principal y una de Func1 están en diferentes marcos de pila, pueden tener el mismo nombre.

El ejemplo 4 llama recursivamente N veces, abre N marcos de pila y cada marco de pila usa un espacio constante. La complejidad del espacio es O(N)

Al llamar, cree un marco de pila;

Al regresar, destruir. (regresar al sistema operativo)

4. Comparación de complejidad común

La complejidad común del algoritmo general es la siguiente:

Ilustración:

 Al final de este capítulo, si hay alguna deficiencia, por favor corríjame.

Supongo que te gusta

Origin blog.csdn.net/weixin_65186652/article/details/131036702
Recomendado
Clasificación