Iniciantes se preparam para o teste escrito de algoritmo de grandes fabricantes (1) - iteração, recursão, complexidade de tempo, complexidade de espaço

Iterar

Iteração é uma estrutura de controle que executa repetidamente uma tarefa. Na iteração, o programa executará repetidamente um determinado código se certas condições forem atendidas, até que a condição não seja mais atendida.

para loop

forLoops são uma das formas mais comuns de iteração e são adequados para uso quando o número de iterações é conhecido antecipadamente .

Pitão:

def for_loop(n: int) -> int:
    """for 循环"""
    res = 0
    # 循环求和 1, 2, ..., n-1, n
    for i in range(1, n + 1):
        res += i
    return res

Ir:

/* for 循环 */
func forLoop(n int) int {
    
    
    res := 0
    // 循环求和 1, 2, ..., n-1, n
    for i := 1; i <= n; i++ {
    
    
        res += i
    }
    return res
}

enquanto loop

Semelhante aos forloops, whileos loops também são uma forma de implementar iteração. No whileloop, o programa primeiro verificará a condição a cada rodada e, se a condição for verdadeira, continuará a execução, caso contrário, encerrará o loop.

Pitão:

def while_loop(n: int) -> int:
    """while 循环"""
    res = 0
    i = 1  # 初始化条件变量
    # 循环求和 1, 2, ..., n-1, n
    while i <= n:
        res += i
        i += 1  # 更新条件变量
    return res

Ir:

/* while 循环 */
func whileLoop(n int) int {
    
    
    res := 0
    // 初始化条件变量
    i := 1
    // 循环求和 1, 2, ..., n-1, n
    for i <= n {
    
    
        res += i
        // 更新条件变量
        i++
    }
    return res
}

Em whileum loop, como as etapas de inicialização e atualização de variáveis ​​de condição são independentes da estrutura do loop, ele possui forum grau de liberdade maior que um loop . Em geral, foro código para loops é mais compacto , whileos loops são mais flexíveis e ambos podem implementar estruturas iterativas. A escolha de qual utilizar deve ser baseada nas necessidades do problema específico.

Loops aninhados

Podemos aninhar outra estrutura de loop dentro de uma estrutura de loop, tomando forloop como exemplo:

Pitão:

def nested_for_loop(n: int) -> str:
    """双层 for 循环"""
    res = ""
    # 循环 i = 1, 2, ..., n-1, n
    for i in range(1, n + 1):
        # 循环 j = 1, 2, ..., n-1, n
        for j in range(1, n + 1):
            res += f"({
      
      i}, {
      
      j}), "
    return res

Ir:

/* 双层 for 循环 */
func nestedForLoop(n int) string {
    
    
    res := ""
    // 循环 i = 1, 2, ..., n-1, n
    for i := 1; i <= n; i++ {
    
    
        for j := 1; j <= n; j++ {
    
    
            // 循环 j = 1, 2, ..., n-1, n
            res += fmt.Sprintf("(%d, %d), ", i, j)
        }
    }
    return res
}

recursão

A recursão é uma estratégia algorítmica na qual uma função chama a si mesma para resolver um problema. Consiste principalmente em duas etapas.

  1. Pass : O programa se autodenomina cada vez mais profundamente, geralmente passando parâmetros menores ou simplificados, até que uma "condição de término" seja atingida.
  2. Retorno : Após acionar a "condição de término", o programa retorna camada por camada a partir da função recursiva mais profunda e agrega os resultados de cada camada.

Do ponto de vista da implementação, o código recursivo contém principalmente três elementos.

  1. Condição de rescisão : usada para determinar quando passar de “entrega” para “devolução”.
  2. Chamada recursiva : Correspondente a "recursiva", a função chama a si mesma, geralmente inserindo parâmetros menores ou simplificados.
  3. Resultado de retorno : Corresponde a “return”, retornando os resultados do nível recursivo atual para o nível anterior.

Pitão:

def recur(n: int) -> int:
    """递归"""
    # 终止条件
    if n == 1:
        return 1
    # 递:递归调用
    res = recur(n - 1)
    # 归:返回结果
    return n + res

Ir:

/* 递归 */
func recur(n int) int {
    
    
    // 终止条件
    if n == 1 {
    
    
        return 1
    }
    // 递:递归调用
    res := recur(n - 1)
    // 归:返回结果
    return n + res
}

