Java 数据结构 排序 - 链表归并排序练习

Java 数据结构 排序 - 链表归并排序练习

这是一个小练习, 虽然是排序方面的练习, 但是做完之后发现其实是承上启下的一个练习.计算机这玩意, 每天必须得敲点代码, 本质上也是一门手工艺, 和钢琴一样得天天练. 练习的要求是实现链表元素的归并排序. 并不难, 不过小细节还很多,来看看 链表归并排序的思想 改造链表加入尾节点引用 add, re

这是一个小练习, 虽然是排序方面的练习, 但是做完之后发现其实是承上启下的一个练习.计算机这玩意, 每天必须得敲点代码, 本质上也是一门手工艺, 和钢琴一样得天天练. 练习的要求是实现链表元素的归并排序. 并不难, 不过小细节还很多,来看看
  1. 链表归并排序的思想
  2. 改造链表加入尾节点引用
  3. add, remove与clear方法
  4. 排序方法与重设lastNode方法
  5. 在链表尾部插入节点的方法
  6. 归并排序链表
  7. 下一步要做的

链表归并排序的思想

链表归并排序的思想本质上与数组归并排序的思想没有本质的不同, 不断比较两个链表的头部节点, 然后拼成一个新链表返回. 不过目前为止的链表(指到目前被我们改造过的可以进行就地插入排序的链表: 见这篇文章)只能在头部插入. 如果改造一下可以在尾部插入, 链表排序的代码写起来就轻松多了.

改造链表加入尾节点引用

在已经能够排序的链表上继续改造, 要达到的目的就是可以在尾部插入节点, 因此需要一个指向尾节点的引用, 先在类中添加一个私有域:
private Node lastNode = null;
这个引用起始就是null, 因为新建的链表是空的. 现在就要来思考一下何时需要操作这个数据, 其实也就是在哪些方法中需要操作lastNode:
  1. 在链表头插入新节点, 如果本来lastNode是空的, 插入之后应该指向这个新节点, 并且之后再插入新节点的时候, lastNode无需变动.
  2. 从链表头删除节点, 删除之后如果链表是空的, 那么lastNode也应该是空的, 否则无需变动lastNode
  3. 从链表头删除节点, 删除之后如果链表是空的, 那么lastNode也应该是空的, 否则无需变动lastNode
  4. 排序方法, 中间的过程可以完全不管, 在排序完成之后, 重新设置lastNode
  5. clear方法, 需要把lastNode也清除
  6. 在链表的尾部添加节点, lastNode需要指向新添加的节点
从上边分析可以看出了, 需要修改add, remove, sort和sortDesc方法, 还需要新增一个重新设置lastNode的方法, 以及一个在链表尾部添加节点的方法. 就按这个顺序一个个来.

add, remove与clear方法

add方法需要进行一个判断, 只要lastNode不是null, 就不进行任何操作就可以了. 如果是null, 就引用至newNode即可, 这样只在第一次添加元素的时候会改变lastNode.
public boolean add(T entry) {

    Node newNode = new Node(entry, firstNode);
    firstNode = newNode;

    //如果lastNode为空, 就设置一次, 不为空就不变
    if (this.lastNode == null) {
        this.lastNode = newNode;
    }

    numberOfEntries++;
    isSorted = false;
    isSortedDesc = false;
    return true;
}
remove方法要考虑到移除元素之后, 当前链表是否为空的情况, 可以判断firstNode也可以再调用isEmpty():
public T remove() {
    T result = null;

    if (!isEmpty()) {
        result = firstNode.data;
        firstNode = firstNode.next;
        numberOfEntries--;

        //删除完一个元素之后, 如果firstNode为空就将last设置为空, 否则就不改变lastNode
        if (firstNode == null) {
            lastNode = null;
        }
    }
    return result;
}
这样add和remove方法都好了, 仅在第一次添加的时候设置lastNode, remove仅在删除到空的时候修改lastNode. clear方法则添加一行即可:
public void clear() {
    this.firstNode = null;
    this.numberOfEntries = 0;
    this.lastNode = null;
}

