数据结构与算法——链表

简介: 前面说到了数组,利用连续的内存空间来存储相同类型的数据,其最大的特点是支持下标随机访问,但是删除和插入的效率很低。今天来看另一种很基础的数据结构——链表。链表不需要使用连续的内存空间,它使用指针将不连续的内存块连接起来,形成一种链式结构。

1. 概述


前面说到了数组,利用连续的内存空间来存储相同类型的数据,其最大的特点是支持下标随机访问,但是删除和插入的效率很低。今天来看另一种很基础的数据结构——链表。链表不需要使用连续的内存空间,它使用指针将不连续的内存块连接起来,形成一种链式结构。


2. 单链表


首先来看看单链表,存储数据的内存块叫做节点,每个节点保存了一个 next 指针,指向下一个节点的内存地址,结合下图你就很容易看明白了:

6c19cf1d5070dd22dff514965a1b6b2.png

其中有两个节点指针比较的特殊,首先是链表头节点的指针,它指向了链表的第一个节点的地址,利用它我们可以遍历得到整个链表。其次是尾结点的指针,它指向了 null ,表示链表结束。

不难看出,单链表的最大特点便是使用指针来连接不连续的节点,这样我们不用担心扩容的问题了,并且,链表的插入和删除操作也非常的高效,只需要改变指针的指向即可。

f6686eb85e3d4dfbae1252f2e6c41fc.png

结合上图不难理解,单链表能够在 O(1) 复杂度内删除和添加元素,这就比数组高效很多。但是,如果我们要查找链表数据怎么办呢?链表的内存不是连续的,不能像数组那样根据下标访问,所以只能通过遍历链表来查找,时间复杂度为 O(n)。下面是单链表的代码示例:

public class SingleLinkedList {
    private Node head = null;//链表的头节点
    //根据值查找节点
    public Node findByValue(int value) {
        Node p = head;
        while (p != null && p.getData() != value)
            p = p.next;
        return p;
    }
    //根据下标查找节点
    public Node findByIndex(int index) {
        Node p = head;
        int flag = 0;
        while (p != null){
            if (flag == index)
                return p;
            flag ++;
            p = p.next;
        }
        return null;
    }
    //插入节点到链表头部
    public void insertToHead(Node node){
        if (head == null) head = node;
        else {
            node.next = head;
            head = node;
        }
    }
    public void insertToHead(int value){
        this.insertToHead(new Node(value));
    }
    //插入节点到链表末尾
    public void insert(Node node){
        if (head == null){
            head = node;
            return;
        }
        Node p = head;
        while (p.next != null) p = p.next;
        p.next = node;
    }
    public void insert(int value){
        this.insert(new Node(value));
    }
    //在某个节点之后插入节点
    public void insertAfter(Node p, Node newNode){
        if (p == null) return;
        newNode.next = p.next;
        p.next = newNode;
    }
    public void insertAfter(Node p, int value){
        this.insertAfter(p, new Node(value));
    }
    //在某个节点之前插入节点
    public void insertBefore(Node p, Node newNode){
        if (p == null) return;
        if (p == head){
            insertToHead(newNode);
            return;
        }
        //寻找节点p前面的节点
        Node pBefore = head;
        while (pBefore != null && pBefore.next != p){
            pBefore = pBefore.next;
        }
        if (pBefore == null) return;
        newNode.next = pBefore.next;
        pBefore.next = newNode;
    }
    public void insertBefore(Node p, int value){
        insertBefore(p, new Node(value));
    }
    //删除节点
    public void deleteByNode(Node p){
        if (p == null || head == null) return;
        if (p == head){
            head = head.next;
            return;
        }
        Node pBefore = head;
        while (pBefore != null && pBefore.next != p){
            pBefore = pBefore.next;
        }
        if (pBefore == null) return;
        pBefore.next = pBefore.next.next;
    }
    //根据值删除节点
    public void deleteByValue(int value){
        Node node = this.findByValue(value);
        if (node == null) return;
        this.deleteByNode(node);
    }
    //打印链表的所有节点值
    public void print(){
        Node p = head;
        while (p != null){
            System.out.print(p.getData() + "  ");
            p = p.next;
        }
        System.out.println();
    }
        //定义链表节点
    public static class Node{
        private int data;
        private Node next;
        public Node(int data) {
            this.data = data;
            this.next = null;
        }
        public int getData() {
            return data;
        }
    }
}


3. 循环链表


循环链表和单链表的唯一区别便是链表的尾结点指针并不是指向 null,而是指向了头节点,这样便形成了一个环形的链表结构:

560da01cc468962f3ba15776f0115b6.png


4. 双向链表


双向链表,顾名思义,就是链表不只是存储了指向下一个节点的 next 指针,还存储了一个指向前一个节点的 prev 指针,如下图:

5bd7e31315176e1beb3fe85e5085f13.png

为什么要使用这种具有两个指针的链表呢?主要是为了解决链表删除和插入操作的效率问题。

