【Java 数据结构】双向链表(上)

简介: 上期我们实现了一下单链表,在Java(1.8)中,链表为 LinkedList,而底层是一个双向链表,跟 ArrayList 一样,LinkedList 也实现了 List 接口,这里我们画一个图,让大家简单见识下双向链表

1、什么是双向链表

上期我们实现了一下单链表,在Java(1.8)中,链表为 LinkedList,而底层是一个双向链表,跟 ArrayList 一样,LinkedList 也实现了 List 接口,这里我们画一个图,让大家简单见识下双向链表:

如图我们可以看出,双向链表最少有三个域,分别是数据域和两个指针域,分别指向节点的前驱和后继,第一个节点没有前驱,最后一个节点没有后继。  

2、实现一个双向链表

2.1 实现前的约定

链表的每个元素是一个节点,我们仍然采用内部类的方式,既然是双向的,那么我们还需要在外部定义一个head和last,分别为头节点和尾节点的引用。

public class MyLinkedList {
    private class ListNode {
        private int val; //数据域
        private ListNode prev; //前指针域
        private ListNode next; //后指针域
        private ListNode(int val) {
            this.val = val;
        }
    }
    private ListNode head; //头节点引用
    private ListNode last; //尾节点引用
    private int size; //链表有效数据个数
}

同时我们还要实现以下几个方法:

public void addFirst(int data); //头插法
public void addLast(int data); //尾插法
public boolean addIndex(int index,int data) //任意位置插入,第一个数据节点为0号下标
public boolean contains(int key); //查找是否包含关键字key是否在单链表当中
public void removeAllKey(int key); //删除所有值为key的节点
public void clear(); //清空链表
public int size(); //得到链表的长度

2.2 addFirst 方法

//头插法
public void addFirst(int data) {
    ListNode newNode = new ListNode(data);
    // 链表为空的情况
    if (this.head == null) {
        this.head = newNode;
        this.last = newNode;
        this.size++;
        return;
    }
    newNode.next = this.head; //新节点的后一个为头节点
    this.head.prev = newNode; //头节点的前一个为新节点
    this.head = newNode; //新节点成为新的头节点
    this.size++;
}

与单链表不同,由于双向链表有头尾节点引用,所以这里我们要在第一次插入元素的时候进行特殊处理,当第一次插入元素我们需要将头尾节点的引用都指向这个节点,后续插入只需要改变头节点的引用即可,最后插入完成别忘记链表有效节点个数自增1哦!

2.3 addLast 方法

//尾插法
public void addLast(int data) {
    ListNode newNode = new ListNode(data);
    // 链表为空的情况
    if (this.head == null) {
        this.head = newNode;
        this.last = newNode;
        this.size++;
        return;
    }
    newNode.prev = this.last; //新节点的前一个为尾节点
    this.last.next = newNode; //尾节点的后一个为新节点
    this.last = newNode; //新节点成为新的尾节点
    this.size++;
}

与头插法相差不多,无非就是需要修改尾节点的引用,以及注意新节点的指针域指向问题,这里小伙伴们可以结合我的代码注释,尝试去理解,配合画图,相信你就能掌握好头插法和尾插法了!

2.4 addIndex 方法

//任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index,int data) {
    // 判断index下标的合法性
    if (index < 0 || index > this.size) {
        return false;
    }
    // index为0表示头插
    if (index == 0) {
        addFirst(data);
        return true;
    }
    // index为size长度表示尾插
    if (index == this.size) {
        addLast(data);
        return true;
    }
    //其他情况为中间插入
    ListNode newNode = new ListNode(data);
    ListNode cur = this.head;
    while (index != 0) {
        cur = cur.next;
        index--;
    }
    newNode.prev = cur.prev; //新节点的前一个为cur的前一个
    newNode.next = cur; //新节点的后一个为cur
    cur.prev.next = newNode; //cur的前节点的后一个为新节点
    cur.prev = newNode; //cur的前节点为新节点
    return true;
}

