一、链表与数组
我们最为常见的两个数据结构应该就是链表和数组了。
1、数组
所谓数组就是存储一系列相同类型,内存存储空间连续的一种线性表数据结构。
- 存储相同类型,并且每个值都有对应的序号(index)访问
- 内存空间连续,创建数组时,指定内存大小
图形示意:
2、链表
链表就是存储一系列相同类型的值,存储空间并不连续,上一个节点的指明下个节点的地址的一种数据结构。从三个方面来理解链表:
- Value:每个Node都存储了一个具体类型的数据
- Next:每个Node都存储了下个Node的地址
- LinkedList:所有Node依次链接,就形成了LinkedList
图形示意:
3、数组 vs 链表
数组和链表对比,不难发现以下几个特点:
- 数组需要连续的内存空间,而链表不需要,所以如果内存空间不足,往数组插入数据就会产生大块的数据迁移,会影响性能。
- 由于数组每个index对应一个值,数组更易于查询,时间复杂度在O(1), 而链表查询只能一个个遍历,时间复杂度在O(n)
- 链表优势在于插入,删除,时间复杂度都在O(1), 而数据中的插入删除,都会涉及到数据的遍历,时间复杂度在O(n),另外还涉及到数据的迁移,影响性能。
- 数组的内存空间是创建时指定的,而链表是动态分配内存空间
到底选择数组还是链表,大家可以根据自己的业务场景做分析。
二、链表种类
目前涉及的链表种类,总共有三种,分别是单链表,循环链表和双向链表,下面给大家一一讲解。
1、单链表
这是最常用的一个链表类型。节点的next指针仅仅指向下一个节点,依次链接,直到尾节点。
图例:
代码示例:
type ListNode struct {
data interface{}
next *ListNode
}
2、循环链表
与单链表唯一的区别是,链表的尾节点,指向头节点。其带来的一个好处就是尾节点可以直接找到头节点,某些特定的问题,使用这种结构会比较简洁,比如约瑟夫问题
代码示例:
type ListNode struct {
data interface{}
next *ListNode
}
3、双链表
与单链表相比,每个节点增加了一个prev指针,指向上一个节点的地址,提供了双向遍历的可能性。
代码示例:
type ListNode struct {
data interface{}
next *ListNode
prev *ListNode
}
三、链表的操作
在实际应用中,单链表是最常用的,徒手写单链表,也成了面试的经典考题。我们着重的聊一下单链表的插入,删除,以及后续的一些经典问题解决。比如链表倒置,是否有环,寻找中间值等等
1、插入节点
创建一个新节点B,假设左边节点是A, 右边节点是C,在A和C之间插入节点B,该如何操作
第一步: 将B的Next指向C
C = A.next
B.next = C
第二步:将A的next地址指向B
A.next = B
2、删除节点
现有一个链表,三个节点A->B->C,如果要删除B,该如何操作:
第一步:将A的Next指向C
C = B.next
A.next = C
第二步:将B的Next断掉,既置为null
B.next = nil
四、总结
链表的代码的书写,比较考验逻辑能力,如果之前没怎么接触过,或者不熟悉其实很难写的出来,这是正常现象。如果要想熟悉,只有多练习,没有其他办法。练习时,需要多从以下几个点多想想:
1、链表的边界条件的处理,比如头节点和尾节点
2、很多链表问题可以采用快慢两个指针实现,如果没有思路时,可以试试两个指针
3、在链表操作的时候,注意指针的内存释放,免得泄露
4、大脑想不出来时,就画图帮助。
链表还有很多其他有意思的问题,后面我会在其他文章一一讲解,我会将实现代码传到github上,有需要的可以看看。