链表

一、链表

1.1 概述

  • 链表是一种物理存储单元上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

  • 链表由一系列结点组成(链表中每一个元素称为结点),结点可以在运行时动态生成。

  • 每个结点包括两个部分:一个是存储数据元素的数据域(data域),另一个是存储下一个结点地址的指针域(next域)。

  • 链表分带头节点的链表没有头节点的链表,根据实际的需求来确定。

  • 示意图

在这里插入图片描述

二、单向链表

2.1 实现思路

需求:使用带头节点的单向链表,完成对学生对象的增删改查操作。

  • 添加方式一(链尾添加)

在这里插入图片描述

  • 添加方式二(根据顺序(指定位置)添加)

在这里插入图片描述

  • 修改节点:遍历并通过数据域信息进行判断,找到该节点后完成相应的修改操作。
  • 删除节点

在这里插入图片描述

2.2 代码示例

需求:实现单向链表的增(顺序链尾添加、指定位置添加)删改查、找到单链表倒数第几的节点、获取当前链表大小(即有效数据个数)、链表反转(改变链表结构)、反向打印(不破坏链表结构)、合并两个链表并排序

public class SingleLinkedListDemo {

    public static void main(String[] args) {
        SingleLinkedList singleLinkedList = new SingleLinkedList();

        System.out.println("---[顺序添加]结果如下:---");
        singleLinkedList.add(new Student(1, "张三"));
        singleLinkedList.add(new Student(2, "李四"));
        singleLinkedList.add(new Student(4, "田七"));
        SingleLinkedList.foreach(singleLinkedList.getHead());
        // ---[顺序添加]结果如下:---
        // no=1, name=张三
        // no=2, name=李四
        // no=4, name=田七

        System.out.println();
        System.out.println("---[指定位置添加]结果如下:---");
        singleLinkedList.addByOrder(new Student(3, "(赵六)"));
        SingleLinkedList.foreach(singleLinkedList.getHead());
        // ---[指定位置添加]结果如下:---
        // no=1, name=张三
        // no=2, name=李四
        // no=3, name=(赵六)
        // no=4, name=田七

        System.out.println();
        System.out.println("---[链表反转]结果如下:---");
        SingleLinkedList.reverse(singleLinkedList.getHead());
        SingleLinkedList.foreach(singleLinkedList.getHead());
        // ---[链表反转]结果如下:---
        // no=4, name=田七
        // no=3, name=(赵六)
        // no=2, name=李四
        // no=1, name=张三

        System.out.println();
        System.out.println("---[链表反向打印]结果如下:---");
        SingleLinkedList.reversePrint(singleLinkedList.getHead());
        // ---[链表反向打印]结果如下:---
        // no=1, name=张三
        // no=2, name=李四
        // no=3, name=(赵六)
        // no=4, name=田七

        System.out.println();
        System.out.println("---[修改节点]结果如下:---");
        singleLinkedList.update(new Student(3, "(王九)"));
        SingleLinkedList.foreach(singleLinkedList.getHead());
        // ---[修改节点]结果如下:---
        // no=4, name=田七
        // no=3, name=(王九)
        // no=2, name=李四
        // no=1, name=张三

        System.out.println();
        System.out.println("---[删除节点]结果如下:---");
        singleLinkedList.delete(3);
        SingleLinkedList.foreach(singleLinkedList.getHead());
        // ---[删除节点]结果如下:---
        // no=4, name=田七
        // no=2, name=李四
        // no=1, name=张三

        System.out.println();
        System.out.println("---获取[当前链表中有效数据的个数]结果如下:---");
        System.out.println(SingleLinkedList.size(singleLinkedList.getHead()));
        // ---获取[当前链表中有效数据的个数]结果如下:---
        // 3

        System.out.println();
        System.out.println("---获取[当前链表中倒数第二个数据]结果如下:---");
        Student reciprocalNode = SingleLinkedList.findReciprocalNode(
                singleLinkedList.getHead(), 2);
        System.out.println(reciprocalNode);
        // ---获取[当前链表中倒数第二个数据]结果如下:---
        // no=2, name=李四
    }

