2021-10-15 二、链表

1. 链表的定义

  1. 链表是有序的列表,是以节点的方式在内存中来存储的,是链式存储
  2. 每个节点最少都包含两个域:data 域(存放数据)next 域(指向下一个结点的地址)
  3. 链表的各个节点不一定是连续存储。
  4. 链表分带头节点的链表没有头节点的链表,根据实际的需求来确定。
  • 一定要学好链表,他是后面数据结构中 树 和 图 的基础

2. 单向链表的基本说明

2.1 带头单链表在内存中的存储

在这里插入图片描述
如上图所示,可以明确单链表中节点之间的内存地址不一定连续头指针里面没有任何数据,只是单纯的指向第一个节点,起到一个指向作用。

2.2 带头单链表的逻辑结构

在这里插入图片描述
如上图所示,a表示data域,后面的表示next,指向下一个结点。
链表之间在逻辑上是连续的,但是注意在内存地址中不一定连续。

2.3 无序链表设计思路

首先创建一个SingleLinkedList类来当链表,HeroNode类来充当链表里面的结点,里面必须有data域和next域。

在这个链表中我们主要涉及两个方法:插入(add)和遍历(list在这里插入图片描述
插入:

  1. 在new链表对象创建实例的时候,先创建一个私有变量head 来表示头节点。
  2. 每添加一个结点,就直接把结点放到链表的最后。

遍历:

  1. 因为头结点不能动,所以我们需要创建一个辅助变量来做循环,然后输出结点。

2.4 代码实现

// 链表结点实体类
public class HeroNode {
    public int no;         // 好汉排行
    public String name;    // 好汉姓名
    public String nickname;// 好汉外号
    public HeroNode next;  //指向下一个节点(好汉)
    
    //构造器
    public HeroNode(int no, String name, String nickname) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }
    
    //为了显示方法,我们重写toString方法
    @Override
    public String toString() {
        return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
    }
    
}

// 链表实体类
public class SingleLinkedList{
    /**
     * 先初始化一个头节点,头节点不能动,不存放具体的数据
     */
    private HeroNode head = new HeroNode(0, null, null);
    
    /**
     * 返回头结点
     */
    public HeroNode getHead() {
        return head;
    }
    
    /**
     * 添加节点到单向链表(不考虑编号no顺序)
     *
     * 思路,
     * 1. 找到当前链表的最后节点
     * 2. 将最后这个节点的next 指向 新的节点
     */
    public void add(HeroNode heroNode) {
        // 因为head节点不能动,因此我们需要一个辅助变量 temp 来指向head结点
        HeroNode temp = head;
        // 遍历链表,找到最后
        while (temp.next != null) {
            // 找到链表的最后是null
            // 若没有找到最后,则后移
            temp = temp.next;
        }
        // 当退出while循环时,temp就指向了链表的最后
        // 将最后这个节点的next 指向 待插入的结点
        temp.next = heroNode;
    }
    
    /**
     * 遍历链表
     */
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空~~~");
            return;
        }
        // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
        HeroNode temp = head.next;

        // 遍历链表,临界条件为 辅助变量不为空
        while (temp != null) {
            // 输出节点信息
            System.out.println(temp);
            // 将节点后移
            temp = temp.next;
        }
    }
}

public class Demo4 {
    public static void main(String[] args) {
        // 先创建节点
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

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


		// 把结点加入链表中
		singleLinkedList.add(hero1);
		singleLinkedList.add(hero4);
		singleLinkedList.add(hero2);
		singleLinkedList.add(hero3);

        singleLinkedList.list();

    }
}

输出结果

HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=4, name=林冲, nickname=豹子头]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]

2.5 出现的问题

1、无法按照英雄编号no排序,只能按照插入顺序。
2、并且同排行序号的英雄,也可以重复添加。

2.6 有序链表设计思路

