【数据结构】链表从实现到应用,保姆级攻略

简介: 本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。

🍁1. 链表的介绍

链表是数据结构中一种非常重要的基础结构,它不同于数组,链表中的元素在物理存储上并不连续,而是通过指针(或引用)连接在一起。在Java中,链表的应用非常广泛,尤其是在需要动态添加或删除元素的场景中。

🍁2. 链表的实现

🍁2.1 单向链表

单链表中的每个元素都称为节点(Node),每个节点包含两个部分:一部分存储数据(value),另一部分存储指向列表中下一个节点的引用(next)。最后一个节点的next引用为null,表示链表的结束。

所以采用内部类的形式进行创建:

public class MySingleList {
    static class ListNode {
        public int value;
        public ListNode next;
        public ListNode(int value) {
            this.value = value;
        }
    }
    public ListNode head;
}

还可以创建一个IList接口,对其中的增删查改等方法进行规范,之后MySingleList对接口进行实现

public interface IList {
    void display();
    int size();
    boolean contains(int key);
    void addFirst(int key);
    void addLast(int key);
    void addIndex(int key,int index);
    void remove(int key);
    void removeAllKey(int key);
    void clear();
}

接下来就是方法的实现

🍁2.1.1 size()

返回长度:

只需要将head依次往末尾移动,并记录移动次数进行返回就可以了,当head为null时就表示已经遍历完成

public int size() {
        ListNode cur = head;
        int cnt = 0;
        while (cur != null) {
            cnt++;
            cur = cur.next;
        }
        return cnt;
    }

🍁2.1.2 display()

遍历打印:

遍历的话需要找到头节点,接着依次往后移动,为了不该变头节点的指向,创建一个cur节点辅助遍历,同样的,结束的标志也是最后的指向不为空

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

🍁2.1.3 contains(int key)

判断值是否存在链表中,这里同样需要依次遍历,然后比较value的值

