从零开始学算法:时间复杂度、空间复杂度与数据结构完全指南

1,299 阅读6分钟

大家好,我是算法探险家FogLetter,今天要带大家彻底搞懂算法学习的核心要点!很多刚接触编程的小伙伴都会对"算法"这个词感到既熟悉又陌生。熟悉是因为总听别人提起,陌生是因为不知道它到底是什么。简单来说,算法就是解决问题的明确步骤和方法。就像做菜的食谱一样,告诉你第一步做什么,第二步做什么,最终做出美味佳肴。

📌 1. 算法学习路线:从入门到Offer

1.1 为什么要学习算法?

  • 大厂敲门砖:几乎所有一线互联网公司的技术面试都会考察算法能力
  • 编程思维训练:培养逻辑思维和问题解决能力
  • 代码优化:写出更高效、更优雅的代码
  • 职业发展:是进阶高级工程师的必经之路

1.2 高效学习路径

  • LeetCode热题100:大厂高频考题集中营
  • 代码随想录:系统性算法学习宝典

"我带的实习生靠这两样拿了6个大厂offer" —— 某字节跳动技术总监

📌 2. 算法效率的黄金标准

2.1 时间复杂度:你的代码跑得有多快?

时间复杂度是用来估计算法运行时间的一个指标。我们常用大O表示法来描述时间复杂度。

常见复杂度排序: O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(2ⁿ) < O(n!)

