数据机构与算法学习(四)- 链表

数组和链表是两个非常基础、非常常用的数据结构。

两者的区别:从底层的存储结构来看,数组需要一块连续的内存空间来存储,对内存的要求比较高。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。

三种常见的链表结构:单链表、双链表和循环链表。

单链表

概念定义:

我们把内存块称为链表的节点。记录下一个节点的指针叫做后继指针next。把第一个节点叫做头节点。把最后一个节点叫做尾节点

插入、删除数据操作,只需要考虑相邻节点的指针改变,对应的事件复杂度为 O(1).由于链表的特点随机访问就不是很高效了,需要进行遍历,时间复杂度为O(n).

循环链表

循环链表是一种特殊的单链表。与单链表唯一的区别就在尾节点。单链表指向空,循环链表指向头节点。优点是从链尾到链头比较方便。当腰处理的数据具有环型结构特点时,就特别适合采用循环链表,比如著名的约瑟夫问题。

双向链表

支持两个方向,每个节点的指针不止有一个后继指针next,还有一个前驱指针prev指向前面的节点。双向链表可以支持O(1)的时间复杂度的情况下找到前驱节点。

删除操作:第一种, 删除节点中“值等于某个给定值”的结点; 这种情况的单链表与双链表都要做遍历O(n)的时间复杂度。第二种,删除给定指针指向的结点。这种情况对于单链表双链表就比较有优势了,单链表需要重新遍历 而双链表不需要。

插入操作亦是如此。这是一种用空间换时间的设计思想。双链表需要两个指针,空间占用较大。

 

链表代码的写作技巧

技巧一:理解指针或引用的含义

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

技巧二:警惕指针丢失和内存泄漏

插入节点或者删除节点时,一定要注意操作的顺序。

删除节点时,也一定要记得手动释放内存空间。

技巧三:利用哨兵简化实现难度

单链表的插入

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

如果向一个空链表中插入第一个节点,上面的逻辑不能用了,需要特殊处理。

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

删除节点

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

如果删除的是最后一个节点,需要特殊处理。

if(head->next == null){
    head = null;
}

可以看出, 针对链表的插入、删除操作,需要对插入第一个节点和删除最后一个节点的情况进行特殊处理。这样的代码实现起来就会很繁琐不简洁,而且也容易因为考虑不全而出错。如何来解决这个问题呢?

这里引入哨兵来解决问题。如果我们引入哨兵节点,在任何时候,不管链表是不是空,head指针都会一致指向这个哨兵节点。我们也把这种有哨兵节点的链表叫带头链表,相反,没有哨兵节点的链表就叫做不带头链表。哨兵节点一致存在,所以插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点,都可以统一为相同的代码实现逻辑了。

技巧四:重点留意边界条件处理

  • 如果链表为空时,代码能正常工作?
  • 如果链表只包含一个节点时,代码能否正常工作?
  • 如果链表只包含两个节点时,代码能够正常工作?
  • 代码逻辑在处理头节点和尾节点的时候,能否正常工作?

技巧五:举例画图,辅助思考

使用举例法和画图法。将它画在纸上,把各种情况都举一个例子,画出来。

技巧六:多写多练,没有捷径

把一下几个操作都能写熟练。

  • 单链表反转
  • 链表中环的检测
  • 两个有序的链表合并
  • 删除链表倒数第n个节点
  • 求链表的中间节点

 

posted on 2021-03-15 14:17  成长的皮球  阅读(29)  评论(0编辑  收藏  举报