希赛王勇.202105.软考中级软件设计师 | 完结

4 阅读6分钟

数据结构是软件设计师考试(软考中级)中上午选择题和下午填空题的重头戏。很多考生觉得这一块难,其实是因为光看书、背概念,缺乏对逻辑流转的直观感受。

这里我结合考试的高频考点,分别梳理了线性表、树、图和算法策略的核心知识,并用 Python 代码演示了其实现原理。记住,代码不一定是为了让你在考场上敲键盘,而是帮你把这些抽象的"结构"在脑海中立体化。

一、 线性表:顺序存储与链式存储

线性表是最基础的数据结构,考点主要集中在这两种存储方式的优缺点对比以及操作的时间复杂度上。

1. 顺序表(数组实现)

核心考点:支持随机访问(O(1)),但插入和删除需要移动大量元素(O(n))。

python

复制

class ArrayList:
    def __init__(self, capacity=10):
        self.capacity = capacity
        self.size = 0
        self.data = [None] * capacity

    def append(self, value):
        if self.size >= self.capacity:
            self._resize()
        self.data[self.size] = value
        self.size += 1

    def insert(self, index, value):
        """在指定位置插入元素,需移动后续元素"""
        if index < 0 or index > self.size:
            raise IndexError("Index out of bounds")
        if self.size >= self.capacity:
            self._resize()
        
        # 从后向前移动元素,腾出位置
        for i in range(self.size, index, -1):
            self.data[i] = self.data[i-1]
        
        self.data[index] = value
        self.size += 1

    def _resize(self):
        """扩容操作"""
        new_capacity = self.capacity * 2
        new_data = [None] * new_capacity
        for i in range(self.size):
            new_data[i] = self.data[i]
        self.data = new_data
        self.capacity = new_capacity
        print(f"[*] 数组扩容至: {new_capacity}")

    def display(self):
        print(f"数组内容: {self.data[:self.size]}")

# 测试顺序表插入操作
arr = ArrayList(capacity=3)
arr.append(10)
arr.append(20)
arr.insert(1, 15) # 触发插入逻辑
arr.display()

2. 链表(单链表实现)

核心考点:插入和删除只需修改指针(O(1),如果已知位置),但不支持随机访问(需遍历,O(n))。

python

复制

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node

    def delete(self, value):
        current = self.head
        prev = None
        
        while current and current.data != value:
            prev = current
            current = current.next
            
        if current:
            if prev:
                prev.next = current.next # 跳过当前节点
            else:
                self.head = current.next # 删除的是头节点
            print(f"[-] 删除节点: {value}")
        else:
            print(f"[!] 未找到节点: {value}")

    def display(self):
        elements = []
        current = self.head
        while current:
            elements.append(str(current.data))
            current = current.next
        print(f"链表内容: {' -> '.join(elements)}")

# 测试链表操作
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.display()
ll.delete(2)
ll.display()

二、 树:二叉树与遍历算法

树的考点集中在二叉树的存储、遍历方式(前序、中序、后序)以及完全二叉树、满二叉树的性质(节点数计算)。

二叉树遍历演示

核心考点:理解递归在树遍历中的作用,以及不同遍历顺序的应用场景(如前序用于复制树,中序用于BST排序)。

python

复制

class TreeNode:
    def __init__(self, val=0):
        self.val = val
        self.left = None
        self.right = None

def build_sample_tree():
    """
        构建如下二叉树:
            1
           / \
          2   3
         / \
        4   5
    """
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    return root

# 遍历函数
def preorder_traversal(node):
    """前序遍历: 根 -> 左 -> 右"""
    if not node:
        return
    print(node.val, end=' ')
    preorder_traversal(node.left)
    preorder_traversal(node.right)

def inorder_traversal(node):
    """中序遍历: 左 -> 根 -> 右"""
    if not node:
        return
    inorder_traversal(node.left)
    print(node.val, end=' ')
    inorder_traversal(node.right)

def postorder_traversal(node):
    """后序遍历: 左 -> 右 -> 根"""
    if not node:
        return
    postorder_traversal(node.left)
    postorder_traversal(node.right)
    print(node.val, end=' ')

# 执行
root = build_sample_tree()
print("前序遍历 (Root-Left-Right):", end=" ")
preorder_traversal(root)
print("\n中序遍历 (Left-Root-Right):", end=" ")
inorder_traversal(root)
print("\n后序遍历 (Left-Right-Root):", end=" ")
postorder_traversal(root)