pilha de chamadas

Cada vez que uma função recursiva chama a si mesma, o sistema aloca memória para a função recém-aberta para armazenar variáveis ​​locais, chamar endereços e outras informações. Isto terá duas consequências.

  • Os dados de contexto da função são armazenados em uma área de memória chamada “stack frame space” e não serão liberados até que a função retorne. Portanto, a recursão geralmente consome mais espaço de memória do que a iteração .
  • Chamar funções recursivamente incorre em sobrecarga adicional. Portanto, a recursão geralmente é menos eficiente em termos de tempo do que o loop .

recursão de cauda

Se uma função fizer uma chamada recursiva como a última etapa antes de retornar , a função poderá ser otimizada pelo compilador ou interpretador para torná-la tão eficiente em termos de espaço quanto a iteração. Esta situação é chamada de recursão de cauda.

  • Recursão ordinária : Quando a função retorna para a função de nível superior, ela precisa continuar executando o código, então o sistema precisa salvar o contexto da chamada de nível anterior.
  • Recursão final : A chamada recursiva é a última operação antes do retorno da função, o que significa que após a função retornar ao nível anterior, não há necessidade de continuar a realizar outras operações, portanto o sistema não precisa salvar o contexto do função de nível anterior.

Pitão:

def tail_recur(n, res):
    """尾递归"""
    # 终止条件
    if n == 0:
        return res
    # 尾递归调用
    return tail_recur(n - 1, res + n)

Ir:

/* 尾递归 */
func tailRecur(n int, res int) int {
    
    
    // 终止条件
    if n == 0 {
    
    
        return res
    }
    // 尾递归调用
    return tailRecur(n-1, res+n)
}

Meu entendimento pessoal da recursão final é que assim que a última etapa da chamada recursiva for concluída, todas as funções terminarão e não há necessidade de retornar à camada anterior de funções, uma após a outra.

árvore de recursão

Problema Típico: Sequência de Fibonacci

Pitão:

def fib(n: int) -> int:
    if n == 1 or n == 2:
        return 1
    return fib(n - 1) + fib(n - 2)

Ir:

func fib(n int) int {
    
    
	if n == 1 || n == 2 {
    
    
		return 1
	}
	return fib(n-1) + fib(n-2)
}

Duas funções são chamadas recursivamente dentro de uma função, o que significa que duas ramificações de chamada surgem de uma chamada . Chamadas recursivas contínuas como essa acabarão por produzir uma árvore recursiva com n níveis.

complexidade de tempo

A análise de complexidade de tempo não conta o tempo de execução do algoritmo, mas a tendência de crescimento do tempo de execução do algoritmo à medida que a quantidade de dados aumenta .

O conceito de "tendência de crescimento temporal" é relativamente abstrato. Vamos entendê-lo através de um exemplo. Suponha que o tamanho dos dados de entrada seja n, dadas três funções de algoritmo Ae B:C

Pitão:

# 算法 A 的时间复杂度:常数阶
def algorithm_A(n: int):
    print(0)
# 算法 B 的时间复杂度:线性阶
def algorithm_B(n: int):
    for _ in range(n):
        print(0)
# 算法 C 的时间复杂度:常数阶
def algorithm_C(n: int):
    for _ in range(1000000):
        print(0)
// 算法 A 的时间复杂度:常数阶
func algorithm_A(n int) {
    
    
    fmt.Println(0)
}
// 算法 B 的时间复杂度:线性阶
func algorithm_B(n int) {
    
    
    for i := 0; i < n; i++ {
    
    
        fmt.Println(0)
    }
}
// 算法 C 的时间复杂度:常数阶
func algorithm_C(n int) {
    
    
    for i := 0; i < 1000000; i++ {
    
    
        fmt.Println(0)
    }
}
  • O algoritmo Apossui apenas uma operação de impressão e o tempo de execução do algoritmo não aumenta à medida que n aumenta. Chamamos a complexidade de tempo deste algoritmo de "ordem constante".
  • A operação de impressão no algoritmo Bprecisa ser repetida n vezes, e o tempo de execução do algoritmo aumenta linearmente à medida que n aumenta. A complexidade de tempo deste algoritmo é chamada de "ordem linear".
  • CA operação de impressão no algoritmo precisa ser repetida 1.000.000 vezes. Embora o tempo de execução seja muito longo, não tem nada a ver com o tamanho n dos dados de entrada. Portanto C, a complexidade de tempo de Aé igual à de "ordem constante".

