数据结构和算法

DSA 主页 DSA 概述 DSA 环境设置 DSA 算法基础 DSA 渐近分析

数据结构

DSA 数据结构基础 DSA 数据结构和类型 DSA 数组数据结构

链接列表

DSA 链接列表数据结构 DSA 双向链接列表数据结构 DSA 循环链表数据结构

堆栈 &队列

DSA 堆栈数据结构 DSA 表达式解析 DSA 队列数据结构

搜索算法

DSA 搜索算法 DSA 线性搜索算法 DSA 二分搜索算法 DSA 插值搜索 DSA 跳跃搜索算法 DSA 指数搜索 DSA 斐波那契搜索 DSA 子列表搜索 DSA 哈希表

排序算法

DSA 排序算法 DSA 冒泡排序算法 DSA 插入排序算法 DSA 选择排序算法 DSA 归并排序算法 DSA 希尔排序算法 DSA 堆排序 DSA 桶排序算法 DSA 计数排序算法 DSA 基数排序算法 DSA 快速排序算法

图形数据结构

DSA 图形数据结构 DSA 深度优先遍历 DSA 广度优先遍历 DSA 生成树

树数据结构

DSA 树数据结构 DSA 树遍历 DSA 二叉搜索树 DSA AVL 树 DSA 红黑树 DSA B树 DSA B+ 树 DSA 伸展树 DSA 尝试 DSA 堆数据结构

递归

DSA 递归算法 DSA 使用递归的汉诺塔 DSA 使用递归的斐波那契数列

分而治之

DSA 分而治之 DSA 最大最小问题 DSA 施特拉森矩阵乘法 DSA Karatsuba 算法

贪婪算法

DSA 贪婪算法 DSA 旅行商问题(贪婪方法) DSA Prim 最小生成树 DSA Kruskal 最小生成树 DSA Dijkstra 最短路径算法 DSA 地图着色算法 DSA 分数背包问题 DSA 作业排序截止日期 DSA 最佳合并模式算法

动态规划

DSA 动态规划 DSA 矩阵链乘法 DSA Floyd Warshall 算法 DSA 0-1 背包问题 DSA 最长公共子序列算法 DSA 旅行商问题(动态方法)

近似算法

DSA 近似算法 DSA 顶点覆盖算法 DSA 集合覆盖问题 DSA 旅行商问题(近似方法)

随机算法

DSA 随机算法 DSA 随机快速排序算法 DSA Karger 最小割算法 DSA Fisher-Yates 洗牌算法

DSA 有用资源

DSA 问答 DSA 快速指南


Fisher-Yates 打乱算法


Fisher-Yates 打乱算法通过生成随机排列来打乱有限元素序列。每个排列发生的可能性均等。该算法的执行过程是:将序列中的元素存储在一个"麻袋"中,然后从中随机抽取每个元素,形成打乱后的序列。

该算法以 Ronald Fisher 和 Frank Yates 的名字命名,以纪念他们设计了最初的打乱方法,具有无偏性。它在相同条件下生成所有排列,因此输出结果不受影响。然而,现代版本的 Fisher-Yates 算法比原始版本更高效。

Fisher-Yates 算法

原始方法

Shuffle 算法的原始方法需要用纸笔对有限序列进行随机排列。生成随机排列的算法如下:−

步骤 1 − 写下有限序列中的所有元素。声明一个单独的列表来存储获得的输出。

步骤 2 − 在输入序列中随机选择一个元素 i 并将其添加到输出列表中。将元素 i 标记为已访问。

步骤 3 −重复步骤 2,直到有限序列中的所有元素都被访问并随机添加到输出列表中。

步骤 4 − 过程终止后生成的输出列表即为生成的随机排列。

现代算法

现代算法是对原始 Fisher-Yates 洗牌算法稍加修改的版本。修改的主要目标是通过降低原始方法的时间复杂度来实现原始算法的计算机化。现代方法由 Richard Durstenfeld 开发,并由 Donald E. Knuth 推广。

因此,现代方法使用交换而不是维护另一个输出列表来存储生成的随机排列。时间复杂度降低到 O(n) 而不是 O(n2)。算法如下 −

