算法导论第三章:数据结构艺术与高效实现

算法导论第三章:数据结构艺术与高效实现

本文是《算法导论》精讲专栏第三章,通过动态操作可视化 、内存布局图解 和性能对比实验,结合完整C语言实现,深入解析数据结构核心原理。包含栈、队列、链表、散列表、二叉树等完整实现,以及工程实践中的优化技巧。

1. 动态集合:算法操作的基石

1.1 基本操作与数据结构选择

动态集合 查询操作 修改操作 Search Minimum Successor Insert Delete Update

不同数据结构操作复杂度对比:

数据结构 Search Insert Delete Minimum Successor
无序数组 O(n) O(1) O(n) O(n) O(n)
有序数组 O(log n) O(n) O(n) O(1) O(1)
链表 O(n) O(1) O(1)* O(n) O(1)
二叉搜索树 O(h) O(h) O(h) O(h) O(h)
散列表 O(1) O(1) O(1) O(n) O(n)

*注:链表删除操作在已知节点位置时为O(1)

2. 栈与队列:线性结构的经典应用

2.1 栈:LIFO(后进先出)结构

操作原理:

复制代码
      push -> |   |   |   | <- pop
             +---+---+---+
             | 3 | 2 | 1 |
             +---+---+---+
             栈顶↑     栈底

C语言实现:

c 复制代码
#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

void init_stack(Stack *s) {
    s->top = -1;
}

int is_empty(Stack *s) {
    return s->top == -1;
}

int is_full(Stack *s) {
    return s->top == MAX_SIZE - 1;
}

void push(Stack *s, int item) {
    if (is_full(s)) {
        printf("Stack Overflow\n");
        return;
    }
    s->data[++s->top] = item;
}

int pop(Stack *s) {
    if (is_empty(s)) {
        printf("Stack Underflow\n");
        return -1;
    }
    return s->data[s->top--];
}

// 应用:括号匹配检查
int is_balanced(char *expr) {
    Stack s;
    init_stack(&s);
    
    for (int i = 0; expr[i]; i++) {
        if (expr[i] == '(' || expr[i] == '[' || expr[i] == '{') {
            push(&s, expr[i]);
        } else if (expr[i] == ')' || expr[i] == ']' || expr[i] == '}') {
            if (is_empty(&s)) return 0;
            
            char top = pop(&s);
            if ((expr[i] == ')' && top != '(') ||
                (expr[i] == ']' && top != '[') ||
                (expr[i] == '}' && top != '{')) {
                return 0;
            }
        }
    }
    return is_empty(&s);
}

2.2 队列:FIFO(先进先出)结构

循环队列实现:

c 复制代码
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int size;
} Queue;

void init_queue(Queue *q) {
    q->front = 0;
    q->rear = -1;
    q->size = 0;
}

int is_empty(Queue *q) {
    return q->size == 0;
}

int is_full(Queue *q) {
    return q->size == MAX_SIZE;
}

void enqueue(Queue *q, int item) {
    if (is_full(q)) {
        printf("Queue Overflow\n");
        return;
    }
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->data[q->rear] = item;
    q->size++;
}

int dequeue(Queue *q) {
    if (is_empty(q)) {
        printf("Queue Underflow\n");
        return -1;
    }
    int item = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->size--;
    return item;
}

// 应用:BFS广度优先搜索
void bfs(int graph[][5], int start, int n) {
    int visited[5] = {0};
    Queue q;
    init_queue(&q);
    
    visited[start] = 1;
    enqueue(&q, start);
    
    while (!is_empty(&q)) {
        int node = dequeue(&q);
        printf("%d ", node);
        
        for (int i = 0; i < n; i++) {
            if (graph[node][i] && !visited[i]) {
                visited[i] = 1;
                enqueue(&q, i);
            }
        }
    }
}

3. 链表:动态内存的优雅组织

3.1 链表类型与性能对比