对于在指定位置插入节点来说,如果给定的 index 位置大于我们的有效节点个数呢?也就是说假设我链表只有 5 个节点,你要在 8 位置插入元素显然是不合法的,其次,如果要在负数的位置插入那更不合法,所以我们要对 index 做判断,往后走我们还可以考虑两个点,如果index == 0 或者 index == size(),那么也就是对应着我们的头插和尾插,那么我们直接调用前面写的头插尾插方法即可,代码接着往后走,剩下的就是中间插入节点的情况了,逻辑很简单,首先要找到 index 对应的节点,接着改变相关节点指针域的指向即可,这里可以结合着代码以及注释,下来画图进行分析。

2.5 contains 方法

//查找是否包含关键字key是否在双链表当中
public boolean contains(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if (cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

这个方法跟我们之前写单链表的时候相差无几,相信有了前面单链表的基础,这个简直是信手拈来了吧,无非就是遍历这个链表,只要 cur 没有遍历到 null,也就是没有到最后一个节点的 next 位置,我们就遍历找有没有 key,找到了返回 true 找不到返回 false 咯!

2.6 removeAllKey 方法

//删除所有值为key的节点
public void removeAllKey(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if (cur.val == key) {
            //如果被删除cur是头节点
            if (cur == this.head) {
                //只有一个节点的情况
                if (this.head.next == null) {
                    this.head = null;
                    this.last = null;
                } else {
                    cur.next.prev = null; //cur的后节点的前节点指针域改为null
                    this.head = cur.next; //头节点变成cur的下一个节点
                }
            } else {
                //如果被删除cur是尾节点
                if (cur == this.last) {
                    this.last = cur.prev; //尾节点变成cur的前节点
                } else {
                    cur.next.prev = cur.prev; //cur的后节点的前节点指针域改为cur的前节点
                }
                cur.prev.next = cur.next; //cur的前节点的后节点指针域改为cur的后节点
            }
            this.size--;
        }
        cur = cur.next;
    }
}

要删除所有值为 key 的节点,这道题思想不难,还是遍历链表嘛,如果值一样,修改相关节点引用即可,但是问题来了,删除头节点和尾节点,和删除中间节点的修改指向逻辑可不一样,所以我们要分别处理,分开处理这三种情况之后,如果只有一个节点的情况呢?也得处理,于是就有了我们上面的代码,当然可以有很多种写法,你只要把各种情况捋清楚了就好,至于修改指针域指向的逻辑画画图就能理解了!

相关文章
|
22天前
|
Java
【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
ArrayList是基于可动态扩展的数组实现的,支持快速随机访问,但在插入和删除操作时可能需要数组复制而性能较差。
|
26天前
|
存储 设计模式 算法
JAVA中的常见数据结构
JAVA中的常见数据结构
|
6天前
|
存储 Java 索引
【数据结构】链表从实现到应用,保姆级攻略
本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。
28 10
【数据结构】链表从实现到应用,保姆级攻略
|
20天前
|
存储 Java
|
22天前
|
存储 算法 Java
"解锁Java对象数据结构的奥秘:从基础到实战,与热点技术共舞,让你的编程之路更激情四溢!"
【8月更文挑战第21天】Java以对象为核心,它是程序的基本单元与数据处理的基础。对象源自类,拥有属性(字段)和方法。对象在内存中分为对象头(含哈希码、GC信息等)和实例数据区(存储属性值)。例如,`Student`类定义了姓名、年龄等属性及相应的方法。通过`new`关键字实例化对象并调用其方法进行数据操作,是Java编程的关键技能。
26 0
|
22天前
|
存储 Java
【Java集合类面试十】、HashMap中的循环链表是如何产生的?
在多线程环境下,HashMap在扩容时如果发生条件竞争,元素的插入顺序可能形成循环链表,导致死循环。
|
24天前
|
存储 算法
【初阶数据结构篇】顺序表和链表算法题
此题可以先找到中间节点,然后把后半部分逆置,最近前后两部分一一比对,如果节点的值全部相同,则即为回文。
|
24天前
|
存储 测试技术
【初阶数据结构篇】双向链表的实现(赋源码)
因为头结点的存在,plist指针始终指向头结点,不会改变。
|
24天前
|
存储 测试技术
【初阶数据结构篇】单链表的实现(附源码)
在尾插/尾删中,都需要依据链表是否为空/链表是否多于一个节点来分情况讨论,目的是避免对空指针进行解引用造成的错误。
|
26天前
|
算法
【数据结构与算法】共享双向链表
【数据结构与算法】共享双向链表
10 0