Java实现单向链表

最近学习了集合的实现,相比于数组,Java的集合的概念我就不去详细说了,我就简单说一下我对集合的理解

1.集合是一个可以动态存放数据,不像数组一样,一旦确定了长度就不可变了

2.数组需要存放相同数据结构的数据,集合存放的都是Object类型的,也就是引用数据结构

3.无法直接获取数组实际存储的元素个数,length用来获取数组的长度,但集合可以通过size()直接获取集合实际存储的元素个数。

差不多就先概况一下这些基础

在Java中集合的实现是基于Collection这个超级接口来实现的,这里我就不详细讲述Collection了,大家可以去看看大佬们对它的详解

在Collection集合中主要分为有序集合和无序集合有序集合存放的数据是有下标索引且可以重复的,无序集合存放的数据是没有下标索引且不可重复

而我今天是针对有序集合中的链表数据结构进行一下我的讲解

ps:在集合中是基于双向链表实现的

好了现在让我先来讲一下什么是单向链表,后面我再写一下双向链表

单向链表与数组不同,数组存放数据的内存地址是连续的,而链表存放数据的内存地址不是连续的

所以我们可以知道,数组擅长查询,因为数组的查询效率高,无论有多少数据,它查询的时间复杂度都是一样的,因为数组是获取了数组头的内存地址,通过计算来知道数组其他数据的内存地址的但是对于增删来说,数组的效率很低,因为它需要保持数据内存地址的连贯性,会进行数据的移动

而对于链表来说是和数组相反的,它的查询效率低,增删效率高,我来画一幅图简单说一下

(如果想更清楚的了解数据结构,大家搜搜大佬们的讲解,我这里就是为了实现简单说一下)

链表是通过节点与节点的连接组成的,节点又分为两个部分

一个是当前节点存放的数据

一个是连接的下一个节点的内存地址

在这个链表中,张三是头部节点,赵六是尾部节点

所以我们可以看见

张三(0x10)   指向     李四(0x11)

李四(0x11)   指向     王五(0x12)

王五(0x12)   指向     赵六(0x13)

而赵六作为尾部节点,所以它的下一个节点为null,是空的

为什么说链表的效率高,这里举例,我们要删除王五这个节点,再画一个图:

我们删除一个节点,只需要将这个节点与它前后节点的连接链条断掉就可以了,所以我们可以直接将李四指向赵六。这个过程就是,

李四连接王五,所以李四可以获取到王五的下一个节点是赵六,也就是说,

李四获取到了王五的下一个节点的内存地址(赵六0x13)

将李四存放的指向下一个节点的内存地址修改为赵六(0x13)

将赵五置空

我们可以看到,没有任何一个节点发生了位置的改变

下面我们如果再把王五增加过来呢?

原理其实也是一样的,断开李四和赵六的连接

王五先获取李四存放的指向下一个节点的内存地址(赵六0x13)

然后将李四的指向下一个节点的内存地址修改为王五(0x12)

