一、数据结构到底是什么?

说实话,很多初学者一听到「数据结构」四个字就头大。我当年也一样,觉得这玩意儿太抽象了。但后来做了十几年嵌入式开发,我才真正明白——数据结构说白了就是「组织数据的方式」。

你想想看,我们写程序天天跟数据打交道。数据怎么存、怎么取、怎么管理,这就是数据结构要解决的问题。我做过一个智能家居项目,传感器数据量一大,用数组硬存,结果查个数据要遍历半天。后来换成哈希表,速度直接起飞。

1.1 数据结构的分类

数据结构分两大类:逻辑结构存储结构。逻辑结构是数据之间的逻辑关系,存储结构是数据在内存里怎么放的。

逻辑结构又分四种:

  • 集合:数据之间没啥关系,就是一堆东西放一起。比如你手机里的联系人,谁跟谁都没关系。
  • 线性结构:数据排成一队,一对一的关系。数组、链表、栈、队列都属于这一类。
  • 树形结构:一对多的关系,像公司组织架构。二叉树、堆、红黑树都是典型。
  • 图形结构:多对多的关系,像地铁线路图。图论里全是这个。

存储结构就两种:

  • 顺序存储:数据在内存里挨着放,数组就是典型。优点是随机访问快,缺点是插入删除麻烦。
  • 链式存储:数据不挨着,靠指针连起来。链表就是典型。优点是插入删除方便,缺点是不能随机访问。

核心要点:逻辑结构是「怎么想」,存储结构是「怎么存」。同一个逻辑结构可以用不同的存储结构实现。比如线性表,既可以用数组实现,也可以用链表实现。

二、算法复杂度分析——到底快不快?

写代码谁都会,但写得好不好,就看你对复杂度的理解深不深。我面试嵌入式工程师时,必问复杂度分析。为什么?因为嵌入式资源有限,一个低效算法可能直接让系统崩溃。

2.1 时间复杂度

时间复杂度不是代码跑了几秒,而是算法执行时间随数据规模增长的变化趋势。说白了,就是数据量翻倍时,你的代码要多跑多久。

举个简单的例子:

// 例1:O(1) 常数时间
int getFirst(int arr[]) {
    return arr[0];  // 不管数组多大,都是一步
}

// 例2:O(n) 线性时间
int sum(int arr[], int n) {
    int s = 0;
    for (int i = 0; i < n; i++) {
        s += arr[i];  // 数组越大,循环次数越多
    }
    return s;
}

// 例3:O(n²) 平方时间
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

看到没?同样的数据规模,O(n²)的冒泡排序比O(n)的求和慢了不止一个数量级。我在项目中遇到过,一个排序算法没选好,导致系统响应时间从10ms飙到500ms。嗯,后来果断换了快速排序。

2.2 空间复杂度

空间复杂度衡量的是算法运行过程中占用的额外内存。嵌入式开发里,内存比时间更金贵。我曾经在一个STM32项目里,为了省128字节的RAM,把递归改成了迭代。

// 例4:O(1) 常数空间
int add(int a, int b) {
    return a + b;  // 就用了两个参数,不额外占内存
}

// 例5:O(n) 线性空间
int* duplicate(int arr[], int n) {
    int* newArr = (int*)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) {
        newArr[i] = arr[i];  // 额外分配了n个int的空间
    }
    return newArr;
}

避坑指南:我曾经在递归实现斐波那契数列时,没注意空间复杂度,结果n=50时直接栈溢出。递归虽然代码简洁,但每层调用都要压栈,空间复杂度是O(n)。后来改成迭代,空间复杂度降到O(1)。

三、大O表示法——怎么算?

大O表示法,说白了就是忽略常数和低阶项,只关注增长最快的部分。为什么?因为数据规模足够大时,常数和低阶项的影响可以忽略不计。

比如一个算法执行了 3n² + 5n + 10 步,大O表示法就是 O(n²)。因为 n 很大时,3n² 占绝对主导,5n 和 10 根本不值一提。

计算大O的步骤:

  1. 找出基本操作(循环体、递归调用等)
  2. 计算基本操作的执行次数
  3. 保留最高阶项,去掉常数系数

举个例子:

// 双重循环,基本操作是 arr[i][j] 的访问
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        printf("%d ", arr[i][j]);  // 执行 n * n = n² 次
    }
}
// 时间复杂度:O(n²)

小技巧:我习惯先看循环嵌套层数。一层循环大概率O(n),两层大概率O(n²),三层大概率O(n³)。当然,递归和分治算法要单独分析,比如二分查找是O(log n),归并排序是O(n log n)。

四、常见复杂度对比

不同复杂度的算法,性能差距有多大?我直接给你看数据:

大O表示法 名称 n=10 n=100 n=1000 n=10000
O(1) 常数时间 1 1 1 1
O(log n) 对数时间 3 7 10 14
O(n) 线性时间 10 100 1000 10000
O(n log n) 线性对数时间 30 700 10000 140000
O(n²) 平方时间 100 10000 1000000 100000000
O(2ⁿ) 指数时间 1024 巨大 巨大 巨大

看到没?n=10000时,O(n²)需要1亿步,而O(n log n)只需要14万步。差距是700多倍。这就是为什么我总说,选对算法比优化代码细节重要得多。

常见算法的复杂度:

  • O(1):数组随机访问、哈希表查找
  • O(log n):二分查找、平衡二叉树查找
  • O(n):线性查找、数组遍历
  • O(n log n):快速排序、归并排序、堆排序
  • O(n²):冒泡排序、选择排序、插入排序
  • O(2ⁿ):递归斐波那契、旅行商问题暴力解

我的经验:实际开发中,O(n²)以上的算法基本要避免。嵌入式环境尤其如此。如果实在避不开,一定要限制数据规模。我曾经在一个数据采集项目里,用O(n²)的算法处理1000个点,结果MCU直接死机。后来改成O(n log n)的算法,问题解决。

五、知识体系总览

下面这张图,把本章的核心内容串起来了。我建议你多看几遍,建立整体认知。

数据结构与算法基础 数据结构 逻辑结构 存储结构 集合 / 线性 树形 / 图形 顺序存储 链式存储 算法复杂度 时间复杂度 空间复杂度 大O表示法 常见复杂度 递归栈空间 额外内存 常见复杂度对比(n=1000) O(1) → 1步 O(log n) → 10步 O(n) → 1000步 O(n log n) → 10000步 O(n²) → 1000000步 O(2ⁿ) → 巨大 选对算法,比优化代码细节重要100倍!

这张图把数据结构的两大分支和算法复杂度的核心内容都串起来了。你仔细看看,逻辑结构决定你怎么组织数据,存储结构决定你怎么实现,而复杂度分析帮你判断算法好不好。

好了,这一章的内容就到这里。数据结构不是背出来的,是用出来的。我建议你每学一个数据结构,都亲手写代码实现一遍,然后分析它的复杂度。这样印象才深。