在这个链表中我们主要涉及如下几个方法:有序插入(addByOrder)、删除(delete)、修改(update
有序插入:
在这里插入图片描述

  1. 创建一个辅助变量temp来帮助我们循环遍历链表。
  2. 根据需要不能插入重复编号no的结点,所以定义一个布尔值的flag来表示是否重复。
  3. 循环遍历链表,如果下一个结点的编号大于待插入的结点,那么就找到位置,可以直接插入了。

删除:
在这里插入图片描述

  1. 我们同样需要创建一个辅助变量temp来帮助我们循环遍历链表
  2. 循环遍历链表,如果下一个结点的编号等于要删除的编号,那么就让当前结点跳过下一个结点,直接指向下一个结点的下一个结点,达到删除的效果。

修改:

  1. 我们同样需要创建一个辅助变量temp来帮助我们循环遍历链表
  2. 循环遍历链表,如果下一个结点的编号等于要修改的编号,那么就直接修改数据。

2.7 代码实现

// 链表实体类
public class SingleLinkedList{
    /**
     * 先初始化一个头节点,头节点不能动,不存放具体的数据
     */
    private HeroNode head = new HeroNode(0, null, null);
    

    /**
     * 按编号顺序添加(如果有这个排名,则添加失败,并给出提示)
     */
    public void addByOrder(HeroNode heroNode) {
        // 因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
        // 因为单链表,因为我们找的temp 是位于 添加位置的前一个节点,否则插入不了
        HeroNode temp = head;
        // flag标志添加的编号是否存在,默认为false
        boolean flag = false;
        // 循环遍历整个链表,当辅助变量的下一个结点为null说明已经到链表的最后了
        while (temp.next != null) {
            // 位置找到,就在 temp后面
            // 因为 它满足了 按顺序 ,所以可以插入
            if (temp.next.no > heroNode.no) {
                break;
            }
            // 已经存在改排行的编号(不可重复)
            if (temp.next.no == heroNode.no) {
                flag = true;
                break;
            }
            // 没满足以上,后移下一次节点继续找
            temp = temp.next;
        }

        // 对 flag 进行判断 true则表示不能添加,说明编号已经存在
        if (flag) {
            System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
        } else {
            // 插入到链表中,temp的后面
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }


    /**
     * 删除
     */
    public void delete(int no) {
        HeroNode temp = head;
        // 循环遍历整个链表,当辅助变量的下一个结点为null说明已经到链表的最后了
        while (temp.next != null) {
            if (temp.next.no == no) {
                // 已经找到,让当前结点指向下一个结点的下一个结点,也就是跳过下一个结点
                temp.next = temp.next.next;
                return;
            }
            // 移动继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要修改的英雄", no);
    }
    
    /**
     * 更新节点的信息, 根据no编号来修改,即no编号不能改
     */
    public void update(HeroNode heroNode) {
        // 判空
        if (head.next == null) {
            System.out.println("链表为空~~");
            return;
        }
        // 根据 no 找到需要修改的节点
        // 定义一个辅助变量 直接指向头结点的下一个结点(第一个有效数据)
        HeroNode temp = head.next;
        while (temp != null) {
            // 节点已经找到
            if (temp.no == heroNode.no) {
                // 找到,直接修改姓名和外号
                temp.name = heroNode.name;
                temp.nickname = heroNode.nickname;
                return;
            }
            // 节点移动,继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要修改的英雄", heroNode.no);

    }




	// =====================下面是无序链表的方法 ===================
	
    /**
     * 返回头结点
     */
    public HeroNode getHead() {
        return head;
    }
    
    /**
     * 添加节点到单向链表(不考虑编号no顺序)
     *
     * 思路,
     * 1. 找到当前链表的最后节点
     * 2. 将最后这个节点的next 指向 新的节点
     */
    public void add(HeroNode heroNode) {
        // 因为head节点不能动,因此我们需要一个辅助变量 temp 来指向head结点
        HeroNode temp = head;
        // 遍历链表,找到最后
        while (temp.next != null) {
            // 找到链表的最后是null
            // 若没有找到最后,则后移
            temp = temp.next;
        }
        // 当退出while循环时,temp就指向了链表的最后
        // 将最后这个节点的next 指向 待插入的结点
        temp.next = heroNode;
    }
    
    /**
     * 遍历链表
     */
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空~~~");
            return;
        }
        // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
        HeroNode temp = head.next;

        // 遍历链表,临界条件为 辅助变量不为空
        while (temp != null) {
            // 输出节点信息
            System.out.println(temp);
            // 将节点后移
            temp = temp.next;
        }
    }
}
// 测试类
public class Demo5 {
    public static void main(String[] args) {
        //先创建节点
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

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

        // 有序插入
        System.out.println("准备插入数据");
        singleLinkedList.addByOrder(hero1);
        singleLinkedList.addByOrder(hero4);
        singleLinkedList.addByOrder(hero2);
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.list();

        // 插入重复数据
        System.out.println("准备插入重复数据");
        singleLinkedList.addByOrder(hero3);
        singleLinkedList.list();

        // 修改
        System.out.println("准备修改数据");
        singleLinkedList.update(new HeroNode(1, "送浆", "白日"));
        singleLinkedList.list();

        // 删除
        System.out.println("准备删除数据");
        singleLinkedList.delete(1);
        singleLinkedList.delete(2);
        singleLinkedList.delete(3);
        singleLinkedList.delete(4);
        singleLinkedList.list();

    }
}

输出结果

准备插入数据
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
准备插入重复数据
准备插入的英雄的编号 3 已经存在了, 不能加入
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
准备修改数据
HeroNode [no=1, name=送浆, nickname=白日]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
准备删除数据
链表为空~~~


3. 单向链表的练习题

3.1 求单链表中有效节点的个数

设计思路:
遍历结点,并且统计个数

代码实现

    /**
     * 题目一:获取有效节点个数
     *
     * @param head 头结点(不统计)
     * @return 返回有效节点个数
     */
    public int getLength(HeroNode head) {
        // 判空
        if (head.next == null) {
            return 0;
        }
        int length = 0;
        // 定义辅助变量 temp,此处没有统计 头节点
        HeroNode temp = head.next;
        while (temp != null) {
            length++;
            // 指向下一个,继续遍历
            temp = temp.next;
        }
        return length;
    }

3.2 查找单链表中的倒数第k个结点

设计思路

  1. 先遍历得到 有效数据个数(总长度–不包括头节点)
  2. 有效个数 - index = 倒数 K(size-index = k )
  3. 找到则返回当前节点,找不到返回 null
    /**
     * 题目二:查找单链表中的倒数第k个结点 【新浪面试题】
     * 思路:
     * 1、先遍历得到 有效数据个数(总长度--不包括头节点)
     * 2、有效个数 - index = 倒数 K(size-index = k )
     * 3、找到则返回当前节点,找不到返回 null
     *
     * @param head  传入的头结点
     * @param index 需要查找的 倒数节点数据
     */
    public HeroNode findLastIndexNode(HeroNode head, int index) {
        // 判空
        if (head.next == null) {
            return null;
        }
        // 第一次遍历得到链表的长度(节点个数)
        int size = getLength(head);
        
        // 第二次遍历 size-index 位置,就是我们倒数的第K个节点
        // 先做一个index的校验
        if (index <= 0 || index > size) {
            // 查找的位置不能大过总长
            return null;
        }

        // 定义给辅助变量, for 循环定位到倒数的index
        // 例如:8-2=6,倒数第二个就是第六个
        HeroNode temp = head.next;
        for (int i = 0; i < size - index; i++) {
            temp = temp.next;
        }
        return temp;
    }

3.3 单链表的反转

设计思路

  1. 定义一个新的头结点来存放反转的链表。
  2. 遍历原先的单链表,每遍历到一个结点就将其取出,放到新链表的头部(头插法)

代码实现

    /**
     * 题目三:单链表的反转
     */
    public void reverseList(HeroNode head) {
        // 若链表为空,或者只有一个则不需要反转
        if (head.next == null || head.next.next == null) {
            return;
        }
        // 定义一个辅助指针,用于遍历原始未反转的那个链表
        HeroNode temp = head.next;
        // 用于动态指向 当前节点的下一个节点
        HeroNode next;
        // 定义新链表(反转后的)的链表头
        HeroNode reverseHead = new HeroNode(0, "", "");
        // 思路: 遍历原始链表,遍历一个节点将其取出,并放在新链表头(reverseHead)后面的最前端,
        // 临界条件为当前结点不为空
        while (temp != null) {
            // 暂时先保存当前节点的下一个节点,用于temp后移(防止存放在堆中的数据被回收)
            next = temp.next;
            // 将遍历出来的结点,指向反转链表头结点的后面(头插法)
            temp.next = reverseHead.next;
            // 让反转链表的头结点指向当前结点temp
            reverseHead.next = temp;
            // 把temp指回原先链表的下一个结点
            temp = next;
        }
        // 将head.next 指向 reverseHead.next , 实现单链表的反转
        head.next = reverseHead.next;
    }

3.4 反向打印单链表

设计思路
(1) 方式1: 先将单链表进行反转操作,然后再遍历即可,这样做的问题是会破坏原来的单链表的结构,不建议
(2) 方式2:可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,实现逆序打印的效果

代码实现

    /**
     * 题目四:反向打印链表(栈)
     */
    public void reversePrint(HeroNode head) {
        // 判空
        if (head.next == null) {
            return;
        }
        // 创建一个栈,用于节点出入栈,实现反向打印
        Stack<HeroNode> stack = new Stack<>();
        // 定义辅助指针,用于遍历原始链表
        HeroNode temp = head.next;
        // 将链表所有节点入栈
        while (temp != null) {
            // 节点 入栈
            stack.push(temp);
            // 节点后移
            temp = temp.next;
        }
        // 所有节点依次出栈
        while (stack.size() > 0) {
            //stack的特点是先进后出
            System.out.println(stack.pop());
        }
    }

3.5 合并两个有序的单链表,合并之后的链表依然有序

设计思路
方案1: 比较两个链表,然后把小的结点放在第三个链表,然后将结点指向下一个
方案2:把两个链表分成一个主链表,一个辅链表,一起遍历两个链表,把辅链表的结点按照顺序塞到主链表里面去

代码实现

    /**
     * 题目五:合并两个有序的单链表,合并之后的链表依然有序
     * 方案1: 比较两个链表,然后把小的结点放在第三个链表,然后将结点指向下一个
     * 方案2: 把两个链表分成一个主链表,一个辅链表,一起遍历两个链表,把辅链表的结点按照顺序塞到主链表里面去
     */
    public HeroNode sumLinkedList(HeroNode head1, HeroNode head2) {
        // 判空
        if (head1.next == null && head2.next == null) {
            return head1;
        }
        // ===================方案二===================
        // 定义两个结点,分别指向两个链表,
        // 注意,主链表(head1)要指向头结点,这样方便插入数据,
        // 辅链表直接指向第一个有效数据即可
        HeroNode p1 = head1, p2 = head2.next;
        // 临界条件,当两个链表中的数据都不为空时则循环
        while (p1.next != null && p2 != null) {
            // 如果p1结点的下一个结点的编号no大于p2结点的编号no,那么就把p2结点插入到p1结点后面
            if (p1.next.no > p2.no) {
                // 把p2下一个结点的数据保留在堆中
                HeroNode temp = p2.next;
                // 把p2的下一个结点指向p1的下一个结点
                p2.next = p1.next;
                // 把p1的下一个结点指向p2
                p1.next = p2;
                // 把p2指回temp
                p2 = temp;
            }
            p1 = p1.next;
        }
        return head1;

        // ===================方案一===================
        // 定义一个新的头结点 用于存储合并
//        HeroNode newHead = new HeroNode(0, "", "");
//        // 让p1,p2直接指向链表的有效数据,因为头结点不能动,所以定义一个辅助指针指向新的头结点
//        HeroNode p1 = head1.next, p2 = head2.next, temp = newHead;
//        // 临界条件,p1和p2都不为空,(只要有一个为空就可以跳出循环了)
//        while (p1 != null && p2 != null) {
//            if (p1.no < p2.no) {
//                temp.next = p1;
//                p1 = p1.next;
//                temp = temp.next;
//            } else {
//                temp.next = p2;
//                p2 = p2.next;
//                temp = temp.next;
//            }
//        }
//        // 拼接链表剩下的结点
//        temp.next = p1 == null ? p2 : p1;
//        return newHead;
    }

完整代码

public class SingleLinkedList {
    /**
     * 先初始化一个头节点,头节点不能动,不存放具体的数据
     */
    private HeroNode head = new HeroNode(0, null, null);

    /**
     * 返回头结点
     */
    public HeroNode getHead() {
        return head;
    }

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


    /**
     * 题目五:合并两个有序的单链表,合并之后的链表依然有序
     * 方案1: 比较两个链表,然后把小的结点放在第三个链表,然后将结点指向下一个
     * 方案2: 把两个链表分成一个主链表,一个辅链表,一起遍历两个链表,把辅链表的结点按照顺序塞到主链表里面去
     */
    public HeroNode sumLinkedList(HeroNode head1, HeroNode head2) {
        // 判空
        if (head1.next == null && head2.next == null) {
            return head1;
        }
        // ===================方案二===================
        // 定义两个结点,分别指向两个链表,
        // 注意,主链表(head1)要指向头结点,这样方便插入数据,
        // 辅链表直接指向第一个有效数据即可
        HeroNode p1 = head1, p2 = head2.next;
        // 临界条件,当两个链表中的数据都不为空时则循环
        while (p1.next != null && p2 != null) {
            // 如果p1结点的下一个结点的编号no大于p2结点的编号no,那么就把p2结点插入到p1结点后面
            if (p1.next.no > p2.no) {
                // 把p2下一个结点的数据保留在堆中
                HeroNode temp = p2.next;
                // 把p2的下一个结点指向p1的下一个结点
                p2.next = p1.next;
                // 把p1的下一个结点指向p2
                p1.next = p2;
                // 把p2指回temp
                p2 = temp;
            }
            p1 = p1.next;
        }
        return head1;

        // ===================方案一===================
        // 定义一个新的头结点 用于存储合并
//        HeroNode newHead = new HeroNode(0, "", "");
//        // 让p1,p2直接指向链表的有效数据,因为头结点不能动,所以定义一个辅助指针指向新的头结点
//        HeroNode p1 = head1.next, p2 = head2.next, temp = newHead;
//        // 临界条件,p1和p2都不为空,(只要有一个为空就可以跳出循环了)
//        while (p1 != null && p2 != null) {
//            if (p1.no < p2.no) {
//                temp.next = p1;
//                p1 = p1.next;
//                temp = temp.next;
//            } else {
//                temp.next = p2;
//                p2 = p2.next;
//                temp = temp.next;
//            }
//        }
//        // 拼接链表剩下的结点
//        temp.next = p1 == null ? p2 : p1;
//        return newHead;
    }

    /**
     * 题目四:反向打印链表(栈)
     */
    public void reversePrint(HeroNode head) {
        // 判空
        if (head.next == null) {
            return;
        }
        // 创建一个栈,用于节点出入栈,实现反向打印
        Stack<HeroNode> stack = new Stack<>();
        // 定义辅助指针,用于遍历原始链表
        HeroNode temp = head.next;
        // 将链表所有节点入栈
        while (temp != null) {
            // 节点 入栈
            stack.push(temp);
            // 节点后移
            temp = temp.next;
        }
        // 所有节点依次出栈
        while (stack.size() > 0) {
            //stack的特点是先进后出
            System.out.println(stack.pop());
        }
    }

    /**
     * 题目三:单链表的反转
     */
    public void reverseList(HeroNode head) {
        // 若链表为空,或者只有一个则不需要反转
        if (head.next == null || head.next.next == null) {
            return;
        }
        // 定义一个辅助指针,用于遍历原始未反转的那个链表
        HeroNode temp = head.next;
        // 用于动态指向 当前节点的下一个节点
        HeroNode next;
        // 定义新链表(反转后的)的链表头
        HeroNode reverseHead = new HeroNode(0, "", "");
        // 思路: 遍历原始链表,遍历一个节点将其取出,并放在新链表头(reverseHead)后面的最前端,
        // 临界条件为当前结点不为空
        while (temp != null) {
            // 暂时先保存当前节点的下一个节点,用于temp后移(防止存放在堆中的数据被回收)
            next = temp.next;
            // 将遍历出来的结点,指向反转链表头结点的后面(头插法)
            temp.next = reverseHead.next;
            // 让反转链表的头结点指向当前结点temp
            reverseHead.next = temp;
            // 把temp指回原先链表的下一个结点
            temp = next;
        }
        // 将head.next 指向 reverseHead.next , 实现单链表的反转
        head.next = reverseHead.next;
    }

    /**
     * 题目二:查找单链表中的倒数第k个结点 【新浪面试题】
     * 思路:
     * 1、先遍历得到 有效数据个数(总长度--不包括头节点)
     * 2、有效个数 - index = 倒数 K(size-index = k )
     * 3、找到则返回当前节点,找不到返回 null
     *
     * @param head  传入的头结点
     * @param index 需要查找的 倒数节点数据
     */
    public HeroNode findLastIndexNode(HeroNode head, int index) {
        // 判空
        if (head.next == null) {
            return null;
        }
        // 第一次遍历得到链表的长度(节点个数)
        int size = getLength(head);

        // 第二次遍历 size-index 位置,就是我们倒数的第K个节点
        // 先做一个index的校验
        if (index <= 0 || index > size) {
            // 查找的位置不能大过总长
            return null;
        }

        // 定义给辅助变量, for 循环定位到倒数的index
        // 例如:8-2=6,倒数第二个就是第六个
        HeroNode temp = head.next;
        for (int i = 0; i < size - index; i++) {
            temp = temp.next;
        }
        return temp;
    }

    /**
     * 题目一:获取有效节点个数
     *
     * @param head 头结点(不统计)
     * @return 返回有效节点个数
     */
    public int getLength(HeroNode head) {
        // 判空
        if (head.next == null) {
            return 0;
        }
        int length = 0;
        // 定义辅助变量 temp,此处没有统计 头节点
        HeroNode temp = head.next;
        while (temp != null) {
            length++;
            // 指向下一个,继续遍历
            temp = temp.next;
        }
        return length;
    }

    /**
     * 按编号顺序添加(如果有这个排名,则添加失败,并给出提示)
     */
    public void addByOrder(HeroNode heroNode) {
        // 因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
        // 因为单链表,因为我们找的temp 是位于 添加位置的前一个节点,否则插入不了
        HeroNode temp = head;
        // flag标志添加的编号是否存在,默认为false
        boolean flag = false;
        // 循环遍历整个链表,当辅助变量的下一个结点为null说明已经到链表的最后了
        while (temp.next != null) {
            // 位置找到,就在 temp后面
            // 因为 它满足了 按顺序 ,所以可以插入
            if (temp.next.no > heroNode.no) {
                break;
            }
            // 已经存在改排行的编号(不可重复)
            if (temp.next.no == heroNode.no) {
                flag = true;
                break;
            }
            // 没满足以上,后移下一次节点继续找
            temp = temp.next;
        }

        // 对 flag 进行判断 true则表示不能添加,说明编号已经存在
        if (flag) {
            System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
        } else {
            // 插入到链表中,temp的后面
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    /**
     * 添加节点到单向链表(不考虑编号no顺序)
     * <p>
     * 思路,
     * 1. 找到当前链表的最后节点
     * 2. 将最后这个节点的next 指向 新的节点
     */
    public void add(HeroNode heroNode) {
        // 因为head节点不能动,因此我们需要一个辅助变量 temp 来指向head结点
        HeroNode temp = head;
        // 遍历链表,找到最后
        while (temp.next != null) {
            // 找到链表的最后是null
            // 若没有找到最后,则后移
            temp = temp.next;
        }
        // 当退出while循环时,temp就指向了链表的最后
        // 将最后这个节点的next 指向 待插入的结点
        temp.next = heroNode;
    }

    /**
     * 遍历链表
     */
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空~~~");
            return;
        }
        // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
        HeroNode temp = head.next;

        // 遍历链表,临界条件为 辅助变量不为空
        while (temp != null) {
            // 输出节点信息
            System.out.println(temp);
            // 将节点后移
            temp = temp.next;
        }
    }

    /**
     * 删除
     */
    public void delete(int no) {
        HeroNode temp = head;
        // 循环遍历整个链表,当辅助变量的下一个结点为null说明已经到链表的最后了
        while (temp.next != null) {
            if (temp.next.no == no) {
                // 已经找到,让当前结点指向下一个结点的下一个结点,也就是跳过下一个结点
                temp.next = temp.next.next;
                return;
            }
            // 移动继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要修改的英雄", no);
    }

    /**
     * 更新节点的信息, 根据no编号来修改,即no编号不能改
     */
    public void update(HeroNode heroNode) {
        // 判空
        if (head.next == null) {
            System.out.println("链表为空~~");
            return;
        }
        // 根据 no 找到需要修改的节点
        // 定义一个辅助变量 直接指向头结点的下一个结点(第一个有效数据)
        HeroNode temp = head.next;
        while (temp != null) {
            // 节点已经找到
            if (temp.no == heroNode.no) {
                // 找到,直接修改姓名和外号
                temp.name = heroNode.name;
                temp.nickname = heroNode.nickname;
                return;
            }
            // 节点移动,继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要修改的英雄", heroNode.no);

    }
}
// 测试类
public class Demo6 {
    public static void main(String[] args) {

        // 先创建节点
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
        HeroNode hero5 = new HeroNode(5, "鲁智深", "花和尚");
        HeroNode hero6 = new HeroNode(6, "武松", "行者");

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

      singleLinkedList.addByOrder(hero1);
      singleLinkedList.addByOrder(hero4);
      singleLinkedList.addByOrder(hero2);
      singleLinkedList.addByOrder(hero3);

        singleLinkedList.list();

        System.out.println("--------------------有效个数---------------------------");
        // 有效个数
        int length = singleLinkedList.getLength(singleLinkedList.getHead());
        System.out.printf("有效个数为:%d\n",length);

        int index = 3;
        System.out.printf("--------------------倒数第 %d 的节点---------------------------\n",index);

        // 倒数第 K 的节点
        HeroNode resultNode = singleLinkedList.findLastIndexNode(singleLinkedList.getHead(), index);
        System.out.println(resultNode);

        System.out.println("--------------------链表反转---------------------------");
        // 链表反转
        singleLinkedList.reverseList(singleLinkedList.getHead());
        singleLinkedList.list();

        System.out.println("--------------------利用栈反向打印---------------------------");
        // 利用栈反向打印
        singleLinkedList.reversePrint(singleLinkedList.getHead());

        System.out.println("--------------------合并两个链表---------------------------");

        SingleLinkedList singleLinkedList1 = new SingleLinkedList();
        SingleLinkedList singleLinkedList2 = new SingleLinkedList();

        singleLinkedList1.addByOrder(hero2);
        singleLinkedList1.addByOrder(hero4);
        singleLinkedList1.addByOrder(hero6);
        singleLinkedList2.addByOrder(hero1);
        singleLinkedList2.addByOrder(hero3);
        singleLinkedList2.addByOrder(hero5);


        HeroNode newHead = singleLinkedList.sumLinkedList(singleLinkedList1.getHead(),
                singleLinkedList2.getHead());
        // 方案二直接输出singleLinkedList1即可
        singleLinkedList1.list();
        // 方案一需要重新定义一个链表,然后输出
/*        SingleLinkedList singleLinkedList3 = new SingleLinkedList();
        singleLinkedList3.setHead(newHead);
        singleLinkedList3.list();*/


    }
}

输出结果

HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
--------------------有效个数---------------------------
有效个数为:4
--------------------倒数第 3 的节点---------------------------
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
--------------------链表反转---------------------------
HeroNode [no=4, name=林冲, nickname=豹子头]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=1, name=宋江, nickname=及时雨]
--------------------利用栈反向打印---------------------------
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
--------------------合并两个链表---------------------------
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
HeroNode [no=5, name=鲁智深, nickname=花和尚]
HeroNode [no=6, name=武松, nickname=行者]

4. 双向链表

4.1 双向链表和单向链表对比

  1. 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
  2. 单向链表不能自我删除,需要靠辅助(前一个)节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到待删除节点的前一个节点,让temp.next = 待删除结点temp,然后执行temp.next = temp.next.next 达到删除的效果。

4.2 设计思路

在这里插入图片描述
注意,双向链表需要定义两个指针来分别指向当前结点的前一个结点(pre)和后一个结点(next)

  1. 遍历(list):方法和单链表一样,只是可以向前查找,也可以向后查找。

  2. 添加和单向链表一样分两种
    2.1 不做比较,直接添加到双向链表的最后
    (1) 先找到双向链表的最后这个节点
    (2) 让最后一个结点的(next)指向待插入结点
    (3) 让待插入结点的(pre)指向当前最后一个结点
    2.2 有序添加
    (1) 先根据待插入结点的编号no来确定要插入的位置
    (2) 如果链表中已经存在则直接返回,不插入
    (3) 如果不存在,则在合适的位置进行插入,绑定对应的pre和next结点

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

  4. 删除
    (2) 直接找到要删除的这个节点,比如temp
    (3) 让待删除结点的上一个结点直接指向待删除结点的下一个结点temp.pre.next = temp.next;
    (4) 让待删除结点的下一个结点直接指回待删除结点的上一个结点temp.next.pre = temp.pre;

4.3 代码实现

public class DoubleLinkedList {
    /**
     * 先初始化一个头节点,头节点不能动,不存放具体的数据
     */
    final private HeroNode head = new HeroNode(0, null, null);

    /**
     * 返回头结点
     */
    public HeroNode getHead() {
        return head;
    }


    /**
     * 遍历链表
     */
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空~~~");
            return;
        }
        // 因为头节点不能动,因此我们需要一个辅助变量来遍历
        HeroNode temp = head.next;

        // 判断是否到链表的最后
        while (temp != null) {
            // 输出节点信息
            System.out.println(temp);
            // 将节点后移,注意要加!!
            temp = temp.next;
        }
    }

    /**
     * 不做比较,直接添加到双向链表的最后
     */
    public void add(HeroNode heroNode) {
        // 辅助结点
        HeroNode temp = head;
        // 直接遍历找到最后一个
        while (temp.next != null) {
            temp = temp.next;
        }
        // 插入到最后
        temp.next = heroNode;
        heroNode.pre = temp;
    }

    /**
     * 有序添加
     */
    public void addByOrder(HeroNode heroNode) {
        // 因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
        HeroNode temp = head;
        // 遍历链表,寻找合适的位置,直到最后
        while (temp.next != null) {
            // 如果下一个结点的编号no大于待插入结点,说明找到位置,直接跳出循环
            if (temp.next.no > heroNode.no) {
                break;
            }
            if (temp.next.no == heroNode.no) {
                // 已经存在改排行的编号(不可重复)
                System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入\n", heroNode.no);
                return;
            }
            temp = temp.next;
        }
        // 插入到链表中,temp的后面
        heroNode.next = temp.next;
        temp.next = heroNode;
        heroNode.pre = temp;
    }


    /**
     * 更新节点的信息, 根据no编号来修改
     */
    public void update(HeroNode heroNode) {
        // 判空
        if (head.next == null) {
            System.out.println("链表为空~~");
            return;
        }
        // 定义一个辅助变量
        HeroNode temp = head.next;
        while (temp != null) {
            if (temp.no == heroNode.no) {
                temp.name = heroNode.name;
                temp.nickname = heroNode.nickname;
                return;
            }
            // 节点移动,继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要修改的英雄", heroNode.no);

    }

    /**
     * 删除
     */
    public void delete(int no) {
        // 判空
        if (head.next == null) {
            return;
        }
        // 定义一个辅助指针指向链表的第一个有效数据
        HeroNode temp = head.next;
        while (temp.next != null) {
            if (temp.next.no == no) {
                // 让上一个结点跳过待删除结点,直接指向待删除结点的下一个结点
                temp.pre.next = temp.next;
                // 让待删除的下一个结点跳过待删除结点,指向待删除结点的上一个结点
                temp.next.pre = temp.pre;
                return;
            }
            // 移动继续找
            temp = temp.next;
        }
        System.out.printf("没有找到排行为【%d】要删除的英雄", no);
    }
}
// 测试类
public class Demo7 {
    public static void main(String[] args) {
        // 测试
        System.out.println("双向链表的测试");
        // 先创建节点
        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
        // 创建一个双向链表
        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
        doubleLinkedList.add(hero1);
        doubleLinkedList.add(hero2);
        doubleLinkedList.add(hero3);
        doubleLinkedList.add(hero4);

        doubleLinkedList.addByOrder(hero4);
        doubleLinkedList.addByOrder(hero3);
        doubleLinkedList.addByOrder(hero1);
        doubleLinkedList.addByOrder(hero2);

        doubleLinkedList.list();

        // 修改
        HeroNode newHeroNode = new HeroNode(4, "公孙胜", "入云龙");
        doubleLinkedList.update(newHeroNode);
        System.out.println("修改后的链表情况");
        doubleLinkedList.list();
        HeroNode newHeroNode2 = new HeroNode(5, "公孙胜", "入云龙");
        doubleLinkedList.update(newHeroNode2);
//
        // 删除
        doubleLinkedList.delete(3);
        System.out.println("删除后的链表情况~~");
        doubleLinkedList.list();
        doubleLinkedList.delete(6);
    }
}

输出结果

双向链表的测试
准备插入的英雄的编号 4 已经存在了, 不能加入
准备插入的英雄的编号 3 已经存在了, 不能加入
准备插入的英雄的编号 1 已经存在了, 不能加入
准备插入的英雄的编号 2 已经存在了, 不能加入
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=林冲, nickname=豹子头]
修改后的链表情况
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=2, name=卢俊义, nickname=玉麒麟]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=公孙胜, nickname=入云龙]
没有找到排行为【5】要修改的英雄删除后的链表情况~~
HeroNode [no=1, name=宋江, nickname=及时雨]
HeroNode [no=3, name=吴用, nickname=智多星]
HeroNode [no=4, name=公孙胜, nickname=入云龙]
没有找到排行为【6】要删除的英雄

