Zig 语言实战:实现高性能快速排序算法

在上一篇博客中,我们深入探讨了如何在 Rust 中利用 Ord Trait 和 checked_sub 来实现一个安全的快速排序。今天,我们将视角转向 Zig 语言

Zig 被设计为 C 语言的现代替代品,它没有隐藏的控制流,内存管理完全由开发者掌控。在实现快速排序时,Zig 的代码往往更加直观,性能也通常与 C 语言持平。

本文将带你手写一个包含三数取中优化的快速排序,体验 Zig 简洁而强大的语法特性。


算法核心思路

快速排序的核心依然是分治法(Divide and Conquer):

  1. 选取基准(Pivot):从数组中挑出一个元素,这里我们使用"三数取中法"来避免最坏情况。
  2. 分区(Partition):将比基准小的元素移到左边,比基准大的移到右边。
  3. 递归(Recursion):对左右两个子数组递归执行上述步骤。

Zig 的实现与 Rust 的主要区别在于:

  • 泛型 :使用 comptime T 而非 Trait Bounds。
  • 交换 :直接使用标准库的 mem.swap 或内置函数。
  • 边界 :Zig 的 usize 下溢行为是截断(Truncate),而非 Panic,所以我们需要更小心,或者利用切片的特性。

完整代码实现

以下是完整的 Zig 快速排序实现。你可以直接将这段代码保存为 quick_sort.zig 并运行测试。

rust 复制代码
const std = @import("std");
const mem = std.mem;

/// 快速排序主函数
/// 
/// # 参数
/// - `arr`: 需要排序的切片引用
/// 
/// # 示例
/// ```
/// var arr = [_]i32{ 3, 1, 4, 1, 5 };
/// try quickSort(&arr);
/// std.debug.print("{any}\n", .{arr}); // 输出: { 1, 1, 3, 4, 5 }
/// ```
pub fn quickSort(comptime T: type, arr: []T) void {
    if (arr.len <= 1) return;
    quickSortImpl(T, arr, 0, arr.len - 1);
}

/// 快速排序递归实现
fn quickSortImpl(comptime T: type, arr: []T, left: usize, right: usize) void {
    // 递归终止条件
    if (left >= right) {
        return;
    }

    // 分区并获取基准位置
    const pivot = partition(T, arr, left, right);

    // 递归排序左侧 [left, pivot-1]
    // 注意:Zig 中 usize 下溢会回绕,需手动保护
    if (pivot > 0) {
        quickSortImpl(T, arr, left, pivot - 1);
    }
    
    // 递归排序右侧 [pivot+1, right]
    // 当 pivot == right 时,pivot + 1 可能越界,但递归函数开头会拦截
    quickSortImpl(T, arr, pivot + 1, right);
}

/// 三数取中法选择基准索引
fn medianOfThree(comptime T: type, arr: []T, left: usize, right: usize) usize {
    const mid = left + (right - left) / 2;
    
    // 将 left, mid, right 三个位置的元素按大小排序,确保 arr[mid] 是中位数
    if (arr[left] > arr[mid]) {
        mem.swap(T, &arr[left], &arr[mid]);
    }
    if (arr[left] > arr[right]) {
        mem.swap(T, &arr[left], &arr[right]);
    }
    if (arr[mid] > arr[right]) {
        mem.swap(T, &arr[mid], &arr[right]);
    }
    
    return mid;
}

/// 分区函数 (Hoare Partition)
fn partition(comptime T: type, arr: []T, left: usize, right: usize) usize {
    // 1. 使用三数取中,并将中位数交换到 left 位置作为基准
    const mid_index = medianOfThree(T, arr, left, right);
    mem.swap(T, &arr[left], &arr[mid_index]);

    // 2. 初始化双指针
    var i = left + 1;
    var j = right;

    while (true) {
        // 从左找 >= 基准的元素
        while (i <= j and arr[i] < arr[left]) : (i += 1) {}
        
        // 从右找 <= 基准的元素
        while (i <= j and arr[j] > arr[left]) : (j -= 1) {}
        
        // 指针相遇,退出
        if (i >= j) break;

        // 交换逆序对
        mem.swap(T, &arr[i], &arr[j]);
        i += 1;
        j -= 1;
    }

    // 3. 将基准放到正确的位置 (j)
    mem.swap(T, &arr[left], &arr[j]);
    return j;
}

// ================== 测试代码 ==================

