5. 基础: 链表

相比数组(Array),链表(Linked list)是一种稍微复杂一些的数据结构,它有若干个变种如:单向链表,双向链表,循环链表。跟数组一样它也属于线性表,符合线性表的特征:即除了首尾,每个元素只有一个前驱和一个后继。

数组要求一段连续的内存空间,对内存的要求比较高,比如当前内存剩余空间有100M,但非连续的,此时我们申请一个空间占用100M的数组就会失败。

而链表恰恰相反,它不需要空间连续,通过指针将一组零散的内存块串起来,所以申请一个空间大小为100M的链表可以成功。

为了将链表每个结点串联起来,每个结点除了记录数据本身以外还需要记录指向下一个结点的指针。链表有两个结点比较特殊,一个是头结点,用来记录首个数据结点的地址,有了它才能遍历整个链表。另一个是尾结点,它的next指针指向NULL,说明链表结束。

跟数组一样,链表同样支持插入(insert), 查找(search) 和 删除(delete)操作。数组做插入,删除操作时因为要保持数组空间连续的特性,如果不是插入/删除数组的尾巴,那时间复杂度就是O(n)。而链表在插入和删除时时间复杂度一直能保持O(1)。

  • 插入时
    插入到链表尾部,则只要将尾结点的next指向新结点,新结点的next置为NULL即可。
    插入到链表中间,则将前后位置的结点关系断开,前结点的next指向新结点,新结点的next指向后结点即可。
    所以时间复杂度是O(1)

  • 删除时
    删除尾元素时将尾元素的上一个元素的next置为NULL即可。
    删除中间元素时,将待删除元素的前元素的next指向待删除元素的next即可。
    所以时间复杂度是O(1)

  • 查找
    链表相比数组无法支持随机查找,即即使知道第几个结点,也需要按顺序从头结点开始遍历,所以时间复杂度是O(n)

循环链表

即一种特殊的单链表,它跟单链表的唯一区别就是尾结点。即单链表的尾结点的next指向NULL,而循环链表的next指向链表的头结点。
循环链表的优点就是从尾结点导航到链表头更方便,当要处理的数据具有环形结构时,就特别适合采用循环链表,如著名的约瑟夫问题。

双向链表

实际软件开发中更常用的是双向链表,双向链表相比单向链表不仅有next指针指向下一个结点,还有prev指针指向前驱结点。虽然相比单链表需要占用更多的内存空间,但可以支持双向遍历,更灵活。

因为双向链表可以随时获取prev指针,在做插入,删除,查找时更方便。双向链表是典型的用空间换时间的设计思想,即虽然多耗费了内存空间,但带来了算法了便捷性和高性能。

对于执行较慢的程序,可以采用空间换时间策略来进行优化。而内存受限的场景可以采用时间换空间的思想来降低内存的消耗。

数组 vs 链表

数组简单易用,且使用连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表不是连续存储,对CPU缓存不友好,没办法预读。

数组的缺点是大小固定,一经声明就要占用整块连续的内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致OutOfMemory。如果数组声明的过小,则可能出现不够用的情况。这时只能再申请一个更大的数组,将原数组数据拷贝进去,非常费时。而链表天然可以支持无限扩容。

如何写好链表代码

一些复杂的链表操作,如链表反转,有序链表合并等不太容易写好。有若干个技巧需要掌握:

  • 理解指针的含义
    将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能访问这个变量。

  • 警惕指针丢失和内存泄漏
    在插入结点时,一定要注意操作的顺序。同时删除链表结点时,也一定要记得手动释放内存空间(C语言)

  • 引用哨兵简化实现难度

链表的插入操作通常是:

new_node -> next = p -> next;
p -> next = new_node;

但细细思考一下一种情况不能这么写,当链表为空时,此时还没有头结点,这时需要这么写:

if (head == null) {
   head = new_node;
}

链表删除结点时,通常是将当前待删除结点的上一个结点的next指向当前结点的下一个结点:

p -> next = p -> next -> next;

但如果待删除结点是尾结点呢?此时需要如此兼容:

if (curr -> next == null){
	curr -> prev -> next = null;
}

所以能看的出来针对链表的插入,删除操作,对于链表的头结点和尾结点需要特殊处理。此时可引入哨兵来解决边界问题。

如果引入哨兵结点,在任何时候,不管链表是否为空,head指针都会一直指向这个哨兵结点,这种有哨兵结点的链表称为带头链表。相反无哨兵结点的链表称为不带头链表

这样插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点就可以统一为相同的代码实现逻辑了。

下图即带头链表,本身不存储数据,它的next指向第一个数据结点。
在这里插入图片描述

  • 重点留意边界条件处理
    在软件开发中,代码在边界或异常情况时,最容易产生bug。经常需要留意一下几个边界条件:
  1. 如果链表为空,代码是否能正常工作?
  2. 链表只包含一个结点时,代码是否能正常工作?
  3. 代码逻辑在处理头结点和尾结点时,是否能正常工作?

ps: 在写任何代码时,在业务正常情况之下,要多想想,可能遇到的边界情况或异常情况。如果有该如何应对,这样写出来的代码才够健壮。

相关算法题
  • 单链表反转

  • 链表中环的检测

  • 两个有序链表的合并

  • 删除链表倒数第n个结点

  • 求链表的中间结点

小结

写链表代码非常考验逻辑思维能力。链表会包含很多指针的操作,边界条件的处理,很容易产生bug。很多面试官喜欢让人手写链表代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值