步骤 1 −写下有限序列中从 1 到 n 的元素。

步骤 2 − 在输入序列中随机选择一个元素 i,并将其与列表中最后一个未访问的元素交换。

步骤 3 − 重复步骤 2,直到有限序列中的所有元素都被访问并交换。

步骤 4 − 过程终止后生成的列表即为随机置换序列。

伪代码

在以下现代方法伪代码中,混洗是从数组的最高索引到最低索引进行的。

Fisher-Yates Shuffle (array of n elements):
for i from n−1 downto 1 do
   j ← random integer such that 0 ≤ j ≤ i
   exchange a[j] and a[i]

在以下现代方法伪代码中,从数组的最低索引到最高索引进行混洗。

Fisher-Yates Shuffle (array of n elements):
for i from 0 to n−2 do
   j ← random integer such that i ≤ j < n
   exchange a[i] and a[j]

原始方法示例

为了更好地描述该算法,我们对给定的字母表前六个字母的有限序列进行置换。输入序列:A B C D E F。

步骤 1

这称为纸笔法。我们考虑一个存储有限序列的输入数组和一个存储结果的输出数组。

input_sequence

步骤 2

随机选择任意元素,并在将其标记为已选中后将其添加到输出列表中。在本例中,我们选择元素 C。

output_list

步骤 3

下一个随机选择的元素是 E,它被标记并添加到输出列表中。

chosen_randomly_E

步骤 4

然后,随机函数选择下一个元素 A,并在将其标记为已访问后将其添加到输出数组中。

next_element_A

步骤 5

然后从中选择 F输入序列中剩余的元素,并在标记为已访问后添加到输出中。

F_selected

步骤 6

下一个被选中添加到随机排列中的元素是 D。它被标记并添加到输出数组中。

output_array

步骤 7

输入列表中的最后一个元素是 B,因此它被标记并最终添加到输出列表中。

input_list_B

现代方法

为了降低原始方法的时间复杂度,引入了现代算法。现代方法使用交换来打乱序列——例如,该算法的工作原理类似于通过交换原始牌堆中牌的位置来洗牌。让我们通过一个例子来理解现代版 Fisher-Yates 算法的工作原理。

步骤 1

将字母表的前几个字母作为输入,并使用现代方法对其进行打乱。

modern_method

步骤 2

随机选择元素 D,并将其与序列中最后一个未标记元素(在本例中为 F)交换。

swapped_output choosing_D

步骤3

下一步,我们选择元素 B 与最后一个未标记元素"E"交换,因为 F 在上一步交换后已被移动到 D 的位置。

choose_element_B choosed_element_B

步骤 4

接下来,我们将元素 A 与 F 交换,因为它是列表中最后一个未标记元素。

swap_A_with_F last_unmarked_element

步骤 5

然后将元素 F 与最后一个未标记元素 C 交换。

F_swapped_C unmarked_element_C

步骤 6

序列中剩余的元素最终可以交换,但由于随机函数选择了 E 作为元素,因此保留原样。

chose_E chosed_E

步骤 7

剩余元素 C 保持原样,不进行交换。

C_left final_output_array

交换后得到的数组即为最终输出数组。

示例

