WEB前端要知道什么是时间复杂度

630 阅读6分钟

image.png

时间复杂度是用来描述算法执行时间随输入规模变化的一个指标,通常使用大 O 符号来表示。以下是一些常见的时间复杂度表示法及其含义:

常见时间复杂度

  1. O(1) - 常数时间复杂度

下载.png

  • 曲线:水平线
  • 无论输入规模如何,算法的执行时间都是一个固定值。
  • 例如:访问数组的某个元素。
function getFirstElement(arr) {
   return arr[0]; // 无论数组大小如何,访问第一个元素的时间都是常数
}

const numbers = [1, 2, 3, 4, 5];
console.log(getFirstElement(numbers)); // 输出: 1

  1. O(log n) - 对数时间复杂度

下载.png

  • 曲线:逐渐上升的曲线,增长速度逐渐减缓
  • 执行时间随着输入规模的增加而以对数方式增长,通常出现在二分查找等算法中。
  • 例如:在有序数组中查找元素(二分查找)。
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;

    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            return mid; // 找到目标,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1; // 在右半部分继续查找
        } else {
            right = mid - 1; // 在左半部分继续查找
        }
    }
    return -1; // 未找到目标
}

const sortedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(binarySearch(sortedArray, 5)); // 输出: 4

  1. O(n) - 线性时间复杂度

下载.png

  • 曲线:斜率为 1 的直线
  • 执行时间与输入规模成正比,输入规模增加一倍,执行时间也增加一倍。
  • 例如:遍历数组并计算总和。
function sumArray(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i]; // 每个元素都被访问一次
    }
    return sum;
}

const numbers = [1, 2, 3, 4, 5];
console.log(sumArray(numbers)); // 输出: 15

  1. O(n log n) - 线性对数时间复杂度

下载.png

  • 曲线:比 O(n) 增长更快,但比 O(n^2) 增长慢
  • 这种复杂度通常出现在高效的排序算法中,如归并排序和堆排序。它的执行时间是线性增长和对数增长的组合。
  • 例如:归并排序和堆排序。

归并排序

function mergeSort(arr) {
    if (arr.length <= 1) return arr; // 基本情况

    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid)); // 递归排序左半部分
    const right = mergeSort(arr.slice(mid)); // 递归排序右半部分

    return merge(left, right); // 合并已排序的两个部分
}

function merge(left, right) {
    const result = [];
    let i = 0;
    let j = 0;

    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result.push(left[i++]); // 将较小的元素放入结果中
        } else {
            result.push(right[j++]);
        }
    }

    return result.concat(left.slice(i)).concat(right.slice(j)); // 合并剩余元素
}

const unsortedArray = [5, 3, 8, 1, 2];
console.log(mergeSort(unsortedArray)); // 输出: [1, 2, 3, 5, 8]

堆排序

function heapSort(arr) {
    const n = arr.length;

    // 建立最大堆
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }

    // 一个个从堆中取出元素
    for (let i = n - 1; i > 0; i--) {
        // 将当前最大元素移到数组的末尾
        [arr[0], arr[i]] = [arr[i], arr[0]]; // 交换
        heapify(arr, i, 0); // 重新调整堆
    }

    return arr;
}

// 堆化过程
function heapify(arr, n, i) {
    let largest = i; // 初始化最大元素为根节点
    const left = 2 * i + 1; // 左子节点
    const right = 2 * i + 2; // 右子节点

    // 如果左子节点比根节点大
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }

    // 如果右子节点比当前最大元素大
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }

    // 如果最大元素不是根节点
    if (largest !== i) {
        [arr[i], arr[largest]] = [arr[largest], arr[i]]; // 交换
        heapify(arr, n, largest); // 递归堆化
    }
}

// 测试堆排序
const unsortedArray = [5, 3, 8, 1, 2];
const sortedArray = heapSort(unsortedArray);
console.log(sortedArray); // 输出: [1, 2, 3, 5, 8]

  1. O(n^2) - 平方时间复杂度

下载.png

  • 曲线:抛物线
  • 执行时间与输入规模的平方成正比。常见于简单的排序算法,如冒泡排序和选择排序。
  • 例如:嵌套循环遍历数组(冒泡排序)。
function bubbleSort(arr) {
    const n = arr.length;
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr;
}

const unsortedArray = [5, 3, 8, 1, 2];
console.log(bubbleSort(unsortedArray)); // 输出: [1, 2, 3, 5, 8]

  1. O(2^n) - 指数时间复杂度

