2015-01-22 1 views
1

У меня есть рекурсивный алгоритм, который я использую для итерации по иерархической структуре данных, но, к сожалению, с некоторыми данными иерархическая структура настолько глубока, что Я получаю StackOverflowError. Я видел, как это происходит с глубиной около 150 узлов, в то время как данные потенциально могут расти намного дальше. Для контекста этот код будет работать в ограниченных средах, и изменение размера стека JVM не является опцией, а структура данных является заданной и представляет собой различные файловые системы с каталогами и файлами.Преобразование рекурсивного метода (где рекурсия выполняется внутри цикла) в итеративный метод

Чтобы обойти переполнение стека, я попытался преобразовать алгоритм в итеративный. Это не то, что мне приходилось делать раньше, поэтому я начал с некоторых примеров, показывающих, как это сделать с простой рекурсией, но я не уверен, как применить это к рекурсии внутри цикла. Я нашел способ сделать это, который, похоже, работает, но код довольно сумасшедший.

Вот упрощенная версия моего оригинального рекурсивного метода:

private CacheEntry sumUpAndCacheChildren(Node node) { 
    final CacheEntry entry = getCacheEntry(node); 

    if (entryIsValid(entry)) 
     return entry; 

    Node[] children = node.listChildren(); 

    long size = 0; 

    if (children != null) {   
     for (Node child : children) { 
      if (child.hasChildren()) { 
       size += sumUpAndCacheChildren(child).size;     
      } else {      
       size += child.size(); 
      } 
     }     
    } 

    return putInCache(node, size);  
} 

Каждый узел лист имеет размер, в то время как размер для любого предка узла считается размер всех его потомков. Я хочу знать этот размер для каждого узла, поэтому размер агрегируется и кэшируется для каждого узла.

Вот итеративная версия:

private CacheEntry sumUpAndCacheChildren(Node initialNode) { 
    class StackFrame { 
     final Node node; 
     Node[] children; 

     // Local vars 
     long size; 

     // Tracking stack frame state 
     int stage; 
     int loopIndex; 

     StackFrame(Node node) { 
      this.node = node; 
      this.children = null; 
      this.size = 0; 
      this.stage = 0; 
      this.loopIndex = 0; 
     } 
    } 

    final Stack<StackFrame> stack = new Stack<StackFrame>(); 
    stack.push(new StackFrame(initialNode)); 
    CacheEntry retValue = getCacheEntry(initialNode); 

    outer: 
    while (!stack.isEmpty()) { 
     final StackFrame frame = stack.peek(); 
     final Node node = frame.node; 

     switch(frame.stage) { 
      case 0: { 
       final CacheEntry entry = getCacheEntry(node); 

       if (entryIsValid(entry)) { 
        retValue = entry; 
        stack.pop(); 
        continue;  
       } 

       frame.children = node.asItem().listChildren(); 
       frame.stage = frame.children != null ? 1 : 3; 
      } break; 
      case 1: { 
       for (int i = frame.loopIndex; i < frame.children.length; ++i) { 
        frame.loopIndex = i; 
        final Node child = frame.children[i]; 

        if (child.hasChildren()) { 
         stack.push(new StackFrame(child)); 
         frame.stage = 2; // Accumulate results once all the child stacks have been calculated. 
         frame.loopIndex++; // Make sure we restart the for loop at the next iteration the next time around. 
         continue outer; 
        } else { 
         frame.size += child.size(); 
        } 
       } 

       frame.stage = 3; 
      } break; 
      case 2: { 
       // Accumulate results 
       frame.size += retValue.size; 
       frame.stage = 1;   // Continue the for loop 
      } break; 
      case 3: { 
       retValue = putInCache(node, frame.type); 
       stack.pop(); 
       continue; 
      } 
     } 
    } 

    return retValue; 
} 

Это просто чувствует себя более безумным, чем это должно быть, и было бы болезненным, чтобы сделать это во всех местах в коде, где я рекурсию в дети и делают по-разному ops на них. Какие методы я могу использовать, чтобы упростить рекурсию, когда я агрегирую на каждом уровне и делаю это в цикле над детьми?

EDIT:

Я был в состоянии значительно упростить с помощью приведенных ниже ответов. Код теперь почти такой же сжатый, как исходная рекурсивная версия. Теперь мне просто нужно применять одни и те же принципы везде, где я рекурсирую по одной и той же структуре данных.

+0

Порядок, в котором вы посещаете детей, не имеет значения, не так ли? – immibis

+1

Вам необходимо проанализировать динамическую структуру данных. При реализации рекурсивного алгоритма переданные параметры создают структуру данных. В некоторых случаях эта структура тривиальна, в других - нет. Вам нужно выяснить, как поддерживать логически эквивалентную структуру данных в вашем итеративном алгоритме. –

+0

Порядок не имеет значения, пока я могу получить сумму всех потомков на каждом узле. У меня есть некоторые другие варианты использования, хотя я делаю другие вещи для узлов, например. дублируя их или перемещая их на другое дерево. –

ответ

1

Поскольку вы имеете дело с древовидной структурой и хотите вычислить кумулятивные размеры, попробуйте DFS, отслеживая родительский элемент каждого узла. Я предполагаю, что вы не можете изменить или подкласс Node, и я сохранил все подписи функций, которые вы использовали.

private class SizedNode { 
    public long cumulativeSize; 
    public Node node; 
    public SizedNode parent; 

    public SizedNode(SizedNode parent, Node node) { 
     this.node = node; 
     this.parent = parent; 
    } 

    public long getSize() { 
     if (node.hasChildren()) { 
      return cumulativeSize; 
     } 
     else { 
      return node.size(); 
     } 
    } 
} 

private void sumUpAndCacheChildren(Node start) 
{ 
    Stack<SizedNode> nodeStack = new Stack<SizedNode>(); 

    // Let's start with the beginning node. 
    nodeStack.push(new SizedNode(null, start)); 

    // Loop as long as we've got nodes to process 
    while (!nodeStack.isEmpty()) { 

     // Take a look at the top node 
     SizedNode sizedNode = nodeStack.peek();    
     CacheEntry entry = getCacheEntry(sizedNode.node); 

     if (entryIsValid(entry)) { 
      // It's cached already, so we have computed its size 
      nodeStack.pop(); 

      // Add the size to the parent, if applicable. 
      if (sizedNode.parent != null) { 
       sizedNode.parent.cumulativeSize += sizedNode.getSize(); 

       // If the parent's now the top guy, we're done with it so let's cache it 
       if (sizedNode.parent == nodeStack.peek()) { 
        putInCache(sizedNode.parent.node, sizedNode.parent.getSize()); 
       } 
      } 
     } 
     else { 
      // Not cached. 
      if (sizedNode.node.hasChildren()) { 
       // It's got a bunch of children. 
       // We can't compute the size yet, so just add the kids to the stack. 
       Node[] children = sizedNode.node.listChildren(); 
       if (children != null) { 
        for (Node child : children) { 
         nodeStack.push(new SizedNode(sizedNode, child)); 
        }  
       }      
      } 
      else { 
       // It's a leaf node. Let's cache it. 
       putInCache(sizedNode.node, sizedNode.node.size()); 
      } 
     } 
    } 
} 
0

КИ, собирается им объяснить человеческие слова, так как я не хочу, чтобы закодировать прямо сейчас:

  1. Приобретать верхний уровень элементов и записать в список
  2. LOOP НАЧАТЬ
  3. подсчета элементов на этот уровень и добавьте их на свой счетчик
  4. получить список детей из вашего текущего списка, сохранить отдельно
  5. удалить список текущих элементов
  6. список записи детей, где список текущих элементов был
  7. LOOP END

вам просто нужно положить булеву в петле-заголовок и установить его на ложь, если список детей больше не имеет элементов ... Надеюсь, я смог правильно выразить себя, не стесняйтесь задавать вопросы и/или спрашивать о разъяснениях.

Этот алгоритм будет получить экспоненциально медленнее (-> O (n²)) в каждой итерации, если структура данных хранит «раскладывания», его довольно неэффективным и им вполне уверен, что кто-то может придумать оптимизации - но он будет быстрее, чем с рекурсией, и это не приведет к переполнению стека; но это может привести к исключению OutOfMemoryException для очень больших наборов данных, но так как только один уровень повторяется в любое время, это ... совершенно нереально. Я думаю,

