【问题标题】:Deep copying and swapping subtrees in javajava中的深度复制和交换子树
【发布时间】:2013-11-28 04:24:58
【问题描述】:

感谢您抽出宝贵时间阅读本文(对于文字墙感到抱歉)。

背景

是的,这是一个学校项目。不,我不是要你为我做这件事。我自己完成了所有工作,我只是被 Java 本身难住了,问题不大。

该项目是关于基因编程的基础知识。我随机生成树(代表基本数学表达式),根据来自目标函数的数据(x,y 值)评估它们的适应度,然后通过消除一些非常遥远的树来操纵集合,稍微改变一些,然后执行对两个最合适的表达式进行“交叉”操作。交叉是问题。我创建了一个有效的算法,但它永久地改变了两棵原始树,我想保留它们,以防交叉生成两棵远离的树。

为了解决这个问题,我研究了复制树形结构,结果证明这很难实现。我研究了几种不同的方法,首先是序列化。在尝试并阅读了一些examples 之后,我自己尝试了一下,但失败了。我意识到我对 Java 的掌握不够好,无法使用该技术。所以我决定在 Node 类上使用递归复制构造函数,并在 GPTree 类上使用一个简单的复制构造函数(见下文)。

我无法解释出了什么问题,如果你看不到它尤其困难,但请耐心等待。在 Eclipse 中调试了一段时间后,问题似乎始于 GPTree.crossover 方法。具体如下:

Node t1Node = getRandomNode(t1.root);
Node t1NodeParent = t1Node.parent;
Node t2Node = getRandomNode(t2.root);
Node t2NodeParent = t2Node.parent;

例如,当我查看前两个节点(t1Nodet1NodeParent)的调试器信息时,t1Node 的 id 将是(所有数字由示例组成)18 及其父节点的 id将是 22。然后,在分配给 t1NodeParent 之后,t1NodeParent 的 id 将是 22(就像它应该是的那样),但它的左孩子或右孩子的 id 都不会是 18(就像孩子的) !因此,这些赋值下面的if 子句会变得混乱,然后树也会变得混乱。有时(不是每次,奇怪的是)原始树也会被更改。

我认为这与我尝试复制树木的方式有关,因为这是唯一改变的事情。但是,就我的一生而言,我无法告诉你它为什么会这样。

所以,感谢您阅读这个冗长的问题。 . .我希望你能帮忙。 :/

这是我的严重精简代码,证明了我的问题:

GPEnv.java (gist)

import java.util.ArrayList;

public class GPEnv {

    private final int MAX_HEIGHT = 4;
    private ArrayList<GPTree> trees;

    public GPEnv(int numTrees) {
        trees = new ArrayList<GPTree>();
        for (int i = 0; i < numTrees; i++) {
            trees.add(new GPTree(MAX_HEIGHT));
        }
    }

    public void crossover() {
        System.out.println(trees.get(0));
        System.out.println(trees.get(1));
        System.out.println();

        /*
        This commented version works, but it permanently alters the original trees.
        GPTree[] res = GPTree.crossover(
            trees.get(0),
            trees.get(1)
        );
        */

        GPTree[] res = GPTree.crossover(
            trees.get(0).copy(),
            trees.get(1).copy()
        );

        System.out.println(res[0]);
        System.out.println(res[1]);
        System.out.println();

        System.out.println(trees.get(0));
        System.out.println(trees.get(1));
    }

    public static void main(String[] args) {
        GPEnv env = new GPEnv(2);
        env.crossover();
    }

}

Expression.java (gist)

public abstract class Expression {

    protected static enum Token  {
        PLUS("+"),
        MINUS("-"),
        TIMES("*"),
        DIVIDE("/"),
        NUMBER(""),
        VARIABLE("x");

        private final String symbol;

        Token(String symbol) {
            this.symbol = symbol;
        }

        public String toString() {
            return this.symbol;
        }
    }

    protected static class Node {
        protected Token token;
        protected Node parent, left, right;
        protected double value;

