链表的学习笔记【单链表、双链表、单向环形链表、约瑟夫环】

单链表

链表是有序的列表,但是它在内存中是存储如下

在这里插入图片描述

上图小结:

  1. 链表是以节点的方式来存储,是链式存储
  2. 每个节点都有一个 data 域,next 域:指向下一个节点
  3. 如上图,发现每个链表的各个节点不一定是连续存储的
  4. 链表分带头节点的链表和没有头节点的链表,根据实际的情况来确定
  5. 单链表(带头节点)逻辑结构示意图如下

在这里插入图片描述

单链表的实际应用

使用带头节点数据完成单链表的实现

  • 第一种方法,直接添加到链表的尾部
    在这里插入图片描述

  • 第二种方式,根据编号排名将数据插入到指定的位置

在这里插入图片描述

  • 修改节点功能

    先找到该节点,然后在覆盖原来的数据

  • 删除节点

在这里插入图片描述

代码

package com.romanticlei.linkedlist;

public class SingleLinkedList {

    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, "林冲", "豹子头");
        SingleLinkedListDemo linkedListDemo = new SingleLinkedListDemo();
        // linkedListDemo.add(hero1);
        // linkedListDemo.add(hero4);
        // linkedListDemo.add(hero3);
        // linkedListDemo.add(hero2);

        HeroNode hero3_1 = new HeroNode(3, "小吴", "智多星");
        linkedListDemo.addByOrder(hero1);
        linkedListDemo.addByOrder(hero4);
        linkedListDemo.addByOrder(hero3);
        linkedListDemo.addByOrder(hero2);
        linkedListDemo.addByOrder(hero3_1);
        linkedListDemo.list();

        linkedListDemo.delete(1);
        linkedListDemo.delete(4);
        linkedListDemo.delete(5);
        linkedListDemo.list();

    }
}

// 定义 SingleLinkedList 管理我们的英雄
class SingleLinkedListDemo {
    // 先初始化一个头节点,头节点不要懂,不存储任何具体的数据
    HeroNode head = new HeroNode(0, "", "");

    // 添加节点到单向链表
    // 思路,当不考虑编号顺序时
    // 1.找到当前链表的最后一个结点
    // 2.将最后这个结点的next 指向新的节点
    public void add(HeroNode heroNode) {
        // 头节点不能动,我们需要一个临时变量来存储头节点
        HeroNode temp = head;
        while (true) {
            if (null == temp.next) {
                break;
            }

            // 存在后一个节点,临时节点后移
            temp = temp.next;
        }

        // 当退出while循环时,temp就指向了链表的最后
        // 并将最后这个节点的 next 指向新的节点
        temp.next = heroNode;
    }

    // 对插入的数据进行排序插入到链表
    public void addByOrder(HeroNode heroNode) {
        HeroNode temp = head;
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }

            if (temp.next.no > heroNode.no) {
                break;
            }

            if (temp.next.no == heroNode.no) {
                flag = true;
                break;
            }

            temp = temp.next;
        }

        // 如果排序编号相同,那么我们就覆盖原来的值
        if (flag) {
            heroNode.next = temp.next.next;
            temp.next.next = null;
        } else {
            // 如果编号不相同,那么我们直接插入到链表
            heroNode.next = temp.next;
        }

        temp.next = heroNode;
    }

    public void delete(int no){
        HeroNode temp = head;
        while (true){
            if (temp.next == null){
                System.out.println("删除数据不存在,删除失败");
                break;
            }
            if (temp.next.no == no){
                temp.next = temp.next.next;
                break;
            }
            temp = temp.next;
        }
    }

    // 显示链表
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }

        // 因为头节点不能动,所以仍旧需要一个临时节点
        HeroNode temp = head;
        while (true) {
            if (temp.next == null) {
                System.out.println("遍历完毕");
                break;
            }

            System.out.println(temp.next + "->");
            temp = temp.next;
        }
    }
}

