【Java】链表LinkedList

news2024/12/23 5:55:27

文章目录

  • 一、链表
    • 1.1 链表的概念
    • 1.2 链表的结构
  • 二、LinkedList的简介
  • 三、LinkedList的使用
    • 3.1 构造方法
    • 3.2 常见操作
    • 3.3 遍历方法
  • 四、LinkedList的模拟实现
  • 五、LinkedList 和 ArrayList 的区别


一、链表

1.1 链表的概念

链表(Linked List)是一种常见的数据结构,用于存储和组织数据。它由一系列节点(Node)组成,每个节点包含两个主要部分:数据域(Data)和指针域(Pointer)。

数据域存储节点所需的数据或信息,可以是任意类型的数据,如整数、字符、对象等。指针域则指向链表中的下一个节点,将节点连接起来形成链表结构。

链表中的节点并不一定按照物理上的连续位置存储,而是通过指针域相互连接。这使得链表能够灵活地插入、删除和修改节点,而无需像数组那样进行元素的移动。

链表的优点是可以高效地插入和删除节点,而无需移动其他节点。然而,由于链表中的节点不是连续存储的,访问特定位置的节点需要从头部开始遍历,因此随机访问的效率较低。

1.2 链表的结构

在实际的应用场景中,链表的结构非常多样,以下是常见的链表结构:

  1. 单项链表
    单向链表(Singly Linked List)是一种基本的链表结构,每个节点包含两个主要部分:数据域和指针域。以下是单向链表的结构示意图:

  • 在单向链表中,每个节点包含一个数据域,用于存储节点所需的数据,以及一个指针域,指向链表中的下一个节点。最后一个节点的指针域通常指向空值(NULL),表示链表的结束。

  • 通过这种方式,节点之间通过指针链接在一起,形成一个链表结构。链表的头部节点通过外部引用进行访问,然后可以依次遍历访问链表中的每个节点。

  • 由于单向链表只有一个方向的指针,所以只能从头部节点开始顺序遍历访问,无法直接访问后续节点,也无法在常量时间内进行反向遍历。

  • 单向链表在插入和删除节点时具有较好的性能,因为只需要修改节点的指针,而不需要移动其他节点。然而,在访问特定位置的节点时,需要从头部节点开始遍历整个链表,导致访问效率较低。

  1. 双向链表
    双向链表(Doubly Linked List)的每个节点包含两个指针,分别指向前一个节点和后一个节点。这使得节点可以在两个方向上进行遍历和访问,提供了更多的灵活性和功能。

以下是双向链表的结构示意图:

  • 在双向链表中,每个节点包含一个数据域和两个指针域。除了指向下一个节点的指针域(Next Pointer),还有一个指向前一个节点的指针域(Prev Pointer)。头部节点的 Prev Pointer 通常指向空值(NULL),尾部节点的 Next Pointer 也指向空值。

  • 通过 Prev Pointer 和 Next Pointer,双向链表允许在链表的两个方向上遍历和访问节点。这使得在某些情况下,例如在链表末尾插入或删除节点,可以更高效地操作链表。

  • 与单向链表相比,双向链表的操作稍微复杂一些,因为每个节点需要维护两个指针。但双向链表提供了更多的功能和灵活性,如可以从头部或尾部快速插入或删除节点,以及可以在两个方向上进行遍历和搜索。

  1. 带头节点的链表

带头结点的链表在头部添加了一个额外的节点作为头结点(Header Node)或哑节点(Dummy Node),这个头结点不存储任何实际的数据。

以下是带头结点的链表的结构示意图:

  • 头结点位于链表的起始位置,其主要作用是方便链表的操作和管理。它不存储实际的数据,只包含一个指针域,指向链表中的第一个实际节点。

  • 带头结点的链表的优点在于简化了链表的操作。它确保链表中始终存在一个非空节点,简化了插入、删除和遍历等操作的实现。此外,头结点可以用于存储链表的一些统计信息或提供其他便利的功能。

  • 在带头结点的链表中,链表的第一个实际节点为头结点的下一个节点。通过头结点,可以轻松地访问链表中的实际数据节点,并进行相应的操作。

  • 当遍历带头结点的链表时,通常从头结点的下一个节点开始,直到遇到指针域为 NULL 的节点,表示链表结束。

  1. 循环链表
    循环链表(Circular Linked List)是一种特殊的链表结构,其中最后一个节点的指针指向链表的头部节点,形成一个闭环。

