链表


链表

链表是有序的列表,在内存中存储格式如下图所示:

内存地址 data next
100 b 400
200
300 a 100
400 c 700
500 e null
600
700 d 500

其中头指针指向内存地址 400。根据内存地址则可以还原链表 head -> a -> b -> c -> d -> e

链表的特点

  • 链表是以节点的方式来存储的,是链式存储
  • 每个节点包含 data数据域,next域(指向下一个节点地址)
  • 链表的各个节点不一定是连续存储的
  • 链表分带头节点的链表和不带头节点的链表

单链表

使用带head头节点的单链表实现用户的增删改查操作

单链表的增

在单链表中添加新的节点有两种方法,一种是直接添加到链表的尾部,一种是根据用户的id进行有序的插入添加。

直接添加的思路
  • 首先创建一个head头节点,其作用是表示单链表的头
  • 每次新增一个节点时,直接加入到链表的最后
  • 定义一个辅助变量,其作用是遍历整个链表,定位到链表的最后一个节点
有序添加的思路
  • 定义一个辅助变量,通过辅助变量来遍历链表找到新节点添加的位置,通过节点的id来判断。
  • 找到新节点添加的位置后,此时辅助变量 temp 节点就是新节点 newNode 的位置
  • 所以 newNode.next = temp.next
  • temp.next = newNode

单链表的删除

  • 通过辅助节点首先找到需要删除节点的前一个节点 temp
  • 此时将前一个节点的next域指向删除节点的后一个节点即可,即 temp.next = temp.next.next
  • 被删除的节点没有其他节点的指向,因此在链表中被删除了,同时会被垃圾回收机制回收

单链表的改和查

通过遍历链表来查询链表中的数据,通过指定的条件可以查询到指定的节点并对其进行修改。具体操作看如下代码

链表中节点的创建

// 定义单链表节点
public class HeroNode {
    private int no;
    private String name;
    private String nickname;
    private HeroNode next;

    public HeroNode(int no, String name, String nickname, HeroNode heroNode) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
        this.next = heroNode;
    }

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

    // getter、setter 方法
}

单链表具体功能的实现

/**
 * 管理节点
 */
public class SingleLinkedList {

    // 初始化头节点,不存放具体数据
    private HeroNode head = new HeroNode(0,"","",null);

    public HeroNode getHead() {
        return head;
    }

    public void setHead(HeroNode head) {
        this.head = head;
    }

    /**
     * 添加节点到单链表(不考虑编号顺序)
     *      1、找到当前链表的最后节点
     *      2、将最后节点的next指向新节点
     * @param heroNode
     */
    public void add(HeroNode heroNode){
        HeroNode temp = head;   // 辅助节点 temp,用于遍历链表找到最后一个节点
        while (true){
            // 找打链表的最后,退出
            if (temp.getNext() == null){
                break;
            }
            // 没有找到最后,temp后移
            temp = temp.getNext();
        }
        // 退出while循环后,temp就指向了链表最后
        temp.setNext(heroNode);
    }

    /**
     * 添加节点到单链表(考虑编号顺序)
     * @param heroNode
     */
    public void addByOrder(HeroNode heroNode){
        HeroNode temp = head;   // 辅助节点 temp,用于遍历链表找到最后一个节点
        boolean flag = false;   // 标志添加的节点是否已存在
        while (true){
            // 找打链表的最后,退出
            if (temp.getNext() == null){
                break;
            }
            // 位置找到,就在temp的后面插入
            if (temp.getNext().getNo() > heroNode.getNo()){
                break;
            }
            // 添加的节点已存在
            if (temp.getNext().getNo() == heroNode.getNo()){
                flag = true;
                break;
            }
            // 没有找到最后,temp后移
            temp = temp.getNext();
        }
        if (flag){
            System.out.println("添加的节点已存在,不能加入,新添加的节点编号为:" + heroNode.getNo());
        }else{
            heroNode.setNext(temp.getNext());
            temp.setNext(heroNode);
        }
    }

