【问题标题】:Is List<>.Sort() the best way to reduce the number of checks?List<>.Sort() 是减少检查次数的最佳方法吗?
【发布时间】:2025-12-03 07:35:02
【问题描述】:

我正在制作一个 meme 排名应用程序,它可以将您最喜欢的 meme 排名从最好到最差,并删除排名最低的 meme,以消除旧的和过时的 meme 中的额外膨胀,从而节省磁盘空间。我认为因为List&lt;T&gt;.Sort() 功能非常快,它会很快帮助用户对可能数百个模因进行分类。情况并非如此,因为当我尝试使用下面的方法进行排序时,我得到了一些奇怪的结果。

// Using Task.Run() temp. due to the easy access. Will thread this properly in the future. 
Task.Run(() =>
{
    Manager.Files.Sort(delegate (Photo x, Photo y) {
        // I have Invoke built into the ChangeImage method but having double duty doesn't slow it down.
        Invoke(new MethodInvoker(() =>
        {
            ChangeImage(pictureBox1, x.Filename);
            ChangeImage(pictureBox2, y.Filename);
        }));
        WaitForButtonPress.WaitOne(); // Pauses the thread until an image is chosen. 
        switch (LastButton)
        {
            case 1: // if x is better than y
                return 1;
            case 2: // if y is better than x
                return -1;
            case 3: // if y and x are equals
                return 0;
            default:
                break;
        }
        return 0; 
    });
});

我在这段代码中遇到的问题是,有时pepe.jpgisThisAPidgon.png 经常被相互比较多次,尤其是在连续出现比较后。 pepe.jpg vs.1.jpgpepe.jpg vs.2.png...pepe.jpeg vs.nth.jpgpepe.jpg vs.isThisAPidgon.png,然后又是isThisAPidgon.png vs.pepe.jpg .在发现这种奇怪的行为后,我尝试检查它们被调用了多少次。

static void Main(string[] args)
{
    List<Number> numbers = new List<Number>();
    Random rand = new Random(); 
    for (int i = 0; i < 500; i++)
    {
        numbers.Add(new Number() { Num = rand.Next(0, 500) });
    }

    foreach(Number num in numbers)
    {
        Console.WriteLine(num.num);
    }

    numbers.Sort((Number x, Number y) =>
    {
        int numx = x.Num;
        int numy = y.Num;
        if (numx > numy)
            return 1;
        else if (numy > numx)
            return -1;
        else
            return 0;

        //return x.Num - y.Num;
    });

    int total = 0;
    foreach(Number num in numbers)
    {
        Console.WriteLine($"Num: {num.num} Times Checked: {num.timesChecked}");
        total += num.timesChecked;
    }
    Console.WriteLine($"Finished with {total} checks.");
}

班级编号:

class Number
{
    public Number()
    {

    }

    public int num;

    public int timesChecked = 0;

    public int Num { get { timesChecked++; return num; } set => num = value; }
}

&lt; == &gt; 比较返回 1、-1 或 0 并返回 x.numy.num 的差异,两者产生相同的结果:有些出现的频率比有些高。以下是一些示例。

#checked with differences
Num: 168 Times Checked: 8
Num: 170 Times Checked: 17
Num: 170 Times Checked: 316 #316?
Num: 170 Times Checked: 14
Num: 171 Times Checked: 15

#checked with differences
Num: 237 Times Checked: 12
Num: 237 Times Checked: 9
Num: 240 Times Checked: 105 #More reasonable... 
Num: 241 Times Checked: 14
Num: 242 Times Checked: 15

#checked with differences
Num: 395 Times Checked: 10
Num: 397 Times Checked: 8
Num: 398 Times Checked: 502 #How could it fail to sort this number in more tries than the array is long?
Num: 398 Times Checked: 7
Num: 399 Times Checked: 8

#checked with <==>
Num: 306 Times Checked: 15
Num: 306 Times Checked: 17
Num: 307 Times Checked: 756 #This is ridiculous how does this happen?
Num: 307 Times Checked: 13
Num: 309 Times Checked: 15

似乎差异总检查数低于 10000,但使用 /1,-1,0 方法检查时,总检查数似乎始终高于 15000。有没有一种排序算法专注于减少需要比较对象才能进行排序的次数?

编辑:我在 比较示例中犯了一个错误。我使用x.Numy.Num 两次,结果夸大了。为了解决这个问题,我将这两个属性存储为本地变量,它在 9000 左右将总数从 15000 以上下降到 10000 以下,同时在 8000 左右减去 sill 仍然低于 10000。

【问题讨论】:

  • 你的比较是传递性的吗?这意味着如果A&lt;B 那么另外,B&gt;A 必须是真的?如果A&lt;=BB&lt;=C 那么它给出了A&lt;=C 也必须是真的?
  • @LasseV.Karlsen 是的,这是真的。只要A&lt;B 的真实性没有改变,那么每个内存中出现的顺序无关紧要,所以这肯定意味着A&lt;=BB&lt;=C 因此A&lt;=C

标签: c# arrays sorting


【解决方案1】:

大多数排序算法的复杂度为 O(n log n),这意味着它们需要执行那么多比较才能对数据进行排序。所以,不,您将无法使用 Sort 来完成您正在做的事情。

其次,一些内置的 Sort 方法会根据列表的大小切换行为,因此您的用户界面可能会根据他们选择的算法而感觉非常不同。我以前从未见过有人使用 Sort 来确定 UI 行为,新颖但不寻常。

如果您确实想使用排序算法,可以使用插入排序(使用二进制搜索将每个新项目与现有列表进行比较以找到它的去向)或快速排序(通过将一个元素与所有其他元素进行比较将元素分成两组) .

但是...我认为这两种方法都不会带来出色的用户体验,两者都会让人感到重复。而且,鉴于这是一个主观问题,答案通常不是项目的纯粹线性排序。人们并不一致,当他们这样做时,他们会产生循环 A->B->C->A。

因此,这里有一个关于 UI 体验的建议,该体验感觉重复性较低,可以处理主观异常并且易于实施:

可能会随机选择成对的图像,并要求用户将其中的一个排列在另一个之上。如果他们愿意,让用户不一致。将他们创建的每一对 A->B 放入图表中。找出图表中尚未连接或只有一个连接的任何节点,并重点询问它们与您已经评分的节点的排名情况。

这样,如果他们对 A->B->C 进行排名,然后对 C->D 进行排名,算法就不会一直询问 A 和 B 与 D 的比较情况。

最后应用一种称为Topological Sort 的技术并忽略您发现的任何循环。如果您愿意,可以使用近似拓扑排序。

有一个包含此功能的图形库(我编写的)。有关调用 .TopologicalSortApprox() 的示例,请参阅 this test

一旦所有项目都在图表中,您就可以继续前进,使用尝试使图表更接近直线的比较。但在任何时候,如果用户感到无聊并想停下来(没有人愿意进行 n log n 比较!),您至少有一个可以使用的近似排名。

【讨论】:

  • 所以你的意思是排序函数经常比较它们,因为它比较的是与我正在做的事情无关的情况?如果我理解正确的话。我得到的是制作图 A->B->C 和 D->E->F。如果 C 和 F 是最好的组,则比较 C > 或 C)F、C、E、B、D、A 来自从最好到最坏?
  • @DoshorteDovencio 我更新了答案以试图澄清并添加另一点关于使用内置排序功能来驱动您的 UI 的危险。
最近更新 更多