5. 单向环形链表(约瑟夫问题)

5.1 什么是约瑟夫问题

约瑟夫问题其实也是一个游戏(丢手绢),也称为约瑟夫环。
规则很简单:就是有一群小鬼围起来(例5个)成一个圈,先从第一个小鬼开始报数(1,2,3…n),报到n的那个小鬼就出列。然后出列小鬼的下一个小鬼开始重新报数,报到n的小鬼再次出列…依次类推直到所有小鬼都离开这个圈中。
如下图所示,n=2。
在这里插入图片描述
解决方案:可以使用单向环形链表

5.2 设计思路

在这里插入图片描述

  1. 创建单向环形链表:
    (1). 先创建第一个节点, 让 first 指向该节点,从而形成环形;
    (2). 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可:环中最后一个结点指向当前结点,当前结点指向第一个结点。
  2. 遍历单向环形链表:
    (1). 先让一个辅助指针(变量) curKid,指向first节点
    (2). 然后通过一个while循环遍历该环形链表即可。以curKid.next == first 为结束标识(说明指向链表的最后了)
  3. 约瑟夫问题的解决方案:
    在这里插入图片描述
    (1). 根据用户的输入,生成一个小鬼出圈的顺序,需要指定小鬼的个数(n)从第几个小鬼报数(k)数字报到多少(m)这三个参数。
    (2). 创建一个辅助指针(变量) helper, 事先指向环形链表的最后这个节点,用于帮助结点出圈。
    (3). 报数前,先让 first 和 helper 移动 k - 1次,保证从第k个小鬼开始报数。
    (4). 当小鬼报数时,让first 和 helper 指针同时 的移动 m - 1 次(因为第k个小鬼也要报数)
    (5). 报到指定的数字后,将first 指向的小鬼结点出圈,原来first 指向的节点就没有任何引用,就会被回收。

