【问题标题】:How to create unique string containing numbers and letters without repeating name once used如何创建包含数字和字母的唯一字符串而不重复使用一次
【发布时间】:2021-11-02 14:13:15
【问题描述】:

我正在 C# 中尝试以下编码挑战:

管理机器人出厂设置。

当机器人离开工厂时,它没有名字。

第一次开启机器人时,会在 两个大写字母后跟三个数字的格式,例如 RX837 或 BC811。

每隔一段时间,我们需要将机器人重置为出厂设置 设置,这意味着它的名称被擦除。下次问的时候 该机器人将使用一个新的随机名称进行响应。

名称必须是随机的:它们不应遵循可预测的 序列。使用随机名称意味着有冲突的风险。您的解决方案 必须确保每个现有机器人都有一个唯一的名称。

我创建了一个机器人类,它通过了我的 8 个单元测试中的 7 个。失败的一个是:

[Fact]
public void Robot_names_are_unique()
{
    const int robotsCount = 10_000;
    var robots = new List<Robot>(robotsCount); // Needed to keep a reference to the robots as IDs of recycled robots may be re-issued
    var names = new HashSet<string>(robotsCount);
    for (int i = 0; i < robotsCount; i++) {
        var robot = new Robot();
        robots.Add(robot);
        Assert.True(names.Add(robot.Name));
        Assert.Matches(@"^[A-Z]{2}\d{3}$", robot.Name);
    }
}

我浏览了我的代码,我认为问题在于我正在生成随机值,但在创建多个名称时我不能确保这些值是唯一的。这是我的课:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public class Robot
{
    Random random = new Random();
    Dictionary<string, bool> usedNames = new Dictionary<string, bool>();
    public Robot()
    {
        Name = RandomName();
    }

    private string _name;

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }


    public void Reset()
    {
        Name = RandomName();
    }

    private string RandomName()
    {
        Random rand = new Random();
        int nums = random.Next(000, 1000);
        var val = nums.ToString("000");
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        string letters = new string(Enumerable.Repeat(chars, 2)
            .Select(s => s[random.Next(s.Length)]).ToArray());
        string name = $"{letters}{val}";
        if (usedNames.ContainsKey(name))
        {
// Implement here or refactor with loop?
        }
        return name;
    }
}

但是,在查看了我的代码之后,我觉得有更好的方法。我认为该方法将涉及从头到尾依次遍历名称中可能的数字和字母,以确保每个名称都是唯一的。我在正确的轨道上吗?我能做的更好吗?

【问题讨论】:

  • 随机文字后面可以加ID吗?
  • usedNames 是一个实例字段,因此每个Robot 都会跟踪其使用过的名称,仅此而已。
  • 1) 您应该只使用Random 的一个(静态)实例(参见this)。 2)您目前没有采取任何措施来防止生成重复名称,因为 a)usedNames 未在其他实例之间共享,并且 b)即使是,您的 if 语句当前为空(或者您没有包含该部分? )。
  • 我会创建一个静态的NameGenerator 类来跟踪使用过的名称并生成新的唯一名称,并在您的Robot 类中调用它的CreateName() 方法。
  • @41686d6564,我还没有实现。我想看看重写整个方法以根据可能值的总范围迭代每个可能值是否更有意义......如果这有意义的话。+

标签: c# random


【解决方案1】:

我们只有

26 * 26 * 1000 == 676000

可能的名字。让我们生成它们allshuffle。然后我们可以从names一个接一个地取下一个机器人名称:

// Yates algorithm will be faster then ordering by random (here I've used Guid)
static string[] Names = Enumerable
  .Range(0, 26 * 26)
  .SelectMany(letters => Enumerable
     .Range(0, 1000)
     .Select(i => $"{(char)('A' + letters / 26)}{(char)('A' + letters % 26)}{i:000}"))
  .OrderBy(item => Guid.NewGuid())
  .ToArray();

static int currentIndex = -1;

// Interlocked: let's implement thread safe method 
static string NextName() => 
  Names[Interlocked.Increment(ref currentIndex) % Names.Length]; 

演示:

for (int i = 0; i < 10; ++i)
  Console.WriteLine(NextName());    

结果:(可能因工作站而异)

JQ393
GQ249
JZ370
OC621
GD309
CP822
DK698
AD610
XY300
WV698

编辑:如果我们想重用名称(当机器人设置为出厂默认设置时删除)我们可以使用Queue而不是数组:

static ConcurrentQueue<string> Names = new ConcurrentQueue<string>(Enumerable
  .Range(0, 26 * 26)
  .SelectMany(letters => Enumerable
     .Range(0, 1000)
     .Select(i => $"{(char)('A' + letters / 26)}{(char)('A' + letters % 26)}{i:000}"))
  .OrderBy(item => Guid.NewGuid()));
  
static string NextName() => Names.TryDequeue(out string result) ? result : "???";

static string ScrapName(string name) => Names.Enqueue(name);

static string ResetName(string oldName) {
  string newName = Names.TryDequeue(out string result) 
    ? result 
    : "???";

  if (!string.IsNullOrEmpty(oldName))
    Names.Enqueue(oldName);

  return newName; 
}