以下是上述方法在各种编程语言中的实现 −

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 使用原始方法执行 Fisher-Yates Shuffle 的函数
void fisherYatesShuffle(char arr[], char n) {
    char output[n];  // 创建一个输出数组来存储打乱后的元素
    char visited[n]; // 创建一个布尔数组来跟踪访问过的元素
    // Initialize the visited array with zeros (false)
    for (char i = 0; i < n; i++) {
        visited[i] = 0;
    }
    // 执行 shuffle 算法
    for (char i = 0; i < n; i++) {
        char j = rand() % n; // 在输入数组中生成随机索引
        while (visited[j]) { // 查找下一个未访问的索引
            j = rand() % n;
        }
        output[i] = arr[j]; // 将所选索引处的元素添加到输出数组
        visited[j] = 1;     // 将元素标记为已访问
    }
    // 将打乱顺序的元素复制回原始数组
    for (char i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}
int main() {
    char arr[] = {'A', 'B', 'C', 'D', 'E', 'F'};
    char n = sizeof(arr) / sizeof(arr[0]);

    srand(time(NULL)); // 使用当前时间为随机数生成器播种
    fisherYatesShuffle(arr, n); // 调用 shuffle 函数
    printf("Shuffled array: ");
    for (char i = 0; i < n; i++) {
        printf("%c ", arr[i]); // 打印打乱后的数组
    }
    printf("
");
    return 0;
}

输出

Shuffled array: A B F D E C 
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
// 使用原始方法执行 Fisher-Yates Shuffle 的函数
void fisherYatesShuffle(std::vector<char>& arr) {
    std::vector<char> output; // 创建一个输出向量来存储混洗后的元素
    std::vector<bool> visited(arr.size(), false); // 创建一个布尔向量来跟踪访问过的元素
    // 执行 shuffle 算法
    for (char i = 0; i < arr.size(); i++) {
        char j = rand() % arr.size(); // 在输入向量中生成随机索引
        while (visited[j]) { // 查找下一个未访问的索引
            j = rand() % arr.size();
        }
        output.push_back(arr[j]); // 将所选索引处的元素添加到输出向量
        visited[j] = true; // 将元素标记为已访问
    }
    arr = output; // 将打乱的元素复制回原始向量
}
int main() {
    std::vector<char> arr = {'A', 'B', 'C', 'D', 'E', 'F'};
    srand(time(NULL)); // 使用当前时间为随机数生成器播种
    fisherYatesShuffle(arr); // 调用 shuffle 函数
    std::cout << "Shuffled array: ";
    for (char c : arr) {
        std::cout << c << " "; // 打印打乱后的数组
    }
    std::cout << std::endl;
    return 0;
}

输出

Shuffled array: D B A F C E
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class FisherYatesShuffle {
    // 使用原始方法执行 Fisher-Yates Shuffle 的函数
    public static List<Character> fisherYatesShuffle(List<Character> arr) {
        List<Character> output = new ArrayList<>(); // 创建输出列表来存储打乱后的元素
        boolean[] visited = new boolean[arr.size()]; // 创建一个布尔数组来跟踪访问过的元素
        // 执行 shuffle 算法s
        for (int i = 0; i < arr.size(); i++) {
            int j = new Random().nextInt(arr.size()); // 在输入列表中生成随机索引
            while (visited[j]) { // 查找下一个未访问的索引
                j = new Random().nextInt(arr.size());
            }
            output.add(arr.get(j)); // 将所选索引处的元素添加到输出列表
            visited[j] = true; // 将元素标记为已访问
        }
        return output;
    }
    public static void main(String[] args) {
        List<Character> arr = List.of('A', 'B', 'C', 'D', 'E', 'F');
        Random rand = new Random(); // 使用当前时间为随机数生成器播种
        List<Character> shuffledArray = fisherYatesShuffle(arr); // 调用 shuffle 函数
        System.out.print("Shuffled array: ");
        for (char c : shuffledArray) {
            System.out.print(c + " "); // 打印打乱后的数组
        }
        System.out.println();
    }
}

输出

Shuffled array: D B E C A F 
import random
# 使用原始方法执行 Fisher-Yates Shuffle 的函数
def fisherYatesShuffle(arr):
    output = []  # 创建输出列表来存储打乱后的元素
    visited = [False] * len(
        arr)  # 创建一个布尔列表来跟踪访问过的元素
    # 执行 shuffle 算法
    for i in range(len(arr)):
        j = random.randint(0,
                           len(arr) -
                           1)  # 在输入列表中生成随机索引
        while visited[j]:  # 查找下一个未访问的索引
            j = random.randint(0, len(arr) - 1)
        output.append(
            arr[j])  # 将所选索引处的元素添加到输出列表
        visited[j] = True  # 将元素标记为已访问
    return output
if __name__ == "__main__":
    arr = ['A', 'B', 'C', 'D', 'E', 'F']
    random.seed()  # 使用当前时间为随机数生成器播种
    shuffled_array = fisherYatesShuffle(arr)  # 调用 shuffle 函数
    print("Shuffled array:", shuffled_array)  # 打印打乱后的数组

输出

Shuffled array: ['D', 'C', 'A', 'B', 'F', 'E']