    // 遍历链表
    public void show(){
        // 判断链表是否为空
        if (head.getNext() == null){
            System.out.println("链表为空");
        }
        // 辅助变量来遍历链表
        HeroNode temp = head.getNext();
        while (true){
            // 判断是否到链表最后
            if (temp == null){
                break;
            }
            System.out.println(temp);
            // 将temp节点后移
            temp = temp.getNext();
        }
    }

    // 修改节点
    public void update(HeroNode newNode){
        HeroNode temp = head.getNext();
        // 判断链表是否为空
        if (temp == null){
            System.out.println("链表为空,没有可修改的节点");
            return;
        }
        boolean flag = false;
        while (true){
            // 遍历完列表
            if (temp == null){
                break;
            }
            if (temp.getNo() == newNode.getNo()){
                flag = true;
                break;
            }
            temp = temp.getNext();
        }
        if (flag){
            temp.setName(newNode.getName());
            temp.setNickname(newNode.getNickname());
        }else {
            System.out.println("没有找到节点");
        }
    }

    // 删除节点
    public void deleteNode(HeroNode heroNode){
        if (head.getNext() == null){
            System.out.println("链表为空,没有可删除节点!!!");
        }
        HeroNode temp = head;
        while (true){
            // 遍历结束
            if (temp.getNext() == null){
                break;
            }
            if (temp.getNext().getNo() == heroNode.getNo()){
                temp.setNext(temp.getNext().getNext());
                break;
            }
            temp = temp.getNext();
        }
    }

    // 统计节点个数(有头节点的不算)
    public int getLength(HeroNode head){
        if (head.getNext() == null){
            System.out.println("链表为空");
        }
        HeroNode temp = head;
        int length =0;
        while (temp.getNext() != null){
            length++;
            temp = temp.getNext();
        }
        return length;
    }

    // 查找单链表中的倒数第k个节点
    public HeroNode getDesc(HeroNode head, int node){
        if (head.getNext() == null){
            System.out.println("链表为空");
        }
        int size = getLength(head);
        if (node < 1 || node > size){
            System.out.println("输入节点数错误");
            return null;
        }
        HeroNode temp = head.getNext();
        // 查找倒数第k个节点
        for (int i = 0; i < size - node; i++){
            temp = temp.getNext();
        }
        return temp;
    }

    // 将单链表反转
    public static void reverseList(HeroNode head){
        // 当前链表为空或者只有一个节点,无需反转
        if (head.getNext() == null || head.getNext().getNext() == null){
            return;
        }
        // 定义一个辅助的指针,帮助遍历原来的链表
        HeroNode current = head.getNext();
        // 指向当前节点的下一个节点
        HeroNode next = null;
        HeroNode reverseHead = new HeroNode(0,"","",null);
        // 遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
        while (current != null){
            next = current.getNext();
            current.setNext(reverseHead.getNext());// 将current节点添加到反转链表中的第一个节点,将反转链表中的节点数据接入插入的第一个节点之后
            reverseHead.setNext(current);
            current = next;
        }
        // 头节点转换
        head.setNext(reverseHead.getNext());
    }

    // 逆序打印单链表:利用栈的先进后出的特点,实现逆序打印
    public static void reversePrint(HeroNode head){
        if (head.getNext() == null || head.getNext().getNext() == null){
            return;
        }
        // 创建栈,将链表数据压入栈
        Stack<HeroNode> stack = new Stack<HeroNode>();
        HeroNode temp = head.getNext();
        while (temp != null){
            stack.push(temp);
            temp = temp.getNext();
        }
        // 打印栈中数据
        while (stack.size() > 0){
            System.out.println(stack.pop());
        }
    }

    // 合并两个有序单链表,合并之后依然有序
}

测试单链表功能