三、 图:存储与最短路径

图的考点相对较难,常考图的存储方式(邻接矩阵、邻接表)、遍历(BFS/DFS)以及最小生成树和最短路径算法。这里演示最经典的 Dijkstra 最短路径算法

python

复制

import heapq

def dijkstra(graph, start):
    """
    Dijkstra 算法求单源最短路径
    graph: 邻接表表示 {节点: {邻居: 权重}}
    start: 起始节点
    """
    # 初始化距离字典,起始点距离为0,其余为无穷大
    distances = {node: float('infinity') for node in graph}
    distances[start] = 0
    
    # 优先队列:(距离, 节点)
    priority_queue = [(0, start)]
    
    while priority_queue:
        current_distance, current_node = heapq.heappop(priority_queue)
        
        # 如果当前距离大于记录的距离,说明该路径已过时,跳过
        if current_distance > distances[current_node]:
            continue
            
        # 遍历邻居
        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight
            
            # 如果找到更短路径,更新并加入队列
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(priority_queue, (distance, neighbor))
                
    return distances

# 构建一个加权有向图
#      A
#    4/ \1
#    B   C
#   2|   |3
#    D   E
#     \5/
#      F
weighted_graph = {
    'A': {'B': 4, 'C': 1},
    'B': {'D': 2},
    'C': {'E': 3},
    'D': {'F': 5},
    'E': {'F': 1},
    'F': {}
}

distances_from_A = dijkstra(weighted_graph, 'A')
print("--- 从 A 出发的最短路径 ---")
for node, dist in distances_from_A.items():
    print(f"A -> {node}: {dist}")

四、 算法策略:分治、贪心与动态规划

下午题中常涉及算法策略的选择。理解这三种策略的区别至关重要:

策略核心思想适用场景复杂度
分治大问题拆解为独立的小问题,递归求解后合并归并排序、快速排序O(n log n)
贪心每一步都选择局部最优解,希望达到全局最优霍夫曼编码、最小生成树取决于贪心策略
动态规划保存子问题解,避免重复计算(空间换时间)背包问题、最短路径O(n*m) 等

动态规划:0/1 背包问题(经典下午题考点)

核心考点:理解状态转移方程 dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight] + value)

python

复制

def knapsack_01(weights, values, capacity):
    """
    0/1 背包问题:动态规划解法
    weights: 物品重量列表
    values: 物品价值列表
    capacity: 背包最大承重
    """
    n = len(weights)
    # dp[i][w] 表示前i个物品在容量为w的背包中的最大价值
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(1, capacity + 1):
            # 当前物品索引为 i-1
            if weights[i-1] <= w:
                # 状态转移:不选第i个物品 vs 选第i个物品
                dp[i][w] = max(
                    dp[i-1][w],  # 不选
                    dp[i-1][w - weights[i-1]] + values[i-1]  # 选
                )
            else:
                dp[i][w] = dp[i-1][w] # 装不下,只能不选
    
    # 回溯找出选择了哪些物品(可选,为了理解DP过程)
    selected_items = []
    w = capacity
    for i in range(n, 0, -1):
        if dp[i][w] != dp[i-1][w]:
            selected_items.append(i)
            w -= weights[i-1]
            
    return dp[n][capacity], selected_items[::-1]

# 测试背包问题
ws = [2, 3, 4, 5]   # 重量
vs = [3, 4, 5, 6]   # 价值
cap = 8             # 背包容量

max_val, items = knapsack_01(ws, vs, cap)
print(f"背包容量: {cap}")
print(f"最大价值: {max_val}")
print(f"选择的物品索引: {items}")

五、 备考建议

  1. 手算为王:考试时很多题目会要求你画出链表指针变化后的结果,或者写出二叉树的后序遍历序列。平时练习时,一定要拿纸笔手算,不要只依赖代码运行结果。
  2. 复杂度必背:常见操作的时间复杂度(如链表查找 O(n)、数组查找 O(1))是送分题,务必背熟。
  3. 算法套路:对于动态规划和贪心算法,记住几个经典模型(背包、找零钱、霍夫曼树)的解题模板,考试时即使题目变形,也能快速套用。

数据结构不仅是考试科目,更是写出高质量代码的基石。通过代码去理解结构,你会发现自己对程序的掌控力提升了一个档次!