下载.png

  • 曲线:急剧上升的曲线
  • 执行时间随着输入规模的增加而呈指数增长,通常出现在解决某些组合问题的算法中。
  • 例如:斐波那契数列的递归实现。

斐波那契数列的定义为:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n ≥ 2)

function fibonacci(n) {
    if (n <= 1) {
        return n; // 基础情况
    }
    return fibonacci(n - 1) + fibonacci(n - 2); // 递归调用
}

// 测试斐波那契函数
console.log(fibonacci(5)); // 输出: 5
console.log(fibonacci(10)); // 输出: 55

  1. O(n!) - 阶乘时间复杂度

下载.png

  • 曲线:极其陡峭的曲线
  • 执行时间随着输入规模的增加而呈阶乘增长,通常出现在解决排列组合问题的算法中。
  • 例如:生成一个数组中所有元素的排列组合(全排列的生成)。
function permute(arr) {
    const result = [];
    
    function backtrack(start) {
        if (start === arr.length) {
            result.push([...arr]); // 复制当前排列
            return;
        }

        for (let i = start; i < arr.length; i++) {
            [arr[start], arr[i]] = [arr[i], arr[start]]; // 交换
            backtrack(start + 1); // 递归
            [arr[start], arr[i]] = [arr[i], arr[start]]; // 还原交换
        }
    }

    backtrack(0);
    return result;
}

// 测试全排列函数
const arr = [1, 2, 3];
const permutations = permute(arr);
console.log(permutations); 
// 输出: [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

时间复杂度的解释

  • n 通常代表输入数据的规模,例如数组的长度。
  • O 后面的表达式表示算法在最坏情况下的运行时间增长率。

总结

时间复杂度是评估算法性能的重要指标,理解不同的时间复杂度表示法可以帮助你选择合适的算法以提高程序的效率。

附上时间复杂度曲线图示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>时间复杂度曲线图</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f0f0;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
        }
        .buttons {
            display: flex;
            justify-content: center;
            flex-wrap: wrap;
            gap: 10px;
            margin-bottom: 20px;
        }
        button {
            padding: 10px 15px;
            font-size: 16px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 5px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>时间复杂度曲线图</h1>
        <div class="buttons">
            <button onclick="updateChart('O(1)')">O(1)</button>
            <button onclick="updateChart('O(log n)')">O(log n)</button>
            <button onclick="updateChart('O(n)')">O(n)</button>
            <button onclick="updateChart('O(n log n)')">O(n log n)</button>
            <button onclick="updateChart('O(n^2)')">O(n^2)</button>
            <button onclick="updateChart('O(2^n)')">O(2^n)</button>
            <button onclick="updateChart('O(n!)')">O(n!)</button>
        </div>
        <canvas id="complexityChart" width="400" height="200"></canvas>
    </div>

    <script>
        const ctx = document.getElementById('complexityChart').getContext('2d');
        let complexityChart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '时间复杂度',
                    data: [],
                    borderColor: 'rgb(75, 192, 192)',
                    tension: 0.1
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: {
                        title: {
                            display: true,
                            text: '输入规模 (n)'
                        }
                    },
                    y: {
                        title: {
                            display: true,
                            text: '执行时间'
                        }
                    }
                }
            }
        });

        function updateChart(complexity) {
            const n = Array.from({length: 20}, (_, i) => i + 1);
            let data;
            switch(complexity) {
                case 'O(1)':
                    data = n.map(() => 1);
                    break;
                case 'O(log n)':
                    data = n.map(x => Math.log2(x));
                    break;
                case 'O(n)':
                    data = n;
                    break;
                case 'O(n log n)':
                    data = n.map(x => x * Math.log2(x));
                    break;
                case 'O(n^2)':
                    data = n.map(x => x * x);
                    break;
                case 'O(2^n)':
                    data = n.map(x => Math.pow(2, x));
                    break;
                case 'O(n!)':
                    data = n.map(x => {
                        let result = 1;
                        for(let i = 2; i <= x; i++) result *= i;
                        return result;
                    });
                    break;
            }
            
            complexityChart.data.labels = n;
            complexityChart.data.datasets[0].data = data;
            complexityChart.data.datasets[0].label = complexity;
            complexityChart.update();
        }

        // 初始化图表
        updateChart('O(n)');
    </script>
</body>
</html>