链表

44 阅读6分钟

基本介绍

  1. 链表是有序的列表,但是各个节点在内存中不一定是连续存储;

image.png

  1. 链表是以节点的方式来存储,是链式存储;
  2. 每个节点包含 data 域和 next 域(指向下一个节点);
  3. 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定;
  4. 单链表(带头结点) 逻辑结构示意图如下:

image.png

单链表

实现思路

image.png

实现逻辑

链表结点结构

public class SingleNode {
    // 结点名称
    private String name;
    // 结点别名
    private String alias;
    // 结点排名
    private int rank;
    // 下一个结点
    private SingleNode next;

    public SingleNode(String name, String alias, int rank) {
        this.name = name;
        this.alias = alias;
        this.rank = rank;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SingleNode singleNode = (SingleNode) o;
        return rank == singleNode.rank &&
                Objects.equals(name, singleNode.name) &&
                Objects.equals(alias, singleNode.alias);
    }
...get/set
}

创建空的头节点

private SingleNode head = new SingleNode("", "", 0);

添加结点

public void addNode(SingleNode singleNode) {
    // 默认在头结点后增加结点
    SingleNode temp = head;
    // 循环遍历下一个结点,直到下一个结点为空,可以把新增结点插入到链表最末尾
    while (true) {
        if (null == temp.getNext()) {
            break;
        }
        temp = temp.getNext();
    }
    // 链表最后一个结点的下一个结点指向新增结点
    temp.setNext(singleNode);
}

将结点添加到指定位置(按正序插入)

public void addNodeByOrder(SingleNode singleNode) {
    SingleNode temp = head;
    boolean flag = false;
    while (true) {
        // 说明temp已经在链表结尾
        if (temp.getNext() == null) {
            flag = true;
            break;
        }
        // 说明位置找到了,就在temp后面插入
        if (temp.getNext().getRank() > singleNode.getRank()) {
            flag = true;
            break;
        }
        // 说明希望添加的node的编号已经存在
        if (temp.getNext().getRank() == singleNode.getRank()) {
            flag = false;
            break;
        }
        // 后移,遍历链表
        temp = temp.getNext();
    }
    if (flag) {
        // 插入到链表中,temp的后面
        singleNode.setNext(temp.getNext());
        temp.setNext(singleNode);
    }
}

更新结点

public void update(SingleNode newSingleNode) {
    // 默认在头结点开始遍历,找到满足更新的结点
    SingleNode temp = head;
    // 设置临时变量,便于遍历链表
    boolean flag = false;
    while (true) {
        // 只要排名值一致,就可以认为找到可以更新的结点,退出循环
        if (temp.getRank() == newSingleNode.getRank()) {
            flag = true;
            break;
        }
        // 一直寻找下一个结点,直到该结点的下一个结点为空,说明已到链表末尾,退出循环
        if (temp.getNext() == null) {
            break;
        }
        temp = temp.getNext();
    }
    // 如果找到可以更新结点,就更新名称和别名
    if (flag) {
        temp.setName(newSingleNode.getName());
        temp.setAlias(newSingleNode.getAlias());
    }
}

删除结点

public void delete(SingleNode singleNode) {
    // 默认在头结点开始遍历,找到满足更新的结点
    SingleNode temp = head;
    // 设置临时变量,便于遍历链表
    boolean flag = false;
    while (true) {
        // 判断当前结点的下一个结点的排名是否满足,并退出循环
        if (temp.getNext().getRank() == singleNode.getRank()) {
            flag = true;
            break;
        }
        // 一直寻找下一个结点,直到该结点的下一个结点为空,说明已到链表末尾,退出循环
        if (temp.getNext() == null) {
            break;
        }
        temp = temp.getNext();
    }
    // 如果找到可以删除的结点,就将当前结点的下一个结点 指向当前结点的下一个的下一个结点
    if (flag) {
        temp.setNext(temp.getNext().getNext());
    }
}

遍历链表

public void show() {
    SingleNode temp = head;
    while (true) {
        if (temp.getNext() == null) {
            System.out.print(temp.getNext());
            break;
        }
        temp = temp.getNext();
        System.out.print(temp + " --> ");
    }
    System.out.println();
}

双向链表

实现思路

image.png

分析 双向链表的遍历,添加,修改,删除的操作思路 --->

  1. 遍历方式和单链表一样,可以向前,也可以向后查找;

  2. 添加 (默认添加到双向链表的最后)

    2.1. 先找到双向链表的最后这个节点;

    2.2. temp.next = newHeroNode;

    2.3. newHeroNode.pre = temp;

  3. 修改思路和单向链表的一样

  4. 删除

    4.1. 因为是双向链表,因此可以实现自我删除某个节点;

    4.2. 直接找到要删除的这个节点,比如temp;

    4.3. temp.pre.next = temp.next;

    4.4. temp.next.pre = temp.pre;

实现逻辑

链表结点结构

public class DoubleNode {
    private String name;
    private String alias;
    private int rank;
    // 指向下一个节点, 默认为null
    private DoubleNode next;
    // 指向上一个节点, 默认为null
    private DoubleNode pre;

    public DoubleNode(String name, String alias, int rank) {
        this.name = name;
        this.alias = alias;
        this.rank = rank;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DoubleNode doubleNode = (DoubleNode) o;
        return rank == doubleNode.rank &&
                Objects.equals(name, doubleNode.name) &&
                Objects.equals(alias, doubleNode.alias);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, alias, rank);
    }
}

创建空的头节点

