如何在 C++ 中打乱数组?

How to shuffle an array in C++?

我有一个数组:

names[4]={john,david,jack,harry};

我想随机播放,例如:

names[4]={jack,david,john,harry};

我尝试使用它,但它只是打乱了数组中第一个单词的字母:

random_shuffle(names->begin(), names->end());

这是完整的代码,它从 .txt 文件中读取名称并放入数组中:

#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
using namespace std;


int main() {

    ifstream readName("names.txt");
    string names[197];
    int i = 0;
    for (string line; getline(readName, line); ){
        readName >> names[i];
        i++;
    }
    readName.close();


    random_shuffle(names->begin(), names->end());
    
    for (int i = 0; i < 197; i++) {
        cout << names[i] << endl;
    }
    return 0;
}

我尝试了不同人的一些其他东西,但我无法做到。任何有帮助的东西,谢谢!

这是您的代码,我认为改动最少。有人可能会争辩说我不需要那么多地更改您的第一个 for 循环,但我认为如果您有先见之明知道您正在阅读多少个名字,您不妨使用这些知识。

#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>  // std::begin(), std::end(); required for C-arrays
#include <random>    // std::mt19937; needed to feed std::shuffle()
#include <string>
// using namespace std;  // BAD PRACTICE

int main() {
  constexpr int size = 4;  // Give your magic number a name; only need to change
                           // a single location
  std::ifstream readName("names.txt");
  if (!readName) {  // Always check that you successfully opened the file.
    std::cerr << "Error opening file.\n";
    return 1;
  }

  std::string names[size];
  // int i = 0;
  for (int i = 0; i < size; ++i) {  // Retool the loop entirely
    std::getline(readName, names[i]);
  }
  readName.close();

  // This is a fragile solution. It's only working because the array is in
  // scope
  std::shuffle(std::begin(names), std::end(names),
               std::mt19937{std::random_device{}()});

  for (int i = 0; i < size; i++) {
    std::cout << names[i]
              << '\n';  // Don't use std::endl unless you actually need it
  }
  return 0;
}

不过,这不是理想的代码。对输入文件大小的任何更改都需要更改代码并重新编译。最大的单一变化是摆脱 std::random_shuffle 并使用 std::shuffle() 代替。 std::random_shuffle 在 C++14 中弃用并在 C++17 中移除。使用它是不好的。 std::shuffle() 确实增加了提供 PRNG 的要求,但还不错。如果你有一个 PRNG 需要在一个更大的程序中随机化许多不同的东西,它会产生更好的代码。这是因为最好有一个 PRNG 并让它在您的程序中一直存在,而不是不断构建新的。

而 C 数组只是让事情变得有点笨拙。输入 std::vector.

#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <random>  // std::mt19937; needed to feed std::shuffle()
#include <string>
#include <vector>

int main() {
  std::ifstream readName("names.txt");
  if (!readName) {  // Always check that you successfully opened the file.
    std::cerr << "Error opening file.\n";
    return 1;
  }

  std::vector<std::string> names;
  std::string name;
  while (std::getline(readName, name)) {  // Retool the loop entirely
    names.push_back(name);
  }
  readName.close();

  std::shuffle(std::begin(names), std::end(names),
               std::mt19937{std::random_device{}()});

  for (const auto& i : names) {
    std::cout << i << '\n';
  }

  return 0;
}

矢量可以根据需要增长,因此您会看到读取名称的循环变得多么简单。它也更加灵活,因为您不必提前知道预期有多少条目。它会“正常工作”。在调用 std::shuffle() 时,我保留了 std::begin(names) 语法,因为许多人认为这是最佳实践, 但是 如果需要,您也可以使用 names.begin()因为向量 class 提供了自己的迭代器。

让我们看看你的主要问题:

I tried to use this but it just shuffled the letters of the first word in the array:

random_shuffle(names->begin(), names->end());

它只打乱第一个单词的原因是因为类型和用法。

所以 names 是一个字符串数组。

string names[197];

问题出在C世界。数组是否非常容易衰减为指针(仅通过在表达式中使用)。所以这里 names-> 已经衰减为指向数组第一个元素的指针。这允许您使用通常仅适用于指针的 -> 运算符。因此,您在指向数组中第一个元素的指针上调用函数 begin()end()。因此只有名字被洗牌。

使用 std::begin() 方法修复此问题。

// here std::begin / std::end find the beginning and end
// of the array. So you are shuffling the array.
random_shuffle(std::begin(names), std::end(names));

但我会注意到 random_shuffle() 已经过时了。正如@sweenish 所提到的,您应该使用 std::shuffle() 有关详细信息,请参阅他的回答。


我们可以改进的几件事:

您使用 C 数组存储名称。当然可以,但它容易受到一些问题的影响,因为它无法重新调整大小(除非您认为该文件永远不会被更改,否则这可能是一个问题)。对于遥远的维护者来说可能是一个隐藏的问题。

 std::vector<std::string>  names;  // resizeable container.

我会注意到当前的实现忽略了第一行。然后从每个后续行中读取第一个单词。还有一个小问题,最后一行可能是空的,你将一个空名称读入数组的最后一个元素(但你不会跟踪你读了多少名字,所以除非你使用数组中的所有元素,否则你可能永远不会请注意)。

我会改变它。因为不明显。我会故意和单独地忽略第一行。然后我会简单地将所有第一个单词读入一个向量(所以你知道大小)。

 std::string  line
 std::getline(file, line); // Ignore the first line.

 std::string word
 while(file >> word) {
     names.push_back(word);
     std::getline(file, line);  // ignore the rest of the line.
 }

我们可以想象一下。使用迭代器直接简单地创建数组。

 class Line
 {
     std::string  firstWord;
     friend std::istream& operator>>(std::istream& stream, Line& data) {
         stream >> data.firstWord;
         stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');       retrun stream;
     }
     operator std::string() const {
         return firstWord;
     }
 };

现在您可以在一行中创建和加载矢量:

 std::vector<std::string>   names(std::istream_iterator<Line>(file),
                                  std::istream_iterator<Line>{});

最后,使用 foreach 循环可以更轻松地复制名称。也不要在这样的循环中使用 std::endl 。它会在每一行之后强制刷新底层错误。这是非常低效的。

 for(auto const& name: names) {
     std::cout << name << "\n";
 }

所以结果是:

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>


 class Line
 {
     std::string  firstWord;
     friend std::istream& operator>>(std::istream& stream, Line& data) {
         stream >> data.firstWord;
         stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');       return stream;
     }
     operator std::string() const {
         return firstWord;
     }
 };

int main()
{

    std::ifstream            file("names.txt");

    std::string  line
    std::getline(file, line); // Ignore the first line.


    std::vector<std::string> names(std::istream_iterator<Line>(file),
                                   std::istream_iterator<Line>{});


    random_shuffle(std::begin(names), std::end(names));


    for(auto const& name: names) {
        std::cout << name << "\n";
    }
}