类型 优点 缺点 应用场景
单向链表 插入/删除快,内存小 只能单向遍历 简单列表,LRU缓存
双向链表 双向遍历,删除高效 内存占用大 浏览器历史记录
循环链表 循环访问,无边界 实现复杂 轮询调度,约瑟夫问题
跳跃链表 快速查找(O(log n)) 实现复杂,维护成本高 Redis有序集合

3.2 双向链表完整实现

c 复制代码
typedef struct Node {
    int data;
    struct Node *prev;
    struct Node *next;
} Node;

typedef struct {
    Node *head;
    Node *tail;
    int size;
} DoublyLinkedList;

Node *create_node(int data) {
    Node *new_node = (Node *)malloc(sizeof(Node));
    new_node->data = data;
    new_node->prev = NULL;
    new_node->next = NULL;
    return new_node;
}

void init_list(DoublyLinkedList *list) {
    list->head = NULL;
    list->tail = NULL;
    list->size = 0;
}

void insert_front(DoublyLinkedList *list, int data) {
    Node *new_node = create_node(data);
    
    if (list->head == NULL) {
        list->head = list->tail = new_node;
    } else {
        new_node->next = list->head;
        list->head->prev = new_node;
        list->head = new_node;
    }
    list->size++;
}

void insert_end(DoublyLinkedList *list, int data) {
    Node *new_node = create_node(data);
    
    if (list->tail == NULL) {
        list->head = list->tail = new_node;
    } else {
        list->tail->next = new_node;
        new_node->prev = list->tail;
        list->tail = new_node;
    }
    list->size++;
}

void delete_node(DoublyLinkedList *list, Node *node) {
    if (node == NULL) return;
    
    if (node == list->head) {
        list->head = node->next;
        if (list->head) list->head->prev = NULL;
    }
    
    if (node == list->tail) {
        list->tail = node->prev;
        if (list->tail) list->tail->next = NULL;
    }
    
    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;
    
    free(node);
    list->size--;
}

// 应用:LRU缓存实现
typedef struct {
    int capacity;
    DoublyLinkedList list;
    Node **hash_table; // 简化版哈希表
} LRUCache;

LRUCache *create_cache(int capacity) {
    LRUCache *cache = (LRUCache *)malloc(sizeof(LRUCache));
    cache->capacity = capacity;
    init_list(&cache->list);
    cache->hash_table = (Node **)calloc(1000, sizeof(Node *));
    return cache;
}

int get(LRUCache *cache, int key) {
    if (cache->hash_table[key] == NULL) return -1;
    
    // 移动到链表头部
    Node *node = cache->hash_table[key];
    delete_node(&cache->list, node);
    insert_front(&cache->list, key);
    cache->hash_table[key] = cache->list.head;
    
    return node->data;
}

void put(LRUCache *cache, int key, int value) {
    if (cache->hash_table[key] != NULL) {
        // 更新现有值
        Node *node = cache->hash_table[key];
        node->data = value;
        get(cache, key); // 触发访问更新
        return;
    }
    
    if (cache->list.size >= cache->capacity) {
        // 淘汰最久未使用
        int key_to_remove = cache->list.tail->data;
        delete_node(&cache->list, cache->list.tail);
        cache->hash_table[key_to_remove] = NULL;
    }
    
    // 插入新节点
    insert_front(&cache->list, key);
    cache->list.head->data = value;
    cache->hash_table[key] = cache->list.head;
}

4. 内存中的指针与对象

4.1 三种内存表示方式

数组实现图示:

复制代码
索引:  0     1     2     3     4     5
     +-----+-----+-----+-----+-----+-----+
key: |  10 |  20 |  30 |     |     |     |
next:|  2  |  3  | -1  |     |     |     |
prev:| -1  |  0  |  1  |     |     |     |
     +-----+-----+-----+-----+-----+-----+
     头节点↑

C语言实现:

c 复制代码
#define MAX_SIZE 100

typedef struct {
    int key;
    int next;
    int prev;
} ListNode;

typedef struct {
    ListNode nodes[MAX_SIZE];
    int free_list;
    int head;
    int size;
} ArrayLinkedList;