A fórmula da complexidade do tempo é: T(n) = O(f(n)).A análise da complexidade do tempo consiste essencialmente em calcular "o limite superior assintótico da função de quantidade de operação T(n), e O representa uma relação proporcional. Isto O nome completo da fórmula é: a complexidade de tempo assintótica do algoritmo .

Se houver números reais positivos c e números reais n₀, tais que para todo n > n₀, T(n) ≤ cf(n), então f(n) pode ser considerado como um limite superior assintótico de T(n), Escreva como T(n)=O(f(n)). Calcular o limite superior assintótico é encontrar uma função f(n) tal que quando n tende ao infinito, T(n) e f(n) estão no mesmo nível de crescimento, diferindo apenas por um múltiplo do termo constante c.

Então, como determinar o limite superior assintótico f(n)? O processo geral é dividido em duas etapas: primeiro, conte o número de operações e, em seguida, determine o limite superior assintótico.

Passo 1: Conte o número de operações.
Para o código, basta contar linha por linha, de cima para baixo. Contudo, como o termo constante c no c·f(n) acima pode assumir qualquer tamanho, os vários coeficientes e termos constantes na quantidade de operação T(n) podem ser ignorados. Com base neste princípio, as seguintes técnicas de simplificação de contagem podem ser resumidas.
1. Ignore os termos constantes em T(n). Por serem independentes de n, não têm impacto na complexidade do tempo.
2. Omita todos os coeficientes. Por exemplo, repetir 2n vezes, 5n+1 vezes, etc. pode ser simplificado e registrado como n vezes, porque os coeficientes na frente de n não têm impacto na complexidade do tempo.
3. Use multiplicação ao aninhar loops. O número total de operações é igual ao produto do número de operações no loop externo e no loop interno. As técnicas dos pontos 1 e 2 ainda podem ser aplicadas a cada loop.

Dada uma função, podemos contar o número de operações usando as técnicas acima.

Pitão:

def algorithm(n: int):
    a = 1      # +0(技巧 1)
    a = a + n  # +0(技巧 1)
    # +n(技巧 2)
    for i in range(5 * n + 1):
        print(0)
    # +n*n(技巧 3)
    for i in range(2 * n):
        for j in range(n + 1):
            print(0)

Ir:

func algorithm(n int) {
    
    
    a := 1     // +0(技巧 1)
    a = a + n  // +0(技巧 1)
    // +n(技巧 2)
    for i := 0; i < 5 * n + 1; i++ {
    
    
        fmt.Println(0)
    }
    // +n*n(技巧 3)
    for i := 0; i < 2 * n; i++ {
    
    
        for j := 0; j < n + 1; j++ {
    
    
            fmt.Println(0)
        }
    }
}

A fórmula a seguir mostra os resultados estatísticos antes e depois de usar as técnicas acima. A complexidade de tempo de ambas é O(n²).

Insira a descrição da imagem aqui

Etapa 2: Determine o limite superior assintótico.
A complexidade do tempo é determinada pelo termo de ordem mais alta no polinômio T(n). Isto ocorre porque à medida que n se aproxima do infinito, o termo de ordem mais elevada desempenhará um papel dominante e a influência de outros termos pode ser ignorada.

As métricas comuns de complexidade de tempo incluem:

  • Ordem constante O(1)
  • Ordem logarítmica O(logn)
  • Ordem linear O(n)
  • Ordem logarítmica linear O (nlogn)
  • Ordem quadrada O (n²)
  • Ordem exponencial (2^n)
  • Fatorial (n!)

A complexidade de tempo da sequência acima, de cima para baixo, está ficando cada vez maior, e a eficiência de execução está cada vez menor.

  1. Ordem constante O(1)

O número de operações de ordem constante não tem nada a ver com o tamanho dos dados de entrada n, ou seja, não muda com a mudança de n.

Na função a seguir, embora o número de operações sizepossa ser grande, a complexidade de tempo ainda é O(1), pois é independente do tamanho dos dados de entrada n:

