【问题标题】:How to shuffle an array in C++?如何在 C++ 中对数组进行洗牌?
【发布时间】:2021-12-17 06:52:33
【问题描述】:

我有一个数组:

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;
}

我从不同的人那里尝试了一些其他的东西,但我无法成功。任何有帮助的东西,谢谢!

【问题讨论】:

  • std::shuffle()。停止使用random_shuffle();它在 C++17 中被删除。
  • 你有什么理由使用 C 风格的数组而不是 std::vector
  • 如果你坚持使用 C 数组,那就是 std::begin(names)#include &lt;iterator&gt;
  • 如果您想知道为什么不应该使用 random_shuffle,请参阅stackoverflow.com/questions/19219726/…
  • 正如其他人所说,更喜欢std::shuffle() 而不是std::random_shuffle()(如果使用 C++17 及更高版本)。除此之外,您需要使用合理的迭代器来表示要洗牌的范围。您的代码中的names-&gt;begin()names-&gt;end() 分别等效于names[0].begin()names[0].end(),因此您的代码正在改组单个字符串names[0],而不是数组names。用于随机播放 names 而不是 names[0] 的迭代器是(C++11 和更高版本)std::begin(names)std::end(names) 或(所有 C++ 版本,包括 C++11 之前的版本)namesnames+sizeof(names)/sizeof(*names)

标签: c++ arrays random shuffle


【解决方案1】:

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

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

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

之所以只打乱第一个词,是因为类型和用法的原因。

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

string names[197];

问题源于 C 世界。数组是否非常容易衰减为指针(仅通过在表达式中使用)。所以这里names-&gt; 已经衰减为指向数组第一个元素的指针。这允许您使用通常仅适用于指针的-&gt; 运算符。因此,您在指向数组第一个元素的指针上调用函数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。它会在每个新行之后强制刷新底层的 bugger。这是非常低效的。

 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";
    }
}

【讨论】:

    【解决方案2】:

    这是您的代码,我认为改动最少。有人可能会争辩说,我不需要对您的第一个 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(),因为向量类提供了自己的迭代器。

    【讨论】:

    • 既然names 是一个字符串向量,而不是像整数这样更容易复制的东西,那么const auto &amp; i 不会比auto i 更有效吗?
    • 是的,你是对的。它已经改变了。
    • 与原版略有不同。原版只使用每行的第一个单词(并删除第一行)。您将整行放入names
    • 没有样本输入,任何人都可以猜测什么是正确的。
    猜你喜欢
    • 2021-11-28
    • 1970-01-01
    • 2017-10-17
    • 1970-01-01
    • 2020-02-21
    • 1970-01-01
    • 2018-10-11
    • 1970-01-01
    • 2013-01-14
    相关资源
    最近更新 更多