算法导论第一章:算法基础与排序艺术

算法导论第一章:算法基础与排序艺术

本文是《算法导论》精讲专栏的第一章,通过生动案例和可视化图解,结合完整C语言实现,带你掌握算法核心思维。包含插入排序归并排序 的完整实现与性能对比,以及循环不变式的数学证明方法。

1. 算法:改变世界的隐形力量

1.1 无处不在的算法

  • 外卖路径规划:Dijkstra算法将配送时间缩短30%
  • 视频推荐系统:协同过滤算法处理亿级用户偏好
  • 基因序列比对:动态规划算法加速疾病研究

算法在科技领域的核心作用

应用场景 关键算法 性能提升
高并发支付系统 负载均衡算法 QPS从1万→10万
自动驾驶 A*路径搜索 决策时间<100ms
芯片设计 线性规划优化 功耗降低40%

1.2 算法与程序的关系

问题 算法设计 程序实现 计算机执行 问题解决

2. 插入排序:扑克牌中的算法智慧

2.1 生活场景模拟

想象整理扑克牌的过程:

  1. 左手持已排序的牌(初始为空)
  2. 右手从桌上取一张牌
  3. 将新牌插入左手正确位置
  4. 重复直到所有牌有序

动态过程示意图

复制代码
初始: [5] 3 2 6 4 1
第1步:[3 5] 2 6 4 1
第2步:[2 3 5] 6 4 1
第3步:[2 3 5 6] 4 1
第4步:[2 3 4 5 6] 1
第5步:[1 2 3 4 5 6]

2.2 C语言完整实现

c 复制代码
#include <stdio.h>

void insertion_sort(int arr[], int n) {
    for (int j = 1; j < n; j++) {
        int key = arr[j];
        int i = j - 1;
        
        // 在有序区逆向寻找插入位置
        while (i >= 0 && arr[i] > key) {
            arr[i + 1] = arr[i];  // 元素右移
            i--;
        }
        arr[i + 1] = key;  // 插入正确位置
        
        // 打印每轮排序结果
        printf("Step %d: ", j);
        for (int k = 0; k < n; k++) {
            printf("%d ", arr[k]);
        }
        printf("\n");
    }
}

int main() {
    int data[] = {5, 3, 2, 6, 4, 1};
    int n = sizeof(data) / sizeof(data[0]);
    
    printf("Original: ");
    for (int i = 0; i < n; i++) printf("%d ", data[i]);
    
    insertion_sort(data, n);
    
    printf("\nSorted: ");
    for (int i = 0; i < n; i++) printf("%d ", data[i]);
    
    return 0;
}

2.3 循环不变式:算法正确性的数学证明

三要素验证法

阶段 不变式状态 图示 数学描述
初始化 单元素子数组有序 [■] j=1时成立
保持 新元素插入后仍有序 [■□]→[■■] 对任意j成立
终止 整个数组有序 [■■...■] j=n时成立

数学证明

复制代码
设子数组A[0..j-1]在循环开始时有序
当插入A[j]时:
  1. 找到位置k使得A[k-1] ≤ key < A[k]
  2. 移动元素A[k..j-1]到A[k+1..j]
  3. 插入key到A[k]
⇒ A[0..j]保持有序

3. 算法分析:揭开时间复杂度的面纱

3.1 运行时间量化模型

插入排序成本分析表(n个元素):

操作类型 执行次数 单次耗时(CPU周期) 总成本占比
比较操作 ≈n²/4 3 52%
元素移动 ≈n²/4 5 45%
指针操作 n 2 3%

3.2 渐近记号:算法效率的语言

五种渐近记号对比

复制代码
O(g(n)): 上界       → 最坏情况
Ω(g(n)): 下界       → 最佳情况
Θ(g(n)): 紧确界     → 精确描述
o(g(n)): 非紧上界   → 严格小于
ω(g(n)): 非紧下界   → 严格大于