5.3 代码实现

// 小鬼实体类
public class Kid {
    /**
     * 编号
     */
    private int no;
    /**
     * 指向下一个节点
     */
    private Kid next;

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

    public int getNo() {
        return no;
    }

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

    public Kid getNext() {
        return next;
    }

    public void setNext(Kid next) {
        this.next = next;
    }
}
// 单向循环链表实体类
public class CircleSingleLinkedList {
    /**
     * 创建一个first节点,当前没有编号
     */
    private Kid first = null;

    /**
     * 添加小鬼节点
     */
    public void addKid(int nums) {
        if (nums < 1) {
            System.out.println("数量错误,无法构建环形");
            return;
        }
        // 辅助指针,帮助构建环形链表
        Kid curKid = null;

        // 使用for来创建我们的环形链表
        for (int i = 1; i <= nums; i++) {
            // 根据编号,创建小鬼节点
            Kid kid = new Kid(i);
            // 如果是第一个小鬼,则需要指向自己
            if (i == 1) {
                first = kid;
                // 指向自己构成环
                first.setNext(first);
                // 让curKid指向第一个小鬼
                curKid = first;
            } else {
                // 当前结点指向待插入的kid结点
                curKid.setNext(kid);
                // 构成环
                kid.setNext(first);
                // 当前节点后移
                curKid = kid;
            }
        }
    }