Pitão:

def constant(n: int) -> int:
    """常数阶"""
    count = 0
    size = 100000
    for _ in range(size):
        count += 1
    return count

Ir:

/* 常数阶 */
func constant(n int) int {
    
    
    count := 0
    size := 100000
    for i := 0; i < size; i++ {
    
    
        count++
    }
    return count
}

Quando o código acima é executado, seu consumo não aumenta com o crescimento de uma determinada variável, portanto, não importa o tamanho desse tipo de código, mesmo que tenha dezenas de milhares ou centenas de milhares de linhas, ele pode ser representado por O(1) complexidade de tempo.

  1. Ordem linear O(n)

O número de operações de ordem linear cresce linearmente com o tamanho dos dados de entrada n. Ordens lineares geralmente ocorrem em loops de nível único:

Pitão:

def linear(n: int) -> int:
    """线性阶"""
    count = 0
    for _ in range(n):
        count += 1
    return count

Ir:

/* 线性阶 */
func linear(n int) int {
    
    
    count := 0
    for i := 0; i < n; i++ {
    
    
        count++
    }
    return count
}

Neste código, o código no loop for será executado n vezes, portanto o tempo que ele consome muda com a mudança de N. Portanto, esse tipo de código pode usar O(n) para representar sua complexidade de tempo.

  1. Ordem logarítmica O(logn)

Pitão:

def logarithmic(n: int) -> int:
    i = 1
    while i < n:
        i *= 2
    return i

Ir:

func logarithmic(n int) int {
    
    
	i := 1
	for i < n {
    
    
		i *= 2
	}
	return i
}

Como você pode ver no código acima, no loop while, cada vez que i é multiplicado por 2. Após a multiplicação, i fica cada vez mais próximo de n. Vamos tentar resolvê-lo. Suponha que depois de repetir x vezes, i seja maior que 2. Nesse momento, o loop será encerrado. Ou seja, 2 elevado à potência de x é igual a n. Então x = log2 ^ n, o que significa que ao fazer um loop em log2
^ Depois de n vezes, esse código termina. De acordo com a fórmula de mudança de base, 2 também pode ser outros valores constantes, então a complexidade de tempo deste código é: O(logn)

  1. Ordem logarítmica linear O (nlogn)

A ordem logarítmica linear O(nlogn) é na verdade muito fácil de entender. Se você fizer um loop de um código com complexidade de tempo O(logn) n vezes, então sua complexidade de tempo será n * O(logn), que é O(nlogn) .

Pegue o código acima com uma pequena modificação como exemplo:

Pitão:

def linear_log(n: int):
    for m in range(1, n):
        i = 1
        while i < n:
            i *= 2

Ir:

func linearLog(n int) {
    
    
    for m := 1; m < n; m++ {
    
    
        i := 1
        for i < n {
    
    
            i *= 2
        }
    }
}
  1. Ordem quadrada O (n²)

O número de operações de ordem quadrada cresce quadraticamente com o tamanho dos dados de entrada n. A ordem quadrada geralmente ocorre em loops aninhados, onde os loops externo e interno são O(n), então o total é O(n²) :

Pitão:

def quadratic(n: int) -> int:
    """平方阶"""
    count = 0
    # 循环次数与数组长度成平方关系
    for i in range(n):
        for j in range(n):
            count += 1
    return count

Ir:

/* 平方阶 */
func quadratic(n int) int {
    
    
    count := 0
    // 循环次数与数组长度成平方关系
    for i := 0; i < n; i++ {
    
    
        for j := 0; j < n; j++ {
    
    
            count++
        }
    }
    return count
}

Se n em um dos loops for alterado para m, isto é:

Pitão:

def quadratic(n: int) -> int:
    """平方阶"""
    count = 0
    # 循环次数与数组长度成平方关系
    for i in range(n):
        for j in range(m):
            count += 1
    return count

Ir:

/* 平方阶 */
func quadratic(n int) int {
    
    
    count := 0
    // 循环次数与数组长度成平方关系
    for i := 0; i < n; i++ {
    
    
        for j := 0; j < m; j++ {
    
    
            count++
        }
    }
    return count
}

