Por que a análise de complexidade de tempo é necessária?
Por meio de estatísticas e monitoramento, pode-se obter o tempo de execução do algoritmo e o tamanho da memória ocupada, porém esse método estatístico
apresenta muitas deficiências, como:
- Os resultados do teste dependem do ambiente de teste. Por exemplo, se o chip do PC de teste for alterado de i7 para i5, o tempo de execução aumentará.
- Os resultados do teste dependem do tamanho dos dados de teste, como classificação de dados em pequena escala, a classificação por inserção é mais rápida do que a classificação rápida
Representação da Complexidade do Tempo
Grande notação O (importante)
Definição: Se e somente se houver dois parâmetros c > 0, n0 > 0, para todo n >= n0, f(n) <= cg(n), então f(n) = O(g(n ))
, como mostrado na figura:
Exemplo de complexidade de tempo na notação 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;
}
Assumindo o código acima, o tempo para executar uma linha de código é t, e o tempo total gasto é (2*n^2+2*n+4)*t
. Quando n é muito grande, o tempo gasto pelo código acima
depende apenas de n^2, ou seja, T(n) = O(n 2), a complexidade de tempo do código acima é O(n 2)
Simplificando: a grande notação O é para ignorar as constantes, ordens baixas e coeficientes na fórmula, e só precisa registrar a magnitude da ordem maior
Grande notação Ω (apenas entenda)
Definição: Se houver números positivos c e n0, de modo que para todo n >= n0,
f(n) >= cg(n), então f(n) = Ω(g(n)), conforme mostrado na figura :
Grande notação θ (apenas entenda)
Definição: Uma função é chamada θ(g(n)) se estiver tanto no conjunto O(g(n)) quanto no conjunto Ω(g(n )
). Ou seja, quando os limites superior e inferior são iguais, pode-se utilizar a representação θ grande, conforme a figura:
Escala de complexidade de tempo comumente usada
- Ordem constante O(1)
O(1) é apenas uma representação da complexidade de tempo em nível constante, não que apenas uma linha de código seja executada. Por exemplo, este código, mesmo que tenha 3 linhas, sua complexidade de tempo é O(1),
não O(3).
int i = 8;
int j = 6;
int sum = i + j;
Desde que o tempo de execução do código não aumente com o aumento de n, a complexidade de tempo do código é registrada como O(1). Ou, em geral, desde que não haja instruções de loop ou declarações recursivas no algoritmo
, mesmo que haja dezenas de milhares de linhas de código, sua complexidade de tempo é Ο(1).
- Ordem logarítmica O(logn)
- Ordem linear O(n)
- Ordem linear-logarítmica O(n * logn)
- Ordem quadrada O(n 2), ordem legislativa O(n 3), k ordem quadrada O(n^k), etc.
- Ordem exponencial O(2^n)
- Ordem fatorial O(n!)
Análise de Complexidade de Tempo
Calcule o número de execuções para determinar
A complexidade de tempo de alguns algoritmos pode ser determinada simplesmente contando o número de vezes que eles são executados.
Exemplo 1, um algoritmo para calcular a soma de dois arrays:
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 e n não são iguais, o número de execuções é m+n, então a complexidade de tempo é O(m+n)
Exemplo 2, algoritmo de classificação de bolhas:
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;
}
}
}
}
O número de vezes que o código acima é executado é n*(n+1)/2. Devido à grande notação O, constantes, ordens baixas e coeficientes precisam ser removidos, então a complexidade de tempo é O(n^2)
O exemplo 3 requer um pouco de matemática para obter a complexidade do tempo:
i=1;
while (i <= n) {
i = i * 2;
}
Conforme mostrado no código, o tempo de execução x deste programa satisfaz n = 2^0 * 2^1 * 2^2 * ... *2^x, ou seja, x = logn, então a complexidade do tempo é O( logar)
árvore de execução
Para funções recursivas, o número de chamadas recursivas raramente aumenta linearmente com o tamanho da entrada. Nesse caso, é melhor usar uma árvore de execução, que é uma árvore usada para representar o fluxo de execução de uma função recursiva. Cada nó na árvore representa uma chamada para uma função recursiva. Assim, o número total de nós na árvore corresponde ao número de chamadas recursivas durante a execução.
O seguinte é o problema de programação do número de Fibonacci no leetcode .
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。
该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
O código de resposta é o seguinte (observe que o código a seguir pode ser otimizado por memorização)
class Solution {
public int fib(int N) {
if(N == 0) return 1;
return fib(N-1)+fib(N-2);
}
}
O diagrama abaixo (fonte da imagem leetcode) mostra a árvore de execução usada para calcular o número de Fibonacci f(4).
Em uma árvore binária completa com n níveis, o número total de nós é 2^n -1. Portanto, o limite superior (embora não estrito) do número de recursões em f(n) também é 2^n -1. Então podemos estimar a complexidade de tempo do algoritmo como O(2^n)
outro
Complexidade de tempo do melhor, pior e médio caso
Exemplo, para encontrar a posição de x na 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;
}
Quando x está na posição i=0 do array, o programa só precisa ser executado uma vez, e a complexidade de tempo é O(1);
Quando x não está na matriz, o programa precisa percorrer a matriz e a complexidade de tempo é O(n);
Para expressar a complexidade de tempo diferente do código em diferentes situações, precisamos introduzir três conceitos: complexidade de tempo de melhor caso , complexidade de tempo de pior caso e complexidade de tempo de caso médio . Mas a probabilidade da melhor e pior complexidade de tempo é pequena.Para representar melhor a complexidade do caso médio, precisamos da complexidade de tempo do caso médio.
Tomando o código acima como exemplo, a probabilidade de x existir ou não é 1/2, então a probabilidade de x existir e aparecer na posição 0 ~ n-1 é (1+2+...+n)/2n , e a probabilidade de x não existir é n/2, ou seja, (3*n+1)/4, usando a notação O grande, a complexidade de tempo é O(n)
Complexidade de tempo amortizado
A maioria dos casos de complexidade de execução de código é de baixo nível. Quando casos individuais são de alto nível de complexidade e têm uma relação de tempo, a complexidade individual de alto nível pode ser distribuída uniformemente para a complexidade de baixo nível
. Resultado basicamente amortizado é igual a complexidade de baixo nível
为了更全面,更准确的描述代码的时间复杂度,才要引入最好、最坏、平均情况时间复杂度以及均摊时间复杂度。但是只有代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。
Complexidade de tempo de algoritmos comuns
Durante a entrevista, muitas vezes você é solicitado a escrever o código à mão. A propósito, você é questionado sobre a complexidade de tempo do algoritmo, por isso é muito necessário lembrar a complexidade de tempo dos algoritmos comuns.
Algoritmo de Ordenação
Algoritmo de Ordenação | complexidade de tempo |
---|---|
Tipo de bolha | O(n^2) |
tipo de inserção | O(n^2) |
tipo de seleção | O(n^2) |
ordenação rápida | O(nlogn) |
classificação de mesclagem | O(nlogn) |
tipo de balde | Sobre) |
tipo de contagem | Sobre) |
classificação radix | Sobre) |
Algoritmo de travessia de árvore binária
Algoritmo de travessia de árvore binária | complexidade de tempo |
---|---|
Traversal de pré-ordem (implementação recursiva) | Sobre) |
Traversal de pré-ordem (implementação de iteração) | Sobre) |
Traversal inorder (implementação recursiva) | Sobre) |
Traversal inorder (implementação iterativa) | Sobre) |
Traversal pós-ordem (implementação recursiva) | Sobre) |
Traversal pós-ordem (implementação de iteração) | Sobre) |
referência
- Análise de complexidade de tempo da recursão do leetcode
- MOOC da Universidade Chinesa - Curso de Algoritmo e Estrutura de Dados
- Coluna "The Beauty of Data Structure and Algorithm" da Geek Time