以下是循环链表的结构示意图:

  • 在循环链表中,每个节点仍然包含数据域和指针域,但最后一个节点的指针域不再指向空值(NULL),而是指向链表的头部节点。

  • 这样的设计使得链表形成一个循环结构,可以通过任何节点开始遍历整个链表。从任何节点出发,通过指针的循环跳转,可以访问链表中的所有节点。

  • 循环链表的优点在于在遍历和操作链表时,不需要考虑链表的结束位置。无论从哪个节点开始遍历,总能回到起始位置,避免了在常规链表中遇到的空指针异常。

二、LinkedList的简介

在Java中,LinkedList(链表)是Java集合框架提供的一个实现了List接口的类。它是基于双向链表结构实现的,可以用于存储和操作元素的有序集合。

继承体系结构如下:

三、LinkedList的使用

3.1 构造方法

LinkedList类提供了以下几种构造方法:

  1. LinkedList():创建一个空的LinkedList对象。
LinkedList<String> list = new LinkedList<>();
  1. LinkedList(Collection<? extends E> c):创建一个包含指定集合中的元素的LinkedList对象。集合中的元素将按照迭代器返回的顺序添加到LinkedList中。
List<String> collection = new ArrayList<>();
collection.add("Element 1");
collection.add("Element 2");
LinkedList<String> list = new LinkedList<>(collection);
  1. LinkedList(LinkedList<? extends E> c):创建一个包含指定LinkedList中的元素的LinkedList对象。指定LinkedList中的元素将按照迭代器返回的顺序添加到新的LinkedList中。
LinkedList<String> originalList = new LinkedList<>();
originalList.add("Element 1");
originalList.add("Element 2");
LinkedList<String> newList = new LinkedList<>(originalList);

3.2 常见操作

LinkedList类提供了许多用于操作链表的方法。以下是一些常见的操作方法:

  1. 添加元素:
  • add(E element):在链表末尾添加一个元素。
  • addFirst(E element):在链表开头添加一个元素。
  • addLast(E element):在链表末尾添加一个元素。
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.addFirst("Element 0");
list.addLast("Element 2");
  1. 获取元素:
  • get(int index):获取指定位置的元素。
  • getFirst():获取链表的第一个元素。
  • getLast():获取链表的最后一个元素。
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");
String element = list.get(0);
String firstElement = list.getFirst();
String lastElement = list.getLast();
  1. 删除元素:
  • remove(int index):删除指定位置的元素。
  • removeFirst():删除链表的第一个元素。
  • removeLast():删除链表的最后一个元素。
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");
list.remove(0);
list.removeFirst();
list.removeLast();
  1. 判断元素是否存在:
  • contains(Object element):检查链表是否包含指定元素。
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");
boolean containsElement = list.contains("Element 1");
  1. 获取链表大小和清空链表:
  • size():获取链表中元素的个数。
  • isEmpty():检查链表是否为空。
  • clear():清空链表中的所有元素。
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");
int size = list.size();
boolean isEmpty = list.isEmpty();
list.clear();

3.3 遍历方法

在LinkedList中,可以使用不同的方式进行遍历操作。下面是几种常见的遍历方法:

  1. 使用for循环和get方法遍历:
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");

for (int i = 0; i < list.size(); i++) {
    String element = list.get(i);
    // 处理当前元素
    System.out.println(element);
}
  1. 使用增强型for循环遍历:
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");

for (String element : list) {
    // 处理当前元素
    System.out.println(element);
}
  1. 使用迭代器(Iterator)遍历:
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    // 处理当前元素
    System.out.println(element);
}
  1. 使用Java 8+的Stream API进行遍历:
LinkedList<String> list = new LinkedList<>();
list.add("Element 1");
list.add("Element 2");

list.stream().forEach(element -> {
    // 处理当前元素
    System.out.println(element);
});

四、LinkedList的模拟实现

import java.util.Stack;

public class MyLinkList {
    static class ListNode {
        public int val;
        public ListNode prev;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }

    private ListNode head; // 标记双向链表的头部
    private ListNode tail; // 标记双向链表的尾部

