【复习】数据结构
观前注意:
本博客为作者复习时根据《你好,算法》,以自己对数据结构与算法的理解简记而成,概念定义并不保证完全标准。有异议以权威资料为准。
本文属于【数据结构】篇。
一、前序部分
- 复杂度分析:描述了随着输入数据大小的增加,算法所需时间和空间的增长趋势。
- 时间复杂度推算方法:
- 统计操作数量T(n)
- 取T(n)最高项
- 空间复杂度推算方法:
- 以最差输入数据为准
- 以算法运行中的峰值内存为准

-
迭代:程序在满足一定条件下重复执行某段代码,直至不再满足。自下而上
- for循环:预先知道迭代次数。
- while循环:每轮先检查条件。自由度更高
-
递归:通过调用函数自身来解决问题。自上而下
- 递:不断深入地调用自身,通常传入更小/更简化的参数,直至“终止条件”
- 归:触发“终止条件”后,从最深层逐层返回,汇聚每层结果
【递归三要素】
- 终止条件(最先写)
- 本轮要做的事(只干本轮,不想下层)
- 递归调用(调用自己)
- 普通递归:在“归”时求和,递归不是最后一步,后面还有其他运算操作
- 尾递归:在“递”时求和,递归是最后一步
- 递归树:一个调用产生两个调用分支,最终形成一棵树
-
数据结构
-
逻辑结构:数据元素间的逻辑关系(线、非线(树/网)),组织方式
-
物理结构:通过内存地址访问目标位置的数据,内容类型
【逻辑上有关系,物理上可以查】
所有数据结构都是基于数组、链表或二者结合实现的
-
-
基本数据类型:CPU可以直接进行运算的类型,以二进制形式存储,一个二进制位为一个比特
一字节(byte)=八比特(bit),可以表示28
(256)个数字(XXXX XXXX)- 整型、浮点型、字符型、布尔型
-
数字编码(兼容负数和正负0运算的演进过程)
-
原码:二进制最高位为符号位(0正1负)
-
反码:正数反码与其原码一致;负数反码除最符号位,其余在原码基础上取反(解决负数原码不能直接计算的问题)
-
补码:正数补码与其原码一致;负数补码是在其反码基础上加1(解决正负0的歧义)
计算机中,数字以补码存储
-
二、数组与链表
-
数组:将相同类型的元素存储在连续的内存空间中。
-
地址计算公式:元素内存地址=数组内存地址+元素长度×元素索引
索引本质:内存地址的偏移量
-

-
操作:
插入:将元素依次后移k位(初始尾元素会丢失)
i=i-1,O(n)删除:将元素依次迁移k位(删后尾元素无意义)
i=i+1,O(n)扩容:创建更大的数组,将原数组复制过去,O(n)
-
优点:
- 空间效率高(连续紧凑)
- 随机访问
- 缓存局部,提高后续操作速度
-
缺点:
- 插删时间复杂度高
- 空间不可变,需扩容,开销大
- 空间可能浪费
- 链表:每个元素都是一个节点对象,之间用“引用”连接。各个节点可以分散存储在内存各处

/* 链表节点类 */
class ListNode {
int val;
// 节点值
ListNode next; // 指向下一节点的引用
ListNode(int x) { val = x; } // 构造函数
}
相比数组,链表节点需要占用更多的内存空间。
-
操作:
插删:O(1)
查:O(n)
-
列表:基于数组/链表实现
实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如Python中的ArrayList 、C++ 中的vector 和 C#中的list 、Java 中的List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。
-
操作:
增:add(val) addAll(list)
删:remove(index) clear()
改:set(index,val)
查:get(index)
排序:sort(list)
-
-
内存与缓存(物理结构在很大程度上决定了程序对内存和缓存的使用效率)
计算机中包括三种类型的存储设备:硬盘、内存(RAM)、缓存(cache memory)。硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储
经常访问的数据和指令。数组具有更高的缓存命中率,因此它在操作效率上通常优于链表。在选择数据结构时,应根据具体需求和场景做出恰当选择。
三、栈与队列
-
栈:先到后得
-
组成:
- 栈顶:后来的
- 栈底:先来的
-
操作:
入栈:添加到栈顶。push(val)
出栈:将栈顶弹出。pop()
访问栈顶:peek()
获取大小:size()
是否为空:isEmpty()
-
-
队列:先到先得
-
组成:
- 队首:先来的
- 队尾:后来的
-
操作:
入队:入队尾。offer(val)
出队:队首出。poll()
访问队首:peek()
获取大小:size()
是否为空:isEmpty()
-
四、哈希表
哈希表(hash table)又称散列表,通过简历key和value的映射关系,实现高效查询。
本质:数组+链表/红黑树
-
操作:
- 增删查:O(1)。put(k,v) remove(k) get(k)
- 遍历:通过键值对、键、值。entrySet() keySet() values()
-
哈希冲突
哈希函数的作用是将所有key构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在“多个输入对应相同输出”的情况。
key是唯一的,只有一个对应值,所以可根据key来查找值
-
解决:
-
改良哈希表数据结构,使得哈希表可以在出现哈希冲突时正常工作。
-
链式地址