        public Node() {
            this.parent = null;
            this.left = this.right = null;
        }

        public Node(int number) {
            this();
            this.token = Token.NUMBER;
            this.value = number;
        }

        public Node(Token token) {
            this();
            this.token = token;
            this.value = Double.NaN;
        }

        private Node(Token token, double number, Node parent, Node left, Node right) {
            switch (token) {
            case PLUS:
                this.token = Token.PLUS;
                this.value = Double.NaN;
                break;
            case MINUS:
                this.token = Token.MINUS;
                this.value = Double.NaN;
                break;
            case TIMES:
                this.token = Token.TIMES;
                this.value = Double.NaN;
                break;
            case DIVIDE:
                this.token = Token.DIVIDE;
                this.value = Double.NaN;
                break;
            case NUMBER:
                this.token = Token.NUMBER;
                this.value = Double.parseDouble(number + "");
                break;
            case VARIABLE:
                this.token = Token.VARIABLE;
                this.value = Double.NaN;
                break;
            }

            this.parent = parent;
            this.left = left;
            this.right = right;
        }

        public void setParent(Node rent) {
            this.parent = rent;
        }

        public boolean isOperator() {
            switch (this.token) {
            case PLUS:
            case MINUS:
            case TIMES: // intentional fall-throughs
            case DIVIDE:
                return true;
            default:
                return false;
            }
        }

        public String toString() {
            if (this.token == Token.NUMBER) {
                return this.value + "";
            }

            return this.token.toString();
        }

        public Node copy() {
            Node left = null;
            Node right = null;

            if (this.left != null) {
                left = this.left.copy();
            }

            if (this.right != null) {
                right = this.right.copy();
            }

            return new Node(token, value, parent, left, right);
        }
    }

    protected Node root;

    private void postOrderTraverse(Node node, StringBuilder sb) {
        if (node != null) {
            postOrderTraverse(node.left, sb);
            postOrderTraverse(node.right, sb);
            sb.append(node + " ");
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        postOrderTraverse(this.root, sb);
        return sb.toString();
    }
}

GPTree.java (gist)

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Random;

public class GPTree extends Expression {

    private static final int MIN_HEIGHT = 2;
    private static final int MAX_MUTATED = 4;
    private static final Random rand = new Random();

    private int maxHeight;

    public GPTree(int maxHeight) {
        super();

        this.maxHeight = maxHeight;
        generateRandomTree();
    }

    private GPTree(Node root, int maxHeight) {
        this.root = root;
        this.maxHeight = maxHeight;
    }

    private static Node getRandomNode(Node node) {
        if (node.left == null && node.right == null) {
            return node;
        } else if (rand.nextInt(10) > 6) {
            return node;
        }

        if (rand.nextInt(2) == 0) {
            return getRandomNode(node.left);
        } else {
            return getRandomNode(node.right);
        }
    }

    private void generateRandomTree(Node node, int depth) {
        if (depth == maxHeight) {
            node.left = new Node(rand.nextInt(10));
            node.left.setParent(node);
            node.right = new Node(rand.nextInt(10));
            node.right.setParent(node);
            return;
        }

        if (rand.nextInt(2) == 0) {
            node.left = new Node(Token.values()[rand.nextInt(4)]);
            generateRandomTree(node.left, depth + 1);
        } else {
            // give numbers an increased chance of occuring (60%)
            if (rand.nextInt(10) > 3) {
                node.left = new Node(rand.nextInt(10));
            } else {
                node.left = new Node(Token.VARIABLE);
            }
        }

        if (rand.nextInt(2) == 0) {
            node.right = new Node(Token.values()[rand.nextInt(4)]);
            generateRandomTree(node.right, depth + 1);
        } else {
            // give numbers an increased chance of occuring (60%)
            if (rand.nextInt(10) > 3) {
                node.right = new Node(rand.nextInt(10));
            } else {
                node.right = new Node(Token.VARIABLE);
            }
        }

        if (depth < MIN_HEIGHT && (!node.left.isOperator() && !node.right.isOperator())) {
            if (rand.nextInt(2) == 0) {
                node.left = new Node(Token.values()[rand.nextInt(4)]);
                generateRandomTree(node.left, depth + 1);
            } else {
                node.right = new Node(Token.values()[rand.nextInt(4)]);
                generateRandomTree(node.right, depth + 1);
            }
        }

        node.left.setParent(node);
        node.right.setParent(node);
    }

