【问题标题】:Flood fill recursive algorithm洪水填充递归算法
【发布时间】:2014-02-16 23:20:24
【问题描述】:

我正在尝试制作一种可以在 c# 中填充 int 数组的算法。基本上,作为 MS Paint 中的填充工具,我有一种颜色,如果我在数组中选择 (x,y) 坐标,它会用新颜色替换所有具有相同初始颜色的邻居。

例如:

[0,0,0]
[0,1,0]
[1,1,0]

如果我将 3 放入 (0,0),则数组变为:

[3,3,3]
[3,1,3]
[1,1,3]

所以我尝试了递归,它确实有效,但并非一直有效。实际上,我有时会遇到“堆栈溢出”错误(似乎合适)。 这是我的代码,如果你能告诉我出了什么问题,那就太好了:)

public int[,] fill(int[,] array, int x, int y, int initialInt, int newInt)
{
    if (array[x, y] == initialInt)
    {
        array[x, y] = newInt;

        if (x < array.GetLength(0) - 1)
            array = fill(array, (x + 1), y, initialInt, newInt);
        if (x > 0)
            array = fill(array, (x - 1), y, initialInt, newInt);

        if (y < array.GetLength(1) - 1)
            array = fill(array, x, (y + 1), initialInt, newInt);
        if (y > 0)
            array = fill(array, x, (y - 1), initialInt, newInt);
    }

    return array;
}

谢谢!

【问题讨论】:

  • 是的......这是一个递归调用的废话。可能最好以 C# 之类的语言以程序方式执行此操作,该语言不能保证尾调用优化(并不是说您当前的代码无论如何都会从中受益)
  • 不错 -->“堆栈溢出”

标签: c# algorithm recursion fill stack-overflow


【解决方案1】:

正如已经建议的那样,问题在于递归调用的数量。在 32 位机器上,指针有 4 个字节,所以如果你有一个 512*512 的图像并且你想填充整个东西,那么函数指针单独占用堆栈上的 512*512*4bytes = 1MB 内存。 And that is the default size of the stack。无论函数指针是什么,您都会传递另外 5 个引用(arrayxyinitialIntnewInt),它们都作为临时对象在每次调用时被复制,并且在堆栈中直到函数调用终止。在相同大小的图像上,这些是另外 512*512*5*4bytes = 5MB 的内存。

要解决您的问题,您可以modify(与上面相同的链接)堆栈的大小。

另外,为了节省一些堆栈空间,您可以考虑将参数包装在单个对象中,在这种情况下,每次调用您将只有 4 位临时内存,而不是 20 位。

不过,正如已经指出的那样,最好的解决方案是迭代地重写您的算法。

【讨论】:

  • 请注意,尽管在特定情况下增加堆栈大小可能是一种解决方法,但在提供更大的图像时问题会再次出现。它仍然是一个非常低效的算法,真正的解决方案是修复算法。
【解决方案2】:

使用堆栈/队列来管理剩余的工作怎么样?

public void Fill(int[,] array, int x, int y, int newInt)
{
    int initial = array[x,y];

    Queue<Tuple<int,int>> queue = new Queue<Tuple<int,int>>();
    queue.Push(new Tuple<int, int>(x, y));

    while (queue.Any())
    {
        Tuple<int, int> point = queue.Dequeue();

        if (array[point.Value1, point.Value2] != initial)
            continue;

        array[point.Value1, point.Value2] = newInt;

        EnqueueIfMatches(array, queue, point.Value1 - 1, point.Value2, initial);
        EnqueueIfMatches(array, queue, point.Value1 + 1, point.Value2, initial);
        EnqueueIfMatches(array, queue, point.Value1, point.Value2 - 1, initial);
        EnqueueIfMatches(array, queue, point.Value1, point.Value2 + 1, initial);        
    }
}

private void EnqueueIfMatches(int[,] array, Queue<Tuple<int, int>> queue, int x, int y, int initial)
{
    if (x < 0 || x >= array.GetLength(0) || y < 0 || y >= array.GetLength(1))
        return;

    if (array[x, y] == initial)
       queue.Enqueue(new Tuple<int, int>(x, y));
}

【讨论】:

  • 感谢这个算法和快速的答案,但这不起作用。确实,我有一个“内存不足”异常..有什么想法吗?
【解决方案3】:

这是一个教科书示例,说明为什么使用递归并不总是合适的。递归非常适合遍历本质上是递归的数据结构(例如树),但您的像素数据数组却不是。

我建议在您的代码中添加一个计数器来打印调用fill() 函数的频率。在每次函数调用时,您的机器必须在内存中的堆栈上创建一个新帧(因此它知道在函数结束后它必须返回到哪个内存位置)。递归图像填充算法会大量调用fill() 函数,因此堆栈会增长得非常快。它的大小有限,因此对于较大的图片会溢出。

解决方案是实现一种迭代填充算法,使用循环而不是递归来迭代像素。

参见 Wikipedia on recursionstack overflows,或 Foley 等人的“计算机图形学、原理和实践”。有关基本计算机图形算法的更详细介绍。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多