常见时间复杂度对比

c 复制代码
#include <stdio.h>
#include <math.h>

int main() {
    printf("n\tlog n\tn log n\tn^2\tn^3\t2^n\n");
    for (int n = 2; n <= 64; n *= 2) {
        printf("%d\t%.1f\t%.1f\t%d\t%d\t%.0f\n", 
               n, log2(n), n*log2(n), n*n, n*n*n, pow(2,n));
    }
    return 0;
}

输出结果

复制代码
n       log n   n log n n^2     n^3     2^n
2       1.0     2.0     4       8        4
4       2.0     8.0     16      64       16
8       3.0     24.0    64      512      256
16      4.0     64.0    256     4096     65536
32      5.0     160.0   1024    32768    4294967296
64      6.0     384.0   4096    262144   1.8446744e+19

4. 分治策略:归并排序的魔法

4.1 分治三步曲

原问题 n元素 分解 子问题1 n/2 子问题2 n/2 递归求解 递归求解 合并有序子数组 有序序列

4.2 C语言完整实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// 合并两个有序子数组
void merge(int A[], int p, int q, int r) {
    int n1 = q - p + 1;
    int n2 = r - q;
    
    // 创建临时数组
    int *L = (int*)malloc((n1+1) * sizeof(int));
    int *R = (int*)malloc((n2+1) * sizeof(int));
    
    // 拷贝数据
    for (int i = 0; i < n1; i++) L[i] = A[p + i];
    for (int j = 0; j < n2; j++) R[j] = A[q + 1 + j];
    
    // 设置哨兵值
    L[n1] = INT_MAX;
    R[n2] = INT_MAX;
    
    // 合并过程
    int i = 0, j = 0;
    for (int k = p; k <= r; k++) {
        if (L[i] <= R[j]) {
            A[k] = L[i];
            i++;
        } else {
            A[k] = R[j];
            j++;
        }
    }
    
    free(L);
    free(R);
}

// 归并排序主函数
void merge_sort(int A[], int p, int r) {
    if (p < r) {
        int q = (p + r) / 2;
        merge_sort(A, p, q);
        merge_sort(A, q+1, r);
        merge(A, p, q, r);
    }
}

// 打印数组
void print_array(int arr[], int n) {
    for (int i = 0; i < n; i++) 
        printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int data[] = {12, 3, 7, 9, 14, 6, 11, 2};
    int n = sizeof(data)/sizeof(data[0]);
    
    printf("Original: ");
    print_array(data, n);
    
    merge_sort(data, 0, n-1);
    
    printf("Sorted: ");
    print_array(data, n);
    
    return 0;
}

4.3 时间复杂度分析:递归树法

递归方程

复制代码
T(n) = 2T(n/2) + Θ(n)

递归树展开

复制代码
层级      工作量        节点数
  0       cn           1
  1       c(n/2)       2
  2       c(n/4)       4
  ...     ...          ...
  k       c(n/2^k)     2^k

总工作量计算

复制代码
树高度 h = log₂n
总工作量 = cn * (1 + 1 + 1 + ... + 1)   // logn+1项
         = cn(log₂n + 1)
         = Θ(n log n)

5. 算法实战:插入排序 vs 归并排序

5.1 性能对比实验

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// [插入排序实现]
// [归并排序实现]

int main() {
    srand(time(0));
    const int sizes[] = {100, 1000, 10000, 50000};
    const int num_tests = sizeof(sizes)/sizeof(sizes[0]);
    
    printf("Size\tInsertion Sort(ms)\tMerge Sort(ms)\n");
    printf("------------------------------------------------\n");
    
    for (int i = 0; i < num_tests; i++) {
        int n = sizes[i];
        int *arr1 = (int*)malloc(n * sizeof(int));
        int *arr2 = (int*)malloc(n * sizeof(int));
        
        // 生成随机数组
        for (int j = 0; j < n; j++) {
            int val = rand() % 1000000;
            arr1[j] = val;
            arr2[j] = val;
        }
        
        // 测试插入排序
        clock_t start = clock();
        insertion_sort(arr1, n);
        double time_insert = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
        
        // 测试归并排序
        start = clock();
        merge_sort(arr2, 0, n-1);
        double time_merge = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
        
        printf("%d\t%.2f\t\t\t%.2f\n", n, time_insert, time_merge);
        
        free(arr1);
        free(arr2);
    }
    
    return 0;
}

