MergeSorting LinkedList в Java рекурсивно

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

Класс узла:

public class Node<E extends Comparable<E>>
    {

    public E data;
    public Node<E> next;

    public Node(E data)
    {
        this.data = data;
        next = null;
    }

    public void printData()
    {
        System.out.print(data + " ");
    }
}

Класс LinkedList:

public class LinkedList<E extends Comparable<E>>
{

    protected Node<E> root;
    protected int size = 0;

    public LinkedList()
    {
        root = null;
    }


    public void addBeg(E e)
    {

        Node<E> newNode = new Node<E>(e);
        newNode.next = root;
        root = newNode;

        size++;
    }

    public Node deleteBeg()
    {
        Node<E> temp = root;
        if(!isEmpty())
        {
            root = root.next; 
            size--;
        }
        return temp;
    }

    public void setRoot(Node<E> newRoot)
    {
        root = newRoot;
    }

    public boolean isEmpty()
    {
        return root == null;
    }

    public Node<E> getRoot()
    {
        return root;
    }

    public void printList()
    {
        Node<E> cur = root;   
        while(cur!=null)
            {
                cur.printData();
                cur=cur.next;
            }
        System.out.println();        
    }
}

Класс MergeSorter:

public class MergeSorter<E extends Comparable<E>>
{

    public MergeSorter()
    {

    }

    private void split(LinkedList<E> list, LinkedList<E> firHalf, LinkedList<E> secHalf)
    {
        //if 0 or only 1 elements in the list - it doesn't seem to work, however
        if(list.getRoot() == null || list.getRoot().next == null)firHalf = list;
        else{
            Node<E> slow = list.getRoot(); 
            Node<E> fast = list.getRoot().next; 
            while(fast!=null)
            {
                fast = fast.next;
                if(fast!=null)
                {
                    fast = fast.next;
                    slow = slow.next;
                } 
            }
            //If I use the following line firHalf list is empty when in the caller of this method (it's not in this method, however). Don't understand why ):
            //firHalf = list; 
            firHalf.setRoot(list.getRoot());
            secHalf.setRoot(slow.next);
            slow.next = null;
        }

    }



    private LinkedList<E> merge(LinkedList<E> a, LinkedList<E> b)
     {
        LinkedList<E> mergedList = new LinkedList<E>();    
        Node<E> dummy = new Node<E>(null);
        Node<E> tail = dummy;

        while(true)
        {         
            if(a.getRoot() == null){
                tail.next = b.getRoot();
                break;
            }
            else if(b.getRoot() == null){
                tail.next = a.getRoot();
                break;
            }

            else 
            {
                if(a.getRoot().data.compareTo(b.getRoot().data) <= 0)
                {
                    tail.next = a.getRoot();
                    tail = tail.next;
                    a.setRoot(a.getRoot().next);
                }

                else
                {   
                    tail.next = b.getRoot();
                    tail = tail.next;
                    b.setRoot(b.getRoot().next);
                }
                tail.next = null;
            }

        }
        mergedList.setRoot(dummy.next);
        return mergedList;
    }

    public void mergeSort(LinkedList<E> list)
    {
        Node<E> root = list.getRoot();
        LinkedList<E> left  = new LinkedList<E>();
        LinkedList<E> right = new LinkedList<E>();

        if(root == null || root.next == null) return; //base case
        split(list, left, right); //split

        mergeSort(left);
        mergeSort(right);

        list = merge(left, right); // when this mergeSort returns this list should be  
                                   // referenced by the left or right variable of the 
                                   // current mergeSort call (but it isn't!)
    }
}

Я довольно новичок в Java (исходя из фона C), поэтому я искренне сожалею заранее, если мой код совершенно неверен. Когда я тестирую методы разделения и слияния в классе MergeSorter независимо друг от друга, кажется, что все работает (разбиение списка, состоящего из 0 или 1 элемента, не работает и сводит меня с ума, но для сортировки слиянием это не нужно). Однако метод mergeSort не работает, и я не могу понять, как это сделать. Я пытался отлаживать его сам, и, кажется, есть проблема, когда две половины сливаются в один список, а затем возвращается рекурсия. На недавно объединенный список должна ссылаться либо левая, либо правая переменная текущего вызова mergeSort, но вместо этого я получаю только последний элемент, а не весь список.


person user3192253    schedule 14.01.2014    source источник


Ответы (1)


Аргументы метода в Java всегда передаются по значению.

Это может немного сбивать с толку, поскольку доступ к объектам всегда осуществляется через ссылки, поэтому вы можете подумать, что они передаются по ссылке; но это не так. Вместо этого ссылки передаются по значению.

Это означает, что такой метод:

public void methodThatDoesNothing(Object dst, Object src) {
    src = dst;
}

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

Итак, в вашем коде это:

firHalf = list;

действительно ничего не делает. Я думаю, что вы хотите:

while (! firHalf.isEmpty()) {
    firHalf.deleteBeg();
}
if (! list.isEmpty()) {
    firHalf.addBeg(list.root().data);
}