值得注意的是,当链表很长时,查询效率𝑂(𝑛)很差。此时可以将链表转换为“AVL树”或“红黑树”,从而将查询操作的时间复杂度优化至𝑂(log𝑛)。
-
开放寻址
开放寻址不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。
-
-
仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。
java中HashMap的数组长度(capacity)永远是2n
扩容:表大小(size)>= 0.75 * 数组长度(capacity)
-
-
各编程语言采取了不同的哈希表实现策略,下面举几个例子。
- Python采用开放寻址。字典dict使用伪随机数进行探测。
- Java采用链式地址。自JDK1.8以来,当HashMap 内数组长度达到64且链表长度达到8时,链表会转换为红黑树以提升查找性能。
- Go采用链式地址。Go规定每个桶最多存储8个键值对,超出容量则连接一个溢出桶;当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。
-
-
哈希算法
键值对的分布情况由哈希函数决定
-
目标:
-
确定性
-
高效性
-
均匀分布
-
-
五、树和堆
-
树:
二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。体现了“一分为二”
的分治逻辑。
/* 二叉树节点类 */
class TreeNode {
int val;
// 节点值
TreeNode left; // 左子节点引用
TreeNode right; // 右子节点引用
TreeNode(int x) { val = x; }
}
- 常用术语:
- 根节点(rootnode):位于二叉树顶层的节点,没有父节点。
- 叶节点(leafnode):没有子节点的节点,其两个指针均指向None 。
- 边(edge):连接两个节点的线段,即节点引用(指针)。
- 节点所在的层(level):从顶至底递增,根节点所在层为1。
- 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是0、1、2。
- 二叉树的高度(height):从根节点到最远叶节点所经过的边的数量。
- 节点的深度(depth):从根节点到该节点所经过的边的数量。
- 节点的高度(height):从距离该节点最远的叶节点到该节点所经过的边的数量。

-
二叉树类型:
-
满/完美二叉树:所有层的节点都被填满
-
完全二叉树:只有底层未满且靠左填(适合用数组表示
-
平衡二叉树:|左子树高度- 右子树高度|<=1
-
二叉搜索树(BST):左树节点值<根<右
-
AVL树:严格平衡的BST。适合查询,但由于左右旋复杂(换根,调整子节点归属),不适合插删。
节点平衡因子:节点左子树的高度 - 右子树的高度,
-
红黑树:较宽松的AVL树。适合高频增删。
-
-
遍历:
- 广度优先:层序遍历(借助队列和数组列表)
- 深度优先:前中后序遍历(递归)
- 前:根左右
- 中:左根右
- 后:左右根
- 堆:满足特定条件的完全二叉树。
- 类型:
- 大顶堆:任意节点值 >= 其子节点值。根大
- 小顶堆:任意节点值 <= 其子节点值。根小
六、图
由顶点和边组成。或说节点和引用(可以看成一种从链表扩展而来的数据结构)

-
常用术语:
-
邻接(adjacency):当两顶点之间存在边相连时,称这两顶点“邻接”。
-
路径(path):从顶点A到顶点B经过的边构成的序列被称为从A到B的“路径”。
-
度(degree):一个顶点拥有的边数。
对于有向图,入度表示有多少条边指向该顶点,出度表示有多少条边从该顶点指出。
-
-
图的表示:
- 邻接矩阵:二维数组

- 邻接表:链表(与哈希表的链式地址相似)

- 效率对比

-
图的遍历
- 广度优先

- 深度优先


浙公网安备 33010602011771号