    /**
     * 测试两个链表合并后排序。
     */
    @Test
    public void testMerge() {
        SingleLinkedList s1 = new SingleLinkedList();
        s1.add(new Student(1, "a"));
        s1.add(new Student(3, "c"));
        s1.add(new Student(5, "e"));

        SingleLinkedList s2 = new SingleLinkedList();
        s2.add(new Student(2, "b"));
        s2.add(new Student(4, "d"));

        Student mergeHead = SingleLinkedList.mergeAndSorted(s1.getHead(), s2.getHead());
        assert mergeHead != null;
        System.out.println("---[链表合并排序后]结果如下:---");
        SingleLinkedList.foreach(mergeHead);
        // ---[链表合并排序后]结果如下:---
        // no=1, name=a
        // no=2, name=b
        // no=3, name=c
        // no=4, name=d
        // no=5, name=e
    }
}

/**
 * 学生节点。
 */
class Student {
    public int no;
    public String name;

    /**
     * 指向后一节点。
     */
    public Student next;

    public Student(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "no=" + no +
                ", name=" + name;
    }
}

/**
 * 单向链表实现。
 */
class SingleLinkedList {

    /**
     * 头节点(勿动,仅占位)。
     */
    private final Student head = new Student(0, "");

    public Student getHead() {
        return head;
    }

    /**
     * 断言链表是否为空。
     *
     * @param node 节点
     */
    private static void assertLinkedListIsNull(Student node) {
        if (null == node || null == node.next) {
            throw new NullPointerException("链表为空!");
        }
    }

    /**
     * 迭代器。
     *
     * @param head 头节点
     */
    public static void foreach(Student head) {
        assertLinkedListIsNull(head);
        // 由于头节点不能动,所以这里要添加辅助指针来帮助进行遍历。
        Student pointer = head;
        while (null != pointer.next) {
            System.out.println(pointer.next);
            // 后移。
            pointer = pointer.next;
        }
    }

    /**
     * 链尾添加。
     *
     * @param node 节点
     */
    public void add(Student node) {
        Student pointer = this.head;
        while (null != pointer.next) {
            pointer = pointer.next;
        }
        // 循环终止时,表示到了链尾。
        pointer.next = node;
    }

    /**
     * 根据编号指定位置进行添加。
     *
     * @param node 节点
     */
    public void addByOrder(Student node) {
        Student pointer = this.head;
        boolean exist = false;
        while (null != pointer.next) {
            // 1.找到了插入位置。
            if (pointer.next.no > node.no) {
                break;
                // 2.节点编号已存在。
            } else if (pointer.next.no == node.no) {
                exist = true;
                break;
            }
            // 后移。
            pointer = pointer.next;
        }

        // 判断循环终止的条件。
        if (exist) {
            System.out.printf("编号[%d]的节点已存在,添加失败!\n", node.no);
        } else {
            // 插入。
            node.next = pointer.next;
            pointer.next = node;
        }
    }

    /**
     * 更新节点。
     *
     * @param node 节点
     */
    public void update(Student node) {
        Student pointer = this.head;
        boolean found = false;
        assertLinkedListIsNull(pointer);
        while (null != pointer.next) {
            // 找到了需要修改的节点。
            if (pointer.next.no == node.no) {
                found = true;
                break;
            }
            pointer = pointer.next;
        }

        if (found) {
            pointer.next.name = node.name;
        } else {
            System.out.printf("编号[%d]的节点不存在,修改失败。\n", node.no);
        }
    }

    /**
     * 删除节点。
     *
     * @param no 编号
     */
    public void delete(int no) {
        Student pointer = this.head;
        boolean found = false;
        assertLinkedListIsNull(pointer);
        while (null != pointer.next) {
            if (pointer.next.no == no) {
                found = true;
                break;
            }
            pointer = pointer.next;
        }

        if (found) {
            // 指向下下个节点,被跳过节点将失去引用被垃圾回收机制进行回收。
            pointer.next = pointer.next.next;
        } else {
            System.out.printf("编号[%d]的节点不存在,删除失败。\n", no);
        }
    }

    /**
     * 链表大小(即有效数据个数)。
     *
     * @param head 节点
     * @return int
     */
    public static int size(Student head) {
        Student pointer = head;
        if (null == pointer.next) {
            return 0;
        }

        int size = 0;
        while (null != pointer.next) {
            size++;
            pointer = pointer.next;
        }
        return size;
    }

