软考中级软件设计师——数据结构篇
一、数据结构基础概念
-
数据结构的分类
- 线性结构:数组、链表、栈、队列、串
- 非线性结构:树、图、集合
- 存储方式:顺序存储、链式存储、索引存储、散列存储
-
时间复杂度与空间复杂度
- 常见算法复杂度分析
- 最优子结构问题(如最长公共子序列)的优化方法
二、线性结构
1. 顺序表(数组)
-
特点:
-
内存连续分配,支持随机访问(通过下标直接定位)。
-
插入/删除需移动元素,效率低。
-
-
操作复杂度:
-
查找:O(1)
-
插入/删除:O(n)
-
-
应用:静态数据集合,如矩阵存储。
2. 链表
-
类型:
-
单链表:节点包含数据域 + 指向下一节点的指针。
-
双向链表:节点含前后指针,支持双向遍历。
-
循环链表:尾节点指针指向头节点。
-
-
特点:
-
内存非连续,插入/删除效率高(仅修改指针)。
-
查找需遍历,效率低。
-
-
操作复杂度:
-
插入/删除(已知位置):O(1)
-
查找:O(n)
-
-
应用:动态数据管理,如内存池、LRU缓存。
3. 栈(Stack)
-
特点:后进先出(LIFO),仅允许在栈顶操作。
-
核心操作:
-
push
:入栈。 -
pop
:出栈。 -
peek
:查看栈顶元素。
-
-
应用:函数调用栈、括号匹配、表达式求值(中缀转后缀)。
4. 队列(Queue)
-
特点:先进先出(FIFO),队尾插入,队头删除。
-
核心操作:
-
enqueue(x)
:将元素 x 插入队尾。 -
dequeue()
:删除队头元素并返回。 -
front()
:查看队头元素(不删除)。
-
-
特殊队列:
1.循环队列:利用模运算解决数组实现的“假溢出”问题。
2.优先队列:元素按优先级出队(通常用堆实现)。
-
应用:任务调度、广度优先搜索(BFS)。
5.串(String)
1.基本概念
属性 | 说明 |
---|---|
定义 | 由零个或多个字符组成的有限序列,记为 S = "a₁a₂…aₙ" (n ≥ 0)。 |
术语 | - 空串:长度为0的串。 - 子串:串中任意连续字符组成的子序列。 |
存储结构 | 顺序存储(数组)或链式存储(块链)。 |
2.串的基本操作
操作 | 描述 | 时间复杂度 |
---|---|---|
赋值 | 将一个串复制给另一个串。 | O(n) |
连接 | 将两个串首尾相连。 | O(n + m) |
求子串 | 截取串中从位置 pos 开始的 len 个字符。 | O(len) |
比较 | 按字典序比较两个串的大小。 | O(min(n, m)) |
定位(模式匹配) | 在主串中查找子串的位置。 | 朴素算法 O(nm),KMP算法 O(n + m) |
3.模式匹配算法
1. 朴素算法(Brute-Force)
-
原理:逐个字符比较,失配时主串回溯到起始位置+1。
2. KMP算法
-
核心思想:利用 部分匹配表(PMT) 避免主串回溯。
-
next数组:记录子串前缀与后缀的最长公共长度。
-
构建next数组(以子串
T = "ABABC"
为例):子串位置 0 1 2 3 4 字符 A B A B C next值 -1 0 0 1 2 -
匹配过程:
主串指针i
不回溯,子串指针j
根据next[j]
回退。
3.KMP算法手算next数组
步骤(子串 T = "ABABAA"
):
-
初始化:
next[0] = -1
,next[1] = 0
。 -
递推公式:
-
若
T[k] == T[j]
→next[j+1] = k + 1
。 -
若不等 →
k = next[k]
,循环直到k = -1
。
-
子串索引:
j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
T | A | B | A | B | A | A |
next[j] | -1 | 0 | 0 | 1 | 2 | 3 |
三、树与二叉树
1. 二叉树
-
基本概念:
-
节点:根节点、子节点、叶子节点。
-
度:节点的子树数量(二叉树每个节点度 ≤ 2)。
-
-
特殊类型:
-
满二叉树:所有层节点数达到最大值。
-
完全二叉树:最后一层节点左对齐,其他层全满。
-
-
遍历方式:
-
前序遍历:根 → 左 → 右(用于复制树结构)。
-
中序遍历:左 → 根 → 右(二叉搜索树得到有序序列)。
-
后序遍历:左 → 右 → 根(用于释放树内存)。
-
层次遍历:按层逐级访问(队列实现)。
-
2. 二叉搜索树(BST)
-
性质:左子树节点值 ≤ 根 ≤ 右子树节点值。
-
操作:
-
查找:类似二分查找,复杂度 O(logn)(平衡时)。
-
插入/删除:需调整树结构以维持性质。
-
-
缺点:不平衡时退化为链表,查找效率降至 O(n)。
3. 平衡二叉树(AVL树)
-
平衡条件:任意节点左右子树高度差 ≤ 1。
-
调整操作:
-
左旋:右子树过深时使用。
-
右旋:左子树过深时使用。
-
左右旋:先左旋再右旋。
-
右左旋:先右旋再左旋。
-
-
应用:需频繁查找的场景(如数据库索引)。
4. 哈夫曼树
-
定义:带权路径长度最短的二叉树(权值大的节点靠近根)。
-
构建步骤:
-
将权值作为叶子节点。
-
每次选权值最小的两节点合并,生成新父节点(权值为子节点和)。
-
重复直至只剩一棵树。
-
-
应用:数据压缩(哈夫曼编码)。
5. B树与B+树
-
B树:
-
特点:多路平衡搜索树,节点可包含多个关键字和子树。
-
规则:m阶B树每个节点最多 m 棵子树,关键字数 ∈ [⌈m/2⌉-1, m-1]。
-
-
B+树:
-
特点:叶子节点包含所有关键字,并按链表链接。
-
优势:适合范围查询,磁盘I/O更少。
-
-
应用:数据库索引、文件系统。
四、图
1. 图的存储
-
邻接矩阵:
-
二维数组
matrix[i][j]
表示边权。 -
适合稠密图,空间复杂度 O(n²)。
-
-
邻接表:
-
数组 + 链表,每个节点维护邻接节点列表。
-
适合稀疏图,空间复杂度 O(n+e)。
-
2. 图的遍历
-
深度优先搜索(DFS):
-
递归或栈实现,探索尽可能深的分支。
-
应用:拓扑排序、连通分量检测。
-
-
广度优先搜索(BFS):
-
队列实现,按层遍历。
-
应用:最短路径(无权图)、社交网络好友推荐。
-
3. 最短路径算法
-
Dijkstra算法:
-
单源最短路径(无负权边)。
-
贪心策略,每次选择距离最近的节点。
-
-
Floyd算法:
-
多源最短路径,动态规划思想。
-
三重循环更新距离矩阵。
-
4. 最小生成树(MST)
-
Prim算法:
-
从任意顶点开始,逐步扩展边(选权值最小的边)。
-
适合稠密图,时间复杂度 O(n²)。
-
-
Kruskal算法:
-
按边权升序选择,避免环。
-
适合稀疏图,时间复杂度 O(e log e)。
-
五、查找与排序
1. 查找算法
分类 | 静态查找 | 动态查找 |
---|---|---|
定义 | 数据集合在查找过程中不发生变化(无插入、删除操作)。 | 数据集合在查找过程中动态变化(支持插入、删除、修改操作)。 |
特点 | - 数据稳定,仅需高效查找。 - 存储结构固定(如数组)。 | - 数据动态更新,需保持查找效率。 - 存储结构灵活调整(如树、链表)。 |
常用算法 | 1. 顺序查找 2. 二分查找(有序表) 3. 哈希查找(静态哈希表) | 1. 二叉搜索树(BST) 2. 平衡二叉树(AVL树、红黑树) 3. B树/B+树 4. 哈希表(动态扩展) |
时间复杂度 | - 顺序查找:O(n) - 二分查找:O(logn) - 哈希查找:O(1)(理想情况) | - 平衡树:查找/插入/删除 O(logn) - B树:O(log_m n)(m为阶数) - 哈希表:平均 O(1)(冲突处理影响) |
1. 顺序查找
-
原理:逐个遍历数据元素,直到找到目标值。
-
特点:
-
适用于无序表和顺序存储结构。
-
时间复杂度:O(n)。
-
-
代码示例:
def sequential_search(arr, target):for i in range(len(arr)):if arr[i] == target:return ireturn -1
2. 二分查找(折半查找)
-
原理:在有序表中,每次比较中间元素,缩小一半搜索范围。
-
条件:数据必须有序,且为顺序存储结构。
-
时间复杂度:O(logn)。
-
代码示例:
def binary_search(arr, target):left, right = 0, len(arr) - 1while left <= right:mid = (left + right) // 2if arr[mid] == target:return midelif arr[mid] < target:left = mid + 1else:right = mid - 1return -1
3. 哈希查找
-
原理:通过哈希函数将关键字映射到存储地址,直接访问。
-
核心问题:哈希冲突(不同关键字映射到同一地址)。
-
冲突解决方法:
-
开放定址法:
-
线性探测:冲突后依次查找下一个空位。
-
二次探测:冲突后按平方增量跳跃查找。
-
-
链地址法:冲突位置建立链表,存储所有冲突元素。
-
-
时间复杂度:
-
理想情况(无冲突):O(1)。
-
最坏情况(全冲突):O(n)。
-
-
装填因子:
属性 说明 定义 装填因子(α) = 已存储元素数量 / 哈希表总容量。 作用 衡量哈希表的“填充程度”,直接影响冲突概率和操作效率。 理想范围 - 开放定址法:建议 α ≤ 0.7(如Java HashMap
默认扩容阈值为0.75)。
- 链地址法:可容忍 α > 1(但一般控制在0.5~1之间)。与性能关系 - α 越大 → 冲突概率越高 → 查找/插入效率下降。
- α 过小 → 空间浪费。动态调整 当 α 超过阈值时,触发 扩容(Rehashing):
1. 新建更大容量的哈希表。
2. 重新计算所有元素的哈希值并插入。 -
典型例题:
哈希表长度为7
,哈希函数H(key) = key % 7
,用链地址法处理冲突,插入序列[12, 5, 19, 33]
。
哈希表结构:
0: 33
1:
2:
3: 5 → 19
4:
5: 12
6:
2. 排序算法
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 小规模数据 |
快速排序 | O(n logn) | O(n²) | O(logn) | 不稳定 | 大规模数据(需随机化) |
归并排序 | O(n logn) | O(n logn) | O(n) | 稳定 | 外部排序、稳定需求 |
堆排序 | O(n logn) | O(n logn) | O(1) | 不稳定 | 实时系统、内存有限 |
插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 基本有序的小规模数据 |
1. 冒泡排序
-
原理:相邻元素两两比较,将最大值“冒泡”到末尾。
-
特点:
-
时间复杂度:O(n²)。
-
稳定性:稳定(相同元素相对位置不变)。
-
-
代码示例:
def bubble_sort(arr):n = len(arr)for i in range(n-1):for j in range(n-1-i):if arr[j] > arr[j+1]:arr[j], arr[j+1] = arr[j+1], arr[j]
2. 快速排序
-
原理:分治法,选取基准元素,将小于基准的放左边,大于的放右边,递归处理子序列。
-
核心步骤:
-
划分:选择基准(如第一个元素),划分左右子序列。
-
递归:对左右子序列重复上述步骤。
-
-
时间复杂度:
-
平均:O(n logn)。
-
最坏(已有序):O(n²)。
-
-
稳定性:不稳定(交换可能破坏相同元素顺序)。
-
代码示例:
def quick_sort(arr):if len(arr) <= 1:return arrpivot = arr[0]left = [x for x in arr[1:] if x <= pivot]right = [x for x in arr[1:] if x > pivot]return quick_sort(left) + [pivot] + quick_sort(right)
3. 归并排序
-
原理:分治法,将序列递归分成两半,排序后合并。
-
核心操作:合并两个有序序列。
-
时间复杂度:O(n logn)。
-
稳定性:稳定。
-
代码示例:
def merge_sort(arr):if len(arr) <= 1:return arrmid = len(arr) // 2left = merge_sort(arr[:mid])right = merge_sort(arr[mid:])return merge(left, right)def merge(left, right):result = []i = j = 0while i < len(left) and j < len(right):if left[i] <= right[j]:result.append(left[i])i += 1else:result.append(right[j])j += 1result.extend(left[i:])result.extend(right[j:])return result
4. 堆排序
-
堆的类型:
-
大顶堆:每个节点的值 ≥ 其子节点的值(用于升序排序)。
-
小顶堆:每个节点的值 ≤ 其子节点的值(用于降序排序)。
-
-
存储结构:
-
堆通过 数组 实现,逻辑上是一棵完全二叉树。
-
父子节点关系(数组下标从
0
开始):-
父节点索引:
parent(i) = (i-1) // 2
-
左子节点索引:
left_child(i) = 2*i + 1
-
右子节点索引:
right_child(i) = 2*i + 2
-
-
-
堆的调整:
-
下沉(Sink):从当前节点向下调整,确保子树满足堆性质。
-
上浮(Swim):从当前节点向上调整(堆排序中较少使用)。
-
-
稳定性:不稳定(交换堆顶与末尾元素可能破坏相同值的顺序)。
-
代码示例:
def heap_sort(arr):def sink(parent, length):temp = arr[parent] # 保存父节点值child = 2 * parent + 1 # 左子节点索引while child < length:# 选择较大的子节点if child + 1 < length and arr[child] < arr[child + 1]:child += 1# 父节点 ≥ 子节点,无需调整if temp >= arr[child]:break# 子节点上移arr[parent] = arr[child]parent = childchild = 2 * parent + 1arr[parent] = temp # 最终位置n = len(arr)# 1. 建堆(从最后一个非叶子节点开始调整)for i in range(n//2 - 1, -1, -1):sink(i, n)# 2. 排序(交换堆顶与末尾元素,调整剩余堆)for i in range(n-1, 0, -1):arr[0], arr[i] = arr[i], arr[0] # 最大值放到末尾sink(0, i) # 调整堆顶元素return arr
5. 插入排序
-
原理:将未排序元素逐个插入已排序序列的合适位置。
-
时间复杂度:O(n²)。
-
稳定性:稳定。
-
代码示例:
def insertion_sort(arr):for i in range(1, len(arr)):key = arr[i]j = i - 1while j >= 0 and arr[j] > key:arr[j+1] = arr[j]j -= 1arr[j+1] = key