public class TestSingleLink {
    public static void main(String[] args) {
        // 定义节点
        HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨", null);
        HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟", null);
        HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星", null);
        HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头", null);

        // 创建链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();

        // 链表中加入节点
        singleLinkedList.addByOrder(heroNode1);
        singleLinkedList.addByOrder(heroNode4);
        singleLinkedList.addByOrder(heroNode3);
        singleLinkedList.addByOrder(heroNode2);

        // 查看链表
        singleLinkedList.show();

        // 修改链表中节点的数据
        System.out.println("-------修改节点数据-------");
        HeroNode heroNode5 = new HeroNode(4, "11林冲", "11豹子头", null);
        singleLinkedList.update(heroNode5);
        singleLinkedList.show();

        // 删除链表中的节点
        System.out.println("-------删除节点数据-------");
        singleLinkedList.deleteNode(heroNode1);
        singleLinkedList.deleteNode(heroNode4);
        singleLinkedList.show();
    }
}

查找链表中的倒数第 k 个节点

实现思路:

  • 遍历链表,得到链表的总长度 linkLength
  • 之后再次遍历链表,找到第 (linkLength - k) 个节点,即是倒数第 k 个节点
  • 找到了返回该节点,否则返回null
// 查找单链表中的倒数第k个节点
public HeroNode getDesc(HeroNode head, int node){
    if (head.getNext() == null){
        System.out.println("链表为空");
    }
    int size = getLength(head);
    if (node < 1 || node > size){
        System.out.println("输入节点数错误");
        return null;
    }
    HeroNode temp = head.getNext();
    // 查找倒数第k个节点
    for (int i = 0; i < size - node; i++){
        temp = temp.getNext();
    }
    return temp;
}

单链表的反转

实现思路:

  • 首先定义一个辅助头节点 tempHeadNode
  • 从头开始遍历链表,每遍历一个节点,就将其取出加入到 tempHeadNode 节点之后,新加入的节点的next指向tempHeadNode的next。
  • 遍历原先的链表完成后,修改头节点的指向:head.next == tempHeadNode.next
// 将单链表反转
public static void reverseList(HeroNode head){
    // 当前链表为空或者只有一个节点,无需反转
    if (head.getNext() == null || head.getNext().getNext() == null){
        return;
    }
    // 定义一个辅助的指针,帮助遍历原来的链表
    HeroNode current = head.getNext();
    // 指向当前节点的下一个节点
    HeroNode next = null;
    HeroNode reverseHead = new HeroNode(0,"","",null);
    // 遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
    while (current != null){
        next = current.getNext();
        current.setNext(reverseHead.getNext());// 将current节点添加到反转链表中的第一个节点,将反转链表中的节点数据接入插入的第一个节点之后
        reverseHead.setNext(current);
        current = next;
    }
    // 头节点转换
    head.setNext(reverseHead.getNext());
}

逆序打印单链表

实现思路:

  • 第一种,先将单链表进行反转,在遍历打印即可,但是会破坏原先链表的结构,不建议
  • 第二种,利用栈这个数据结构,将各个节点压入栈中,利用栈的先进后出的特点,实现逆序打印链表
// 逆序打印单链表:利用栈的先进后出的特点,实现逆序打印
public static void reversePrint(HeroNode head){
    if (head.getNext() == null || head.getNext().getNext() == null){
        return;
    }
    // 创建栈,将链表数据压入栈
    Stack<HeroNode> stack = new Stack<HeroNode>();
    HeroNode temp = head.getNext();
    while (temp != null){
        stack.push(temp);
        temp = temp.getNext();
    }
    // 打印栈中数据
    while (stack.size() > 0){
        System.out.println(stack.pop());
    }
}

双向链表

带head头的双向链表,即在一个节点上定义next域指向下一个节点,定义pre域指向上一个节点

单链表与双向链表的对比:
单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
单向链表不能实现自我删除,需要辅助节点,而双向链表,则可以实现自我删除。在单链表进行删除时,需要找到一个辅助节点temp,temp指向删除节点的前一个节点

双向链表的遍历和修改

遍历和修改的思路和单链表是类似的,只是双向链表的遍历可以向前也可以向后遍历

双向链表的添加

首先找到双向链表的最后一个节点,将新加入节点的pre指向最后一个节点,最后一个节点的next指向新加入的节点。
如果是有序插入,步骤和单链表是类似的,需要注意新加入节点的next域和pre域。

双向链表的删除