    // 头插
    public void addFirst(int data) {
        if (this.head == null) {
            this.head = new ListNode(data);
            this.tail = this.head;
        } else {
            ListNode node = new ListNode(data);
            node.next = this.head;
            this.head.prev = node;
            this.head = node;
        }
    }

    //尾插
    public void addLast(int data) {
        if (this.tail == null) {
            this.tail = new ListNode(data);
            this.head = this.tail;
        } else {
            ListNode node = new ListNode(data);
            this.tail.next = node;
            node.prev = this.tail;
            this.tail = node;
        }

    }

    //任意位置插入,第一个数据节点为0号下标
    public boolean addIndex(int index, int data) {

        if (index < 0 || index > size()) {
            System.out.println("index位置不合法!");
            return false;
        }

        if (index == 0) {
            addFirst(data);
        } else if (index == size()) {
            addLast(data);
        } else {
            ListNode cur = head;
            while (index != 0) {
                cur = cur.next;
                index--;
            }

            ListNode node = new ListNode(data);

            ListNode prev = cur.prev;

            cur.prev = node;
            node.next = cur;

            prev.next = node;
            node.prev = prev;

        }

        return true;
    }

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

    //删除第一次出现关键字为key的节点
    public void remove(int key) {

        // 链表为空
        if (head == null) {
            return;
        } else if (head.val == key) {
            // 要删除的元素在头
            head = head.next;
            if (head == null)
                return;
            head.prev = null;
        } else if (tail.val == key) {
            // 要删除的元素在尾
            tail = tail.prev;
            tail.next = null;
        } else {
            // 正常情况
            ListNode cur = head;
            // 注意考虑只有一个元素的情况
            while (cur.next != null && cur != tail) {
                if (cur.val == key) {
                    cur.prev.next = cur.next;
                    cur.next.prev = cur.prev;
                    return;
                }
                cur = cur.next;
            }
        }
    }

    //删除所有值为key的节点
    public void removeAllKey(int key) {
        // 空链表
        if (null == head) {
            return;
        }

        // 从头开始,出现连续的key
        while (head.val == key) {
            head = head.next;
            if (head == null) {
                return;
            }
            head.prev = null;
        }

        // 从尾开始出现连续的key
        while (tail.val == key) {
            tail = tail.prev;
            if (tail == null)
                return;
            tail.next = null;
        }

        // 其他情况,注意只有一个元素的情况
        ListNode cur = head;
        while (cur.next != null && cur != tail) {
            if (cur.val == key) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
            }
            cur = cur.next;
        }
    }

    //得到单链表的长度
    public int size() {
        int cnt = 0;
        ListNode cur = head;
        while (cur != null) {
            cnt++;
            cur = cur.next;
        }
        return cnt;
    }


    public void display() {
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    private void _display2(ListNode node) {
        if (node == null) {
            return;
        }

        if (node.next == null) {
            System.out.print(node.val + " ");
            return;
        }

        _display2(node.next);
        System.out.print(node.val + " ");
    }

    // 递归逆序打印链表
    public void display2() {
        ListNode node = this.head;
        _display2(node);
        System.out.println();
    }


    public void _display3(ListNode node) {
        if (node == null)
            return;
        Stack<ListNode> stack = new Stack<>();
        while (node != null) {
            stack.push(node);
            node = node.next;
        }

        while (!stack.isEmpty()) {
            System.out.print(stack.pop().val + " ");
        }
        System.out.println();
    }

    // 利用栈逆序打印链表
    public void display3() {
        ListNode node = this.head;
        _display3(node);
    }

    public void clear() {
        ListNode cur = head;
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.val = 0;
            cur.prev = null;
            cur.next = null;
            cur = curNext;
        }

        this.head = this.tail = null;
    }
}

五、LinkedList 和 ArrayList 的区别

LinkedList和ArrayList是Java集合框架中的两种不同的List实现,它们有以下几个区别:

  1. 底层数据结构:LinkedList底层基于链表实现,而ArrayList底层基于动态数组实现。

  2. 插入和删除操作:由于LinkedList是基于链表的数据结构,插入和删除元素的操作比较高效,时间复杂度为O(1),因为只需要调整节点的指针。而ArrayList的底层是动态数组,插入和删除操作需要移动其他元素,时间复杂度为O(n),其中n是元素的数量。

  3. 随机访问:ArrayList支持高效的随机访问,可以通过索引快速获取元素,时间复杂度为O(1)。而LinkedList需要从头开始遍历链表才能找到指定位置的元素,时间复杂度为O(n),其中n是索引位置。

  4. 内存消耗:由于LinkedList需要额外的指针来维护节点之间的连接关系,因此在存储相同数量的元素时,LinkedList通常会占用更多的内存空间。而ArrayList只需要连续的内存空间来存储元素。

  5. 迭代器性能:对于迭代器遍历操作,LinkedList的性能较好,因为只需要遍历链表中的节点即可。而ArrayList在使用迭代器遍历时,由于底层是数组,可能会导致性能稍差。