常见的时间复杂度(从快到慢排列):

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

    function getFirstElement(arr) {
        return arr[0];  // 无论数组多长,都只执行一次操作
    }
    
  2. O(logn) :对数时间复杂度

    // 二分查找就是典型的O(logn)算法
    function binarySearch(arr, target) {
        let left = 0, right = arr.length - 1;
        while (left <= right) {
            const mid = Math.floor((left + right) / 2);
            if (arr[mid] === target) return mid;
            if (arr[mid] < target) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }
    
  3. O(n) :线性时间复杂度

    // 遍历数组就是O(n)
    function traverse(arr) {
        for (let i = 0; i < arr.length; i++) {
            console.log(arr[i]);
        }
    }
    
  4. O(nlogn) :线性对数时间复杂度

    // 快速排序、归并排序都是O(nlogn)
    
  5. O(n²) :平方时间复杂度

    // 冒泡排序就是O(n²)
    function bubbleSort(arr) {
        for (let i = 0; i < arr.length; i++) {
            for (let j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                }
            }
        }
        return arr;
    }
    
  6. O(2ⁿ) :指数时间复杂度

    // 斐波那契数列的递归实现就是O(2ⁿ)
    function fibonacci(n) {
        if (n <= 1) return n;
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    
  7. O(n!) :阶乘时间复杂度

    // 旅行商问题的暴力解法就是O(n!)
    

计算法则(三步走):

  1. 计算执行次数T(n)
  2. 忽略常数项、低次项、系数
  3. 取最高阶项

🌰 示例分析:

function example(arr) {
    let sum = 0;                   // 1次
    for (let i = 0; i < arr.length; i++) {  // 1+(n+1)+n次
        sum += arr[i];              // n次
    }
    for (let i = 0; i < arr.length; i++) {  // 1+(n+1)+n次
        for (let j = 0; j < arr.length; j++) { // n*(1+(n+1)+n)
            console.log(arr[i], arr[j]);    // n*n次
        }
    }
    return sum;                     // 1次
}
// T(n) = 1 + (1+(n+1)+n) + n + (1+(n+1)+n) + n*(1+(n+1)+n) + n² + 1  
// = 3n + 4 + 3n² + 3n + 1  
// = 3n² + 6n + 5  
// ≈ O(n²)

2.2 空间复杂度:你的代码吃了多少内存?

空间复杂度是对算法在运行过程中临时占用存储空间大小的度量。

算法运行过程中占用的额外空间大小,记作S(n)

常见的空间复杂度:

  1. O(1) :常数空间

    function sum(arr) {
        let result = 0;       // 只用了固定数量的变量
        for (let num of arr) {
            result += num;
        }
        return result;
    }
    
  2. O(n) :线性空间

    function copyArray(arr) {
        let newArr = [];      // 需要和输入数组同样大小的空间
        for (let i = 0; i < arr.length; i++) {
            newArr[i] = arr[i];
        }
        return newArr;
    }
    
  3. O(n²) :平方空间

    function generateMatrix(n) {
        let matrix = [];
        for (let i = 0; i < n; i++) {
            matrix[i] = [];   // 创建n×n的矩阵
            for (let j = 0; j < n; j++) {
                matrix[i][j] = i * j;
            }
        }
        return matrix;
    }
    

📌 3. 数据结构:算法的基石

3.1 线性结构

结构特点应用场景
数组随机访问O(1)高频查询
链表动态内存分配频繁增删
LIFO(后进先出)函数调用栈
队列FIFO(先进先出)消息队列

3.1.1 数组(Array)

特点

  • 连续的内存空间
  • 通过索引随机访问,时间复杂度O(1)
  • 插入/删除元素可能需要移动其他元素

JS中的数组操作

const arr = new Array(5).fill(0);  // 创建长度为5的数组,初始值为0
arr.push(1);      // 尾部插入,O(1)
arr.pop();        // 尾部删除,O(1)
arr.unshift(1);   // 头部插入,O(n)
arr.shift();      // 头部删除,O(n)
arr.splice(2, 0, 5); // 在索引2处插入5,O(n)

3.1.2 链表(Linked List)

特点

  • 非连续的内存空间
  • 每个节点包含数据和指向下一个节点的指针
  • 插入/删除效率高,但随机访问效率低

实现一个简单的链表

class ListNode {
    constructor(val) {
        this.val = val;
        this.next = null;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
        this.size = 0;
    }
    
    add(val) {
        const node = new ListNode(val);
        if (!this.head) {
            this.head = node;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = node;
        }
        this.size++;
    }
}

3.1.3 栈(Stack)和队列(Queue)

栈(LIFO)实现

class Stack {
    constructor() {
        this.items = [];
    }
    
    push(element) {
        this.items.push(element);
    }
    
    pop() {
        return this.items.pop();
    }
    
    peek() {
        return this.items[this.items.length - 1];
    }
    
    isEmpty() {
        return this.items.length === 0;
    }
}

队列(FIFO)实现

class Queue {
    constructor() {
        this.items = [];
    }
    
    enqueue(element) {
        this.items.push(element);
    }
    
    dequeue() {
        return this.items.shift();
    }
    
    front() {
        return this.items[0];
    }
    
    isEmpty() {
        return this.items.length === 0;
    }
}

3.2 非线性结构

  • :层级关系(DOM树、文件系统)
  • :网络关系(社交网络、路由算法)

3.2.1 树(Tree)

二叉树实现

class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

class BinaryTree {
    constructor() {
        this.root = null;
    }
    
    // 插入方法
    insert(val) {
        const newNode = new TreeNode(val);
        if (!this.root) {
            this.root = newNode;
            return;
        }
        
        let current = this.root;
        while (true) {
            if (val < current.val) {
                if (!current.left) {
                    current.left = newNode;
                    break;
                }
                current = current.left;
            } else {
                if (!current.right) {
                    current.right = newNode;
                    break;
                }
                current = current.right;
            }
        }
    }
}

3.2.2 图(Graph)

邻接表表示法

class Graph {
    constructor() {
        this.adjacencyList = {};
    }
    
    addVertex(vertex) {
        if (!this.adjacencyList[vertex]) {
            this.adjacencyList[vertex] = [];
        }
    }
    
    addEdge(v1, v2) {
        this.adjacencyList[v1].push(v2);
        this.adjacencyList[v2].push(v1);
    }
}

📌 4. JS实现数据结构技巧

4.1 数组的高级玩法

// 初始化技巧
const arr = new Array(10);         // 长度10的空数组
const brr = Array(10).fill(1);     // 10个1的数组

// 遍历性能对比(实测数据):
普通for循环 > forEach > map

4.2 手写栈和队列

// 用数组实现栈
class Stack {
    constructor() {
        this.items = [];
    }
    push(element) {
        this.items.push(element);
    }
    pop() {
        return this.items.pop();
    }
}

// 用数组实现队列
class Queue {
    constructor() {
        this.items = [];
    }
    enqueue(element) {
        this.items.push(element);
    }
    dequeue() {
        return this.items.shift();
    }
}

📌 5. 算法实战:排序算法对比

5.1 冒泡排序 vs 快速排序

算法时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(1)稳定
快速排序O(nlogn)O(logn)不稳定
// 快速排序实现
function quickSort(arr) {
    if (arr.length <= 1) return arr;
    const pivot = arr[0];
    const left = [];
    const right = [];
    for (let i = 1; i < arr.length; i++) {
        arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
    }
    return [...quickSort(left), pivot, ...quickSort(right)];
}

📌 6. 大厂面试避坑指南

6.1 常见误区

  1. 只刷题不总结 → 建立错题本
  2. 死记硬背 → 理解算法思想
  3. 忽视边界条件 → 多写测试用例

6.2 面试黄金法则

  • 先clarify问题
  • 说思路再写代码
  • 分析复杂度
  • 讨论优化空间

🎯 最后的话

算法学习就像打游戏升级:

  1. 新手村(基本数据结构)
  2. 中级副本(经典算法)
  3. 终极Boss(动态规划、图论)

互动环节: 👉 你在学习算法时遇到过哪些困难? 👉 你最喜欢的算法是什么?为什么? 👉 对本文内容有任何疑问,欢迎在评论区留言讨论!

我是算法探险家FogLetter,关注我,带你轻松拿下大厂Offer!🚀

如果觉得这篇文章有帮助,别忘了点赞收藏哦!我们下期再见!