04 链表

一、什么是链表

先从数据结构上看。数组需要一块连续的内存存储,但是,如果存储100M的数据,内存中没有连续的足够打的存储空间时,数组就不适合了。

链表正好相反,他不需要连续的内存空间,通过指针将零散的内存块串起来使用。

链表结构五花八门,我们只介绍最常用的三种。单链表、双向链表、循环连表。

二、单链表

刚刚说道,连表是通过指针将一组零散的内存串联在一起。我们把内存块称为链表的节点(node)。为了将节点串联起来,每个节点除了存储数据外,还要存储一个下一个节点的地址。我们把记录下个节点地址的指针叫做后继指针(next)。

 图上有2个节点是比较特殊的,第一个和最后一个。第一个叫做头节点,最后一个叫做尾节点。头结点用来记录链表的基地址也就是起始地址。尾节点的next指向的是null。通过这个可以判断是否是链表的最后一个节点。

1.插入删除

和数组一样,链表也支持数据的插入,删除、查找。

数组中,为了保持数据的连续性,插入删除一个数据,需要做大量的数据迁移,时间复杂度是O(n)。但是链表不同,他本身就不是连续的,插入删除是非常快的。

 2. 链表的查询

有利就有弊,要想随机访问第k个元素,就没有数组那么容易了。因为他不是连续内存存储,无法向数组那样计算出第k个数据的内存地址。连表需要从头遍历,直到找到相应的数据。

3. 单链表代码实现

/**
 * 链表
 */
public class LinkedList {
    //头
    private Node head;
    //尾
    private Node tail;
    private int size;
    public LinkedList(){
        this.size = 0;
    }

    public void addFirst(int data){
        Node node = new Node(data);
        node.next = this.head;
        this.head = node;
        this.size ++;
    }

    public void addLast(int data){
        add(data, this.size-1);
    }

    public void add(int data, int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("index error");
        }
        if(index == 0){
            addFirst(data);
            return;
        }
        Node pre = this.head;
        for(int i =0;i<index;++i){
            pre = pre.next;
        }
        Node node = new Node(data);
        node.next = pre.next;
        pre.next = node;
        this.size++;
    }

    public int get(int index){
        if(index >= size){
            return -1;
        }
        int i =0;
        Node find = head;
        while(i < index){
            ++i;
            find = find.next;
        }
        return find.data;
    }

    private static class Node{
        int data;
        Node next;
        Node(int data){
            this.data = data;
        }
    }
}

三、循环列表

循环列表是一种特殊的单列表。和单列表唯一的区别就是,循环列表的尾节点指向头节点,而单列表的尾节点是指向null的。

和单链表相比,循环链表的优点是从链表尾到链表头比较方便。需要处理的问题具有环形结构特点时,就适合使用循环链表。比如著名的约瑟夫问题。

四、双向链表

双向链表,顾名思义,它有2个方向,每个节点不仅有一个后继指针next指向下一个节点,还有一个前驱指针prev指向上一个节点。

从图中可以看到,双向链表需要额外的2个空间来存储prev和next指针。虽然占用内存多,单支持双向遍历。

1.优点 

删除操作

  • 删除链表中等于给定值的节点
  • 删除给定指针指向的节点

第一种情况, 单链表和双向链表查找节点都需要遍历,复杂度是O(n), 删除节点复杂度都是O(1)。

对于第二种情况,我们拿到了要删除的节点,但是要删除的话得知道他的前驱节点。因此对于双向链表来说,时间复杂度还是O(1)。但是对于单链表来说,需要从头遍历,指导p->next = q,证明p是q的前驱节点,时间复杂度是O(n)。

其他操作

同理,我们需要在指定节点前面插入一个节点。

或者对于一个有序的链表(比如从小到大排),要查找某个具体的值,我们可以记录上次查询的位置,然后与上次的值比较来决定向前还是向后查找,也会比单链表快。

2. 总结

在实际开发中,尽管双向链表占用内存更多,但比单链表应用更加广泛,比如Java中的LinkedHashMap就用到了双向链表。

这还有一个重要的知识点,空间换时间,当内存充足,追求速度的时候,就可以使用空间复杂度高时间复杂度低的方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七号公园的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值