Java双向链表

一. 什么是链表

通俗的说,链表就是链条串起来的对象,是一种数据容器

  • 对象:用属性保存数据信息
  • 链条:对象的引用(指针)

二. 链表的结构

用图表示一个链表就是这样子
在这里插入图片描述

图中,每个大矩形称为一个节点(本质上就是对象)

  • data:真正存储信息的部分
  • previous:记录上一个节点
  • next:记录下一个节点

三. 与数组的比较

1. 结构比较

数组是连续的内存单元,链表是基于节点和引用的

2. 空间比较

数组空间是连续的,链表的空间是散布的

当内存紧张时,链表可以充分利用内存碎片,存储更多的节点;
而且节点可以随时增加,删除,对整体的影响很小;
但链表的结构决定了,同样存储一个元素,链表消耗的内存要比数组更大

3. 定位比较

数组通过索引就可以访问任意位置,速度很快

链表的每个节点只知道它前一个,后一个节点是谁,想定位某个节点,只能从头到尾遍历,或从尾到头遍历,速度较慢

4. 查找比较

二者都是线性表,查找速度几乎相同

5. 结论

数组适用于:预先大概知道要存储多少元素,主要用于遍历,内存充足
链表适用于:预先不知道要存储多少元素,增加、删除操作频率较高,内存碎片化程度高

四. 实现一个链表

1. 声明节点类

因为节点只给链表服务,所以此处将节点类作为内部类

按照上图所示,我们需要三个属性,存储真实数据的data,两个节点类型的引用

public class LinkTable<E> {
    
    private static class Node<E> {
        E data;
        Node<E> previous;
        Node<E> next;
        
        Node(E data){
            this.data = data;
        }
    }
}

2. 搭建链表结构

观察上图,链表两端的节点,一个previous没有指向,一个next没有指向,这两个特殊的节点,需要特殊处理

不仅如此,这两个特殊的节点也是我们遍历链表的入口节点,二者可以形象的称为头节点尾结点

因为链表必须总是得有头有尾,并未头尾节点不随中间节点变化而变化,所以二者的地位相对固定,即可以作为属性,保存在外部类中

public class LinkTable<E> {

    private static class Node<E> {
        E data;
        Node<E> previous;
        Node<E> next;
        
        Node(E data){
            this.data = data;
        }
    }
    
    private Node<E> head;
    private Node<E> tail;
    
}

3. 增加一个节点

对于双向链表,它的新增操作,就是在其头部或尾部,挂载一个新节点

为了达到这一目的,头或尾节点的指向需要重新调整

① 头、尾节点均为空

也就是说,当前是个空链表,此时新增的节点既是头节点,也是尾结点
在这里插入图片描述

② 链表只有一个节点

这种情况就是上图那样,此时如果新增节点 newNode 添加到尾部

  1. newNode 先去关联 headnewNode.previous = head
  2. head 再去关联 newNodehead.next = newNode
  3. 最后将 newNode 更新为新的 tail
    在这里插入图片描述

③ 头尾兼备

初始状态
在这里插入图片描述
最终状态
在这里插入图片描述

public void addLast(E e){
	if(e == null){
        throw new NullPointerException("传入的值不能为 null");
    }
    Node<E> newNode = new Node<>(e);

    if(head == null){
        head = newNode;
        tail = newNode;
    }else if(head == tail){
        newNode.previous = head;
        head.next = newNode;
        tail = newNode;
    }else{
        newNode.previous = tail;
        tail.next = newNode;
        tail = newNode;
    }
}

如果结合代码稍加分析②、③的情况,就会发现,其实他俩可以合并为一种操作

因为②的判定条件是 head == tail ,也就意味着,将head替换为tail,逻辑也是成立的,所以优化后的代码长这样

public void addLast(E e){
	if(e == null){
        throw new NullPointerException("传入的值不能为 null");
    }
    Node<E> newNode = new Node<>(e);

    if(head == null){
        head = newNode;
    }else{
        newNode.previous = tail;
        tail.next = newNode;
    }
    tail = newNode;
}

4. 插入一个节点

插入节点的操作相较于增加节点复杂一些,因为他首先需要找到目标位置,然后更改原有节点的指向

插入分为两种情况,分别是插入到目标位置之前,和插入到目标位置之后,两种插入的逻辑相似,这里以插入到目标位置之前为例

目标位置又可分为三种情况

  1. 目标位置是头节点,插入到头节点之前,就是把头节点替换为新节点
  2. 目标位置非头节点,这时需要改动目标位置和它前一个节点的指向
  3. 目标位置不存在

不管哪种情况,都少不了链表的遍历
不管怎么插入,都是在那几根连线上做变动

/**
 * @param newE 新节点
 * @param aimE 目标值(以第一次出现为准)
 * @return 插入成功返回 true
 */
public boolean insertBefore(E newE, E aimE){
    //遍历链表基操
    for(Node<E> current = head; current != null; current = current.next){
        //以第一次出现目标为准
        if(current.data == aimE || current.data.equals(aimE)){
            Node<E> newNode = new Node<>(newE);

            if(current == head){
                newNode.next = head;
                head.previous = newNode;
                head = newNode;
            }else{
                //先把 newNode 挂上
                newNode.next = current;
                newNode.previous = current.previous;

                //current前一个节点的next不再指向current,改为指向newNode
                current.previous.next = newNode;

                //current的previous改为指向newNode
                current.previous = newNode;
            }

            return true;
        }
    }
    return false;

5. 查找节点

查找节点简直so easy,就是链表的遍历,无fuck说

public boolean contains(E e) {
   Node<E> current = head;
    while (current != null) {
        if(current.data == e || current.data.equals(e)){
            return true;
        }
        current = current.next;
    }
    return false;
}

6. 删除节点

删除节点和插入节点类似,分为四种情况:删头、删尾、删中间、不存在

具体操作还是摆弄那几根线

    public boolean remove(E e) {
        for(Node<E> current = head; current != null; current = current.next) {
            if(current.data == e || current.data.equals(e)) {
                //删头
                if(current == head){
                    //原头节点后面有节点
                    if(head.next != null){
                        //时刻保证头节点的 previous 为 null
                        head.next.previous = null;
                        head = head.next;
                    }
                    //否则证明当前链表只有一个待删除的节点,删完后为空链表
                    else {
                        head = tail = null;
                    }
                }
                //删尾
                else if(current == tail){
                    if(tail.previous != null){
                        //时刻保证尾节点的 next 为 null
                        tail.previous.next = null;
                        tail = tail.previous;
                    }else {
                        head = tail = null;
                    }
                }
                //删中间
                else{
                    current.next.previous = current.previous;
                    current.previous.next = current.next;
                }
                return true;
            }
        }
        return false;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值