+0

Вещь есть, есть счетчик для каждого узла, а не только один счетчик для всего цикла. В противном случае это было бы намного проще. Я имею в виду, я предполагаю, что я мог бы хранить счетчики в отдельной структуре, а не как часть фрейма стека, а затем итеративный алгоритм стал бы проще, и я мог бы повторять его снова и снова с помощью другого счетчика каждый раз, но это не кажется намного проще, чем то, что у меня есть сейчас. –

+0

ну, вот и облом, тогда - но вам понадобится только один счетчик, объявленный вне цикла .... я бы порекомендовал AtomicInteger – specializt

+0

Закончился поиск более простого способа, но потребовалось некоторое переосмысление проблемы. Определенно лучше, чем способ ASMing рекурсивного подхода. –

1

В основном вы выполняете итеративный обход после N-арного послепорядка; вы можете попытаться найти это для более подробных примеров.

В очень грубом псевдокоде:

Node currentNode; 
Stack<Node> pathToCurrent; 
Stack<Integer> sizesInStack; 
Stack<Integer> indexInNode; 

pathToCurrent.push(rootNode); 
sizesInStack.push(0); 
indexInNode.push(0); 

current = rootNode; 
currentSize = 0; 
currentIndex = 0; 
while (current != null) { 
    if (current.children != null && currentIndex < current.children.size) { 
    //process the next node 
    nextChild = current.children[currentIndex]; 
    pathToCurrent.push(current); 
    sizesInStack.push(currentSize); 
    indexInNode.push(currentIndex); 
    current = nextChild; 
    currentSize = 0; 
    currentIndex = 0; 
    } else { 
    //this node is a leaf, or we've handled all its children 
    //put our size into the cache, then pop off the stack and set up for the next child of our parent 
    currentSize += this.size(); 
    putInCache(this, currentSize); 
    current = pathToCurrent.pop(); //If pop throws an exception on empty stack, handle it here and exit the loop 
    currentSize = currentSize + sizesInStack.pop(); 
    currentIndex = 1 + indexInNode.pop(); 
    } 
} 
+0

Я видел это только после того, как придумал другое решение, но это поможет мне справиться с другими местами в коде, где мне нужно изменить ситуацию на итеративный. Благодаря! –

0

После адаптации @ ответ Мариуса к моему прецеденте, я пришел с этим:

class SizedNode { 
    final Node node; 
    final SizedNode parent; 

    long size; 
    boolean needsCaching; 

    SizedNode(Node node, SizedNode parent) { 
     this.parent = parent; 
     this.node = node; 
    } 
} 

private CacheEntry sumUpAndCacheChildren(Node start) {  
    final Stack<SizedNode> stack = new Stack<SizedNode>(); 
    stack.push(new SizedNode(start, null)); 
    CacheEntry returnValue = getCacheEntry(start); 

    while (!stack.isEmpty()) { 
     final SizedNode sizedNode = stack.pop();   
     final CacheEntry entry = getCacheEntry(sizedNode.folder); 

     if (sizedNode.needsCaching) { 
      // We finished processing all children, and now we're done with this node. 
      if (sizedNode.parent != null) { 
       sizedNode.parent.size += sizedNode.size;     
      } 
      returnValue = putInCache(sizedNode.folder, sizedNode.size); 
     } else if (entryIsValid(entry)) { 
      if (sizedNode.parent != null) { 
       sizedNode.parent.size += entry.size; 
      } 
      returnValue = entry; 
     } else {      
      // The next time we see this node again, it will be after we process all of its children. 
      sizedNode.needsCaching = true; 
      stack.push(sizedNode); 

      for (Node child : sizedNode.node.listChildren()) { 
       if (child.hasChildren()) { 
        stack.push(new SizedNode(child, sizedNode));       
       } else { 
        sizedNode.size += child.size(); 
       } 
      }    
     } 
    } 

    return returnValue; 
} 

Это намного лучше, чем сумасшедший беспорядок я придумал с моим первым пропуском. Просто идет, чтобы показать, что вам действительно нужно подумать о преобразовании алгоритма, чтобы он также имел смысл как итеративный подход. Спасибо всем за помощь!

Смежные вопросы