什么是数据结构

在学习具体的数据结构之前,先理清几个基本概念。这些概念在考试和面试中经常出现,也是理解后续所有章节的基础。

数据

数据是计算机能识别、存储和处理的符号集合。数字、字符、图片、音频——凡是能被计算机处理的东西,都是数据。

数据元素

数据元素是数据的基本单位,在程序中通常作为一个整体进行考虑和处理。一个数据元素可以由多个数据项组成。

举个例子:一个学生记录是一个数据元素,它包含姓名、学号、成绩等多个数据项。

概念类比示例
数据整个通讯录全班同学的联系方式
数据元素通讯录中的一条记录张三的完整信息
数据项记录中的一个字段张三的电话号码

数据结构

数据结构是相互之间存在一种或多种特定关系的数据元素的集合。它包含两个层面:

  1. 逻辑结构:数据元素之间的抽象关系(与存储无关)
  2. 物理结构(存储结构):数据在计算机内存中的实际存储方式

逻辑结构

逻辑结构描述数据元素之间的抽象关系,与计算机如何实现无关。主要分为四大类:

逻辑结构关系说明典型例子
集合结构元素同属一个集合,无其他关系最松散的结构{1, 3, 5, 7}
线性结构一对一(1:1)元素排成一条线[[C_线性表_LinearList
树形结构一对多(1:N)层层分支二叉树、B树、文件系统
图形结构多对多(M:N)任意连接社交网络、地图导航
graph TD
    subgraph 集合结构
        S1((1)) --- S2((3)) --- S3((5)) --- S4((7))
    end
    
    subgraph 线性结构
        L1[1] --> L2[2] --> L3[3] --> L4[4]
    end
    
    subgraph 树形结构
        T1[1] --> T2[2]
        T1 --> T3[3]
        T2 --> T4[4]
        T2 --> T5[5]
        T3 --> T6[6]
    end
    
    subgraph 图形结构
        G1((A)) --- G2((B))
        G1 --- G3((C))
        G2 --- G3
        G2 --- G4((D))
        G3 --- G4
    end

关键区分:逻辑结构关注的是”元素之间有什么关系”,不关心”元素存在哪里”。同一个线性结构,可以用数组存储,也可以用链表存储——这就是逻辑结构和物理结构的分离。线性结构中最基础的抽象是线性表,它的定义与两种实现见 线性表与顺序表


物理结构(存储结构)

物理结构描述数据在计算机内存中的实际存储方式。主要分为两种:

顺序存储

把逻辑上相邻的元素,存储在物理位置也相邻的存储单元中。

内存地址:  1000   1004   1008   1012   1016
存储内容: [  1  ][  2  ][  3  ][  4  ][  5  ]
           ←──── 连续的内存块 ────→

特点

  • 支持随机访问(按下标取值 O(1))
  • 插入/删除需要移动大量元素 O(n)
  • 需要预先分配连续空间

典型代表:数组

链式存储

不要求逻辑上相邻的元素在物理位置上也相邻,元素间的逻辑关系通过附加的指针来表示。

内存地址:  1000      2056      1532
存储内容: [ 1 | ●]→[ 2 | ●]→[ 3 | ●]→NULL
           ↑         ↑         ↑
           不连续!靠指针链接

特点

  • 不支持随机访问(必须从头遍历 O(n))
  • 插入/删除只需修改指针 O(1)
  • 动态分配,不需要预先知道数据规模

典型代表:链表

对比

维度顺序存储链式存储
内存分配预先分配连续空间动态分配,按需申请
随机访问O(1)O(n)
插入/删除O(n)(需移动元素)O(1)(已知位置时)
空间利用可能浪费(预分配过多)按需使用,但指针占额外空间
缓存友好好(连续内存,局部性)差(节点分散,cache miss)

还有一种散列存储:用哈希函数直接计算元素的存储地址,典型代表是哈希表。它既不是纯粹的顺序存储,也不是链式存储,而是通过数学映射直达目标位置。


数据的运算

无论哪种数据结构,基本运算都可以归纳为以下几类:

运算类型说明示例
查找(Search)在数据结构中找到指定元素在数组中找值为 5 的元素
插入(Insert)在数据结构中添加新元素在链表头插入新节点
删除(Delete)从数据结构中移除指定元素删除二叉树的叶节点
修改(Update)更新指定元素的值修改数组 arr[3] 的值
遍历(Traverse)访问数据结构中的每个元素中序遍历二叉树

内存和地址

计算机运行程序时,所有数据都存在内存(RAM)里。可以把内存想象成一排带编号的小格子,每个格子能存一小块数据,格子的编号就是它的地址(类似宾馆的房间号)。

程序里写的变量名、数组名,对 CPU 来说都不存在——CPU 只认地址。所以理解”数据放在哪、怎么找到它”,是理解一切数据结构的第一步。

数据宽度

一个格子能存多少数据,取决于数据宽度。比如:

  • char 占 1 字节
  • int 占 4 字节
  • double 占 8 字节

数据宽度也叫”元素大小”。后面学数组时你会看到:第 i 个元素的地址 = 起始地址 + i × 数据宽度,这个公式几乎贯穿整个教程。

字节

字节(Byte)是计算机存储的最小单位,1 字节 = 8 个二进制位(bit),能表示 0 ~ 255 的数字。

文件大小里的 KB、MB、GB 都基于字节:1 KB = 1024 字节,1 MB = 1024 KB。记住 就够了。

CPU

CPU 是计算机的”大脑”,负责执行指令。它工作时只跟寄存器(CPU 内部的小存储)打交道,而寄存器的数据要从内存读来。CPU 的速度非常快,快得内存跟不上——正是这个速度差距,引出了下面的”缓存”。

这一节不需要深究,只要记住一句话:CPU 读数据是有代价的,从不同地方读,速度差很多倍

磁盘和存储

磁盘(硬盘/SSD)是”断电后数据还在”的地方。程序不运行时存在磁盘里,运行时要先加载到内存。

类比:内存是办公桌,磁盘是抽屉。拿东西桌面上最快,抽屉里要弯腰,慢得多(慢约十万倍)。所以数据结构的性能讨论,通常只关心内存,不关心磁盘。

缓存

因为 CPU 太快、内存太慢,计算机在中间加了一层速度更快的缓存(Cache),自动存最近用过的数据。CPU 每次读数据,会顺带把相邻的一小片数据一起搬进缓存(这叫”局部性”)。

这就是为什么:

  • 数组(连续内存)从头到尾读一遍,非常快——因为一次能搬一大片进缓存;
  • 链表(节点散落各处)跳着读,每次都”扑空”,慢得多。

同样的操作,数据摆得越连续,跑得越快。 这是整个教程反复出现的底层逻辑。

偏移

从某段内存的起点往前”数几格”,这个距离叫偏移(offset)。比如数组的第 3 个元素,距开头偏移是 3。

索引

索引(index)就是数组下标 i,从 0 开始。索引和偏移的关系:

一个 int 数组,arr[3] 的字节偏移是 字节。

地址

地址是内存格子的编号,也是变量真正的”身份证”。数组里每个元素的位置:

(基地址就是 arr[0] 的地址)这个公式是数组 O(1) 随机访问的全部秘密——只要一次乘法和加法,就能算出任意元素在哪。

指针

指针是一个”存的不是数据,而是别人地址”的变量。类比:一张纸条上写着”东西在第 12 号柜子里”。

需要知道的几点:

  • 指针指向一个位置,也叫”引用”它;
  • 指针可以为空(NULL),表示”不指向任何地方”,使用前要判断;
  • 链表、树、图这些结构,本质都是靠指针把一个个分散的节点”串”起来。

数据抽象

数据结构可以拆成两层看:

  • 对外:能干什么(提供的操作,比如”插入""查找”);
  • 对内:怎么存(具体实现,比如数组还是链表)。

这两层可以分开。比如”栈”只规定”后进先出、只能从顶部操作”这个规则,底层用数组实现也行、用链表实现也行——使用者完全不用关心。这种”只看规则、不看实现”的思想就是抽象

所以后面学”栈""队列""哈希表”时,先分清它是一套规则(抽象),还是具体的实现,学习会顺畅很多。

数学基础

对数

的意思是:“2 要自乘几次才等于 n”。例如 ,因为

为什么数据结构里到处都是 O(log n):因为很多算法每做一次操作就”砍掉一半”(二分查找、二叉树的查找都是)。砍一半很快到底——,也就是说处理 100 万个数据,最多只需要约 20 步,这就是 O(log n) 强大之处。

这种”指数”记法。计算机里到处是 2 的幂:、内存容量 4GB/8GB/16GB(翻倍增长)、容器扩容”容量 ×2”。所以看到一个东西成倍翻,先想到幂。

几何级数

一串数每一项都是前一项的固定倍数(比如 1, 2, 4, 8, 16…),叫几何级数。它有个重要性质:

为什么学:vector 扩容是”每次容量翻倍”,把扩容的搬移代价加起来正好是几何级数,总和约 2n——所以”平均每次插入的代价是常数”,这就是均摊复杂度(后面容器章节会细讲)。

取模

取模 % 就是取余数:7 % 3 = 1。它把任意大的数字”圈”进一个固定范围,很像时钟:13 点就是 1 点(模 12)。

哪里会用到:环形队列用 (i + 1) % n 转圈;哈希表用 h(k) % M 把任意键映射进桶数组。“取模让数字循环起来”,记住这一句就够。

位运算

数据在计算机里是二进制(一串 0/1),直接对这串 0/1 做的运算叫位运算:

符号含义例子
&与(按位)i & (-i) 取出最低位的 1
|或(按位)把某些位设为 1
<<左移(×2)1 << 3 = 8
>>右移(÷2)8 >> 1 = 4

这一节只需认识符号,知道”二进制、有位运算这回事”。真正用到的地方(树状数组的 lowbit)会单独讲解,到时候再回头看这里即可。

时间复杂度

描述”数据量变大时,操作次数增长得多快”,用大 O 记号表示:

记号含义例子
O(1)常数次,与数据量无关数组按下标取值
O(log n)每翻一倍数据,只多加一步二分查找
O(n)与数据量成正比从头扫一遍
O(n log n)n 次 log 操作快速排序
O(n²)双重循环冒泡排序

两个要点:

  1. 只看增长趋势,忽略常数:3n 和 100n 都写 O(n);
  2. 常见速度排序:

后面每学一个数据结构,都会配一张复杂度表,看懂这张表之前提就够了。

空间复杂度

和时间复杂度一个道理,只不过算的是”这个结构/算法额外占了多少内存”,一般记 O(1)(固定的几个变量)或 O(n)(需要一份跟数据一样大的空间)。

看表时”空间 O(n)“就是”要额外一份同等大小的内存”,不用想得更复杂。

最优解

同一个问题往往有多种解法。选一个”又快又省”的,就是最优解。注意时间和空间常常矛盾

  • 用空间换时间:多存一些中间结果(比如缓存),换来更快;
  • 用时间换空间:少存东西,多算几遍。

学哈希表、记忆化等技巧时,会反复看到这种权衡。

运行时内存布局

程序运行时,它的内存大致分成几块区域:

  • 代码段:存放程序指令本身;
  • 调用栈(stack):函数调用时自动分配空间,一层层”叠”起来,函数返回时自动回收。特点:自动管理、很快、大小有限(递归太深会溢出);
  • 堆(heap):程序员手动申请的空间(C 的 malloc、C++ 的 new),用完后要手动释放。特点:手动管理、灵活、空间大

注意区分:这里的”栈 / 堆”是内存区域,不是数据结构”栈 / 堆”。
数据结构里的”栈”是指”后进先出”的规则——恰好函数调用也是后进先出(最晚调用的函数最先返回),所以这个内存区域才借用了”栈”的名字。这也解释了为什么”递归”总跟”栈”联系在一起。