private DoubleNode head = new DoubleNode("", "", 0);

添加结点

public void addNode(DoubleNode doubleNode) {
    DoubleNode temp = head;
    // 遍历链表,找到最后位置
    while (true) {
        // 为空,说明已经到链表结尾
        if (Objects.isNull(temp)) {
            break;
        }
        // 如果没有找到最后,就将temp后移
        temp = temp.getNext();
    }
    // 当退出while循环时,temp就指向了链表的最后
    // 形成一个双向链表
    temp.setNext(doubleNode);
    doubleNode.setPre(temp);
}

将结点添加到指定位置(按正序插入)

public void addNodeByOrder(DoubleNode doubleNode) {
    DoubleNode temp = head;
    boolean flag = false;
    // 遍历链表,找到最后位置
    while (true) {
        // 为空,说明已经到链表结尾
        if (Objects.isNull(temp.getNext())) {
            flag = true;
            break;
        }
        if (temp.getNext().getRank() > doubleNode.getRank()) {
            flag = true;
            break;
        }
        if (temp.getNext().getRank() == doubleNode.getRank()) {
            flag = false;
            break;
        }
        // 如果没有找到最后,就将temp后移
        temp = temp.getNext();
    }
    // 当退出while循环时,temp就指向了链表的最后
    // 形成一个双向链表
    if (flag) {
        doubleNode.setNext(temp.getNext());
        temp.setNext(doubleNode);
        doubleNode.setPre(temp);
    }
}

更新结点

public void updateNode(DoubleNode doubleNode) {
    DoubleNode temp = head;
    // 表示是否找到该节点
    boolean flag = false;
    while (true) {
        if (temp.getNext() == null) {
            // 已经遍历完链表
            break;
        }
        if (temp.getRank() == doubleNode.getRank()) {
            // 找到匹配的结点
            flag = true;
            break;
        }
        temp = temp.getNext();
    }
    if (flag) {
        temp.setName(doubleNode.getName());
        temp.setAlias(doubleNode.getAlias());
    }
}

删除结点

public void delNode(DoubleNode doubleNode) {
    DoubleNode temp = head;
    // 标志是否找到待删除节点的
    boolean flag = false;
    while (true) {
        if (temp.getNext() == null) {
            // 已经遍历完链表
            break;
        }
        if (temp.getRank() == doubleNode.getRank()) {
            // 找到匹配的结点
            flag = true;
            break;
        }
        temp = temp.getNext();
    }
    if (flag) {
        temp.getPre().setNext(temp.getNext());
        //如果是最后一个节点,就不需要执行下面这句话,否则出现空指针
        if (Objects.nonNull(temp.getNext())) {
            temp.getNext().setPre(temp.getPre());
        }
    }
}

遍历链表

public void list() {
    DoubleNode temp = head;
    if (Objects.isNull(temp)) {
        return;
    }
    while (true) {
        // 判断是否到链表最后
        if (Objects.isNull(temp)) {
            break;
        }
        // 输出节点的信息
        System.out.println(temp);
        // 将temp后移,一定小心
        temp = temp.getNext();
    }
}

环形单向链表

约瑟夫问题

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

实现思路

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

image.png

构建一个单向的环形链表

  1. 先创建第一个节点, 让 first 指向该节点,并形成环形;
  2. 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可。

遍历环形链表

  1. 先让一个辅助指针(变量)curBoy,指向 firs节点;
  2. 然后通过一个 while 循环遍历该环形链表即可,当 curBoy.next == first 结束。

image.png

实现逻辑

创建Boy节点

public class Boy {
    private int id;
    private Boy next;

    public Boy(int id) {
        this.id = id;
    }
...get/set
}

创建首节点

private Boy first = null;//辅助节点

新增节点,构成环形链表

public void addBoy(int num) {
    Boy curBoy = null;
    for (int i = 0; i < num; i++) {
        Boy boy = new Boy(i);
        if (i == 0) {
            first = boy;
            // 构成环
            first.setNext(first);
            // 指向第一个节点
            curBoy = first;
        } else {
            curBoy.setNext(boy);
            boy.setNext(first);
            curBoy = boy;
        }
    }
}

遍历环形链表

public void showBoy() {
    if (first == null) {
        System.out.println("没有节点");
        return;
    }
    Boy curBoy = first;
    while (true) {
        System.out.println("节点编号为:" + curBoy.getId());
        if (curBoy.getNext() == first) {
            break;
        }
        curBoy = curBoy.getNext();
    }
}

计算节点出圈顺序

public void countBoy(int startNo, int countNum) {
    // 创建一个辅助指针(变量)helper, 事先应该指向环形链表的最后这个节点
    Boy helper = first;
    // 定位到最末尾节点
    while (true) {
        if (helper.getNext() == first) {
            break;
        }
        helper = helper.getNext();
    }
    // 找到当前轮次第一个报数的节点
    for (int i = 0; i < startNo - 1; i++) {
        first = first.getNext();
        helper = helper.getNext();
    }
    while (true) {
        if (helper == first) {
            break;
        }
        for (int i = 0; i < countNum - 1; i++) {
            first = first.getNext();
            helper = helper.getNext();
        }
        // 找到出圈节点
        System.out.println(String.format("小孩:%s出圈", first.getId()));
        // 以出圈节点的下一个节点重新开始计数
        first = first.getNext();
        // 辅助指针(变量)helper指向下一个开始节点
        helper.setNext(first);
    }
    System.out.println("最后一个小孩:" + first.getId());
}

应用场景