【数据结构和算法】(基础篇二)——链表

链表

数组最麻烦的地方就是其在建立之后大小固定,对于增删数据很不方便。链表的出现解决了这个问题,链表的元素不是在内存中连续存储的,而是通过指针链接在一起的节点集合,这样的设计让链表有了动态的大小。链表是树和图结构的基础。

链表由一个个的节点构成,这些节点设计的不同会产生不同的特性和使用场景,链表主要有三种类型:单链表、双向链表和环形链表。

单链表

因为链表的内存不是连续的,为了能够让上一个节点找到下一个节点的位置,每个节点除了存储自身数据之外,还需要存储指向下一个节点的地址,这两部分称为data数据域和next指针域。

单链表的设计中,还可以根据需要选择有没有头尾节点,头尾节点的存在可以简化插入和删除操作,比如要删除第一个节点的话,有头结点的情况让其next域指向第二个节点即可;通常头节点经常使用作为指示链表的开始,尾节点让其next域为null即可。头节点的存在也可以用于判断一个链表是否为空(看其是否指向null)。

链表的逻辑结构示意图:

【示例】创建一个单链表用来存放梁山的108好汉,完成数据的增删改查,需要有两种添加数据的方法,一种是直接在链表尾部添加数据,另一种是按照英雄的座次排名进行插入数据

单链表结尾增加数据:让新节点的next指向null,遍历得到原本的尾节点,让尾节点的next指向新节点

单链表中间增加数据:先找到需要增加数据的位置,比如要在下标为n的位置增加一个节点,此时让新节点先指向n+1个节点,然后让第n个节点指向新节点。这个顺序不能反了,如果先让第n个节点指向新节点,则n+1后面的节点就都找不到了。

java 复制代码
public class SingleLinkedListDemo {
    public static void main(String[] args) {
        //Test
        HeroNode hr1 = new HeroNode(1,"宋江", "及时雨");
        HeroNode hr2 = new HeroNode(2,"卢俊义", "玉麒麟");
        HeroNode hr3 = new HeroNode(3,"吴用", "智多星");
        HeroNode hr4 = new HeroNode(4,"林冲", "豹子头");

        SingleLinkedList sll = new SingleLinkedList();

        sll.addByOrder(hr1);
        sll.addByOrder(hr4);
        sll.addByOrder(hr3);
        sll.addByOrder(hr2);

        sll.list();

        System.out.println("修改后的数据-------------------");
        HeroNode nhr = new HeroNode(2,"卢俊义1", "玉麒麟1");
        sll.update(nhr);
        sll.list();

        System.out.println("删除后的数据-------------------");
        sll.del(hr1);
        sll.list();
        sll.del(hr2);
        sll.list();
        sll.del(hr3);
        sll.list();
        sll.del(hr4);
        sll.list();
    }
}

class SingleLinkedList{
    // 头节点
    HeroNode head = new HeroNode(0,"","");

    public SingleLinkedList() {
        this.head = head;
    }

