数据结构——链表

本文介绍了链表数据结构,包括单向链表的创建、添加、遍历、插入、修改和删除操作。接着讨论了双向链表的优势及其实现,以及单向环形链表在约瑟夫环问题中的应用。提供了相关代码示例和参考资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单向链表

单链表的创建和遍历

在学习链表之前,我还一直在想,数组在内存中是连续的,但是链表不连续,怎么去实现呢?我们难道还能像创建一个数组一样去创建一个叫做“链表”的数据结构?

原来链表是通过创建节点对象来实现的,在节点对象中存放目标数据和下一个对象的内存地址。相当于在队列中需要有一个数组来存放数据一样。

有个问题容易搞混,就是存放的下一个对象的地址,因为对象就是靠地址来区分的,所以地址就可以表示该地址所指向的对象。思考一下就能知道,最后一个有效元素的下一个地址为null,则说明最后元素的后一个元素是null,即已经没有有效数据了。

事实上,只要不给next赋值,它默认就是null。head.getNext()即是当前元素的属性值,也代表着下一个元素。

单向链表的创建、向末尾添加数据和遍历,代码如下:

public class OnewayLinkedListDemo {
    public static void main(String[] args) {
        OnewayLinkedList onewayLinkedList = new OnewayLinkedList();
        onewayLinkedList.show();
        System.out.println("------------------------------");
        Person p1 = new Person("宋江", 30);
        onewayLinkedList.addPerson(p1);
        onewayLinkedList.show();
        System.out.println("------------------------------");
        Person p2 = new Person("燕青", 24);
        onewayLinkedList.addPerson(p2);
        onewayLinkedList.show();
        System.out.println("------------------------------");
        Person p3 = new Person("吴用", 28);
        onewayLinkedList.addPerson(p3);
        onewayLinkedList.show();
        System.out.println("------------------------------");
        Person p4 = new Person("武松", 27);
        onewayLinkedList.addPerson(p4);
        onewayLinkedList.show();
    }
}

class OnewayLinkedList {
    // 先初始化一个头节点,后续不要动这个节点,不然容易乱套
    Person head = new Person("", 0);

    // 直接将数据添加到尾部
    public void addPerson(Person person) {
        // 这里因为不能动头节点,所以需要使用一个中间变量
        // temp一开始指向的是头节点
        Person temp = head;
        // 从头开始遍历,直到找到链表的末尾
        while (true) {
            if (temp.getNext() == null) {
                break;
            }
            // 如果还没到末尾,就把temp指针后移
            temp = temp.getNext();
        }
        // 循环一旦结束,则temp此时指向最后一个元素
        temp.setNext(person);
    }

    // 遍历单向链表
    public void show() {
        Person temp = head.getNext(); // 表示头节点的后一个,即第一个节点数据
        if (head.getNext() == null) {
            System.out.println("链表为空");
            return;
        }
        while (true) {
            if (temp == null) {
                break;
            }
            System.out.println(temp);
            temp = temp.getNext();
        }
    }
}

// 每一个Person对象就是一个节点
class Person {
    private String name;
    private int age;
    private Person next;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person getNext() {
        return next;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", next=" + next +
                '}';
    }
}

测试结果如下(如果不想显示每条数据的下一个地址,可以将toString方法中的next属性值给去掉)

关于head头节点为什么不能动再解释一下,如果只是一次操作的话是可以直接使用head节点的,但是假如添加了两次,第一次添加成功后,head节点的指向变了,还能成功找到开头吗?肯定不能了。或者添加一次后再遍历,此时head也改变了,也会导致遍历错误。所以,需要使用临时变量来代替head节点。

另外要注意,添加和遍历中的temp初始值是不一样的,因为添加时是从head开始查找的,而遍历时是需要从第一个有效元素开始打印的。

以上代码是按照先后顺序将元素添加到链表中的,没有考虑任何排序,现在,如果要求:不管是以什么顺序添加,都是按照年龄大小进行插入的,如果年龄有一样的,则添加失败。

这其实就是链表的插入

(如果有多个有部分冲突的判断,则将严厉的放在前面,宽泛的放在后面,类似于异常捕获时的处理)

插入数据的代码如下

public void addPersonOrderbyAge(Person person) {
        Person temp = head.getNext();
        if (temp == null) {
            head.setNext(person);
            return;
        }
        int targetAge = person.getAge();
        while (true) {
            if (temp.getAge() == targetAge) {
                System.out.println("改年龄已存在,无法添加");
                break;
            }
            if (targetAge > temp.getAge() && temp.getNext() == null) {
                temp.setNext(person);
                break;
            }
            if (targetAge > temp.getAge() && targetAge < temp.getNext().getAge()) {
                person.setNext(temp.getNext());
                temp.setNext(person);
                break;
            }
            if (targetAge < temp.getAge()) {
                head.setNext(person);
                person.setNext(temp);
                break;
            }
            temp = temp.getNext();
        }
    }