再将王五的指向下一个节点的内存地址修改为赵六(0x13)

 在此之前我先简单说一下泛型

    private static class Node1{
        String data;
        Node next;
    }
    private static class Node<E>{
        E data;
        Node<E> next;

Node1和Node的区别就是规定了Node这个对象的数据类型

我们先来看Node1,Node1中的data是String,字符串类型,它还可以是别的引用类型对吗

来看Node中的data是E,所以泛型就是给了它们一个概况,不提前规定类的数据类型,在创建的时候规定

Node<String> mynode = new Node<>();

这时E就为String也就是字符串类型,所以data也是字符串类型

我这里就简单说一下,大家不懂去看看泛型的使用。

我们通过这个小例子应该能理清链表增删的实现了,下面我开始用Java写一个链表出来,代码如下

public class Mylink<E> {
    private int size;
    private Node<E> first;

    public Mylink() {
    }

    public int size() {
        return size;
    }

    //向单向链表的末尾添加一个元素
    public void add(E data){
        //如果first是空,证明这是一个空列表
        Node<E> newNode = new Node<>(data,null);
        if (first == null){
            first = newNode;
            size++;
            return;
        }
        //找到末尾节点
        Node<E> last = findLast();
        //新建节点,并让末尾节点指向新节点
        last.next = newNode;
        size++;
    }

    //找到链表的末尾节点
    private Node<E> findLast() {
        //遍历链表,从头节点开始判断next是否为空,不为空则继续遍历寻找
        Node<E> last = first;
        //因为最后一个节点的next一定是null
        while (last.next != null){
            last = last.next;
        }
        return last;
    }

    public void addFirst(Node<E> newNode){
        newNode.next = first;
        size++;
    }
    //向单向链表的指定位置添加一个元素
    public void add(int index,E data){
        //新建节点对象
        Node<E> newNode = new Node<>(data,null);

        //根据下标找到对应的节点对象,上一个对象
        if (index < size()){
            if (index != 0){
                Node<E> prev = node(index -1);
                newNode.next = prev.next;
                prev.next = newNode;
                size++;
            }else {
                addFirst(newNode);
            }
        }else {
            System.out.println("下标越界,无信息");
        }
    }
    //1,2,3,4,5,6
    //0,1,2,3,4,5
    //返回索引处的节点对象
    private Node<E> node(int index) {
        Node<E> next = first;
        for (int i = 0; i < index; i++) {
            next = next.next;
        }
        return next;
    }

    //删除指定位置的元素
    public void remove(int index){
        //假如删除的是头节点
        if (index == 0){
            Node<E> oldFirst = first;
            first = first.next;
            oldFirst.next = null;
            oldFirst.data = null;
            size--;
            return;
        }
        //删除的节点
        Node<E> removeNode = node(index);
        //删除的上一个节点
        Node<E> prevNode = node(index-1);
        prevNode.next = removeNode.next;
        removeNode.next = null;
        removeNode.data = null;
        size--;
    }

    //修改指定位置的元素
    public E set(int index,E data) {
        //将修改的节点赋值到setNone
        Node<E> setNone = node(index);
        //保存修改的数据
        E oldData = setNone.data;
        //修改数据
        setNone.data = data;

        return oldData;
    }

    //根据下标获取数据
    public E get(int index){
        return node(index).data;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public Node<E> getFirst() {
        return first;
    }

    public void setFirst(Node<E> first) {
        this.first = first;
    }
    
    private static class Node<E>{
        //节点内的数据
        E data;
        //节点指向的下一个对象
        Node<E> next;

        public Node(E data, Node<E> next) {
            this.data = data;
            this.next = next;
        }
    }

}

让我来解释一下这段代码:

首先我们创建一个链表类

public class Mylink<E> {
    private int size;
    private Node<E> first;

并定义了两个属性,一个是size:用来记录链表的节点个数

一个是first:用来标记链表的头节点(因为不知道头节点,是没有办法确定从哪里开始,也就是遍历查找)

这里的Node我设置为内部类了,大家也可以再自定义一个类哈

    //单向链表的节点
    private static class Node<E>{
        //节点内的数据
        E data;
        //节点指向的下一个对象
        Node<E> next;

        public Node(E data, Node<E> next) {
            this.data = data;
            this.next = next;
        }
    }

定义一个静态内部类,也就是链表的节点,节点有两个属性,大家看到这里应该也知道是什么了,一个是节点保存的数据,一个是节点指向的下一个对象。

ps:我这里再多说一嘴,可能大部分看我的都是刚开始学习Java的,我们要知道,链表是一个对象,所以节点同样也是对象,是链表内的对象,每个节点都是独立的,他们是依赖关系。

    public Mylink() {
    }

    public int size() {
        return size;
    }

这是链表的无参构造函数会默认给链表的属性赋默认值,所以就是初始化一个空的链表

size():就是返回当前链表的节点个数

添加方法:(默认向末尾添加)

//向单向链表的末尾添加一个元素
    public void add(E data){
        //如果first是空,证明这是一个空列表
        Node<E> newNode = new Node<>(data,null);
        if (first == null){
            first = newNode;
            size++;
            return;
        }
        //找到末尾节点
        Node<E> last = findLast();
        //新建节点,并让末尾节点指向新节点
        last.next = newNode;
        size++;
    }

添加一个节点,首先要构建出一个节点对象,data为添加的数据,null为节点指向下个节点的地址,默认为空。

在添加的时候要先判断这是不是一个空链表,而判断空链表只需要判断有没有头节点,如果头节点为null证明这是一个空链表

如果是空链表,那这次添加的节点是不是就变成了链表的头节点,那我们让first,也就是头节点变成newNode,因为我们添加了一个节点,所以链表的节点数量要增加,所以size++

如果不是空列表,我们需要知道末尾节点这个对象的地址,然后更改它的next,也就是指向本次添加的节点,同样的,size++

下面我们讲解一下findLast这个方法:

//找到链表的末尾节点
    private Node<E> findLast() {
      
        Node<E> last = first;
        //因为最后一个节点的next一定是null
        while (last.next != null){
            last = last.next;
        }
        return last;
    }

之后的讲解,next就是指向下一个节点的内存地址,data就是该节点的存储内容

我们要先知道,怎么样算是找到了末尾节点,末尾节点的next属性一定是null

所以我们要从头开始寻找,可以将头节点看作末尾节点,还是用刚刚的例子吧:

我们将头节点张三,看作末尾节点,判断它的next是不是空,它不是,所以这时last变量变成了李四

我们再将李四看作末尾节点,判断它的next是不是空,它不是,所以这时last变量变成了王五

以此类推,这时找到了赵六,last = 赵六节点,赵六的next为null,所以它是尾部节点,将赵六返回。

添加方法:(头部添加)

    public void addFirst(Node<E> newNode){
        newNode.next = first;
        size++;
    }

头部添加就比较简单了,只需要将新创建的节点的next改成first不就好了,同样的size++

添加方法:(按照索引添加)

private Node<E> node(int index) {
        Node<E> next = first;
        for (int i = 0; i < index; i++) {
            next = next.next;
        }
        return next;
    }

首先我们来看如何找到指定索引下的节点对象

还是一样的,要从头开始,将头节点赋值next,假如我们指定的索引为2,获取王五节点对象

我们是不是只需要获取到它的上一个节点

因为上一个节点的next,不就是我们需要的节点对象吗

我们指定的索引是2,而恰好只需要循环2次,就可以找到目标节点的,上一个节点对象

所以再去看代码就可以看懂了

下面是实现:

//向单向链表的指定位置添加一个元素
    public void add(int index,E data){
        //新建节点对象
        Node<E> newNode = new Node<>(data,null);

        //根据下标找到对应的节点对象,上一个对象
        if (index < size()){
            if (index != 0){
                Node<E> prev = node(index -1);
                newNode.next = prev.next;
                prev.next = newNode;
                size++;
            }else {
                addFirst(newNode);
            }
        }else {
            System.out.println("下标越界,无信息");
        }
    }

一样的,添加就要新建一个节点对象。我这里严谨了一下添加了判断,防止下标越界,因为假如一共5个数据,而你指定的索引为5,是没有这个数据的。

然后在里面再加一个判断,如果传入的是0,就直接调用头部添加

如果是非0,我们需要获取到上一个节点对象,所以要将索引的值-1,这时再将添加的节点对象的next修改成上一个节点对象的next,参考我上面的举例,然后再让上一个节点对象,指向新节点,最后个数加1

删除节点

    //删除指定位置的元素
    public void remove(int index){
        //假如删除的是头节点
        if (index == 0){
            Node<E> oldFirst = first;
            first = first.next;
            oldFirst.next = null;
            oldFirst.data = null;
            size--;
            return;
        }
        //删除的节点
        Node<E> removeNode = node(index);
        //删除的上一个节点
        Node<E> prevNode = node(index-1);
        prevNode.next = removeNode.next;
        removeNode.next = null;
        removeNode.data = null;
        size--;
    }

先进行一下判断,是不是删除的为头节点,因为头节点是没有节点指向它的,也就是说它没有上一个节点,首先将头节点对象保存到oldfirst中,然后更改头节点对象,变成头节点的next,然后将oldfirst的数据和next置空

如果不是头节点,我们就要先获取该节点的上一个节点,然后将上一个节点的next修改为删除节点的next,不懂可以翻上去看一下举例。然后将数据和next置空,size--

修改与查找

    //修改指定位置的元素
    public E set(int index,E data) {
        //将修改的节点赋值到setNone
        Node<E> setNone = node(index);
        //保存修改的数据
        E oldData = setNone.data;
        //修改数据
        setNone.data = data;

        return oldData;
    }

    //根据下标获取数据
    public E get(int index){
        return node(index).data;
    }

这里我就将修改和查找一起说了,上面理解了这块就很简单了,修改中,调用node方法获取目标节点对象,保存下来,然后修改节点的data值,返回修改之前的值,方便查看。

查询就是直接调用node方法查询目标节点对象的data值

下面是一段测试程序:

public class Text {

    public static void main(String[] args) {
        //创建一个单向链表
        Mylink<String> mylink = new Mylink<>();

        //添加元素
        mylink.add("zhangsan");
        mylink.add("lisi");
        mylink.add("wangwu");
        mylink.add("zhaoliu");

        //在指定位置增加元素
        mylink.add(1,"李四");
        for (int i = 0; i < mylink.size(); i++) {
            System.out.println(mylink.get(i));
        }
        System.out.println("==============");

        //删除下标的元素
        mylink.remove(1);
        for (int i = 0; i < mylink.size(); i++) {
            System.out.println(mylink.get(i));
        }
        System.out.println("==============");
        //修改
        System.out.println("修改的内容为:" + mylink.set(1,"李四"));

        System.out.println("==============");
        for (int i = 0; i < mylink.size(); i++) {
            System.out.println(mylink.get(i));
        }

    }
}

这样一个单向列表就完成了

如果我有不对的地方感谢大家的指出,谢谢大家的观看!

  • 38
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值