test "quick sort" {
    const expect = @import("std").testing.expect;
    
    var arr = [_]i32{ 3, 1, 4, 1, 5, 9, 2, 6 };
    quickSort(i32, &arr);
    
    try expect(mem.eql(i32, &arr, &[_]i32{ 1, 1, 2, 3, 4, 5, 6, 9 }));
}

// 编译并运行测试: `zig test quick_sort.zig`

核心代码解析

1. 泛型的写法:comptime T

与 Rust 的 T: Ord 不同,Zig 使用 comptime T(编译时类型)。Zig 的泛型是在编译时展开的,这意味着 quickSort(i32, ...)quickSort(f64, ...) 会被编译器生成两份完全独立的代码。这种方式没有运行时多态的开销,性能极高。

2. 内存交换:mem.swap

Zig 标准库提供了 std.mem.swap,它利用了 @ptrCast@sizeOf 来实现类型安全的内存交换。这比 Rust 的 Clone 更高效,因为它直接操作内存,不涉及构造函数或析构函数。

3. 边界安全:usize 下溢

这是 Zig 和 Rust 最大的区别之一:

  • Rust0usize - 1 在 Debug 模式下会 Panic,保护了程序。
  • Zig0 - 1 的结果是 std.math.maxInt(usize)(一个极大值)。这虽然快,但极其危险。
  • 我们的处理 :在递归调用左侧前,我们显式地加了 if (pivot > 0) 判断。这是一种显式的防御性编程,符合 Zig 的设计哲学------"显式优于隐式"。
4. 三数取中优化

我们在 medianOfThree 函数中,不仅计算了中位数的索引,还直接通过交换把这三个数排好了序(arr[left] <= arr[mid] <= arr[right]),然后直接返回 mid。这使得基准值的质量更高,提升了整体排序效率。


️ Rust vs Zig:思维转换

如果你刚从 Rust 转向 Zig,以下几点值得注意:

特性 Rust 实现 Zig 实现 评价
泛型约束 T: Ord (Trait) comptime T (模板) Zig 更底层,无 Trait 对象开销。
元素交换 arr.swap(i, j) mem.swap(T, &a, &b) Rust 语法糖更多;Zig 更显式。
错误处理 checked_sub (Option) 手动 if (pivot > 0) Rust 编译器帮你防错;Zig 需要你自己小心。
内存安全 编译时借用检查 运行时手动管理 Zig 性能上限更高,但责任全在开发者。

总结

通过这个例子,我们可以看到 Zig 语言的简洁高效。它没有隐藏的运行时机制,所有的操作都是你明确写出的。

虽然在处理 usize 下溢时需要比 Rust 多一份小心,但换来的是对内存布局和执行流程的绝对控制权。对于系统编程、嵌入式开发或者对性能有极致要求的算法场景,Zig 是一个非常值得考虑的选择。

小贴士 :在实际的 Zig 项目中,你可以直接使用标准库的 std.sort.sort,它已经实现了 IntroSort(内省排序),性能非常强悍。手写此算法主要用于学习算法原理和熟悉语言特性。

相关推荐
leoufung9 小时前
LeetCode动态规划经典题:Unique Paths 网格路径计数详解
算法·leetcode·动态规划
李泽辉_9 小时前
深度学习算法学习(四):深度学习-最简单实现一个自行构造的找规律(机器学习)任务
深度学习·学习·算法
hz_zhangrl9 小时前
CCF-GESP 等级考试 2025年12月认证C++六级真题解析
c++·算法·青少年编程·程序设计·gesp·c++六级·gesp2025年12月
小沈同学呀9 小时前
基于时间片划分的提醒算法设计与实现
服务器·数据库·算法
千金裘换酒9 小时前
LeetCode 两数之和 Java
java·算法·leetcode
汽车仪器仪表相关领域9 小时前
光轴精准校准,安全检测基石——JZD-1/2前照灯检测仪用校准灯项目实战分享
数据库·算法·安全·汽车·压力测试·可用性测试
Mintopia9 小时前
🌍 AI 自主决策:从文字到图像与声音的三元赋能之路
人工智能·算法·aigc
半夏知半秋9 小时前
rust学习-探讨为什么需要标注生命周期
开发语言·笔记·学习·算法·rust
漫随流水9 小时前
leetcode算法(二叉树的层序遍历Ⅱ)
数据结构·算法·leetcode·二叉树
源代码•宸9 小时前
Leetcode—166. 加一【简单】new(big.Int)法
经验分享·算法·leetcode·职场和发展·golang·new.bigint