排序方法与重设lastNode方法

从前边分析可以知道, 排序之后顺序乱了, 原来的lastNode未必是最后一个, 因此需要重新设置一下, 这个代码也很简单, 遍历到链表尾部就行了, 由于是内部使用, 设置为私有即可:
private void refreshLastNode() {
    if (isEmpty()) {
        this.lastNode = null;
    } else {
        this.lastNode = firstNode;
        while (lastNode.next != null) {
            lastNode = lastNode.next;
        }
    }
}
之后在两个排序方法中, 在排序完成之后, 调用这个方法即可, 至于核心的插入节点的私有方法是在过程中使用, 无需进行任何改变:
public void sort() {
    if (!isSorted) {
        Node unsortedNode = firstNode;
        firstNode = null;

        while (unsortedNode != null) {
            Node currentNode = unsortedNode;
            unsortedNode = unsortedNode.next;
            insertIntoNodes(currentNode, false);
        }
        isSorted = true;
        isSortedDesc = false;
        refreshLastNode();
    }
}
public void sortDesc() {

    if (!isSortedDesc) {
        Node unsortedNode = firstNode;
        firstNode = null;

        while (unsortedNode != null) {
            Node currentNode = unsortedNode;
            unsortedNode = unsortedNode.next;
            insertIntoNodes(currentNode, true);

        }
        isSortedDesc = true;
        isSorted = false;
        refreshLastNode();
    }
}

在链表尾部插入节点的方法

然后是用来辅助归并排序的关键方法, 也就是在链表尾部插入节点. 这个方法也可以放开给外部使用, 所以设置为public. 其逻辑也很简单, 先将lastNode.next指向新节点, 然后再将lastNode指向新节点即可. 不要忘记更新是否排序的两个布尔变量.
public boolean addToTail(T entry) {
    //由于修改过add方法, 如果链表为空, 直接调用另外一个方法
    if (isEmpty()) {
        add(entry);
    } else {
        lastNode.next = new Node(entry);
        lastNode = lastNode.next;
        numberOfEntries++;
        isSorted = false;
        isSortedDesc = false;
    }
    return true;
}

归并排序链表

前边进行了一堆准备工作, 就为了能够让链表支持在尾部插入节点. 这里我们归并排序, 采用合并当前链表和另外一个链表, 然后返回一个新链表的方式. 因为有了从链表尾部插入节点的方法, 所以逻辑和数组排序完全相同. 只需要注意的是在方法的开始要进行两个链表是否有序的判断, 在完成归并排序之后, 需要根据升序还是降序来给新链表设置好表示是否排序的布尔变量. 由于我们的从尾部插入的方法已经设置好了尾节点, 因此不用重复设置尾节点.
/**
 * 合并另外一个有序链表的方法, 仅在两个链表都有同样的序的时候工作
 * @param another 要合并的另外一个链表
 * @return 一个新的链表
 */
