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

相关文章
|
7月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
238 1
|
5月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
288 3
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
216 4
|
7月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
744 1
|
9月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
339 30
|
9月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
440 25
|
10月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
488 5
|
11月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
11月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
176 5
|
12月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
351 5