双向链表的删除可以不用借助辅助节点即可完成节点的删除。

  • 首先找到需要删除的节点 deleteNode
  • deleteNode.pre.next = deleteNode.next
  • deleteNode.next.pre = deleteNode.pre

代码实现

public class DoubleLinkedListDemo {

    public static void main(String[] args) {
        // 定义节点
        HeroNodes heroNode1 = new HeroNodes(1, "宋江", "及时雨");
        HeroNodes heroNode2 = new HeroNodes(2, "卢俊义", "玉麒麟");
        HeroNodes heroNode3 = new HeroNodes(8, "吴用", "智多星");
        HeroNodes heroNode4 = new HeroNodes(6, "林冲", "豹子头");
        // 创建双向链表
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
        doubleLinkedList.add(heroNode1);
        doubleLinkedList.add(heroNode2);
        doubleLinkedList.add(heroNode3);
        doubleLinkedList.add(heroNode4);
        doubleLinkedList.list();

        // 顺序添加节点
        System.out.println("-------顺序添加节点-------");
        HeroNodes heroNode5 = new HeroNodes(5, "公孙胜", "入云龙");
        doubleLinkedList.addByOrder(heroNode5);
        doubleLinkedList.list();

       System.out.println("-------修改节点数据-------");
       HeroNodes heroNode5 = new HeroNodes(4, "风雪林冲", "豹子头+-+");
       doubleLinkedList.update(heroNode5);
       doubleLinkedList.list();

       System.out.println("-------删除节点数据-------");
       doubleLinkedList.del(4);
       doubleLinkedList.list();
    }
}

// 创建双向链表的类
class DoubleLinkedList{
    // 初始化头节点
    private HeroNodes head = new HeroNodes(0,"", "");

    // 返回头节点
    public HeroNodes getHead(){
        return head;
    }

    // 遍历双向链表的方法
    public void list(){
        if (head.next == null){
            System.out.println("链表为空");
            return;
        }
        HeroNodes temp = head.next;
        while (temp != null){
            System.out.println(temp);
            temp = temp.next;
        }
    }

    // 双向链表最后添加节点
    public void add(HeroNodes heroNode){
        HeroNodes temp = head;
        while (temp.next != null){
            temp = temp.next;
        }
        temp.next = heroNode;
        heroNode.per = temp;
    }

    // 顺序添加节点
    public void addByOrder(HeroNodes heroNode){
        HeroNodes temp = head;
        // 链表为空时
        if (temp.next == null){
            temp.next = heroNode;
            heroNode.per = temp;
        }
        // 链表不为空
        while (true){
            // 插入
            if (temp.next != null){
                if (temp.next.no > heroNode.no){
                    heroNode.next = temp.next;
                    temp.next.per = heroNode;
                    temp.next = heroNode;
                    heroNode.per = temp;
                    break;
                }
                temp = temp.next;
            }else {
                // 循环到最后一个节点
                temp.next = heroNode;
                heroNode.per = temp;
                break;
            }
        }
    }