// 定义 HeroNode,每个 HeroNode 对象就是一个节点
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 + '\'' +
                '}';
    }

}
HeroNode{no=1, name='宋江', nickName='及时雨'}->
HeroNode{no=2, name='卢俊义', nickName='玉麒麟'}->
HeroNode{no=3, name='小吴', nickName='智多星'}->
HeroNode{no=4, name='林冲', nickName='豹子头'}->
遍历完毕
删除数据不存在,删除失败
HeroNode{no=2, name='卢俊义', nickName='玉麒麟'}->
HeroNode{no=3, name='小吴', nickName='智多星'}->
遍历完毕

单链表面试题

求单链表中有效节点的个数
/**
  * 获取单链表的个数
  *
  * @param head 头节点
  * @return 节点总个数
  */
public static int getLength(HeroNode head) {
    int length = 0;
    HeroNode cur = head;

    while (true) {
        if (cur.next == null) {
            return length;
        }
        length++;
        cur = cur.next;
    }
}
查找单链表中的倒数第k个结点 【新浪面试题】
// 查找单链表中的倒数第k个结点
// 编写一个方法,接受head节点,同时接受一个index,index指的是倒数第几个节点
// 先遍历一遍链表,获得到链表总长度
// 得到size 后,我们从链表开始遍历(size - index)个就可以得到
// 如果找到了,就返回,否则返回null
public static HeroNode findLastIndexNode(HeroNode head, int index){
    if (head == null){
        return null; // 没有找到
    }

    // 第一遍遍历得到的链表长度
    int size = getLength(head);
    // 第二遍遍历链表到 size - index位置。就是我们倒数的第k个
    HeroNode cur = head.next;
    if (index < 0 || index > size){
        return null;
    }

    for (int i = 0; i < size - index; i++) {
        cur = cur.next;
    }

    return cur;
}
单链表的反转【腾讯面试题,有点难度】
// 单链表的反转【腾讯面试题,有点难度】利用头插法完成链表反转
public static void reversetList(HeroNode head) {
    // 如果当前链表为空或者只有一个结点,那么无需反转直接返回即可
    if (head == null || head.next == null) {
        return ;
    }

    // 创建一个新头节点
    HeroNode newNode = new HeroNode(0, "", "");
    // 定义一个辅助变量,即原结点的第一个值
    HeroNode cur = head.next;

    while (cur != null) {
        // 取出原节点的当前值值,然后将当前值后移
        HeroNode oldCur = cur;
        cur = cur.next;

        oldCur.next = newNode.next;
        newNode.next = oldCur;
    }

    // 将 head.next 指向 newNode.next实现反转
    head.next = newNode.next;
}
从尾到头打印单链表 【百度,要求方式 1:反向遍历 。 方式 2:Stack 栈】

测试state 栈的使用方法

package com.romanticlei.linkedlist;

import java.util.Stack;

public class TestStack {

    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        // 入栈
        stack.add("jack");
        stack.add("tom");
        stack.add("smith");

        // 出栈
        while (stack.size() > 0){
            System.out.println(stack.pop());
        }
    }
}

单链表的逆序打印代码:

// 可以利用这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点实现逆序打印
public static void reversePrint(HeroNode head){
    if (head.next == null){
        return; // 空链表,不能打印
    }

    // 创建一个栈,将各个节点压入栈中
    Stack<HeroNode> stack = new Stack<>();
    HeroNode cur = head.next;
    while (cur != null){
        stack.push(cur);
        cur = cur.next;
    }

    while (stack.size() > 0){
        System.out.println(stack.pop());
    }
}
合并两个有序的单链表,合并之后的链表依然有序
// 合并两个有序单链表
public static HeroNode mergeSingleLinkedList(HeroNode node1, HeroNode node2){
    // 判断有没有链表是空
    if (node1.next == null && node2.next == null){
        return null;
    }

    if (node1.next == null){
        return node2.next;
    }

    if (node2.next == null){
        return node1.next;
    }

    HeroNode newNode = new HeroNode(0, "", "");
    HeroNode cur;

    // 比较第一个有效值的大小
    if (node1.next.no > node2.next.no){
        // 将小值放在新链表后面
        newNode = node2.next;
        // 头结点后移一位,即移到第一个有效值
        node1 = node1.next;
        // 头结点后移两位,即移动到第二个值
        node2 = node2.next.next;

        // 声明一个临时变量
        cur = newNode;
        cur.next = null;

    }else {
        newNode = node1.next;
        node1 = node1.next.next;
        node2 = node2.next;

        cur = newNode;
        cur.next = null;
    }

    // node1 node2 都已指向了实际的数据
    while (node1 != null && node2 != null){
        if (node1.no > node2.no){
            cur.next = node2;
            cur = cur.next;
            node2 = node2.next;

        } else {
            cur.next = node1;
            cur = cur.next;
            node1 = node1.next;
        }
    }

    if (node1 == null){
        cur.next = node2;
    }else {
        cur.next = node1;
    }

    return newNode;
}