public boolean contains(int key) {
        ListNode cur = head;
        while (cur != null) {
            if (cur.value == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

🍁2.1.4 addFirst(int key),addLast(int key),addIndex(int key, int index)

头插:

头插比较简单,直接创建一个节点,并初始化值,指向原来的head节点,接着改为新的head节点

public void addFirst(int key) {
        ListNode node = new ListNode(key);
        node.next = head;
        head = node;
    }

尾插:

尾插就需要判断head节点是否为null,接着找到尾节点进行插入

public void addLast(int key) {
        ListNode node = new ListNode(key);
        //头结点为null,直接插入
        if (head == null) {
            head = node;
            return;
        }
        //找到尾节点进行插入
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

在指定索引插入:

在指定索引插入就更加麻烦一些,需要对传入的索引进行判断,如果是0.就调用头插的方法,如果等于链表的长度,就调用尾插的方法,如果是中间的索引,就遍历链表,找到该索引进行插入

public void addIndex(int key, int index) {
        ListNode node = new ListNode(key);
        //调用头插
        if (index == 0) {
            addFirst(key);
            return;
        }
        //调用尾插
        if (index == this.size()) {
            addLast(key);
            return;
        }
        //传入索引不合法的情况
        if (index < 0 || index > this.size()) {
            throw new IndexOutOfBoundsException();
        }
        //找到目标索引进行插入
        ListNode cur = head;
        while (index - 1 != 0) {
            cur = cur.next;
            index--;
        }
        node.next = cur.next;
        cur.next = node;
    }

🍁2.1.5 remove(int key),removeAllKey(int key)

删除指定元素:

如果head为空,直接返回,如果head的value就是目标元素,就把head的下一个节点作为头结点,其他情况就根据value的值寻找目标索引,如果没找到就返回,也就是cur节点为null,找到的话把cur的引用指向cur的之后第二个节点

//根据元素找到目标索引
private ListNode findIndexofKet(int key) {
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.next.value == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }
   
    public void remove(int key) {
    //头结点为空
        if (head == null) {
            return;
        }
        //头结点为目标元素
        if (head.value == key) {
            head = head.next;
        }
        //其他节点为目标元素
        ListNode cur = findIndexofKet(key);
        if (cur == null) {
            return;
        }
        cur.next = cur.next.next;
    }

删除所有指定元素:

需要有两个指针,同时往后遍历,删除cur节点所指元素需要将pre节点的next指向cur的next,循环判断,最后还要判断head节点是否为指定元素

public void removeAllKey(int key) {
        //头结点为null,直接返回
        if (this.head == null) {
            return;
        }
        ListNode pre = head;
        ListNode cur = head.next;
        //循环删除
        while (cur != null) {
            if (cur.value == key) {
                pre.next = cur.next;
                cur = cur.next;
            } else {
                pre = cur;
                cur = cur.next;
            }
        }
        //判断头结点
        if (head.value == key) {
            head = head.next;
        }
    }

🍁2.1.6 clear()

清空链表:

清空链表只需要遍历链表所有节点,将每一个节点置为null即可,因为是从头结点开始,如果直接将head置为null,后续再找head.next就会报错,所以还需要用一个中间变量cur辅助遍历

public void clear() {
        ListNode cur = head;
        while (cur != null) {
            //创建变量,解决空指针异常
            ListNode curn = cur.next;
            cur = null;
            cur = curn.next;
        }
        head = null;
    }

🍁2.2 双向链表

双向链表有两个指针域,一个指向前一个节点,一个指向后一个节点

public class MyLinkedList implements IList {
    static class TListNode {
        public int value;
        TListNode pre;
        TListNode next;
        public TListNode(int value) {
            this.value = value;
        }
    }
    public TListNode head;
    public TListNode last;
}

双向链表的size() ,display(),contains(int key)和单向链表是一样的,下面来实现其他的方法

🍁2.2.1 addFirst(int key)

头插:

public void addFirst(int key) {
        TListNode node = new TListNode(key);
        if (head == null) {
            head = last = node;
        } else {
            node.next = head;
            head.pre = node;
            head = node;
        }
    }

🍁2.2.2 addLast(int key)

尾插:

public void addLast(int key) {
        TListNode node = new TListNode(key);
        if (head == null) {
            head = last = node;
        } else {
            last.next = node;
            node.pre = last;
            last = last.next;
        }
    }

🍁2.2.3 addIndex(int key, int index)

指定位置插入:

public void addIndex(int key, int index) {
        TListNode node = new TListNode(key);
        if(index < 0 || index > size()) return;
        if (index == 0) {
            addFirst(key);
            return;
        }
        if (index == size()) {
            addLast(key);
        }
        if (index > 0 && index < size()) {
            TListNode cur = head;
            //可以直接到indext的位置,因为双向链表可以找到前一个节点
            while (index-- != 0) {
                cur = cur.next;
            }
            node.next = cur;
            cur.pre.next = node;
            node.pre = cur.pre;
            cur.pre = node;
        }
    }

需要修改四个位置,把要插入的节点node的next 指向cur,再把cur.pre的next指向node,此时节点的next都有了指向,接着node的pre指向cur.pre节点,cur的pre再指向node,此时就完成了插入

🍁2.2.4 remove(int key)和removeAllKey(int key)

首先找到要删除的值的索引

private TListNode findIndexofKet(int key) {
        TListNode cur = head;
        while (cur != null) {
            if (cur.value == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

删除的时候还要考虑是否为头结点和尾节点

public void remove(int key) {
        TListNode cur = findIndexofKet(key);
        if(cur == null){
            return;
        }
        //头节点的情况
        if(cur == head){
            head = cur.next;
            //只有一个节点时,指向next后head为null所以当head!=空时才能把pre置为null
            if (head != null) {
                head.pre = null;
            }
        }else{
            cur.pre.next = cur.next;
            //尾节点的情况
            if(cur.next == null){
                last = last.pre;
            }else{
                cur.next.pre = cur.pre;
            }
        }
    }

相比于单向链表,双向链表的删除所有指定元素就非常简单了,只需要在原来删除一个的基础上稍加修改就可以了

public void removeAllKey(int key) {
        TListNode cur = head;
        while (cur != null) {
            if (cur.value == key) {
                if (cur == head) {
                    head = cur.next;
                    if (head != null) {
                        head.pre = null;
                    }
                } else {
                    cur.pre.next = cur.next;
                    if (cur.next == null) {
                        last = last.pre;
                    } else {
                        cur.next.pre = cur.pre;
                    }
                }
            }
            cur = cur.next;
        }
    }

🍁2.2.5 clear()

清空链表还是依次遍历每一个节点,把每一个节点都置为null,最后把head和last也置为null

public void clear() {
        TListNode cur = head;
        while(cur.next!=null){
            TListNode curn = cur;
            curn.pre = null;
            curn.next = null;
            cur = curn;
        }
        head = last = null;
    }

🍁3. Java中LinkedList的使用

🍁3.1 LinkedList的创建和使用

在上一篇数据结构ArrayList的讲解中已经简单提到过👉点我看回顾,集合的一些基本框架,LinkedList也实现了List接口,所以也可以通过接口创建对象,也可以使用List接口中的方法

public class Demo {
    public static void main(String[] args) {
        LinkedList<Integer> list1 = new LinkedList<>();
        List<Integer> list2 = new LinkedList<>();
        list1.add(1);
        list1.add(2);
        System.out.println(list1);
        list2.add(1);
        list2.add(3);
        System.out.println(list2);
    }
}


可以直接对LinkedList的对象进行打印,也就是说LinkedList重写了toSting方法

这些都是LinkedList中独有的方法,这里就不列举使用了,

🍁3.2 LinkedList的遍历

LinkedList的遍历和ArrayList的遍历方式一样,在上一篇介绍了五种遍历方式,这次再简单回顾一下

public class Demo {
    public static void main(String[] args) {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        list1.add(4);
        //迭代器 ListIterator
        ListIterator<Integer> lit = list1.listIterator();
        while(lit.hasNext()){
            System.out.print(lit.next() + " ");
        }
        System.out.println();
        //Iterator
        Iterator<Integer> it = list1.iterator();
        while(it.hasNext()){
            System.out.print(it.next() + " ");
        }
        System.out.println();
        //增强for
        for(Integer l : list1){
            System.out.print(l + " ");
        }
        System.out.println();
        //普通for
        for(int i = 0;i < list1.size();i++){
            System.out.print(list1.get(i) + " ");
        }
        System.out.println();
        //lambda表达式
        list1.forEach(e -> {
            System.out.print(e + " ");
        });
        System.out.println();
    }
}

🍁4. ArrayList和LinkedList的区别


ArrayList底层是一个动态数组
LinkedList底层是双向链表
ArrayList:访问元素的时间复杂度为
O(1)(直接通过索引)。
LinkedList:访问元素的时间复杂度为
O(n)(需要从头或尾开始遍历到目标位置)。
ArrayList:
在末尾添加元素的时间复杂度为
O(1)
在中间插入或删除元素时,时间复杂度为
O(n),因为需要移动其他元素以保持连续的内存块。
LinkedList:
在任意位置添加或删除元素的时间复杂度为
O(1),只需改变前后节点的指针(需要先找到目标位置,时间复杂度为 O(n))。

使用场景:
ArrayList:
适合频繁读取、随机访问元素的场景。
如需要大量顺序读写、索引访问操作。
LinkedList:
适合频繁插入、删除元素的场景,尤其是在列表中间进行操作。
如需要频繁的增删操作,但不常用到随机访问。



相关文章
|
7月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
414 86
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
295 4
|
9月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
530 1
|
9月前
|
存储 监控 算法
公司员工泄密防护体系中跳表数据结构及其 Go 语言算法的应用研究
在数字化办公中,企业面临员工泄密风险。本文探讨使用跳表(Skip List)数据结构优化泄密防护系统,提升敏感数据监测效率。跳表以其高效的动态数据处理能力,为企业信息安全管理提供了可靠技术支持。
184 0
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
501 30
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
726 25
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
618 5
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
493 5

热门文章

最新文章