который изменяет объект, на который ссылается firHalf, так что он имеет те же элементы нуль или один, что и list.

person ruakh    schedule 14.01.2014
comment
Ааа, я понял твою мысль. Большое спасибо! Однако мне кажется, что это слишком много кода для такой простой задачи. Не так ли: firHalf.setRoot(list.getRoot()); делать ту же работу? И я полагаю, что передача по значению также является причиной того, что слияние в моем методе mergeSort не работает должным образом, когда рекурсия возвращается? (: - person user3192253; 14.01.2014
comment
Итак, в моем методе mergeSort я добавил новый LinkedList‹E›mergedList для хранения объединенного списка левого и правого: mergedList = merge(left, right), а затем я установил корень списка таким же, как корень этого mergedList с помощью list.setRoot(mergedList.getRoot()) и это сработало! Однако я чувствую, что мое решение довольно сложное (по сравнению с тем, как оно выглядело бы, например, на C). Есть ли что-то, что можно улучшить в моем коде? Спасибо еще раз! (: - person user3192253; 14.01.2014
comment
@user3192253: user3192253: firHalf.setRoot(list.getRoot()) установит для firHalf и list один и тот же фактический корневой узел, а не просто одно и то же корневое значение. Это означает, что если бы кто-то написал list.getRoot().value = ... или list.getRoot().next = ..., это изменило бы оба списка. Так . . . да, вы должны это сделать, но только после изменения вашего класса Node, чтобы он был неизменяемым, чтобы его можно было безопасно использовать между несколькими списками. (То есть: Node.value и Node.next должны быть объявлены final и инициализированы в конструкторе.) - person ruakh; 14.01.2014
comment
(В качестве альтернативы вы можете дать своему классу Node метод, который рекурсивно копирует список, а затем написать firHalf.setRoot(list.getRoot().clone()). Но я думаю, что путь неизменности лучше.) - person ruakh; 14.01.2014
comment
Хорошо, большое спасибо.. еще раз. Если, однако, я изменю Node на неизменяемый, то некоторые алгоритмы, такие как обращение связанного списка или добавление двух списков, будет намного сложнее реализовать (поскольку оба требуют изменения Node.next). Если это объявлено как окончательное, то мне нужно будет создать полностью новый связанный список новых узлов, а не просто изменить ссылки на узлы текущего): - person user3192253; 14.01.2014
comment
Кроме того, я не совсем уверен в этом, но если мы абстрактно посмотрим на два связанных списка и у них будет один и тот же корень, то, по сути, это один и тот же список, верно? Поэтому мне кажется разумным, что изменение «одного из списков» повлияет на «другой». - person user3192253; 14.01.2014
comment
Re: ваш первый комментарий: Правильно. Если ваши списки неизменяемы, то реверсирование или добавление означает создание нового списка, который является перевернутой или добавленной копией. Re: ваш второй комментарий: Да, это то, что я говорю. Вы не хотите писать firHalf.setRoot(list.getRoot()), потому что это сделает их одним и тем же списком, а это не то, что вам нужно. Вы просто хотите, чтобы firHalf содержало текущее содержимое list. - person ruakh; 15.01.2014
comment
Может быть, тогда я не получаю разделенную часть mergeSort. Я беру list и делю его на firHalf и secHalf. Мне больше не нужно list. Именно так я бы написал функцию split на C (используя ссылочные указатели). Может быть, это выглядит странно, написанное на Java. Может быть, мне следует переписать его так, чтобы он принимал LinkedList<E> и возвращал пару связанных списков (что означало бы, что мне нужно создать новый класс с именем PairOfLinkedLists и т. д.). Извините.. Я все еще не привык к языку, который я полагаю. - person user3192253; 15.01.2014
comment
@ user3192253: Не нужно извиняться. Re: Я беру list и делю его на firHalf и secHalf. Мне больше не нужно list: Да, я нахожу это странным. У split нет причин уничтожать list. - person ruakh; 15.01.2014
comment
Да, я сам нахожу это немного странным. Но, как ни странно, это имеет смысл (если разделить яблоко, то получится 2 половинки, не новое яблоко, а две половинки :). И я не совсем уверен, но именно так должен вести себя split в эффективной сортировке слиянием (разделение списка на два путем его уничтожения), верно? - person user3192253; 16.01.2014
comment
@user3192253: user3192253: Сборка мусора работает в Java так, что нет особых причин для повторного использования объектов Node (это все, что нужно для уничтожения list). Если вы специально не профилировали и не определили иное в своем случае, я думаю, что преимущества неизменности больше всего перевешивают преимущества повторного использования объектов. (Кроме того, если вы действительно заботитесь об эффективности, я думаю, что наиболее эффективным подходом является копирование списка в массив, выполнение сортировки слиянием снизу вверх, а затем копирование обратно. Это полностью устраняет все этапы разделения.) - person ruakh; 16.01.2014