Então sua complexidade de tempo se torna O(m*n).

  1. Ordem exponencial O (2 ^ n)

A "divisão celular" biológica é um exemplo típico de crescimento exponencial: o estado inicial é 1 célula, após uma rodada de divisão torna-se 2, após duas rodadas de divisão torna-se 4 e assim por diante, após n rodadas de divisão há 2 ^n células.

Pitão:

def exponential(n: int) -> int:
    """指数阶(循环实现)"""
    count = 0
    base = 1
    # 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
    for _ in range(n):
        for _ in range(base):
            count += 1
        base *= 2
    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
    return count

Ir:

/* 指数阶(循环实现)*/
func exponential(n int) int {
    
    
    count, base := 0, 1
    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
    for i := 0; i < n; i++ {
    
    
        for j := 0; j < base; j++ {
    
    
            count++
        }
        base *= 2
    }
    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
    return count
}
  1. Ordem fatorial O(n!)

Fatorial geralmente é implementado usando recursão. Conforme mostrado no código a seguir, a primeira camada divide n peças, a segunda camada divide n-1 peças e assim por diante, até que a enésima camada se divida:

Pitão:

def factorial_recur(n: int) -> int:
    """阶乘阶(递归实现)"""
    if n == 0:
        return 1
    count = 0
    # 从 1 个分裂出 n 个
    for _ in range(n):
        count += factorial_recur(n - 1)
    return count

Ir:

/* 阶乘阶(递归实现) */
func factorialRecur(n int) int {
    
    
    if n == 0 {
    
    
        return 1
    }
    count := 0
    // 从 1 个分裂出 n 个
    for i := 0; i < n; i++ {
    
    
        count += factorialRecur(n - 1)
    }
    return count
}

A eficiência de tempo de um algoritmo muitas vezes não é fixa, mas está relacionada à distribuição dos dados de entrada . Suponha que uma matriz de comprimento n seja input nums, que numsconsiste em números de 1 a n, cada número aparece apenas uma vez; mas a ordem dos elementos é interrompida aleatoriamente e o objetivo da tarefa é retornar o índice do elemento 1.

Pitão:

def random_numbers(n: int) -> list[int]:
    """生成一个数组,元素为: 1, 2, ..., n ,顺序被打乱"""
    # 生成数组 nums =: 1, 2, 3, ..., n
    nums = [i for i in range(1, n + 1)]
    # 随机打乱数组元素
    random.shuffle(nums)
    return nums

def find_one(nums: list[int]) -> int:
    """查找数组 nums 中数字 1 所在索引"""
    for i in range(len(nums)):
        # 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)
        # 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)
        if nums[i] == 1:
            return i
    return -1

Ir:

/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */
func randomNumbers(n int) []int {
    
    
    nums := make([]int, n)
    // 生成数组 nums = { 1, 2, 3, ..., n }
    for i := 0; i < n; i++ {
    
    
        nums[i] = i + 1
    }
    // 随机打乱数组元素
    rand.Shuffle(len(nums), func(i, j int) {
    
    
        nums[i], nums[j] = nums[j], nums[i]
    })
    return nums
}

/* 查找数组 nums 中数字 1 所在索引 */
func findOne(nums []int) int {
    
    
    for i := 0; i < len(nums); i++ {
    
    
        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)
        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)
        if nums[i] == 1 {
    
    
            return i
        }
    }
    return -1
}

Vale ressaltar que raramente utilizamos a complexidade de tempo ideal na prática, porque geralmente ela só pode ser alcançada com uma pequena probabilidade, o que pode ser enganoso. A pior complexidade de tempo é mais prática porque fornece um valor de eficiência e segurança para que possamos usar o algoritmo com confiança.

complexidade do espaço

Como a complexidade do tempo não é usada para calcular o tempo específico gasto pelo programa, também devo entender que a complexidade do espaço não é usada para calcular o espaço real ocupado pelo programa.

A complexidade do espaço é uma medida da quantidade de espaço de armazenamento que um algoritmo ocupa temporariamente durante a operação e também reflete uma tendência.

Método de cálculo

Ao contrário da complexidade do tempo, geralmente nos concentramos apenas na pior complexidade do espaço . Isso ocorre porque o espaço de memória é um requisito difícil e devemos garantir que espaço de memória suficiente seja reservado para todos os dados de entrada.