    public void generateRandomTree() {
        if (this.root == null) {
            this.root = new Node(Token.values()[rand.nextInt(4)]);
        }

        generateRandomTree(this.root, 1);
    }

    public static GPTree[] crossover(GPTree t1, GPTree t2) {
        GPTree[] result = new GPTree[2];

        Node t1Node = getRandomNode(t1.root);
        Node t1NodeParent = t1Node.parent;
        Node t2Node = getRandomNode(t2.root);
        Node t2NodeParent = t2Node.parent;

        t2Node.parent = t1NodeParent;
        if (t1NodeParent == null) {
            t1.root = t2Node;
        } else {
            if (t1NodeParent.left == t1Node) {
                t1NodeParent.left = t2Node;
            } else {
                t1NodeParent.right = t2Node;
            }
        }

        t1Node.parent = t2NodeParent;
        if (t2NodeParent == null) {
            t2.root = t1Node;
        } else {
            if (t2NodeParent.left == t2Node) {
                t2NodeParent.left = t1Node;
            } else {
                t2NodeParent.right = t1Node;
            }
        }

        result[0] = t1;
        result[1] = t2;
        return result;
    }

    public GPTree copy() {
        return new GPTree(root.copy(), maxHeight);
    }

}

【问题讨论】:

  • 您是否需要从头开始实现数据结构?我很想用DOM 来表示树。
  • 技术上没有,但我已经在这方面做了很多工作,而且在到期之前我没有那个。我害怕不得不尝试学习一种新方法,然后实施它。 . .

标签: java tree copy binary-tree deep-copy


【解决方案1】:

您在制作副本时似乎没有尝试修复父链接。

想象一个基本的表达式 1+2。当您复制“+”时,例如 id 22,您将创建一个新的“+”节点,其中包含其两个子节点的副本和对其原始父节点的引用(null)。假设原始“1”节点的 id 为 5,新创建的“1”节点的 id 为 18。在构建新的“1”节点时,您将父链接保留回 22,因为在制作副本时您没有仍然可以访问新的父级。

在深度复制循环数据结构时解决此问题的一种方法是提供一种特殊的复制操作,该操作将新的父节点作为参数。深层副本应调用此特殊副本,并为其每个子节点添加不完整的新父节点。

类似

public Node copy() {
    return copyWithParent( parent );
}
public Node copyWithParent( Node parentOverride ) {
    Node out = new Node( token, value, parentOverride, null, null );

    if (this.left != null) {
        out.left = this.left.copyWithParent( out );
    }

    if (this.right != null) {
        out.right = this.right.copyWithParent( out );
    }

    return out;
}

【讨论】:

  • 所以,在copyWithParent 我会做类似this.setParent(out) 的事情?
  • 查看我的编辑。 copy() 成为以您调用它的节点为根的子树的深层副本,因为否则我们每次都需要复制整个树。
【解决方案2】:

Node.copy() 方法 - 你实际上可以将它命名为Name.clone(),然后让整个类实现Cloneable。更重要的是:您正在克隆和设置孩子,但您忘记了这些孩子会有新的父母。克隆后,您应该将 parent 设置为新的,例如:

  • 克隆孩子,
  • 将它们传递给仅用于克隆的私有构造函数,该构造函数将调用一些setParent(this) 方法。

没有它,您只是在创建使用克隆节点的父节点作为其父节点的新节点,这可能是出错的原因。

【讨论】:

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