总而言之,当需要频繁进行插入和删除操作,而对于随机访问的需求较少时,LinkedList可能是更好的选择。而当需要频繁进行随机访问,而插入和删除操作较少时,ArrayList更为适合。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/726286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

hive数据的导入导出

一、hive 的数据导入 Linux本地文件以及数据格式&#xff1a; 在hive中创建表&#xff1a; create table t_user( id int ,name string ) row format delimited fields terminated by "," lines terminated by \n stored as textfile;stored as常见的几种格式 1.…

举例说明ChatGPT模型是怎么进行无监督学习的

ChatGPT&#xff0c;也称为生成式预训练Transformer&#xff08;GPT&#xff09;&#xff0c;是一种基于Transformer架构的自然语言处理模型。虽然在实际应用中&#xff0c;它主要用于有监督学习任务&#xff0c;但在训练初期&#xff0c;它会经历无监督学习阶段。以下是一个简…

LNMP实战部署(电影网站搭建)

第三阶段基础 时 间&#xff1a;2023年7月5日 参加人&#xff1a;全班人员 内 容&#xff1a; 目录 LNMP架构及应用部署&#xff1a;&#xff08;单台服务器部署&#xff09; 一、安装nginx&#xff1a;&#xff08;源码安装&#xff09; 二、安装mysql数据库&#xf…

LVS+Keepalived 群集及搭建

文章目录 一.keepalived 概述1.keepalived服务重要功能1.1 管理LvS负载均衡器软件1.2 支持故障自动切换 (failover)1.3 实现LVS集中节点的健康检查 (health checking)1.4 实现LVS负载调度器 节点服务器的高可用性 (HA) 2. keepalived 高可用故障切换转移原理及VRRP通信原理3.ke…

【C++】提前声明(Forward declaration)好处

C中提前声明&#xff08;Forward declaration&#xff09;好处小结&#xff1a; 减少编译依赖&#xff1a;通过提前声明&#xff0c;可以减少对头文件的依赖。这样可以降低编译时间&#xff0c;因为编译器不需要查看完整的定义&#xff0c;而仅需要知道类型的名称和成员函数的…

利用langchain-ChatGLM实现基于本地知识库的问答应用

目录 1 原理 2 开发部署 2.1 安装环境 2.2 加载本地模型 刷B站的时候&#xff0c;无意中看到吴恩达的一个langchain的教程&#xff0c;然后去github上搜了下&#xff0c;发现别人利用langchain和chatGLM做的基于本地知识库的问答应用挺好的&#xff0c;学习下。 1 原理 基…

yolov8-mnn C++部署

权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 本文链接&#xff1a;https://blog.csdn.net/zaibeijixing/article/details/131581809 ———————————————— 目录 准备工作 1、MNN编…

【Android】CheckBox的自定义样式和使用

需求 在登录页面。我们需要有一个复选框&#xff0c;让用户进行勾选&#xff0c;同意我们的流氓政策和协议&#xff0c;不然就不让用。 实现效果 未选择 已选择 我们知道&#xff0c;这个CheckBox的默认样式是一个正方形&#xff0c;现在改成一个圆形的话&#xff0c;首先需…

我的创作纪念日——我与CSDN的1024天

今天是在CSDN创作的第1024天&#xff0c;是一个非常有意义的天数&#xff0c;故写下此篇文章以表纪念之意。 机缘 我为什么会在CSDN写文章呢&#xff0c;那滴从很久很久以前说起。记得是大一的时候&#xff0c;我们的组原课程的授课老师在一次课堂上问起我们&#xff0c;问我…

AIGC - Easy Diffusion (Stable Diffusion) 图像生成工具的环境配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131524075 版本v2.5.41 Stable Diffusion 图像生成工具是一种基于深度学习的技术&#xff0c;可以从随机噪声中生成高质量的图像&#x…