性能对比结果

数据规模 插入排序(ms) 归并排序(ms) 性能比
100 0.15 0.25 0.6x
1,000 15.2 1.8 8.4x
10,000 1,520 21 72x
50,000 38,500 120 320x

5.2 选择排序算法的黄金法则

  1. 小数据场景(n<100):插入排序更优

    • 常数因子小
    • 缓存友好(顺序访问)
  2. 大数据场景(n>1000):归并排序更优

    • O(n log n) 时间优势明显
    • 稳定排序(相同元素顺序不变)
  3. 工程实践中的优化

    c 复制代码
    // 混合排序:在归并排序中,当子数组足够小时切换为插入排序
    void hybrid_sort(int A[], int p, int r, int threshold) {
        if (r - p < threshold) {
            insertion_sort(A+p, r-p+1);
        } else {
            int q = (p + r) / 2;
            hybrid_sort(A, p, q, threshold);
            hybrid_sort(A, q+1, r, threshold);
            merge(A, p, q, r);
        }
    }

6. 算法思维扩展:逆序对计数问题

6.1 问题定义

给定数组A,计算逆序对数量:满足 i < j 且 A[i] > A[j] 的(i, j)对

应用场景

  • 衡量数据有序程度
  • 股票交易策略分析
  • 推荐系统协同过滤

6.2 分治解决方案

c 复制代码
int merge_count(int A[], int p, int q, int r) {
    int inversions = 0;
    int n1 = q - p + 1;
    int n2 = r - q;
    
    int *L = (int*)malloc(n1 * sizeof(int));
    int *R = (int*)malloc(n2 * sizeof(int));
    
    for (int i = 0; i < n1; i++) L[i] = A[p + i];
    for (int j = 0; j < n2; j++) R[j] = A[q + 1 + j];
    
    int i = 0, j = 0, k = p;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            A[k++] = L[i++];
        } else {
            // 关键点:L[i] > R[j] 产生逆序对
            A[k++] = R[j++];
            inversions += n1 - i;  // L中剩余元素都与R[j]构成逆序对
        }
    }
    
    // 处理剩余元素
    while (i < n1) A[k++] = L[i++];
    while (j < n2) A[k++] = R[j++];
    
    free(L);
    free(R);
    return inversions;
}

int count_inversions(int A[], int p, int r) {
    if (p >= r) return 0;
    
    int q = (p + r) / 2;
    int left = count_inversions(A, p, q);
    int right = count_inversions(A, q+1, r);
    int cross = merge_count(A, p, q, r);
    
    return left + right + cross;
}

6.3 性能对比

方法 时间复杂度 空间复杂度 n=1,000,000耗时
暴力枚举 O(n²) O(1) >10天
分治策略 O(n log n) O(n) 0.5秒
树状数组 O(n log n) O(n) 0.3秒

7. 从理论到工程:算法优化实战

7.1 插入排序优化技巧

  1. 二分查找优化
