C++两种洗牌算法比较:Fisher-Yates与快速排序划分算法

引言: 在C++编程中,洗牌算法是处理数组的一种常见需求。两种常见的洗牌算法是Fisher-Yates洗牌算法和快速排序中的划分算法。本文将详细介绍这两种算法的原理、性能和适用性,帮助读者更好地选择和应用合适的洗牌算法。

一、Fisher-Yates洗牌算法

1. 原理: Fisher-Yates洗牌算法是一种从数组的最后一个元素开始,逐步向前与当前位置的元素交换的算法。具体步骤如下:

  • 从数组的最后一个元素开始,将其与当前位置的元素交换;
  • 递归地对该位置前面的元素重复上述步骤,直到数组的第一个元素。

2. 时间复杂度: Fisher-Yates洗牌算法的时间复杂度为O(n),其中n是数组的长度。
3. 空间复杂度: Fisher-Yates洗牌算法是原地算法,不需要额外的存储空间,空间复杂度为O(1)。
4. 稳定性: Fisher-Yates洗牌算法是稳定的,洗牌前后,元素在数组中的相对位置不变。
5. 适用性: Fisher-Yates洗牌算法适用于任何类型的数组,特别是当数组元素类型复杂或者数组较大时。
示例
Fisher-Yates洗牌算法是一种简单且高效的算法,它通过遍历数组元素,从最后一个元素开始与当前位置的元素交换,直到遍历完所有元素。

#include <iostream>
#include <vector>
#include <random>
void fisherYatesShuffle(std::vector<int>& v) {
    std::random_device rd;
    std::mt19937 g(rd());
    for (size_t i = v.size() - 1; i > 0; --i) {
        std::uniform_int_distribution<> dist(0, i);
        std::swap(v[i], v[dist(g)]);
    }
}
int main() {
    std::vector<int> deck = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    fisherYatesShuffle(deck);
    for (int card : deck) {
        std::cout << card << ' ';
    }
    return 0;
}

二、随机化算法(快速排序划分)

1. 原理: 快速排序的划分算法是在数组中选择一个元素作为"基准"(pivot),然后将数组划分为两部分,左边都是小于基准的元素,右边都是大于基准的元素。具体步骤如下:

  • 选择一个基准元素;
  • 将数组划分为两部分,使得左边的元素都小于基准,右边的元素都大于基准;
  • 对左右两部分递归地应用划分算法。

2. 时间复杂度: 快速排序的平均时间复杂度为O(n log n),最坏情况下的时间复杂度为O(n^2),但这种最坏情况在实际应用中很少见。
3. 空间复杂度: 快速排序的空间复杂度为O(log n)到O(n),因为递归栈的深度可能达到log n,在最坏情况下可能需要n级别的栈空间。
4. 稳定性: 快速排序的划分算法是不稳定的,因为在划分过程中,相等的元素可能会被调换位置。
5. 适用性: 快速排序的划分算法适用于有大量重复元素的数组,但不适合作为洗牌算法,因为它不保证洗牌的随机性。
示例
随机化算法,如快速排序中的划分算法,通常用于将数组划分为小于和大于基准值的两部分。这种算法也可以用来洗牌,但是它不是原地算法,并且在最坏情况下可能导致性能下降。

#include <iostream>
#include <vector>
#include <random>
void randomizeShuffle(std::vector<int>& v, int start, int end) {
    if (start >= end) {
        return;
    }
    std::random_device rd;
    std::mt19937 g(rd());
    std::uniform_int_distribution<> dist(start, end);
    std::swap(v[start], v[dist(g)]);
    randomizeShuffle(v, start + 1, end);
    randomizeShuffle(v, start, end - 1);
}
int main() {
    std::vector<int> deck = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    randomizeShuffle(deck, 0, deck.size() - 1);
    for (int card : deck) {
        std::cout << card << ' ';
    }
    return 0;
}

性能和正确性比较

Fisher-Yates洗牌算法:

  • 性能:时间复杂度为O(n),因为每个元素都只被访问一次。
  • 正确性:算法是确定性的,可以完全随机化数组。

随机化算法(快速排序划分):

  • 性能:平均时间复杂度为O(n),但最坏情况下可以达到O(n^2)。
  • 正确性:算法也是确定性的,可以随机化数组。

实际应用中的选择

在实际应用中,如果洗牌的随机性非常重要,且数组不大,Fisher-Yates算法是一个很好的选择,因为它简单、高效且保证随机性。如果数组很大,且对性能有更高的要求,可以考虑使用随机化算法,但需要注意最坏情况下的性能问题。
测试和比较

为了比较这两种算法的性能,可以在相同的数据集上运行它们,并测量运行时间。通常,对于小数组,Fisher-Yates算法会更快,而对于大数组,两者的性能差异可能不那么明显。在实际应用中,还可以考虑使用更高级的随机化技术,如随机数生成器种子的一致性设置,以确保重复运行时得到相同的随机结果。

总结

本文比较了 Fisher-Yates 算法和 Knuth-Durstenfeld 算法两种经典的洗牌算法。这两种算法都可以在 O(n) 时间复杂度内完成洗牌操作,并且可以直接在原地进行操作,不需要额外的空间。在实际编程中,可以根据具体情况选择合适的算法来实现洗牌操作。

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白话Learning

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值