【讨论】:

  • 总体思路是正确的,但需要进行修改以支持问题中提到的恢复出厂设置。否则,仍然可能出现重复名称。
  • @Pharaz Fadaei:只要我们想要一个新名称(无论是新机器人还是出厂重置),我们只需调用 NextName()Names 中的所有 (626000) 名称都是唯一的,因此在我们用尽所有 626000 之前不会有重复
  • @PharazFadaei 下一个名称索引是静态的,它应该总是从列表中拉出一个新的。它不会重复。
  • 1.所有 676000 个机器人都有自己的名字。 2. 最后一个机器人重置(无 676000) 3. 下一次最后一个机器人查找名称时,它将获得第一个机器人的名称(currentIndex + 1 % 长度将为 0),而索引 676000 - 1 处的名称为未使用。
  • @defaultUsernameN 那是另一回事。当您只有一个选项时,随机性没有意义。如果您重置其中两个,则下一个名称不应该是可预测的。
【解决方案2】:

一种选择是创建一个类来生成名称。该类应跟踪已创建的名称。如果机器人数量不多,这种方法效果更好。

public class NameGenerator
{
    static HashSet<string> created = new HashSet<string>();
    static Random rand = new Random();
    const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
    public static string GetName()
    {
        if (created.Count == 676000) {
            // Throw an exception?
        }

        string name;
        do {
            name = $"{chars[rand.Next(chars.Length)]}{chars[rand.Next(chars.Length)]}{rand.Next(0, 1000):D3}";
        } while (!created.Add(name));
        return name;
    }

    public static void Reset() {
        created = new HashSet<string>();
    }
}

一些快速分析:

Number of IDs generated Time (s) Time (ms) to create last Approx. mem used (MB) }
1,000 ~0 <1 0.05
10,000 0.005 <1 0.52
50,000 0.032 <1 2.4
100,000 0.078 <1 4.9
250,000 0.229 <1 11.1
500,000 0.626 <1 22.8
600,000 0.961 <1 25.1
625,000 1.143 <1 25.8
650,000 1.390 <1 26.3
676,000 5.386 293 38.5

一旦接近676,000 限制,显然会有很大的增加。

【讨论】:

  • 最后会变得很慢,当你检查你已经创建的所有 675999 个名字时,直到你设法直接找到你需要的那个......我想几分钟到几小时=)
  • 当我们走到尽头时(比如说,除了两个或三个之外的所有名字都被给出),我们将不得不多次尝试(并且失败)来获得未使用的名字。这可能需要很长时间(这个过程会很慢)
  • 是的,我同意。如果机器人的数量很大,那么@DmitryBychenko 的解决方案会更好。一个简单的秒表测试在 0.3788292 秒内生成了 500000 个 ID。
  • 我的意思是问题是 - 运行 .GetName() 的时间与名称本身一样随机。您可能会在第一次迭代时获得一个免费名称,或者即使在第 10 亿次迭代时也不会获得它。此函数可能需要一毫秒,也可能需要数年。
  • @JohnnyMopp 实际上是公平的。我想我被“这个算法的最坏情况意味着它永远运行”而偏离了方向,并没有注意到“这对很多情况来说都很好,即使它在宇宙的整个生命中都有一天发生最坏情况的可能性极小,怀疑它会在那一刻驾驶阿波罗”。谢谢你的解释。我撤回了我的案子 =)
【解决方案3】:

有很多可能的名字。除非您计划拥有近 50 万个机器人,否则一个好的解决方案是创建一个自定义的、可重复使用的生成器来跟踪所有生成的名称。

public class UniqueNameGenerator
{
   private readonly HashSet<string> generatedNames;
   private readonly Random generator;

   public UniqueNameGenerator(Random random = null)
   {
      this.generatedNames = new HashSet<string>();
      this.generator = random ?? new Random();
   }

   public string GenerateName()
   {
      string name;

      do
      {
          name = this.TryGenerateName();
      }
      while(this.generatedNames.Contains(name));

      this.generatedNames.Add(name);
      return name;
   }

   private string TryGenerateName()
   {
      var nameBuilder = new StringBuilder();

      nameBuilder.Append(this.PickRandomLetter('A', 'Z'));
      nameBuilder.Append(this.PickRandomLetter('A', 'Z'));
      nameBuilder.Append(this.PickRandomNumber(0, 1000));

      return nameBuilder.ToString();
   }

   private int PickRandomNumber(int min, int max)
   {
      return this.generator.Next(min, max + 1);
   }

   private char PickRandomLetter(char from, char to)
   {
      var letterIndex = this.generator.Next((int)from, (int)to);
      return (char)letterIndex;
   }
}

在 Robot 类中保留 this 的静态实例,或者更好的是,创建一个 RobotFactory,它使用 UniqueNameGenerator 的单个实例创建机器人。

【讨论】:

    【解决方案4】:

    使用 random.choice 选择 2 个随机字符和 3 个随机数

    import random
    
    def generate_license():
        letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        numbers = "0123456789"
        license = ""
        for i in range(2):
            license += random.choice(letters)
        for i in range(3):
            license += random.choice(numbers)
        return license
    
    for i in range(30):
        print(generate_license())
    

    输出:

    FD508 FI820 TY975 NR415 GD041 IK313 GR103 WR994 PL631 WT808 紫外线119 KO727 LK584 GM629 BM545 VX728 UN773 AM000 UW267 KE949 KW182 TL030 YW536 AF038 PQ493 TT153 NP626 JK151 WA536 OU825

    【讨论】:

    • 你用新许可证检查旧许可证,如果不是唯一的,则重复该功能。否则使用 guid 获取唯一编号。 c#可以很方便的映射到python。
    • str(uuid.uuid4()) 将创建一个 guid,其长度将超过 5 个字符
    猜你喜欢
    • 1970-01-01
    • 2016-01-17
    • 2013-07-21
    • 2021-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-09
    相关资源
    最近更新 更多