HFSS仿真T型波导学习笔记

HFSS仿真T型波导 文章目录 HFSS仿真T型波导1、求解器设置2、建模3、激励方式设置4、边界条件设置5、扫频设置6、设计检查&#xff0c;仿真分析7、数据后处理 设计要求&#xff1a; 仿真工作频率为10GHz的T型波导 1、求解器设置 模式驱动求解 2、建模 整个T型波导结构分为2…

【NAS群晖drive异地访问】远程连接drive挂载电脑硬盘

文章目录 前言1.群晖Synology Drive套件的安装1.1 安装Synology Drive套件1.2 设置Synology Drive套件1.3 局域网内电脑测试和使用 2.使用cpolar远程访问内网Synology Drive2.1 Cpolar云端设置2.2 Cpolar本地设置2.3 测试和使用 3. 结语 转载自cpolar极点云文章&#xff1a;【群…

java飞起基础----黑马程序员

阿里代码命名规约 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格&#xff0c;必须遵从驼峰形式。正例&#xff1a; localValue / getHttpMessage() / inputUserId【强制】类名使用 UpperCamelCase 风格&#xff0c;必须遵从驼峰形式&#xff0c;…

在外远程访问NAS威联通(QNAP)-免费内网穿透

文章目录 前言1. 威联通安装cpolar内网穿透2. 内网穿透2.1 创建隧道2.2 测试公网远程访问 3. 配置固定二级子域名3.1 保留二级子域名3.2 配置二级子域名 4. 使用固定二级子域名远程访问 转载自cpolar极点云文章&#xff1a;无需公网IP&#xff0c;在外远程访问NAS威联通QNAP【内…

企业产品指导手册怎么弄成在线版本的啊?

企业产品指导手册作为企业宣传推广和产品销售的重要工具&#xff0c;传统上通常是以纸质形式发放给客户。但随着互联网技术的快速发展&#xff0c;越来越多的企业开始将产品指导手册转化为在线版本&#xff0c;以便更好地满足客户的需求。本文将介绍如何将企业产品指导手册弄成…

优维DevOps全新产品——双态部署重磅上线

优维又一全新产品上线啦&#xff01; 最近&#xff0c;优维召开了一场发布会&#xff0c;宣布「DevOps全新产品——双态部署」重磅上线。发布会现场&#xff0c;重新审视了DevOps市场现状&#xff0c;深度剖析了行业面临的挑战与机遇&#xff0c;同时也深入浅出的揭晓了“双态…

图像增广:强化深度学习的视觉表现力

目录 摘要&#xff1a; 1. 图像增广简介 2. 图像增广的原理 3. 常见的图像增广技术 4. 如何在实际项目中应用图像增广 5.实际应用 摘要&#xff1a; 当今&#xff0c;深度学习已经在计算机视觉领域取得了令人瞩目的成就。图像增广作为一种数据处理技术&#xff0c;让我们…

【Cache】Redis的高可用与持久化

文章目录 一、Redis 高可用1. 概念2. 高可用技术以及作用2.1 持久化2.2 主从复制2.3 哨兵2.4 集群 二、Redis 持久化1. 持久化的功能2. Redis 持久化方式 三、RDB 持久化1. 概述2. 触发条件2.1 手动触发2.2 自动触发2.3 其他自动发机制 3. 执行流程4. 启动时加载 四、AOF 持久化…

【UEFI实战】UEFI图形显示(字符输出)

HII Font 接下来介绍EFI_HII_FONT_PROTOCOL&#xff0c;它在UEFI代码中完成了字符到像素的转换&#xff0c;本节主要介绍这个转换关系&#xff0c;它的实现代码在edk2\MdeModulePkg\Universal\HiiDatabaseDxe\HiiDatabaseDxe.inf中&#xff0c;除了EFI_HII_FONT_PROTOCOL&…

【Axure教程】多选树穿梭选择器

多选树在有分层的领域是经常用到的&#xff0c;例如不同城市下的门店、不同部门的员工等等&#xff0c;用多选树就可以让我们在不同层级快速挑选到对应的对象。 今天作者就教大家在Axure中如何制作多选树穿梭选择器的原型模板&#xff0c;我们会以不同部门之间挑选员工位案例。…