Observando o código a seguir, o “pior” na pior complexidade de espaço tem dois significados.

  • Com base nos piores dados de entrada: quando n<10, a complexidade do espaço é O(1); mas quando n>10, o array inicializado nums ocupa O(n) espaço; portanto, a pior complexidade do espaço é O(n) .
  • Com base no pico de memória durante a execução do algoritmo: por exemplo, o programa ocupa O(1) espaço antes de executar a última linha; ao inicializar o array nums, o programa ocupa O(n) espaço; portanto, a pior complexidade de espaço é O( n).

Pitão:

def algorithm(n: int):
    a = 0               # O(1)
    b = [0] * 10000     # O(1)
    if n > 10:
        nums = [0] * n  # O(n)

Ir:

func algorithm(n int) {
    
    
    a := 0                      // O(1)
    b := make([]int, 10000)     // O(1)
    var nums []int
    if n > 10 {
    
    
        nums := make([]int, n)  // O(n)
    }
    fmt.Println(a, b, nums)
}

Em funções recursivas, você precisa prestar atenção às estatísticas de espaço do quadro de pilha . Por exemplo no código a seguir:

  • A função loop() chama function() n vezes no loop, e function() em cada rodada retorna e libera o espaço do quadro de pilha, então a complexidade do espaço ainda é O(1).
  • A função recursiva recur() terá n recur()s não retornados ao mesmo tempo durante a operação, ocupando assim O(n) espaço de quadro de pilha.

Pitão:

def function() -> int:
    # 执行某些操作
    return 0

def loop(n: int):
    """循环 O(1)"""
    for _ in range(n):
        function()

def recur(n: int) -> int:
    """递归 O(n)"""
    if n == 1: return
    return recur(n - 1)

Ir:

func function() int {
    
    
    // 执行某些操作
    return 0
}

/* 循环 O(1) */
func loop(n int) {
    
    
    for i := 0; i < n; i++ {
    
    
        function()
    }
}

/* 递归 O(n) */
func recur(n int) {
    
    
    if n == 1 {
    
    
        return
    }
    recur(n - 1)
}

A complexidade de espaço mais comumente usada é: O(1), O(logn), O(n), O(n²), O(2^n), organizado de baixo para alto, vamos dar uma olhada abaixo:

  1. Ordem constante O(1)

A ordem constante é comum em constantes, variáveis ​​e objetos cujo número não tem nada a ver com o tamanho n dos dados de entrada.

Deve-se notar que a memória ocupada pela inicialização de variáveis ​​​​ou pela chamada de funções em um loop será liberada após a entrada no próximo loop, portanto o espaço ocupado não será acumulado e a complexidade do espaço ainda será O (1):

Pitão:

def function() -> int:
    """函数"""
    # 执行某些操作
    return 0

def constant(n: int):
    """常数阶"""
    # 常量、变量、对象占用 O(1) 空间
    a = 0
    nums = [0] * 10000
    node = ListNode(0)
    # 循环中的变量占用 O(1) 空间
    for _ in range(n):
        c = 0
    # 循环中的函数占用 O(1) 空间
    for _ in range(n):
        function()

Ir:

/* 函数 */
func function() int {
    
    
    // 执行某些操作...
    return 0
}

/* 常数阶 */
func spaceConstant(n int) {
    
    
    // 常量、变量、对象占用 O(1) 空间
    const a = 0
    b := 0
    nums := make([]int, 10000)
    ListNode := newNode(0)
    // 循环中的变量占用 O(1) 空间
    var c int
    for i := 0; i < n; i++ {
    
    
        c = 0
    }
    // 循环中的函数占用 O(1) 空间
    for i := 0; i < n; i++ {
    
    
        function()
    }
    fmt.Println(a, b, nums, c, ListNode)
}
  1. Ordem linear O(n)

A ordem linear é comum em arrays, listas vinculadas, pilhas, filas, etc. onde o número de elementos é proporcional a n:

Pitão:

def linear(n: int):
    """线性阶"""
    # 长度为 n 的列表占用 O(n) 空间
    nums = [0] * n
    # 长度为 n 的哈希表占用 O(n) 空间
    hmap = dict[int, str]()
    for i in range(n):
        hmap[i] = str(i)