void init_array_list(ArrayLinkedList *list) {
    // 初始化空闲列表
    for (int i = 0; i < MAX_SIZE - 1; i++) {
        list->nodes[i].next = i + 1;
    }
    list->nodes[MAX_SIZE - 1].next = -1;
    list->free_list = 0;
    list->head = -1;
    list->size = 0;
}

int allocate_object(ArrayLinkedList *list) {
    if (list->free_list == -1) {
        printf("Out of space\n");
        return -1;
    }
    int index = list->free_list;
    list->free_list = list->nodes[index].next;
    return index;
}

void free_object(ArrayLinkedList *list, int index) {
    list->nodes[index].next = list->free_list;
    list->free_list = index;
}

void insert_sorted(ArrayLinkedList *list, int key) {
    int new_index = allocate_object(list);
    if (new_index == -1) return;
    
    list->nodes[new_index].key = key;
    list->nodes[new_index].next = -1;
    list->nodes[new_index].prev = -1;
    
    // 查找插入位置
    int curr = list->head;
    int prev = -1;
    
    while (curr != -1 && list->nodes[curr].key < key) {
        prev = curr;
        curr = list->nodes[curr].next;
    }
    
    if (prev == -1) {
        // 插入头部
        list->nodes[new_index].next = list->head;
        if (list->head != -1) {
            list->nodes[list->head].prev = new_index;
        }
        list->head = new_index;
    } else {
        // 插入中间
        list->nodes[new_index].next = curr;
        list->nodes[new_index].prev = prev;
        list->nodes[prev].next = new_index;
        if (curr != -1) {
            list->nodes[curr].prev = new_index;
        }
    }
    list->size++;
}

5. 树结构:层次化数据组织

5.1 二叉树表示法

三种表示法对比:

表示法 存储结构 优点 缺点
左右链表示法 节点存储左右子节点指针 结构灵活,操作高效 内存占用大
数组表示法 按层级顺序存储在数组中 内存紧凑,访问快速 不适合动态变化
父亲链表示法 节点存储父节点指针 适合找父节点操作 查找子节点效率低

C语言实现(左右链法):

c 复制代码
typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

TreeNode *create_node(int data) {
    TreeNode *node = (TreeNode *)malloc(sizeof(TreeNode));
    node->data = data;
    node->left = NULL;
    node->right = NULL;
    return node;
}

// 二叉搜索树插入
TreeNode *insert(TreeNode *root, int data) {
    if (root == NULL) {
        return create_node(data);
    }
    
    if (data < root->data) {
        root->left = insert(root->left, data);
    } else if (data > root->data) {
        root->right = insert(root->right, data);
    }
    
    return root;
}

// 三种遍历方式
void preorder(TreeNode *root) {
    if (root) {
        printf("%d ", root->data);
        preorder(root->left);
        preorder(root->right);
    }
}

void inorder(TreeNode *root) {
    if (root) {
        inorder(root->left);
        printf("%d ", root->data);
        inorder(root->right);
    }
}

void postorder(TreeNode *root) {
    if (root) {
        postorder(root->left);
        postorder(root->right);
        printf("%d ", root->data);
    }
}

5.2 树的遍历应用

表达式树求值:

复制代码
        *
       / \
      +   -
     / \ / \
    2  3 5  1
c 复制代码
int evaluate_expression(TreeNode *root) {
    if (root == NULL) return 0;
    
    // 叶子节点为操作数
    if (!root->left && !root->right) {
        return root->data;
    }
    
    int left_val = evaluate_expression(root->left);
    int right_val = evaluate_expression(root->right);
    
    switch (root->data) {
        case '+': return left_val + right_val;
        case '-': return left_val - right_val;
        case '*': return left_val * right_val;
        case '/': return left_val / right_val;
    }
    return 0;
}

6. 散列表:快速访问的魔法

6.1 散列函数设计原则

  1. 均匀性:键值均匀分布到各个槽
  2. 高效性:计算速度快
  3. 确定性:相同键产生相同散列值

常见散列函数:

c 复制代码
// 除法散列法:h(k) = k mod m
int division_hash(int key, int m) {
    return key % m;
}