public LinkedList<T> merge(LinkedList<T> another) {
    LinkedList<T> result = new LinkedList<>();

    //两个都是升序的情况下
    if (this.isSorted) {
        if (another.isSorted) {
            Node currentNode1 = firstNode;
            Node currentNode2 = another.firstNode;

            //两个当前都不为空, 即链表没有遍历完的情况下
            while (currentNode1 != null && currentNode2 != null) {
                //都是升序排列, 所以进行比较, 小的先放到新链表尾部去

                //如果当前的小就放当前的
                if (currentNode1.data.compareTo(currentNode2.data) < 0) {
                    result.addToTail(currentNode1.data);
                    currentNode1 = currentNode1.next;
                } else {
                    result.addToTail(currentNode2.data);
                    currentNode2 = currentNode2.next;
                }
            }
            //循环完毕之后有一个链表遍历完了, 检查另外一个没有遍历完的链表, 挨个把元素放进去
            if (currentNode1 != null) {
                while (currentNode1 != null) {
                    result.addToTail(currentNode1.data);
                    currentNode1 = currentNode1.next;
                }
            }

            if (currentNode2 != null) {
                while (currentNode2 != null) {
                    result.addToTail(currentNode2.data);
                    currentNode2 = currentNode2.next;
                }
            }
            //设置已经升序排列好
            result.isSorted = true;
        } else {
            throw new RuntimeException("链表不是都有序, 无法合并");
        }
    //两个都是降序的情况下
    } else if (isSortedDesc) {
        if (another.isSortedDesc) {
            Node currentNode1 = firstNode;
            Node currentNode2 = another.firstNode;

            //两个当前都不为空, 即链表没有遍历完的情况下
            while (currentNode1 != null && currentNode2 != null) {
                //都是降序排列, 所以进行比较, 大的先放到新链表尾部去

                //如果当前是大的就先放当前的
                if (currentNode1.data.compareTo(currentNode2.data) > 0) {
                    result.addToTail(currentNode1.data);
                    currentNode1 = currentNode1.next;
                } else {
                    result.addToTail(currentNode2.data);
                    currentNode2 = currentNode2.next;
                }
            }
            //循环完毕之后有一个链表遍历完了, 检查另外一个没有遍历完的链表, 挨个把元素放进去
            if (currentNode1 != null) {
                while (currentNode1 != null) {
                    result.addToTail(currentNode1.data);
                    currentNode1 = currentNode1.next;
                }
            }

            if (currentNode2 != null) {
                while (currentNode2 != null) {
                    result.addToTail(currentNode2.data);
                    currentNode2 = currentNode2.next;
                }
            }
            //设置已经降序排列好
            result.isSortedDesc = true;
        } else {
            throw new RuntimeException("链表不是都有序, 无法合并");

        }
    }else {
        throw new RuntimeException("链表不是都有序, 无法合并");
    }

    return result;
}
这里代码有些重复, 想再抽离出来也是可以的, 不过由于逻辑已经很简单了, 也没有必要做进一步分离. 这样就写好了.

下一步要做的

此时的链表, 如果我从尾部添加一个元素, 然后从头部remove一个元素, 这就是一个队列了... 昨晚又回顾了一下至今以来的计算机学习, 发现还是要好好的打基础才可以. 提高最快的并不是一开始学着做web开发, 工程实践, 而是在我重新看过C语言和CSAPP之后. 对于一些基本概念理解更深厚, 以及又一遍开始看数据算法和结构之后, 就发现很多代码真的可以自己写出来了, 知道一小片数据需要如何操作. 这方面提升才是最巨大的. 于是后边翻出来这个老的知乎收藏编程入门指南导读 v1.0, 然后综合五门CS基础课推荐对其中的内容进行了扩充, 列表如下:

    启蒙

  1. 编码 - 隐匿在计算机软硬件背后的语言
  2. 计算机科学导论
  3. 入门

  4. 笨办法学Python
  5. Python基础教程
  6. 抽象基础

  7. CS 61A: Structure and Interpretation of Computer Programs Using Python
  8. CS 61B Data Structures, Spring 2020
  9. http://composingprograms.com/
  10. 计算机系统要素
  11. 语言扩充

  12. 语言扩充 C语言编程:一本全面的C语言入门教程(第三版) C primer plus
  13. 计算机组成原理

  14. 计算机组成与设计
  15. CSAPP
  16. 现代操作系统
  17. 程序设计语言 实践之路 第三版
  18. 语言扩充

  19. C++ primer 第五版
  20. Java核心技术 卷1
  21. 软件工程基础
  22. 代码大全 第二版
下一步我的打算, 就是开始上CS 61A, 开始好好的跟一门课看看.
LICENSED UNDER CC BY-NC-SA 4.0
Comment