【问题标题】:Storing the last N keystrokes存储最后 N 次击键
【发布时间】:2012-02-04 20:42:26
【问题描述】:

我有点不知所措。我想让特定的击键序列执行特定的操作。

我基本上需要存储最后N次击键,当一个键被按下时,寻找与最近一次击键匹配的序列。

假设我有 2 个序列:

yes
no

当我键入时,我的击键历史如下所示:

a
ab
abc
abcn
abcno

此时它应该识别序列no 并执行适当的操作。

它还需要处理以下序列:

year
yell

和输入如:

yeayell

键序列的长度是有限的,因此可以丢弃旧的击键,使用类似于循环缓冲区的东西,在这种情况下,最佳大小为 3。

我的击键用 Keys 枚举表示。

我应该使用什么数据结构或算法来存储最后 N 次击键并在最后找到序列?

【问题讨论】:

标签: c# .net


【解决方案1】:

这是一个概念验证,可让您使用任何字符序列集合。我假设您只匹配字符(而不是其他键,例如 Keys.Left)。

// Initialize the collection of strings to be matched against here.
string[] stringSequences = new string[] { "yes", "no", "hello" };
int maxLength = stringSequences.Max(s => s.Length);

// The buffer to hold the sequence of the last N characters.
string buffer = "";

while (true)
{
    // Read the next character, and append it to the end of the buffer.
    ConsoleKeyInfo next = Console.ReadKey();
    buffer += next.KeyChar;

    // If the buffer has exceeded our maximum length, 
    // trim characters from its start.
    if (buffer.Length > maxLength)
        buffer = buffer.Substring(1);

    // Check whether the last n characters of the buffer
    // correspond to any of the sequences.
    string match = stringSequences.FirstOrDefault(s => buffer.EndsWith(s));
    if (match != null)
    {
        // Match! Perform any custom processing here.
        Console.WriteLine(Environment.NewLine + "Match: " + match);
    }
}

编辑:适用于按键。

我无法轻松地针对 Keys 进行测试,因此我改用了 ConsoleKey;不过,翻译代码应该不会太难。

// Initialize the collection of key sequences to be matched against here.
ConsoleKey[][] keysSequences = new ConsoleKey[][]
{ 
    new ConsoleKey[] { ConsoleKey.Y, ConsoleKey.E, ConsoleKey.S },
    new ConsoleKey[] { ConsoleKey.N, ConsoleKey.O },
    new ConsoleKey[] { ConsoleKey.H, ConsoleKey.E, ConsoleKey.L, ConsoleKey.L, ConsoleKey.O },
};
int maxLength = keysSequences.Max(ks => ks.Length);

// The buffer to hold the sequence of the last N keys.
List<ConsoleKey> buffer = new List<ConsoleKey>();

while (true)
{
    // Read the next key, and append it to the end of the buffer.
    ConsoleKeyInfo next = Console.ReadKey();
    buffer.Add(next.Key);

    // If the buffer has exceeded our maximum length, 
    // trim keys from its start.
    if (buffer.Count > maxLength)
        buffer.RemoveAt(0);

    // Check whether the last n keys of the buffer
    // correspond to any of the sequences.
    ConsoleKey[] match = keysSequences.FirstOrDefault(ks => 
        buffer.Skip(buffer.Count - ks.Length).SequenceEqual(ks));
    if (match != null)
    {
        // Match! Perform any custom processing here.
        Console.WriteLine(Environment.NewLine + "Match: " + 
            string.Concat(match.Select(k => k.ToString()).ToArray()));
    }
}

【讨论】:

  • 这能以某种方式适应Keys 的集合吗?它背后有正确的想法。我想你的意思是buffer = buffer.Substring(1)
  • System.Windows.Forms.Keys 还是System.ConsoleKey? (是的,你对缓冲区的看法是正确的;已修复。)
  • Microsoft.Xna.Framework.Input.Keys,几乎等同于System.Windows.Forms.Keys
  • 谢谢,看起来很有希望。 SkipSequenceEqual 就是这样。也许我应该再次复习一下我的 LINQ 知识。 :)
  • 是的,这条线是算法的关键。我强烈推荐 LINQ;它为压缩代码创造了奇迹☺
【解决方案2】:

您可以使用循环缓冲区:

char[] buf = new char[3];
int pos = 0;

// on key press
buf[pos] = key;

if (buf[pos] == 'o' && buf[(pos + 2) % 3] == 'n')
    No();

if (buf[pos] == 's' && buf[(pos + 2) % 3] == 'e' && buf[(pos + 1) % 3] == 'y')
    Yes();

pos = (pos + 1) % 3;

【讨论】:

  • 你能解释一下如何在没有硬编码序列的情况下使用它吗?类似于将每个序列存储为List&lt;Keys&gt;
【解决方案3】:

一个简单的state machine 应该可以很好地工作。

它可以在任何不符合您的规则的输入上重置。

enum States
{
 initial,
 y,
 e,
 s,
 n,
 o
}

if(char == 'n' && state == states.Initial)
{
  state = States.n;
}

if(char == 'o' && state == states.n)
{
  state = States.o;
}

... // etc for y, e, s - resetting to `Initial` where needed

... // Check for states o or s 

【讨论】:

  • 以通用方式创建这种状态机并非易事。以“nananaba”为例,输入“nananaba”
  • 我有 10 多个序列,它们有很多公共键。在这种情况下,这将无法正常工作。
  • @kendfrey - 这只是一个基于问题的简单示例。状态也可以是yeeyeodgye。这取决于您如何构建状态图。
【解决方案4】:

快速而简单的方法是使用Rolling hash

【讨论】:

    【解决方案5】:

    不一定是最佳的,但也许是最简单和可重复使用的:

    创建一个CircularBuffer&lt;T&gt; 通用容器。

    这在构造函数中接受一个参数N,即缓冲区中的最大元素数。

    该类有一个T 数组和N 元素和一个索引变量i,它保存最后添加值的索引,将其初始化为-1。

    CircularBuffer 有两种方法,Add(T) 和ToString()。

    Add 方法递增 i,如果 i = N 将其环绕到 0,并将值存储在数组中的适当位置。这应该允许您将值添加到缓冲区,同时仅将其中的 N 保留在内存中。

    ToString 将输出一个与存储在缓冲区中的字符串等效的字符串。

    对于N = 4,假设你有这个数组:

                             a b c d
    
    the indices are          0 1 2 3
    
    the last added value is      ^
    
    `ToString` should return 'dabc'
    

    这里的包装逻辑是给用户的练习。

    每次按下某个键时,将其添加到循环缓冲区,调用其ToString 方法并查看它是否包含您要查找的序列。

    【讨论】:

      猜你喜欢
      • 2021-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-05
      • 2018-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多