// 乘法散列法:h(k) = floor(m * (k * A mod 1))
int multiplication_hash(int key, int m) {
    double A = 0.6180339887; // 黄金分割
    double val = key * A;
    return (int)(m * (val - (int)val));
}

// 全域散列法(避免最坏情况)
int universal_hash(int key, int a, int b, int p, int m) {
    return ((a * key + b) % p) % m;
}

6.2 冲突解决策略

6.2.1 链地址法

索引0 键1 键2 索引1 键3 索引2 键4 键5

C语言实现:

c 复制代码
#define TABLE_SIZE 10

typedef struct HashNode {
    int key;
    int value;
    struct HashNode *next;
} HashNode;

typedef struct {
    HashNode **buckets;
    int size;
} HashMap;

HashMap *create_hash_map() {
    HashMap *map = (HashMap *)malloc(sizeof(HashMap));
    map->size = TABLE_SIZE;
    map->buckets = (HashNode **)calloc(TABLE_SIZE, sizeof(HashNode *));
    return map;
}

int hash(int key) {
    return key % TABLE_SIZE;
}

void put(HashMap *map, int key, int value) {
    int index = hash(key);
    HashNode *new_node = (HashNode *)malloc(sizeof(HashNode));
    new_node->key = key;
    new_node->value = value;
    new_node->next = NULL;
    
    if (map->buckets[index] == NULL) {
        map->buckets[index] = new_node;
    } else {
        HashNode *current = map->buckets[index];
        while (current->next != NULL) {
            if (current->key == key) {
                current->value = value; // 更新现有键
                free(new_node);
                return;
            }
            current = current->next;
        }
        current->next = new_node;
    }
}

int get(HashMap *map, int key) {
    int index = hash(key);
    HashNode *current = map->buckets[index];
    
    while (current != NULL) {
        if (current->key == key) {
            return current->value;
        }
        current = current->next;
    }
    return -1; // 未找到
}
6.2.2 开放寻址法

探查序列比较:

方法 探查序列 优点 缺点
线性探查 h(k,i) = (h'(k) + i) mod m 实现简单 聚集现象严重
二次探查 h(k,i) = (h'(k)+c₁i+c₂i²) mod m 减少聚集 可能无法探查所有槽
双重散列 h(k,i) = (h₁(k)+i*h₂(k)) mod m 最接近均匀探查 计算复杂

双重散列实现:

c 复制代码
#define TABLE_SIZE 10
#define EMPTY -1
#define DELETED -2

int hash1(int key) {
    return key % TABLE_SIZE;
}

int hash2(int key) {
    return 7 - (key % 7); // 确保不为0
}

int double_hash(int key, int i) {
    return (hash1(key) + i * hash2(key)) % TABLE_SIZE;
}

void insert_open_addressing(int table[], int key) {
    int i = 0;
    do {
        int j = double_hash(key, i);
        if (table[j] == EMPTY || table[j] == DELETED) {
            table[j] = key;
            return;
        }
        i++;
    } while (i < TABLE_SIZE);
    printf("Hash table overflow\n");
}

int search_open_addressing(int table[], int key) {
    int i = 0;
    do {
        int j = double_hash(key, i);
        if (table[j] == EMPTY) return -1;
        if (table[j] == key) return j;
        i++;
    } while (i < TABLE_SIZE);
    return -1;
}

7. 工程优化与性能分析

7.1 动态数组:STL vector 原理

c 复制代码
typedef struct {
    int *data;
    int size;
    int capacity;
} DynamicArray;

void init_dynamic_array(DynamicArray *arr, int capacity) {
    arr->data = (int *)malloc(capacity * sizeof(int));
    arr->size = 0;
    arr->capacity = capacity;
}

void push_back(DynamicArray *arr, int value) {
    if (arr->size >= arr->capacity) {
        // 扩容策略:倍增容量
        int new_capacity = arr->capacity * 2;
        int *new_data = (int *)realloc(arr->data, new_capacity * sizeof(int));
        if (new_data) {
            arr->data = new_data;
            arr->capacity = new_capacity;
        } else {
            printf("Memory allocation failed\n");
            return;
        }
    }
    arr->data[arr->size++] = value;
}

// 时间复杂度分析:均摊O(1)
// 数学证明:考虑连续插入n个元素的总成本
// T(n) = n + (1 + 2 + 4 + ... + 2^{k}) < n + 2^{k+1} < 3n
// ∴ 单次操作均摊成本 = T(n)/n < 3

7.2 内存池优化

c 复制代码
#define POOL_SIZE 1000

typedef struct {
    Node *nodes[POOL_SIZE];
    int free_index;
} MemoryPool;

MemoryPool *create_memory_pool() {
    MemoryPool *pool = (MemoryPool *)malloc(sizeof(MemoryPool));
    for (int i = 0; i < POOL_SIZE - 1; i++) {
        pool->nodes[i] = (Node *)malloc(sizeof(Node));
        pool->nodes[i]->next = i + 1; // 使用next指针连接空闲节点
    }
    pool->nodes[POOL_SIZE - 1] = (Node *)malloc(sizeof(Node));
    pool->nodes[POOL_SIZE - 1]->next = -1;
    pool->free_index = 0;
    return pool;
}

Node *allocate_node(MemoryPool *pool) {
    if (pool->free_index == -1) {
        printf("Memory pool exhausted\n");
        return NULL;
    }
    Node *node = pool->nodes[pool->free_index];
    pool->free_index = (int)(node->next); // 转换指针为索引
    return node;
}

void free_node(MemoryPool *pool, Node *node) {
    node->next = (Node *)(pool->free_index);
    pool->free_index = (int)(node - pool->nodes[0]); // 计算索引
}

总结与思考

本章深入探讨了数据结构的设计与实现:

  1. 基础结构:栈、队列、链表的实现与应用
  2. 内存管理:指针与数组表示的权衡
  3. 树结构:二叉树表示与遍历算法
  4. 散列表:冲突解决策略与性能优化
  5. 工程实践:动态数组、内存池等优化技术

关键洞见:没有完美的数据结构,只有最适合特定场景的选择。理解操作特性和性能瓶颈是设计高效系统的关键。

下章预告:第四章《高级设计与分析技术》将探讨:

  • 分治策略的数学证明
  • 动态规划的原理与应用
  • 贪心算法的正确性证明
  • 摊还分析的三种方法

本文完整代码已上传至GitHub仓库:Algorithm-Implementations

思考题:

  1. 在LRU缓存实现中,为什么使用双向链表而不是单向链表?
  2. 当散列表的装载因子超过阈值时,如何实现高效的重散列(rehashing)操作?
  3. 对比链式散列和开放寻址法,各自适合什么应用场景?
  4. 如何扩展二叉树结构以支持区间查询操作?
相关推荐
cu14332 分钟前
细谈GM8229的具体功能与其应用
c语言·c++·人工智能·单片机
需要8261 小时前
分布式 ID 生成:雪花算法、号段模式与时钟回拨
分布式·算法
垆边人似月.1 小时前
华为机考题:字符串分隔
算法·华为
CQU_JIAKE1 小时前
9.4【A】
算法
梦帮科技1 小时前
量子张量网络破局大模型:从矩阵乘积态 (MPS) 到张量列 (TT-SVD) 低秩收缩全推导
网络·数据结构·数据库·线性代数·矩阵·架构·模拟退火算法
CQU_JIAKE1 小时前
9.13[A]
算法
懿路向前1 小时前
【唤醒实战笔记】2026-09-29 | 智能体提示词迭代史(v1 → v8)
linux·笔记·算法
@陈小鱼2 小时前
基于CNN-Transformer的无袖带血压估计
人工智能·深度学习·神经网络·算法·cnn·transformer·血压
大侠归来2 小时前
C语言内存管理:从栈到堆的完整指南
c语言·开发语言·python
cu1432 小时前
细谈GM7123C的具体功能与其应用
c语言·c++·人工智能·嵌入式硬件