    /**
     * 找到倒数第几的节点。
     *
     * @param head  头节点
     * @param index 倒数第几
     * @return {@link Student}
     */
    public static Student findReciprocalNode(Student head, int index) {
        assertLinkedListIsNull(head);
        int size = size(head);
        if (0 >= index || index > size) {
            return null;
        }
        Student pointer = head;
        // 假设3个数据,需要倒数第2个,3-2=1。则循环1次。
        for (int i = 0; i < (size - index); i++) {
            pointer = pointer.next;
        }
        return pointer.next;
    }

    /**
     * 链表反转(改变链表结构)。
     *
     * @param head 头节点
     */
    public static void reverse(Student head) {
        // 链表中没有节点或者只有一个节点时,无需反转。
        if (null == head.next || null == head.next.next) {
            return;
        }
        // 辅助指针。
        Student pointer = head.next;
        // 临时变量,用于存储当前节点的下一节点。
        Student temp;
        // 新的链表头。
        Student reverseHead = new Student(0, "");
        while (null != pointer) {
            temp = pointer.next;
            // 顺序取出后进行赋值。
            pointer.next = reverseHead.next;
            reverseHead.next = pointer;
            // 后移。
            pointer = temp;
        }
        // 节点指向,完成反转。
        head.next = reverseHead.next;
    }

    /**
     * 反向打印(不破坏链表结构)。
     *
     * @param head 头
     */
    public static void reversePrint(Student head) {
        assertLinkedListIsNull(head);
        Student pointer = head.next;
        Stack<Student> stack = new Stack<>();
        // 压栈。
        while (null != pointer) {
            stack.push(pointer);
            pointer = pointer.next;
        }
        // 弹栈。
        while (!stack.isEmpty()) {
            System.out.println(stack.pop());
        }
    }

    /**
     * 合并两个链表并排序。
     *
     * @param head1 head1
     * @param head2 head2
     * @return {@link Student}
     */
    public static Student mergeAndSorted(Student head1, Student head2) {
        if (null == head1.next && null == head2.next) {
            return null;
        }
        // 较小头节点。
        Student head = head1.no <= head2.no ? head1 : head2;
        // 较小头节点的下一节点。
        Student cur1 = head.next;
        // 较大头节点。
        Student cur2 = (head == head1) ? head2 : head1;
        // 辅助指针
        Student pointer = head;
        while (null != cur1 && null != cur2) {
            // 较小值插入链表。
            if (cur1.no <= cur2.no) {
                pointer.next = cur1;
                cur1 = cur1.next;
            } else {
                pointer.next = cur2;
                cur2 = cur2.next;
            }
            // 后移。
            pointer = pointer.next;
        }
        pointer.next = (null != cur1) ? cur1 : cur2;
        return head.next;
    }
}

三、双向链表

3.1 实现思路

与单向链表不同的是,双向链表增加了指向前一节点的属性。

  • 两种链表区别

    • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找

    • 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除。所以前面我们单链表删除

      时节点,总是需要找到 pointer 的下一个节点。

  • 双向链表增删改查实现思路分析:

    • 遍历:实现与单链表一样,只是可以向前,也可以向后查找。
    • 添加:默认添加到双向链表的最后。
    • 修改:实现与单链表一样。
    • 删除:因为是双向链表,因此,我们可以实现自我删除某个节点。
  • 示意图

在这里插入图片描述

3.2 代码示例

需求:实现双向链表的增(顺序链尾添加、指定位置添加)删改查

public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();

        System.out.println("---[顺序添加]结果如下:---");
        doubleLinkedList.add(new Teacher(1, "张老师"));
        doubleLinkedList.add(new Teacher(2, "李老师"));
        doubleLinkedList.add(new Teacher(4, "田老师"));
        doubleLinkedList.foreach();
        // ---[顺序添加]结果如下:---
        // no=1, name=张老师
        // no=2, name=李老师
        // no=4, name=田老师

        System.out.println();
        System.out.println("---[指定位置添加]结果如下:---");
        doubleLinkedList.addByOrder(new Teacher(3, "(赵老师)"));
        doubleLinkedList.foreach();
        // ---[指定位置添加]结果如下:---
        // DEBUG INFO (addByOrder result) : prev.no=[2] ,cur.no=[3] ,next.no=[4]
        // no=1, name=张老师
        // no=2, name=李老师
        // no=3, name=(赵老师)
        // no=4, name=田老师

        System.out.println();
        System.out.println("---[修改节点]结果如下:---");
        doubleLinkedList.update(new Teacher(3, "(王老师)"));
        doubleLinkedList.foreach();
        // ---[修改节点]结果如下:---
        // no=1, name=张老师
        // no=2, name=李老师
        // no=3, name=(王老师)
        // no=4, name=田老师

        System.out.println();
        System.out.println("---[删除节点]结果如下:---");
        doubleLinkedList.delete(3);
        doubleLinkedList.foreach();
        // ---[删除节点]结果如下:---
        // DEBUG INFO (delete before) : prev.no=[2] ,cur.no=[3] ,next.no=[4]
        // no=1, name=张老师
        // no=2, name=李老师
        // no=4, name=田老师
    }
}