上面的代码是找到就进行插入,其实也可以先找到位置,再统一插入,补充和优化代码如下:

    public void addPersonOrderbyAge2(Person person) {
        // 从head开始才能遍历到每一个数据,必须从head开始
        Person temp = head;
        int targetAge = person.getAge();
        boolean isAgeExisted = false;
        // 找到第一个比目标值大的数据
        while (true) {
            // 如果始终没有找到,就说明它自己就是最大的,此时temp就是最后一个数据
            if (isNextNull(temp)) {
                break;
            }
            if (targetAge == temp.getNext().getAge()) {
                isAgeExisted = true;
                break;
            }
            // 如果它不是最大的,则肯定能找到第一个比它大的
            if (targetAge < temp.getNext().getAge()) {
                break;
            }
            temp = temp.getNext();
        }
        if (isAgeExisted) {
            System.out.println("年龄已存在,无法添加!");
        } else {
            person.setNext(temp.getNext());
            temp.setNext(person); // 注意这两行代码的顺序,如果反了,则会出现无限循环
        }
    }

    // 遍历单向链表
    public void show() {
        if (isNextNull(head)) {
            System.out.println("链表为空");
            return;
        }
        Person temp = head;
        while (isNextNotNull(temp)) {
            System.out.println(temp.getNext());
            temp = temp.getNext();
        }
    }

    private boolean isNextNull(Person person) {
        return person.getNext() == null;
    }

    private boolean isNextNotNull(Person person) {
        return person.getNext() != null;
    }

单向链表的修改,根据年龄(因为这里规定了年龄是唯一的)来修改节点的数据,即姓名,代码如下:

    public void updatePersonByAge(int age, String name) {
        Person temp = head;
        boolean isAgeExisted = false;
        while (isNextNotNull(temp)) {
            if (temp.getNext().getAge() == age) {
                isAgeExisted = true;
                break;
            }
            temp = temp.getNext();
        }
        if (isAgeExisted) {
            temp.getNext().setName(name);
        } else {
            System.out.println("不存在该年龄的人物!");
        }
    }

另外还有链表的删除,就是根据条件找到对应的值比如temp.getNext().getAge() == age,此时,将temp.getNext().getNext()的值赋给temp,即temp.setNext(temp.getNext().getNext()),这样,temp.getNext()这个节点就不在链表中了,等待垃圾回收即可。此处代码省略。

几个小问题:

1、单链表在统计节点个数时,需要将头节点去掉,只统计有效节点数

2、单链表的倒数第k个,可以用总长度length来转换成正向第length - k个

单链表的逆序(待实现)

思路蛮多的,这里给出一些参考

1、转换成双向链表来逆序:https://blog.youkuaiyun.com/lwkrsa/article/details/82015364?utm_source=distribute.pc_relevant.none-task

2、头插法实现逆序:https://blog.youkuaiyun.com/qq_41028985/article/details/82859199?utm_source=distribute.pc_relevant.none-task


双向链表

单向链表的缺点:

1、只有一个方向,而双向链表可以向前或者向后查找;

2、单向链表不能自我删除,需要依靠辅助节点,从上面的说明中可以知道单链表的删除总是需要依靠目标节点的上一个节点,如上面说明中提到的temp;

双向链表示意图:

双向链表的实现就是在单向链表的基础上多了一个pre属性,即前一个节点的地址,增删改查的实现思路如下:

1、增加时和单链表类似,不过要处理双向地址(分为顺序添加和插入)

2、删除时可以直接找到目标节点,然后将其前后节点连接,不需要依靠目标节点的前一个节点才能删除

3、修改和单向链表类似

4、遍历的话就等于是从头尾两个方向来遍历单向链表

双向链表详解参考:https://blog.youkuaiyun.com/javazejian/article/details/53047590?utm_source=distribute.pc_relevant.none-task


单向环形链表(约瑟夫环)

单向循环链表基本与单向链表相同,唯一的区别就是单向循环链表的尾节点指向的不是null,而是头节点(注意:不是头指针).
因此,单向循环链表的任何节点的下一部分都不存在NULL值。

由于单向循环链表的特性,它在处理一些环状数据的时候十分有效。大名鼎鼎的约瑟夫环问题就可以用循环单向链表求解,下面我们会有进一步的介绍。

由于单向循环链表和单向链表的差别真的不大,增添改查原理都相同,因此在这里我们不详细讲解。

单向循环链表的应用----约瑟夫环问题

问题来历

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着(其实最后一个人也可以自成环)。然而Josephus 和他的朋友并不想遵从。问题是,虽然给定了总数和间隔数,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

思路分析

首先所有的人是围城一圈的,而且需要循环很多圈才能够将所有人依次排除,而这非常适合刚刚完成的单向循环链表才解决,尾节点的下一个节点又重新拿到的头节点,刚刚和问题中的情况契合。
首先我们只要拿到链表的头节点,然后依次通过头节点的next指针往后拿到下一个节点,找到第3个移除链表,然后依次循环直到链表为空,移除的顺序就是我们需要的死亡顺序。

代码如下

import java.util.Scanner;

public class JosephRing {
    public static void main(String[] args){
        int sum=0;
        int space=0;
        String s="";
        System.out.println("输入环数和间隔");
        Scanner sc=new Scanner(System.in);
        sum=sc.nextInt();
        space=sc.nextInt();
        SingleLink<Integer> sl=new SingleLink<>();
        sl.initlist();
        //编号add进链表
        for(int i=0;i<sum;i++){
            sl.add(i+1);
        }
        Node<Integer> n=sl.first;
        while(n.next!=n){
            for(int i=1;i<space;i++){
                n=n.next;
            }
            int a=n.next.data;
            n.next=n.next.next;
            s=s+a+",";
        }
        System.out.println(s);
    }
}
/*
    输入:41
          3
    输出:3,6,9,12,15,18,21,24,27,30,33,36,39,1,5,10,14,19,23,28,32,37,41,7,13,20,26,34,40,8,17,29,38,11,25,2,22,4,35,16,
 */

注:环形链表部分来自https://www.cnblogs.com/sang-bit/p/11610181.html

补充

注:小孩报数前,要让helper和first移动k-1次,因为不一定是从一开始的位置开始的,如果k不等于1,就算等于1也要有这一步操作,使得辅助指针first指向第一个报数的小孩。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值