    /**
     * 遍历当前环形链表
     */
    public void showKid() {
        // 判空
        if (first == null) {
            System.out.println("没有任何小鬼~~");
            return;
        }

        // 因为first不能动,因此我们仍然使用一个辅助指针完成遍历
        Kid curKid = first;
        while (curKid.getNext() != first) {
            System.out.printf("小鬼的编号 %d \n", curKid.getNo());
            // 后移
            curKid = curKid.getNext();
        }
    }

    /**
     * 约瑟夫问题解决方法
     *
     * @param startNo  从第几个小鬼报数
     * @param countNum 数字报到多少
     * @param nums     小鬼的个数
     */
    public void countKid(int startNo, int countNum, int nums) {
        // 数据校验
        if (first == null || startNo < 1 || startNo > nums) {
            System.out.println("参数输入有误, 请重新输入");
            return;
        }

        // 创建辅助指针,讨债鬼,作用催命
        Kid helper = first;
        // 需求创建一个辅助指针(变量) helper , 事先指向环形链表的最后这个节点
        while (helper.getNext() != first) {
            // 后移
            helper = helper.getNext();
        }

        // 小鬼报数前,先让 first 和 helper 移动 k - 1(startNo - 1)次,到达指定位置
        for (int j = 0; j < startNo - 1; j++) {
            first = first.getNext();
            helper = helper.getNext();
        }

        // 这里是一个循环操作,直到圈中只有一个节点
        while (helper != first) {
            // 让 first 和 helper 指针同时 的移动 countNum - 1
            for (int j = 0; j < countNum - 1; j++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            // 这时first指向的节点,就是要出圈的小鬼节点
            System.out.printf("小鬼%d出圈\n", first.getNo());
            // 这时将first指向的小鬼节点出圈
            // first指向下一个结点,helper直接指向first
            first = first.getNext();
            helper.setNext(first);

        }
        System.out.printf("最后留在圈中的小鬼编号%d \n", first.getNo());
    }
}

// 测试类
public class Demo8 {
    public static void main(String[] args) {
        // 构建环形链表
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        // 加入5个小鬼节点
        circleSingleLinkedList.addKid(5);
        // 遍历
        circleSingleLinkedList.showKid();

        System.out.println("测试小鬼出圈");
        // 2->4->1->5->3
        circleSingleLinkedList.countKid(1, 2, 5);
    }
}

输出结果

小鬼的编号 1 
小鬼的编号 2 
小鬼的编号 3 
小鬼的编号 4 
测试小鬼出圈
小鬼2出圈
小鬼4出圈
小鬼1出圈
小鬼5出圈
最后留在圈中的小鬼编号3 
上一篇总目录下一篇
一、数组与队列数据结构篇(Java)目录三、栈
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值