/**
 * 双向链表实现。
 */
class DoubleLinkedList {

    /**
     * 头节点勿动。
     */
    private final Teacher head = new Teacher(0, "");

    public Teacher getHead() {
        return head;
    }

    /**
     * 链尾添加。
     *
     * @param node 节点
     */
    public void add(Teacher node) {
        Teacher pointer = this.head;
        while (null != pointer.next) {
            pointer = pointer.next;
        }
        pointer.next = node;
        node.prev = pointer;
    }

    /**
     * 根据编号指定位置进行添加。
     *
     * @param node 节点
     */
    public void addByOrder(Teacher node) {
        Teacher pointer = this.head;
        boolean exist = false;
        while (null != pointer.next) {
            if (pointer.next.no > node.no) {
                break;
            } else if (pointer.next.no == node.no) {
                exist = true;
                break;
            }
            pointer = pointer.next;
        }

        if (exist) {
            System.out.printf("编号[%d]的节点已存在,添加失败!\n", node.no);
        } else {
            // 后一节点双链指向。no.3 <-> no.2
            // 注意这里后一节点可能为空
            if (null != pointer.next) {
                node.next = pointer.next;
                node.next.prev = node;
            }

            // 前一节点双链指向。no.1 <-> no.2
            pointer.next = node;
            node.prev = pointer;

            // 结果验证。
            System.out.printf("DEBUG INFO (addByOrder result) : prev.no=[%d] ,cur.no=[%d] ,next.no=[%d]\n",
                    node.prev.no,
                    node.no,
                    (null == node.next ? null : node.next.no));
        }
    }

    /**
     * 更新节点。
     *
     * @param node 节点
     */
    public void update(Teacher node) {
        Teacher pointer = this.head;
        assertLinkedListIsNull(pointer);
        boolean found = false;
        while (null != pointer.next) {
            if (pointer.next.no == node.no) {
                found = true;
                break;
            }
            pointer = pointer.next;
        }

        if (found) {
            // 只是当前节点的值变了,前后链接指向关系没有动。
            pointer.next.name = node.name;
        } else {
            System.out.printf("编号[%d]的节点不存在,修改失败。\n", node.no);
        }
    }

    /**
     * 删除节点。
     *
     * @param no 编号
     */
    public void delete(int no) {
        Teacher pointer = this.head.next;
        assertLinkedListIsNull(pointer);
        boolean found = false;
        while (null != pointer) {
            if (pointer.no == no) {
                found = true;
                break;
            }
            pointer = pointer.next;
        }
        if (found) {
            // before: no.2(pointer.prev) <-> no.3(pointer) <-> no.4(pointer.next)
            // after: no.2 <-> no.4
            System.out.printf("DEBUG INFO (delete before) : prev.no=[%d] ,cur.no=[%d] ,next.no=[%d]\n",
                    pointer.prev.no,
                    pointer.no,
                    pointer.next.no);

            // 后链被删除的后一节点。
            pointer.prev.next = pointer.next;
            // 注意:如果是最后一个节点,就不需要执行下面这句话,否则出现空指针!
            if (pointer.next != null) {
                // 被删除的后一节点往前链。
                pointer.next.prev = pointer.prev;
            }
        } else {
            System.out.printf("编号[%d]的节点不存在,删除失败。\n", no);
        }
    }

    /**
     * 断言链表是否为空。
     *
     * @param node 节点
     */
    private static void assertLinkedListIsNull(Teacher node) {
        if (null == node || null == node.next) {
            throw new NullPointerException("链表为空!");
        }
    }

    /**
     * 迭代器。
     */
    public void foreach() {
        Teacher pointer = this.head;
        assertLinkedListIsNull(pointer);
        while (null != pointer.next) {
            System.out.println(pointer.next);
            pointer = pointer.next;
        }
    }
}

/**
 * 教师节点。
 */