c 复制代码
void binary_insertion_sort(int arr[], int n) {
    for (int j = 1; j < n; j++) {
        int key = arr[j];
        int left = 0, right = j-1;
        
        // 二分查找插入位置
        while (left <= right) {
            int mid = left + (right - left)/2;
            if (arr[mid] > key) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        // 移动元素
        for (int i = j-1; i >= left; i--) {
            arr[i+1] = arr[i];
        }
        arr[left] = key;
    }
}
  1. 性能对比 (n=10,000):
    | 版本 | 比较次数 | 移动次数 | 总时间(ms) |
    |--------------|--------------|--------------|------------|
    | 原始插入排序 | ≈50,000,000 | ≈25,000,000 | 650 |
    | 二分插入排序 | ≈130,000 | ≈25,000,000 | 320 |
    | 希尔排序 | ≈1,200,000 | ≈1,500,000 | 35 |

7.2 归并排序工程优化

  1. 避免重复内存分配
c 复制代码
void merge_sort_opt(int A[], int temp[], int p, int r) {
    if (p < r) {
        int q = (p + r)/2;
        merge_sort_opt(A, temp, p, q);
        merge_sort_opt(A, temp, q+1, r);
        merge_using_temp(A, temp, p, q, r);
    }
}

// 调用前一次性分配临时内存
void optimized_merge_sort(int A[], int n) {
    int *temp = (int*)malloc(n * sizeof(int));
    merge_sort_opt(A, temp, 0, n-1);
    free(temp);
}
  1. 非递归实现
c 复制代码
void iterative_merge_sort(int A[], int n) {
    int curr_size;
    int left_start;
    
    int *temp = (int*)malloc(n * sizeof(int));
    
    for (curr_size = 1; curr_size <= n-1; curr_size *= 2) {
        for (left_start = 0; left_start < n-1; left_start += 2*curr_size) {
            int mid = left_start + curr_size - 1;
            int right_end = (left_start + 2*curr_size - 1) < n-1 ? 
                           (left_start + 2*curr_size - 1) : n-1;
            
            merge_using_temp(A, temp, left_start, mid, right_end);
        }
    }
    free(temp);
}

总结与思考

本章深入探讨了算法设计与分析的核心概念:

  1. 算法基础:通过扑克牌模型理解插入排序
  2. 数学证明:循环不变式验证算法正确性
  3. 分治策略:归并排序的递归实现与时间复杂度分析
  4. 工程实践:混合排序策略与内存优化技巧

关键洞见:没有绝对"最好"的算法,只有最适合特定场景的算法。在小规模数据中表现优异的插入排序,在大规模数据处理中会被归并排序超越,而实际工程中往往采用混合策略。

下章预告:第二章《算法分析的数学基础》将深入探讨:

  • 递归式求解的三种方法:代入法、递归树法、主方法
  • 概率分析与随机化算法
  • 堆排序与优先队列的实现

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

思考题

  1. 当输入数据几乎有序时,哪种排序算法最具优势?为什么?
  2. 如何修改归并排序,使其在O(n)时间内检测数组是否已排序?
  3. 在内存受限环境中(如嵌入式系统),应如何调整归并排序的实现?
相关推荐
@老蝴2 小时前
C语言 — 通讯录模拟实现
c语言·开发语言·算法
L-ololois2 小时前
【AI】模型vs算法(以自动驾驶为例)
人工智能·算法·自动驾驶
安全系统学习4 小时前
网络安全之RCE简单分析
开发语言·python·算法·安全·web安全
GEEK零零七5 小时前
Leetcode 3299. 连续子序列的和
算法·leetcode·动态规划
飞飞是甜咖啡6 小时前
【机器学习】Teacher-Student框架
人工智能·算法·机器学习
蒟蒻小袁6 小时前
力扣面试150题--单词接龙
算法·leetcode·面试
ghie90906 小时前
LMD分解通过局部均值分解重构信号实现对信号的降噪
算法·均值算法·重构
零叹7 小时前
篇章十 数据结构——排序
java·数据结构·算法·排序算法
涛哥码咖7 小时前
前端十种排序算法解析
前端·算法·排序算法
学习噢学个屁7 小时前
基于STM32汽车温度空调控制系统
c语言·stm32·单片机·嵌入式硬件·汽车