双向链表

双向链表的操作分析和实现

使用带 head 头的双向链表实现

管理单向链表的缺点分析:

  1. 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。 (删除单向链表需要找到待删除节点的前一个结点,而双向链表找到当前结点删除即可)。

  2. 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除 时节点,总是找到 temp,temp 是待删除节点的前一个节点。

  3. 分析了双向链表如何完成遍历,添加,修改和删除的思路。

在这里插入图片描述

对上图的说明:

分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现

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

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

(1) 先找到双向链表的最后这个节点

(2) temp.next = newHeroNode

(3) newHeroNode.pre = temp;

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

  2. 删除

(1) 因为是双向链表,因此,我们可以实现自我删除某个节点

(2) 直接找到要删除的这个节点,比如 temp

(3) temp.pre.next = temp.next

(4) temp.next.pre = temp.pre;

package com.romanticlei.linkedlist;

public class DoubleLinkedList {

    public static void main(String[] args) {
        System.out.println("双向链表的测试");
        HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");
        HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");
        HeroNode2 hero3 = new HeroNode2(5, "吴用", "智多星");
        HeroNode2 hero4 = new HeroNode2(6, "林冲", "豹子头");
        //创建一个双向链表
        DoubleLinkedListDemo doubleLinkedList = new DoubleLinkedListDemo();
        doubleLinkedList.add(hero1);
        doubleLinkedList.add(hero2);
        doubleLinkedList.add(hero3);
        doubleLinkedList.add(hero4);
        doubleLinkedList.list();

        // 修改
        System.out.println("修改后的链表~~~");
        HeroNode2 hero5 = new HeroNode2(3, "公孙胜", "入云龙");
        doubleLinkedList.update(hero5);
        doubleLinkedList.list();

        // 删除
        System.out.println("删除指定编号的链表为~~~");
        doubleLinkedList.delete(3);
        doubleLinkedList.list();

        // 有序加入链表
        System.out.println("按序加入到双向链表");
        HeroNode2 hero6 = new HeroNode2(7, "公孙胜", "入云龙");
        doubleLinkedList.addByOrder(hero6);
        doubleLinkedList.list();
    }
}

class DoubleLinkedListDemo {
    // 先初始化一个头节点,头节点不要懂,不存储任何具体的数据
    HeroNode2 head = new HeroNode2(0, "", "");

    // 获取到头节点
    public HeroNode2 getHead() {
        return head;
    }

    // 遍历显示双向链表
    public void list() {
        // 判断链表是否为空
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }

        // 因为头节点不能动,所以仍旧需要一个临时节点
        HeroNode2 temp = head;
        while (true) {
            if (temp.next == null) {
                System.out.println("遍历完毕");
                break;
            }

            System.out.println(temp.next + "->");
            temp = temp.next;
        }
    }

    // 添加一个数据到双向链表的尾部
    public void add(HeroNode2 heroNode) {
        // 头节点不能动,我们需要一个临时变量来存储头节点
        HeroNode2 temp = head;
        while (true) {
            if (null == temp.next) {
                break;
            }

            // 存在后一个节点,临时节点后移
            temp = temp.next;
        }

        // 当退出while循环时,temp就指向了链表的最后
        // 并将最后这个节点的 next 指向新的节点
        temp.next = heroNode;
        heroNode.pre = temp;

    }

    // 按序插入到双向唤醒列表
    public void addByOrder(HeroNode2 node) {
        HeroNode2 cur = head;
        boolean flag = false;

        while (true){
            if (cur.next == null){
                flag = true;
                break;
            }

            if (cur.next.no > node.no){
                break;
            }

            if (cur.next.no == node.no){
                update(node);
                return;
            }

            cur = cur.next;
        }

        if (flag){
            node.pre = cur;
            cur.next = node;
        } else {
            node.next = cur.next;
            cur.next.pre = node;
            cur.next = node;
            node.pre = cur;
        }

    }

    public void update(HeroNode2 newHeroNode) {
        if (head == null) {
            System.out.println("链表为空");
            return;
        }

        // 找到需要修改的结点,根据编号no
        HeroNode2 temp = head.next;
        boolean flag = false;
        while (true) {
            if (temp == null) {
                break;
            }

            if (temp.no == newHeroNode.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }

        if (flag) {
            temp.name = newHeroNode.name;
            temp.nickName = newHeroNode.nickName;
        } else {
            System.out.println("没有找到编号为" + newHeroNode.no + "的节点");
        }
    }

    // 删除节点
    public void delete(int no) {
        if (head.next == null) {
            System.out.println("链表为空,无法删除");
            return;
        }

        HeroNode2 temp = head.next;
        while (true) {
            if (temp == null) {
                System.out.println("删除数据不存在,删除失败");
                break;
            }
            if (temp.no == no) {
                temp.pre.next = temp.next;
                // 如果是最后一个节点,就不需要执行这句话
                if (temp.next != null) {
                    temp.next.pre = temp.pre;
                }
                temp.next = null;
                temp.pre = null;
                break;
            }
            temp = temp.next;
        }
    }
}

// 定义 HeroNode,每个 HeroNode 对象就是一个节点
class HeroNode2 {
    public int no;
    public String name;
    public String nickName;
    public HeroNode2 next; // 指向下一个节点
    public HeroNode2 pre; // 指向前一个节点

    public HeroNode2(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 + '\'' +
                '}';
    }

}

单向环形链表

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

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

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

单向环形链表介绍

在这里插入图片描述

Josephu 问题

在这里插入图片描述

约瑟夫问题-创建环形链表的思路图解

在这里插入图片描述

约瑟夫问题-小孩出圈的思路分析图

在这里插入图片描述

package com.romanticlei.linkedlist;

public class Josephu {

    public static void main(String[] args) {

        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.addBoy(5);
        circleSingleLinkedList.showBoy();

        System.out.println("测试约瑟夫出圈问题");
        circleSingleLinkedList.countBoy(1, 2, 5);
    }
}

// 创建一个环形的单向链表
class CircleSingleLinkedList {
    // 创建一个first 节点,没有编号
    private Boy first = null;

    public void addBoy(int nums) {
        if (nums < 1) {
            System.out.println("nums 的值不正确");
            return;
        }

        // 创建一个辅助指针,帮助构建环形链表
        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 指向第一个节点
                curBoy = boy;
            } else {
                curBoy.setNext(boy);
                boy.setNext(first);
                curBoy = boy;
            }
        }
    }

    // 遍历当前环形链表
    public void showBoy() {
        // 判断链表是否为空
        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();
        }

        // 出圈遍历前,先让first 和 helper 移动 k-1次(因为当前值也算一个数)
        for (int i = 0; i < startNo - 1; i++) {
            first = first.getNext();
            helper = helper.getNext();
        }
        // 找到出圈节点,让 first 和 helper 移动 countNum-1次
        while (true){
            if (first == helper){
                break;
            }
            // 让 first 和 helper 指针同时移动 countNum - 1
            for (int i = 0; i < countNum - 1; i++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            // 这时 first 指向的节点,就是要出圈的节点
            System.out.println("出圈的节点编号为 " + first.getNo());
            first = first.getNext();
            helper.setNext(first);
        }

        System.out.println("最后留在圈中的编号为 " + first.getNo());
    }
}

// 创建一个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;
    }
}
  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值