class Teacher {

    public int no;
    public String name;

    /**
     * 指向前一节点。
     */
    public Teacher prev;

    /**
     * 指向后一节点。
     */
    public Teacher next;

    public Teacher(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "no=" + no +
                ", name=" + name;
    }
}

四、单向环形链表

4.1 约瑟夫问题

约瑟夫问题,是一个计算机科学和数学中的问题,在计算机编程的算法中,类似问题又称为约瑟夫环,又称“丢手绢问题”。

据说著名犹太历史学家 Josephus 有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与 Josephus 及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而 Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus 要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

  • 问题简述:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。

4.2 实现思路

  • 用一个不带头结点的循环链表来处理 Josephus 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。

  • 构建单向环形链表:

    • 先创建第一个节点,让 first 指向该节点 ,并形成环
    • 后面每创建一个新节点,就把该节点加入到环形链表中。
  • 遍历单向环形链表:

    • 通过辅助指针,指向first节点。
    • 循环至指针指向下一轮 first节点出现为止。
  • 示意图

在这里插入图片描述

4.3 代码示例

public class Josephus {
    public static void main(String[] args) {
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();

        System.out.println("[添加节点]结果如下:");
        circleSingleLinkedList.add(5);
        circleSingleLinkedList.foreach();
        // [添加节点]结果如下:
        // no=1
        // no=2
        // no=3
        // no=4
        // no=5

        System.out.println();
        System.out.println("[出圈]结果如下:");
        // 圈中总计节点个数5,从1开始计数,每次数两下,被点到的编号出圈。
        // 期望结果:2 -> 4 -> 1  -> 5  -> 3
        circleSingleLinkedList.count(1, 2, 5);
        // [出圈]结果如下:
        // 编号[2]出圈
        // 编号[4]出圈
        // 编号[1]出圈
        // 编号[5]出圈
        // 圈中剩余编号为[3]
    }
}

class CircleSingleLinkedList {


    /**
     * 首节点。
     */
    private Node first = null;


    /**
     * 添加节点。
     *
     * @param nums 生成的节点个数
     */
    public void add(int nums) {
        // 值校验,添加的点需要大于1。
        if (1 > nums) {
            throw new IllegalArgumentException();
        }
        // 辅助指针。
        Node pointer = null;
        for (int i = 1; i <= nums; i++) {
            Node node = new Node(i, null);
            if (1 == i) {
                // 构造环(首尾相连)。
                first = node;
                first.setNext(first);
                // 并指向第一个节点。
                pointer = first;
            } else {
                // 节点往后添加。
                if (null != pointer) {
                    pointer.setNext(node);
                    // 闭环。
                    node.setNext(first);
                    pointer = node;
                }
            }
        }
    }


    /**
     * 迭代器。
     */
    public void foreach() {
        if (null == this.first || null == this.first.getNext()) {
            throw new NullPointerException();
        }
        Node pointer = this.first;
        while (true) {
            System.out.println(pointer);
            // 一圈结束。
            if (first == pointer.getNext()) {
                break;
            }
            pointer = pointer.getNext();
        }
    }

    /**
     * 根据用户的输入,计算出圈的顺序。
     *
     * @param start 从第几开始
     * @param nums  数几次
     * @param total 最初总共有多少个
     */
    public void count(int start, int nums, int total) {
        if (null == this.first || 1 > start || start > total) {
            throw new IllegalArgumentException();
        }
        Node pointer = this.first;
        while (first != pointer.getNext()) {
            pointer = pointer.getNext();
        }
        // 先让首节点和辅助指针后移 start - 1 次。
        for (int i = 0; i < (start - 1); i++) {
            first = first.getNext();
            pointer = pointer.getNext();
        }
        // 圈中只有一个节点时退出。
        while (first != pointer) {
            // 让首节点和辅助指针后移 nums - 1 次。
            for (int i = 0; i < (nums - 1); i++) {
                first = first.getNext();
                pointer = pointer.getNext();
            }
            System.out.printf("编号[%d]出圈\n", first.getNo());
            first = first.getNext();
            pointer.setNext(first);
        }
        System.out.printf("圈中剩余编号为[%d] \n", first.getNo());
    }
}

class Node {

    private int no;
    private Node next;

    public Node(int no, Node next) {
        this.no = no;
        this.next = next;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    @Override
    public String toString() {
        return "no=" + no;
    }
}

五、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值