实战:排序算法可视化(SFML/控制台动画)

排序算法这东西,学的时候总觉得懂了,但真要你手写一遍,再看着它一步步跑,感觉完全不一样。我当年刚入行时,面试官让我讲快速排序,我背得滚瓜烂熟。结果他问了一句:「你能画出它每一轮的分区过程吗?」我当场卡壳了。

从那以后,我就养成了一个习惯——把算法「画」出来。今天这个实战,就是带你做一个排序算法的可视化工具。你可以用 SFML 做图形界面,也可以用控制台动画来演示。两种方式我都会讲。

为什么需要可视化?

说白了,排序算法的核心是「比较」和「交换」。但光看代码,你很难直观感受到:

  • 冒泡排序为什么慢?因为相邻元素在「慢慢挪」
  • 快速排序为什么快?因为它在「跳着换」
  • 插入排序在近乎有序的数据上为什么表现好?因为很少移动

可视化能让你一眼看穿这些本质。我在教团队新人时,经常让他们先跑一遍可视化,再去看代码。效果比直接啃书好得多。

整体思路

我们先理清要做的事。核心逻辑其实很简单:

  1. 准备一组数据(比如数组)
  2. 用柱状图表示每个元素的值
  3. 在排序过程中,每次比较或交换后,重新绘制柱状图
  4. 加入延时,让人眼能跟上节奏

嗯,这里要注意:可视化不是「排序完再画」,而是「每走一步就画一次」。所以你的排序函数里,要嵌入绘制逻辑。

核心原则:排序算法每执行一次「比较」或「交换」,就触发一次重绘。这样你才能看到完整的演变过程。

方案一:控制台动画

如果你不想引入 SFML 这样的图形库,控制台动画其实也够用。我早期做原型验证时,就经常用这种方式。它轻量、跨平台,而且调试方便。

控制台动画的原理是:每次重绘前清屏,然后逐行打印柱状图。每个柱子可以用字符拼出来,比如用 #

#include <iostream>
#include <vector>
#include <thread>
#include <chrono>

void draw(const std::vector<int>& arr) {
    system("clear"); // Windows 下用 system("cls")
    for (int val : arr) {
        for (int i = 0; i < val; ++i)
            std::cout << "#";
        std::cout << "\n";
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
}

void bubbleSortVisual(std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
                draw(arr); // 每次交换后重绘
            }
        }
    }
}

int main() {
    std::vector<int> data = {5, 3, 8, 4, 2, 7, 1, 6};
    draw(data);
    bubbleSortVisual(data);
    return 0;
}

这段代码里,draw() 函数负责清屏和打印。每次交换后调用它,你就能看到柱子一点点变有序。我曾经用这个给同事演示冒泡排序,他看完说:「原来冒泡真的像泡泡一样往上冒啊。」

小技巧:控制台动画的延时建议设在 100~300 毫秒之间。太快了看不清,太慢了又无聊。你可以加一个参数让用户自己调速度。

方案二:SFML 图形界面

如果你想要更炫酷的效果,那就上 SFML。它是个轻量级的 C++ 图形库,用来做 2D 动画非常顺手。我个人的项目里,经常用它来做数据结构的可视化演示。

SFML 的思路是:创建一个窗口,在窗口里画矩形(代表柱子)。每次排序步骤后,更新矩形的位置或高度,然后刷新窗口。

#include <SFML/Graphics.hpp>
#include <vector>
#include <thread>
#include <chrono>

void drawBars(sf::RenderWindow& window, const std::vector<int>& arr) {
    window.clear(sf::Color::White);
    float barWidth = window.getSize().x / arr.size();
    for (size_t i = 0; i < arr.size(); ++i) {
        sf::RectangleShape bar(sf::Vector2f(barWidth - 2, arr[i] * 10));
        bar.setPosition(i * barWidth, window.getSize().y - arr[i] * 10);
        bar.setFillColor(sf::Color::Blue);
        window.draw(bar);
    }
    window.display();
}

void selectionSortVisual(sf::RenderWindow& window, std::vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n - 1; ++i) {
        int minIdx = i;
        for (int j = i + 1; j < n; ++j) {
            if (arr[j] < arr[minIdx])
                minIdx = j;
        }
        std::swap(arr[i], arr[minIdx]);
        drawBars(window, arr);
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }
}