在单链表中,要删除一个节点,必须找到其前面的节点,这样就要遍历链表,时间开销较高。但是在双向链表中,由于每个节点都保存了指向前一个节点的指针,这样我们能够在 O(1) 的时间复杂度内删除节点。

插入操作也类似,比如要在节点 p 之前插入一个节点,那么也必须遍历单链表找到 p 节点前面的那个节点。但是双向链表可以直接利用前驱指针 prev 找到前一个节点,非常的高效。

这也是双向链表在实际开发中用的更多的原因,虽然每个节点存储了两个指针,空间开销更大,这就是一种典型的用空间换时间的思想。

下面是双向链表的代码示例:

public class DoubleLinkedList {
    private Node head = null;//链表的头节点
    //在某个节点之前插入节点,这里方能体现出双向链表的优势
    public void insertBefore(Node p, Node newNode) {
        if (p == null) return;
        if(p == head) {
            this.insertToHead(newNode);
            return;
        }
        newNode.prev = p.prev;
        p.prev.next = newNode;
        newNode.next = p;
        p.prev = newNode;
    }
    public void insertBefore(Node p, int value) {
        this.insertBefore(p, new Node(value));
    }
    //删除某个节点
    public void deleteByNode(Node node) {
        if(node == null || head == null) return;
        if (node == head) {
            head = head.next;
            if(head != null) head.prev = null;
相关文章
|
5天前
|
算法 Java
Java数据结构与算法:双向链表
Java数据结构与算法:双向链表
|
5天前
|
算法 Java
Java数据结构与算法:循环链表
Java数据结构与算法:循环链表
|
5天前
|
算法
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
|
5天前
|
算法
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
|
5天前
|
算法
【数据结构与算法 经典例题】随机链表的复制(图文详解)
【数据结构与算法 经典例题】随机链表的复制(图文详解)
|
5天前
|
算法 C语言
【数据结构与算法 经典例题】链表的回文结构(图文详解)
【数据结构与算法 经典例题】链表的回文结构(图文详解)
|
1天前
|
算法 安全 数据库
基于结点电压法的配电网状态估计算法matlab仿真
**摘要** 该程序实现了基于结点电压法的配电网状态估计算法,旨在提升数据的准确性和可靠性。在MATLAB2022a中运行,显示了状态估计过程中的电压和相位估计值,以及误差随迭代变化的图表。算法通过迭代计算雅可比矩阵,结合基尔霍夫定律解决线性方程组,估算网络节点电压。状态估计过程中应用了高斯-牛顿或莱文贝格-马夸尔特法,处理量测数据并考虑约束条件,以提高估计精度。程序结果以图形形式展示电压幅值和角度估计的比较,以及估计误差的演变,体现了算法在处理配电网状态估计问题的有效性。
|
5天前
|
机器学习/深度学习 自然语言处理 算法
m基于深度学习的OFDM+QPSK链路信道估计和均衡算法误码率matlab仿真,对比LS,MMSE及LMMSE传统算法
**摘要:** 升级版MATLAB仿真对比了深度学习与LS、MMSE、LMMSE的OFDM信道估计算法,新增自动样本生成、复杂度分析及抗频偏性能评估。深度学习在无线通信中,尤其在OFDM的信道估计问题上展现潜力,解决了传统方法的局限。程序涉及信道估计器设计,深度学习模型通过学习导频信息估计信道响应,适应频域变化。核心代码展示了信号处理流程,包括编码、调制、信道模拟、降噪、信道估计和解调。
27 8
|
7天前
|
算法
基于GA遗传优化的混合发电系统优化配置算法matlab仿真
**摘要:** 该研究利用遗传算法(GA)对混合发电系统进行优化配置,旨在最小化风能、太阳能及电池储能的成本并提升系统性能。MATLAB 2022a用于实现这一算法。仿真结果展示了一系列图表,包括总成本随代数变化、最佳适应度随代数变化,以及不同数据的分布情况,如负荷、风速、太阳辐射、弃电、缺电和电池状态等。此外,代码示例展示了如何运用GA求解,并绘制了发电单元的功率输出和年变化。该系统原理基于GA的自然选择和遗传原理,通过染色体编码、初始种群生成、适应度函数、选择、交叉和变异操作来寻找最优容量配置,以平衡成本、效率和可靠性。
|
8天前
|
机器学习/深度学习 算法
基于鲸鱼优化的knn分类特征选择算法matlab仿真
**基于WOA的KNN特征选择算法摘要** 该研究提出了一种融合鲸鱼优化算法(WOA)与K近邻(KNN)分类器的特征选择方法,旨在提升KNN的分类精度。在MATLAB2022a中实现,WOA负责优化特征子集,通过模拟鲸鱼捕食行为的螺旋式和包围策略搜索最佳特征。KNN则用于评估特征子集的性能。算法流程包括WOA参数初始化、特征二进制编码、适应度函数定义(以分类准确率为基准)、WOA迭代搜索及最优解输出。该方法有效地结合了启发式搜索与机器学习,优化特征选择,提高分类性能。