    // 添加数据
    public void add(HeroNode heroNode){
        // 先使用一个临时变量找到链表尾,将数据添加到链表尾
        HeroNode temp = head;

        while (true){
            if (temp.next == null){
                // 找到了链表尾
                break;
            }
            // 没找到链表尾就继续遍历
            temp = temp.next;
        }
        // 在循环结束后,得到的temp就处于链表尾
        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;
            } else if (temp.next.no == heroNode.no) {  // 已经存在相同数据
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){  //
            System.out.printf("您要插入的第%d位的英雄数据已经存在,添加数据失败\n", heroNode.no);
        }else{
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    // 修改链表中的信息
    public void update(HeroNode newheroNode){
        // 根据英雄的排名(no)修改信息
        if(head.next == null){
            System.out.println("链表为空,不能修改");
            return;
        }
        HeroNode temp = head;
        boolean flag = false;
        while (true){
            if (temp.next==null){
                // 遍历结束,没有找到对应的no
                break;
            } else if (temp.no == newheroNode.no) {
                // 找到正确的位置
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){
            temp.name = newheroNode.name;
            temp.nickname = newheroNode.nickname;
        }else {
            System.out.printf("链表中没有排名为 %d 的英雄,无法修改", newheroNode.no);
        }
    }

    // 删除链表中的数据
    public void del(HeroNode target){
        HeroNode temp = head;
        boolean flag = false;

        while (true){
            if (temp.next.no == target.no){
                // temp的下一个数据就是需要删除的数据
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){
            // 如果没有变量指向被删除的内存空间,它就会被垃圾回收机制回收掉
            temp.next = temp.next.next;
        }
        else {
            System.out.println("你所找的数据不在链表中,无法删除");
        }

    }

    // 查看链表中的数据
    public void list(){
        if (head.next == null){
            System.out.println("链表为空,没有数据");
            return;
        }
        HeroNode temp = head;

        while (true){
            if (temp.next == null){
                // 找到了链表尾
                break;
            }
            // 没找到链表尾就继续遍历
            System.out.println(temp);
            temp = temp.next;
        }
        System.out.println(temp);
    }
}

class HeroNode{
    int no;
    String name;
    String nickname;
    HeroNode next;

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

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

【面试算法题目】单链表的逆序打印

思路:方法一,先用上面的方法把单链表反转,再进行打印,但是会破坏原来的链表结构,不推荐使用;方法二,使用栈先入后出的特性实现来实现

java 复制代码
    // 单链表逆序打印
    public static void reversePrint(HeroNode head){
        // 判断是否为空
        if (head.next == null){
            return;
        }
        // 创建一个栈
        Stack<HeroNode> stack = new Stack();
        // 遍历
        HeroNode cur = head.next;
        while (cur != null){
            stack.add(cur);
            cur = cur.next;
        }
        // 从栈中取数据
        while (stack.size()>0){
            System.out.println(stack.pop());
        }
    }

双向链表

单向链表中的每一个节点都只能够指向下一个节点,也就是说,只能进行单向的遍历。双向链表可以解决这个问题,双向链表(Doubly Linked List)是一种线性数据结构,其中的每个元素都是一个节点,每个节点包含三个部分:数据、指向其前一个节点的指针和指向其后一个节点的指针。与单向链表相比,双向链表中的节点具有额外的指向前一个节点的链接,这使得在链表中进行前后移动都变得可能。

  • 插入:可以在链表的头部、尾部或任意位置插入一个新的节点。插入时需要更新新节点的前后指针以及相邻节点的前后指针。
  • 删除:可以从链表中删除一个指定节点。删除时需要更新被删除节点的前一个节点和后一个节点的指针。
  • 遍历:可以从前向后或从后向前遍历链表,访问每一个节点。
  • 查找:在链表中查找特定值的节点,可以通过遍历来实现。
  • 反转:双向链表可以很容易地反转,只需要交换所有节点的前后指针即可。

【示例】创建一个双向链表完成上面的需求

java 复制代码
public class DoubleLinkedListDemo {
    public static void main(String[] args) {
        HeroNode2 hr1 = new HeroNode2(1,"宋江", "及时雨");
        HeroNode2 hr2 = new HeroNode2(2,"卢俊义", "玉麒麟");
        HeroNode2 hr3 = new HeroNode2(3,"吴用", "智多星");
        HeroNode2 hr4 = new HeroNode2(4,"林冲", "豹子头");

        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();

        doubleLinkedList.addByOrder(hr1);
        doubleLinkedList.addByOrder(hr3);
        doubleLinkedList.addByOrder(hr2);
        doubleLinkedList.addByOrder(hr4);

        doubleLinkedList.list();

        /*System.out.println("删除最后一个数据");
        doubleLinkedList.del(hr4);
        doubleLinkedList.list();

        System.out.println("修改第二个数据后:");
        HeroNode2 hr5 = new HeroNode2(2,"卢俊义2", "玉麒麟2");
        doubleLinkedList.update(hr5);
        doubleLinkedList.list();*/
    }
}

class DoubleLinkedList{
    // 头节点
    HeroNode2 head = new HeroNode2(0,"","");

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

    public DoubleLinkedList() {
        this.head = head;
    }

    // 按照英雄座次排序插入节点
    public void addByOrder(HeroNode2 heroNode){
        HeroNode2 temp = head;
        boolean flag = false;
        while (true){
            if (temp.next == null){ //已经把整个链表都遍历玩了
                break;
            }
            if (temp.next.no > heroNode.no){  // 就是正确的位置,在temp的后面插入数据
                break;
            } else if (temp.next.no == heroNode.no) {  // 已经存在相同数据
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){  //
            System.out.printf("您要插入的第%d位的英雄数据已经存在,添加数据失败\n", heroNode.no);
        }else{
        if (temp.next != null){
            temp.next.pre = heroNode;
            heroNode.next = temp.next;
        }
            temp.next = heroNode;
            heroNode.pre = temp;

        }
    }

    // 修改节点数据
    public void update(HeroNode2 newheroNode){
        // 根据英雄的排名(no)修改信息
        if(head.next == null){
            System.out.println("链表为空,不能修改");
            return;
        }
        HeroNode2 temp = head;
        boolean flag = false;
        while (true){
            if (temp.next==null){
                // 遍历结束,没有找到对应的no
                break;
            } else if (temp.no == newheroNode.no) {
                // 找到正确的位置
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){
            temp.name = newheroNode.name;
            temp.nickname = newheroNode.nickname;
        }else {
            System.out.printf("链表中没有排名为 %d 的英雄,无法修改", newheroNode.no);
        }
    }

    // 删除数据,自我删除
    public void del(HeroNode2 target){
        // 直接自我删除,从第一个有效数据节点开始
        HeroNode2 temp = head.next;
        boolean flag = false;

        while (temp != null){
            if (temp.no == target.no){
                // temp的下一个数据就是需要删除的数据
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){
            // 如果没有变量指向被删除的内存空间,它就会被垃圾回收机制回收掉
            temp.pre.next = temp.next;
            // 只有不是尾节点时才执行
        if (temp.next !=null){
            temp.next.pre = temp.pre;
        }
        }
        else {
            System.out.println("你所找的数据不在链表中,无法删除");
        }

    }

    // 添加数据到链表尾
        public void add(HeroNode2 heroNode){
        // 先使用一个临时变量找到链表尾,将数据添加到链表尾
        HeroNode2 temp = head;

        while (true){
            if (temp.next == null){
                // 找到了链表尾
                break;
            }
            // 没找到链表尾就继续遍历
            temp = temp.next;
        }
        // 在循环结束后,得到的temp就处于链表尾
        temp.next = heroNode;
        heroNode.pre = temp;
    }

    // 遍历
    public void list(){
        // 先判断链表是否为空
        if (head.next == null){
            System.out.println("链表为空,没有数据");
            return;
        }
        HeroNode2 temp = head;

        while (true){
            if (temp.next == null){
                // 找到了链表尾
                break;
            }
            // 没找到链表尾就继续遍历
            System.out.println(temp.next);
            temp = temp.next;
        }
    }
}

class HeroNode2{
    int no;
    String name;
    String nickname;
    HeroNode2 next;
    HeroNode2 pre;
    HeroNode2 head;

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

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

环形链表

环形链表(Circular Linked List)是一种特殊的链表形式,其中最后一个节点的"next"指针不是指向None,而是指向链表的头节点,形成了一个闭环。这种结构使得从链表的任何一点出发都能通过"next"指针遍历整个链表并回到起点,从而提供了连续访问所有元素的能力。

环形链表可以是单向的,也可以是双向的。在双向环形链表中,除了每个节点都有一个指向前一个节点的"prev"指针外,最后一个节点的"next"指针会指向头节点,而头节点的"prev"指针也会指向最后一个节点。

创建环形单链表:先创建一个象征链表逻辑开始的头节点,最开始的头结点也应该指向自己,在链表为空时,head.next指向head本身,可以作为一种标志,表明链表当前没有其他节点。头结点除了在添加第一个数据节点时都不应该进行改动。

添加数据到环形单链表末尾时,需要一个临时变量作为链表的逻辑结尾,temp.next == first表示到达逻辑结尾

添加数据到环形单链表中间的时候,需要先进行遍历找到要添加的前一个位置

遍历环形单链表:使用一个临时变量实现,当temp.next == first表示遍历结束

【示例】约瑟夫环问题:假设有一群人围成一圈,从某个人开始报数,每数到第m个人,就将这个人从圈中移除,然后从下一个人继续报数,直到只剩下最后一个人为止。问题要求确定在初始状态下,哪个人会是最后存活下来的。

  1. 创建一个辅助变量helper指向first的前一个,用来帮助实现出队列(因为单链表的删除数据需要将辅助变量指向前一个)
  2. 在开始出队列之前,先把first指向指定的位置,helper也要同步移动
  3. 根据指定的数数个数,进行循环,每次的first指向的节点就是需要被删除的节点,first = first.next; helper.next = first.
java 复制代码
package dataStructure.list;

public class Josephues {
    public static void main(String[] args) {
        // 测试创建和遍历循环链表
        CircleLinkedList circleLinkedList = new CircleLinkedList();
        circleLinkedList.add(5);
        circleLinkedList.showBoys();
        circleLinkedList.countBoy(1,2,5);
    }
}

class CircleLinkedList{
    // 先创建first变量
    Boy first = null;

    /**
     * 约瑟夫出圈问题
     * @param startNo   从哪里开始数
     * @param countNo   每次数几个数
     * @param sum    最初的总节点个数
     */
    public void countBoy(int startNo, int countNo, int sum){
        // 数据校验
        if(first == null || startNo < 1 || startNo > sum || countNo > sum){
            System.out.println("输入参数有误");
            return;
        }
        // 创建辅助变量,置于队尾
        Boy helper = first;
        while (true){
            helper = helper.getNext();
            if (helper.getNext() == first){
                break;
            }
        }

        // 将first移动到指定的位置,即startNo
        for (int i = 0; i < startNo-1; i++) {
            first = first.getNext();
            helper = helper.getNext();
        }

        // 进行出队列,每次循环countNo次,直到链表中只剩下一个节点(helper == first)
        while (true){
            for (int i = 0; i < countNo-1; i++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            //此时的first 指向的就是要出队列的节点
            System.out.printf("小孩%d出圈\n", first.getNo());
            first = first.getNext();
            helper.setNext(first);
            if (helper == first){
                break;
            }
        }
        System.out.println("队列中的最后一个孩子编号为:" + first.getNo());

    }

    // 添加数据, nums 表示需要添加的个数
    public void add(int nums){
        if (nums < 1){
            System.out.println("nums的值不正确");
            return;
        }
        Boy curboy = null;
        for (int i = 1; i <= nums ; i++) {
            // 根据i作为no创建boy对象
            Boy boy = new Boy(i);
            if (i == 1){ //第一个节点需要覆盖掉first
                first = boy;
                first.setNext(first);
                curboy = first;
            }
            curboy.setNext(boy);
            boy.setNext(first);
            curboy = boy;
        }
    }

    // 遍历
    public void showBoys(){
        // 判断链表是否为空
        if(first == null){
            System.out.println("链表中没有数据了。。。");
            return;
        }
        // 使用一个临时变量辅助遍历
        Boy curboy = first;
        while (true){
            System.out.println("当前小孩的编号为:" + curboy.getNo());
            if (curboy.getNext() == first){
                break;
            }
            curboy = curboy.getNext();
        }
    }
}

class Boy{
    private int no;
    private Boy next;

    public int getNo() {
        return no;
    }

    public Boy getNext() {
        return next;
    }

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

    public Boy(int no) {
        this.no = no;
    }
}
相关推荐
幼儿园园霸柒柒20 分钟前
第七章: 7.3求一个3*3的整型矩阵对角线元素之和
c语言·c++·算法·矩阵·c#·1024程序员节
忘梓.1 小时前
排序的秘密(1)——排序简介以及插入排序
数据结构·c++·算法·排序算法
福大大架构师每日一题1 小时前
文心一言 VS 讯飞星火 VS chatgpt (384)-- 算法导论24.5 4题
算法·文心一言
云卓科技1 小时前
无人车之路径规划篇
人工智能·嵌入式硬件·算法·自动驾驶
摆烂小白敲代码1 小时前
背包九讲——背包问题求方案数
c语言·c++·算法·背包问题·背包问题求方案数
头真的要秃啦2 小时前
Linux 无名管道
linux·运维·算法
极智视界2 小时前
无人机场景数据集大全「包含数据标注+划分脚本+训练脚本」 (持续原地更新)
算法·yolo·目标检测·数据集标注·分割算法·算法训练·无人机场景数据集
passer__jw7672 小时前
【LeetCode】【算法】208. 实现 Trie (前缀树)
算法·leetcode
shenweihong2 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
stm 学习ing2 小时前
C语言 循环高级
c语言·开发语言·单片机·嵌入式硬件·算法·嵌入式实时数据库