int main() {
    sf::RenderWindow window(sf::VideoMode(800, 600), "排序可视化");
    std::vector<int> data = {5, 3, 8, 4, 2, 7, 1, 6};

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)
                window.close();
        }
        drawBars(window, data);
        selectionSortVisual(window, data);
        // 排序完成后保持窗口
        while (window.isOpen()) {
            sf::Event e;
            while (window.pollEvent(e))
                if (e.type == sf::Event::Closed)
                    window.close();
        }
    }
    return 0;
}

这段代码里,drawBars() 负责把数组画成柱状图。选择排序每找到最小值就交换一次,然后重绘。你想想看,每次交换后柱子都会「跳」一下,视觉效果非常直观。

注意:SFML 的窗口事件循环不能阻塞。如果你在排序循环里用了 sleep,窗口可能会显示「无响应」。建议把排序放在一个单独的线程里,或者用 SFML 的 sf::Clock 来控制帧率。

核心逻辑流程图

下面这张图展示了排序可视化的整体流程。从数据初始化,到排序循环,再到每一步的重绘,最后输出结果。

开始 初始化随机数组 排序循环(比较/交换) 重绘柱状图 排序完成? 结束

扩展:支持多种排序算法

你肯定不想只做一个排序算法。我建议你把可视化框架抽象出来,然后往里「插」不同的排序算法。这样你就能对比冒泡、选择、插入、快速、归并等算法的执行过程。

具体做法是:写一个通用的 Sorter 基类,里面包含一个 draw() 回调。每个排序算法继承这个基类,在比较或交换时调用 draw()

class Sorter {
public:
    virtual void sort(std::vector<int>& arr) = 0;
    void setDrawCallback(std::function<void(const std::vector<int>&)> cb) {
        drawCallback = cb;
    }
protected:
    void draw(const std::vector<int>& arr) {
        if (drawCallback) drawCallback(arr);
    }
private:
    std::function<void(const std::vector<int>&)> drawCallback;
};

class QuickSort : public Sorter {
public:
    void sort(std::vector<int>& arr) override {
        quickSort(arr, 0, arr.size() - 1);
    }
private:
    void quickSort(std::vector<int>& arr, int low, int high) {
        if (low < high) {
            int pi = partition(arr, low, high);
            draw(arr); // 每次分区后重绘
            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }
    int partition(std::vector<int>& arr, int low, int high) {
        // 标准分区逻辑
    }
};

这样设计的好处是:你只需要写一次绘制逻辑,然后可以随意切换排序算法。我当年做教学工具时就是这么干的,学生可以一键切换算法,直观对比它们的「步调」差异。

避坑指南

做可视化时,有几个坑我踩过,分享给你:

  • 延时控制:不要用 sleep 硬等。在 SFML 里用 sf::Clock 控制帧率,在控制台里用 std::chronosleep_for 但注意别阻塞事件循环。
  • 数据范围:柱子的高度要适配窗口大小。如果数据值很大,记得做归一化处理,否则柱子会超出窗口。
  • 颜色标记:我习惯用不同颜色标记当前正在比较或交换的元素。比如红色表示正在比较,绿色表示已排好。这样视觉上更清晰。
  • 窗口关闭:排序过程中如果用户关闭窗口,要能优雅退出。否则程序会崩溃。加一个 window.isOpen() 的判断即可。

我的习惯:在排序循环里,每次重绘前先检查窗口是否还开着。如果关了,直接 return 退出排序。这样用户体验好,代码也健壮。

总结

排序算法可视化,说白了就是把「看不见的逻辑」变成「看得见的过程」。控制台动画适合快速原型,SFML 适合做更精致的演示。无论哪种方式,核心都是:每走一步,画一次

我个人建议你先从控制台动画入手,跑通后再迁移到 SFML。这样你能更专注于算法本身,而不是被图形库的细节绊住。等你把冒泡、选择、插入、快速都实现一遍,你会发现——原来排序算法真的会「动」起来。

公众号:蓝海资料掘金营,微信deep3321