Ir:

/* 线性阶 */
func spaceLinear(n int) {
    
    
    // 长度为 n 的数组占用 O(n) 空间
    _ = make([]int, n)
    // 长度为 n 的列表占用 O(n) 空间
    var nodes []*node
    for i := 0; i < n; i++ {
    
    
        nodes = append(nodes, newNode(i))
    }
    // 长度为 n 的哈希表占用 O(n) 空间
    m := make(map[int]string, n)
    for i := 0; i < n; i++ {
    
    
        m[i] = strconv.Itoa(i)
    }
}

A seguinte situação também é um exemplo: a profundidade de recursão desta função é n, ou seja, há n linear_recur()funções não retornadas ao mesmo tempo, e o espaço do quadro de pilha de tamanho O(n) é usado:

Pitão:

def linear_recur(n: int):
    """线性阶(递归实现)"""
    print("递归 n =", n)
    if n == 1:
        return
    linear_recur(n - 1)

Ir:

/* 线性阶(递归实现) */
func spaceLinearRecur(n int) {
    
    
    fmt.Println("递归 n =", n)
    if n == 1 {
    
    
        return
    }
    spaceLinearRecur(n - 1)
}
  1. Ordem quadrada O (n ^ 2)

A ordem quadrada é comum em matrizes e gráficos, e o número de elementos está relacionado ao quadrado de n:

Pitão:

def quadratic(n: int):
    """平方阶"""
    # 二维列表占用 O(n^2) 空间
    num_matrix = [[0] * n for _ in range(n)]

Ir:

/* 平方阶 */
func spaceQuadratic(n int) {
    
    
    // 矩阵占用 O(n^2) 空间
    numMatrix := make([][]int, n)
    for i := 0; i < n; i++ {
    
    
        numMatrix[i] = make([]int, n)
    }
}

Conforme mostrado abaixo, a profundidade de recursão desta função é n. Em cada função recursiva, uma matriz é inicializada com comprimentos n, n -1,...,2,1 respectivamente. O comprimento médio é n/2, então o valor geral a ocupação é espaço O (n ^ 2):

Pitão:

def quadratic_recur(n: int) -> int:
    """平方阶(递归实现)"""
    if n <= 0:
        return 0
    # 数组 nums 长度为 n, n-1, ..., 2, 1
    nums = [0] * n
    return quadratic_recur(n - 1)

Ir:

/* 平方阶(递归实现) */
func spaceQuadraticRecur(n int) int {
    
    
    if n <= 0 {
    
    
        return 0
    }
    nums := make([]int, n)
    fmt.Printf("递归 n = %d 中的 nums 长度 = %d \n", n, len(nums))
    return spaceQuadraticRecur(n - 1)
}
  1. Ordem exponencial (2^n)

A ordem exponencial é comum em árvores binárias. Observando a figura, o número de nós de uma “árvore binária completa” com altura n é 2 n-1 e ocupa espaço O(2 n):

Pitão:

def build_tree(n: int) -> TreeNode | None:
    """指数阶(建立满二叉树)"""
    if n == 0:
        return None
    root = TreeNode(0)
    root.left = build_tree(n - 1)
    root.right = build_tree(n - 1)
    return root

Ir:

/* 指数阶(建立满二叉树) */
func buildTree(n int) *treeNode {
    
    
    if n == 0 {
    
    
        return nil
    }
    root := newTreeNode(0)
    root.left = buildTree(n - 1)
    root.right = buildTree(n - 1)
    return root
}

Insira a descrição da imagem aqui

  1. Ordem logarítmica O(logn)

Por exemplo, para converter um número em uma string, insira um número inteiro positivo n, cujo número de dígitos é log₁₀n+1, ou seja, o comprimento da string correspondente é log₁₀n+1, então a complexidade do espaço é O(log₁₀n+1) = O(logn).

A redução da complexidade do tempo geralmente ocorre às custas do aumento da complexidade do espaço e vice-versa . Chamamos a ideia de sacrificar espaço de memória para melhorar a velocidade de execução do algoritmo de “troca de espaço por tempo”; por outro lado, é chamada de “troca de tempo por espaço”.

Acho que você gosta

Origin blog.csdn.net/m0_63230155/article/details/132589005
Recomendado
Clasificación