    // 修改双向链表的节点
    public void update(HeroNodes heroNode){
        if (head.next == null){
            System.out.println("双向链表为空");
        }
        HeroNodes temp = head.next;
        boolean flag = true;
        while (temp != null){
            if (temp.no == heroNode.no){
                temp.name = heroNode.name;
                temp.nickname = heroNode.nickname;
                flag = false;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            System.out.println("没有找到可修改的节点");
        }
    }

    // 删除节点
    public void del(int no){
        if (head.next == null){
            System.out.println("双向链表为空");
        }
        HeroNodes temp = head.next;
        boolean flag = false;
        while (true){
            if (temp == null){
                break;
            }
            if (temp.no == no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        // 删除
        if (flag){
            temp.per.next = temp.next;
            if (temp.next != null){
                temp.next.per = temp.per;
            }
        }else {
            System.out.println("删除的节点不存在");
        }
    }
}

// 定义双向链表的节点
class HeroNodes{
    public int no;
    public String name;
    public String nickname;
    public HeroNodes next;  // 指向下一个节点
    public HeroNodes per;   // 指向上一个节点

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

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

Josephu(约瑟夫、约瑟夫环)问题

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

思路

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

构建单向的环形链表思路

  • 创建第一个节点firstNode,让第一个节点fistNode的next指向自己,形成一个环形
  • 当创建一个新的节点newNode时,将新节点加入到该环形链表中,firstNode.next = newNode, newNode.next = firstNode
  • 每创建一个新的节点,就加入到上一个节点之后,新节点的next域指向第一个节点firstNode

遍历单向环形链表思路

  • 创建一个辅助变量temp,指向firstNode节点
  • 通过while循环遍历该环形链表
  • while循环结束的条件是:temp.next = firstNode

Josephu问题思路

  • 定义一个报数指针first,first指向链表的第一个节点;定义一个辅助指针helper,helper指向环形链表的最后节点
  • 在开始报数前,首先让两个指针移动 k-1次
  • 开始报数后,让first和helper同时移动m-1次
  • 此时将first指向的节点读取并删除,即将first节点指向待删节点的下一个节点,同时helper指向待删节点的上一个节点

实现代码

package com.java.linkedlist;

public class JosephusProblem {
    public static void main(String[] args) {
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        // 添加节点
        circleSingleLinkedList.addBoy(5);
        // 遍历节点
        circleSingleLinkedList.list();

        // 出圈
        circleSingleLinkedList.countBoy(1, 2, 5);
    }
}

// 创建环形单向链表
class CircleSingleLinkedList{

    // 创建first节点
    private Boy first = null;

    // 添加节点,构建成一个环形的链表
    public void addBoy(int nums){
        if (nums < 1){
            System.out.println("添加节点的数值不对");
        }
        Boy curBoy = null;  // 辅助指针,帮助构建环形链表
        // 使用for创建环形链表
        for (int i = 1; i <= nums; i++){
            // 根据编号创建节点
            Boy boy = new Boy(i);
            if (i == 1){
                first = boy;
                first.setNext(first);
                curBoy = first;
            } else {
                 curBoy.setNext(boy);
                 boy.setNext(first);
                 curBoy = boy;
            }
        }
    }

    // 遍历环形链表
    public void list(){
        if (first == null){
            System.out.println("链表为空");
            return;
        }
        // first不能动,使用辅助指针完成遍历
        Boy curBoy = first;
        while (true){
            System.out.println("节点:" + curBoy.getNo());
            if (curBoy.getNext() == first){
                // 遍历完毕
                break;
            }
            curBoy = curBoy.getNext();
        }
    }

    /**
     * 根据输入,计算出出圈的顺序
     * @param startNo   表示从第几个节点开始数数
     * @param countNum  表示数几下
     * @param nums      表示最初有多少节点在圈中参与
     */
    public void countBoy(int startNo, int countNum, int nums){
        if (first == null || startNo < 1 || startNo > nums){
            System.out.println("参数有误");
            return;
        }
        // 创建辅助指针,指向最后一个节点
        Boy helper = first;
        while (true){
            if (helper.getNext() == first){
                break;
            }
            helper = helper.getNext();
        }

        // 出圈前,让节点到第k个节点位置
        for (int i = 0; i < startNo - 1; i++){
            first = first.getNext();
            helper = helper.getNext();
        }

        // 出圈
        while (true){
            if (helper == first){
                // 最后一个节点
                System.out.println("出圈节点:" + first.getNo());
                break;
            }
            // 让节点移动至下一次报数的位置
            for (int i = 0; i < countNum - 1; i++){
                first = first.getNext();
                helper = helper.getNext();
            }
            // 报数节点出圈
            System.out.println("出圈节点:" + first.getNo());
            first = first.getNext();
            helper.setNext(first);
        }
    }
}


// 创建boy类,表示节点
class Boy{
    private int no;
    private Boy next;

    public Boy(int no){
        this.no = no;
    }

    public int getNo() {
        return no;
    }

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

    public Boy getNext() {
        return next;
    }

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

可以构造双向循环链表实现此算法


文章作者: zerollone
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zerollone !
  目录