链表初级详解

目录

什么是链表

链表的特点

怎么用链表

链表的基础知识

单链表

循环链表

        双向链表


什么是链表

我们前面说了“数组”,它是一种占用连续内存空间的数据结构。但有时候我们需要存储大量的数据,内存中却没有像样大小的连续空间,这可怎么办呢?我们有一个需要大量增删操作的数据集,而改查操作用的比较少,选择数组进行存储的话,操作起来不构效率怎么办呢?链表应运而生,它并不需要⼀块连续的内存空间,它通过“指针”将⼀组零散的内存块串联起来使⽤

 

链表的特点

为什么链表应运而生呢?因为链表这种结构,刚好和数组互补,本着“优劣同源”的思想,既然数组快改查,慢增删,互补的链表应该就是快增删,慢改查了吧。欸,还真是这样的哈哈哈。

  数组 链表
增删 O(n) O(1)
改查 O(1) O(n)

所以频繁查改的数据集,用数组存最美滋滋;频繁增删的数据集,用链表存就美滋滋啦,完美~

【这里要注意增删有特殊的步骤,单纯的删除操作链表是O(1)滴,但是要找到待增删元素的位置,链表确是O(n)滴~】

有读者大人问啦:那增删改查都多,咋整啊,总不能搞个数组链表出来吧。牛皮!还真是搞个数组链表来解决这种情况,这个咱们下回再说。

说了半天,还没总结一下链表的特点呢,其实定义里也有讲到:

1、链表不需要⼀块连续的内存空间。          有人问了,凭什么就不需要啊?怎么实现的啊? 好问题!我们来想想,数据存在存储空间里,咱们要用的时候,去访问数据在的地址就行,数组之所以需要连续,就是因为想要知道第一个元素的地址就能推断出后面的数据的地址。那咱们如果不想让数据连续存放的话,我们只要知道所有数据的地址就可以了。但这样也太复杂了,于是我们在每个数据的空间里,都记录一下下一个存储空间的内存地址,用指针指向它。这样,离散的数据就被串联起来啦。

2、增删快,改查慢。         和数组互补,我们得不厌其烦的捋捋这个逻辑,数组因为连续,所以增删需要移动元素,这是遍历是O(n)级别的操作,改查直接算出地址一步到位了,这是O(1)的操作。 链表不连续呀,增删咱们直接改变指针指向的地址就可以了,多快呀,这是O(1)的操作,但改查不一样了,因为我们只知道下一个元素的地址,所以我们只能从头到尾慢慢查,这是遍历,是O(n)的操作。

3、扩展性好,不用提前指定大小。   又一个和数组互补的特点来了,数组不灵活,需要提前指定大小。

4、内存利用率低于数组。      链表里的结点,不仅需要存储数据,还需要存储下一个数据的地址,存储相同的数据,所需要的内存肯定就相比不需要存储地址的数组要大些。

怎么用链表

链表分为很多种,咱们先从最简单最基础的单链表开始说起。

链表的基础知识

事实上,文章的第一幅图还有细化的空间,咱们给他细化一下。

比起数组,链表多了一个next结构,用来存储下一个结点的位置。 对于链表而言,一个内存块即是一个结点。

在链表中,较为特殊的就是头结点和尾结点,头结点一般存放链表的基地址,而尾结点的next值向null。

 public class ListNode {  //链表的一个结点
     int val;
     ListNode next;
     ListNode(int x) { val = x; }
  }

单链表

文章的第一张图就是单链表结构,这里的“单”指的是单方向~

链表的基础操作是增删改查。我们来看看怎么实现

增(插):由于链表的结点都是散乱的内存块,所以只需要元素A指向需要新增的元素C的地址,而C再指向A原先指向的地址即可。

插入后:

C.next=A.next;
A.next=C;

删:元素A直接指向元素B指向的元素C即可。

A.next=A.next.next;

改、查

改查的情况比较特殊,由于链表的内存是离散的,所以我们并不能像数组一样“随机访问”,结点的位置,只有指向它的其他结点知道,所以我们想要改查一个结点,必须老老实实的从链表的头开始找起,这是个遍历操作,需要O(n)的时间复杂度。

上图表示想要找到data=3的寻找步骤。

循环链表

循环链表是特殊的单链表,具体特殊在:他的尾节点的next指针,指向了头结点,长成了这样。

和单链表相⽐,循环链表的优点是从链尾到链头⽐较⽅便。当要处理的数据具有环型结构特点时,就特别适合采⽤循环链表。 

基础的增删改查实现了,那么迎来了神奇的需求:删除给定结点?

不就是上面说到的删除结点么,O(1)的复杂度,链表的优势所在。

no no no,上面讲的都是理论层面的东西,咱们仔细捋捋删除操作的具体细节。

删除无非是两种情况:

1、删除值等于给定值的结点

2、删除给定指针指向的结点

 

对于第一种情况来说,删除需要有个查找的过程,单纯的删除操作的确是O(1),但这个查找的过程却是O(n)的。

对于第二种情况来说,我们已经知道了要删除的结点,但要删除这个结点,需要他的前驱结点,在单链表结构中,我们又要憨憨的去遍历寻找前驱结点了。

啊~听起来好麻烦啊,单这类需求又非常常见。于是我们想想能不能对单链表的结构进行修改呢?既然删除结点需要前驱结点,那我们多加一个指针指向前驱结点就好啦。

于是双向链表应运而生。

双向链表

​
 public class ListNode {  //链表的一个结点
     int val;
     ListNode next;
     ListNode pre;
     ListNode(int x) { val = x; }
  }

双向链表相比于单链表来说,他可以⽀持O(1)时间复杂度的情况下找到前驱结点。

所以,针对第⼆种删除情况,单链表删除操作需要O(n)的时间复杂度,⽽双向链表只需要在O(1)的时间复杂度内就搞定了! 

同理,如果我们希望在链表的某个指定结点前⾯插⼊⼀个结点,双向链表⽐单链表有很⼤的优势。双向链表可以在O(1)时间复 杂度搞定,⽽单向链表需要O(n)的时间复杂度。

对于⼀个有序链表,双向链表的按值查询的效率也要⽐单链表⾼⼀些。因为,我们可以记录上次查找的位置p,每次查询时,根据要查找的值与p的⼤⼩关系,决定是往前还是往后查找,所以平均只需要查找⼀半的数据。
 

等等,我们发现,双向链表和循环链表其实并不矛盾,这就意味着,可以搞出一个双向循环链表来。

在实际的软件开发中,不能仅仅利⽤复杂度分析就决定使⽤哪个 数据结构来存储数据。
要对数组和链表的 各种性能进⾏对⽐,综合来选择使⽤两者中的哪⼀个。
 

发布了38 篇原创文章 · 获赞 6 · 访问量 1893

猜你喜欢

转载自blog.csdn.net/weixin_43827227/article/details/105012637