了解链表基础,以及链表和数组的区别文章链接:代码随想录
一、链表理论基础
1. 链表的类型
(1)单链表:链表是由通过一些指针串连起来的线性结构。主要有数据域和指针域(指针域存放的指针指向下一个节点),链表入口节点称为头节点head,最后一个节点的指针域指向NULL。
(2)双链表:单链表的指针只能指向下一个节点,而双链表有两个指针,前面一个指针指向上一个节点,下一个指针指向下一个节点;双链表既可以向前查询,也可以向后查询。
(3)循环链表:即链表首尾相连,可以用来解决约瑟夫环问题。
2. 链表的存储方式
数组是内存中连续分布的;而链表是通过指针链接在内存中的各个节点,所以链表的内存空间不是连续分布的,而是散乱地分布在内存的某地址上,分配机制取决于操作系统的内存管理。
3. 链表的定义
注意:平时在刷 leetcode 时,链表定义是默认给出的,但在面试中一旦要求手写链表,是需要自己进行链表定义的。
以下是python定义链表节点的方式:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
4. 链表的操作
(1)删除节点:
不是直接删除,而是将C节点的指针指向E节点,即移动指针由C-->D到C-->E。C++需要手动释放这块内存,而其他语言有自己的回收机制。
(2)添加节点:要添加节点F,需要先让F的指针指向D节点,然后让C节点的指针指向F节点。
删除和添加操作的时间复杂度都是O(1),但如果要删除最后一个节点,需要通过头指针查找到第N-1个节点进行删除,那么查找的时间复杂度是O(n)。
5.性能分析
插入/删除(时间复杂度) | 查找(时间复杂度) | 适用场景 | |
数组 | O(n) | O(1) | 数据量固定,频繁查找,增删较少 |
链表 | O(1) | O(n) | 数据量不固定,频繁增删,查找较少 |
数组在定义的时候长度就是固定的,如果想改动数组长度,需要重新定义数组。链表长度不固定,并且动态增删,适合数据量不固定、频繁增删、较少查询的应用场景。
本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。题目链接/文章讲解/视频讲解::代码随想录
二、203.移除链表元素
1. 看到这道题的第一想法
首先定义一个虚拟头节点dummy_head,然后让它指向头节点head,因为如果需要删除的节点是头节点,那么需要分类讨论,而采用虚拟头节点的方式可以统一算法规则。
接下来遍历所以的节点,当节点数据域等于给定的整数时,则将上一个节点的指针指向下下个节点,就可以完成删除值等于val的节点操作。
2. 看完代码随想录后的想法
(1)定义虚拟头节点及初始化目前指针指向的节点。
注意目前指针也是指向虚拟头节点,而不是头节点。因为如果current = head,那么当val等于头节点时(即删除的元素是头节点)那么无法进行删除;
首先明确我们需要删除的元素必须是current.next,删除的操作才会是current 指向 current.next.next,也就是把current.next.next赋值给current.next(current.next = current.next.next)。
其次需要注意的是初始化定义虚拟头节点的代码如下:
dummy_head = ListNode(next = head)
(2)循环遍历每一个节点,寻找数据域等于val的节点,并将其删除:首先为了防止空指针的出现(指针异常)所以将current.next非空作为循环条件,接下来判断current.next的值是否为val,如果是的话,则current的下一个节点变更为下下个节点,即current.next = current.next.next,如果不是则继续遍历下一个节点。
(3)最后返回dummy_nead.next,因为如果删除的是头节点的话,那么这个链表就没有头节点了,所以引入虚拟头节点,那么进行删除操作后,返回dummy_head的下一个节点仍然是这个链表的头节点,无论新旧。
这是一道考察链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点题目链接/文章讲解/视频讲解:代码随想录
三、707.设计链表
1. 看到这道题的第一想法
(1)获取第n个节点的数值:
从头节点开始依次遍历一直到下标为n的节点,返回其数值。
(2)头部插入节点:
定义虚拟头节点dummy_head,然后定义一个临时指针指向当前的head头节点(即暂时存放虚拟头节点,因为一旦dummy_head指向新的头节点,那么原始头节点就找不到了,于是先将原始头节点存放起来),然后将dummy_head.next指向新的头节点,再让新的头节点指向临时指针(也就是暂时存放原始头节点的指针)。
(3)尾部插入节点:
从头节点开始遍历所有节点,一直到最后一个节点的next指向的是NULL,然后定义一个新节点,将新节点的指针指向NULL,再让最后一个节点的next指向新节点。
(4)前n个节点前插入:
定义新节点,然后从头节点遍历到第N个节点,将新节点的next指向第n个节点的Next,再移动第n个节点的指针指向新节点。
(5)删除第n个节点:
定义临时指针进行循环遍历链表,直到第n个节点的前一个节点就跳出循环,然后使得current.next指向current.next.next,最后size-1。
2. 看完代码随想录后的想法
这道题目统一使用虚拟头节点的方式进行操作,可以保证操作的一致性。
(1)获取第n个节点的数值:
A.明确链表是从0开始,到n-1结束的;B.要对节点下标做限制,超出合法范围则下标无效,也就是n < 0 或者 n > size-1 则无效;C.需要定义临时指针来遍历链表,并且让临时指针current指向dunmmy_head.next(即头指针),而不能直接操作头指针,否则最后返回头指针而头指针的值已被更改,无法返回完整链表;D.依次遍历链表节点(从0到n-1)就是第n个节点,最后返回第n个节点的数值即可。
(2)头部插入节点:
A.定义一个值为val的新节点NewNode;B.先将新节点的指针NewNode.next指向head(必须写成dummy_head.next),然后再将dummy_head.next指向新节点。注意写代码在操作指针的时候要将self写上,比如self.head而不是head。C. 最后size加1。
(3)尾部插入节点:
A.定义一个新节点NewNode,默认它的next指向NULL;B.定义一个临时指针current用于遍历链表所有节点,一直到尾节点(尾节点的判定条件:next指向的是null);C.让尾节点current直接指向NewNode; D.最后size加1。
(4)前n个节点前插入:
注意:是在第n个节点前插入,而不是在第n个节点后插入!!A.判断index是否有效;B.遍历节点直到第n个节点的前一个节点,就结束循环;C.定义新节点,并且新节点指向第n个节点;.然后将第n个节点的前一个节点指向新节点;D.最后size加1。
(5)删除第n个节点:
A.判断index是否有效;B.遍历节点直到第n个节点的前一个节点,就结束循环;C.current.next = current.next.next;D.最后size-1。
3. 实现过程中遇到的困难及解决
(1)关于界定index下标有没有效的问题:
一般情况下例如删除、查询的时候,index应该满足>=0或者<=self.size-1,只有在添加的时候,index是可以等于self.size的,因为最终它的size+=1。
(2)关于初始化节点的时候:init前后一定是双下划线!!__init__(下划线是两根连起来)
class ListNode:
def __init__(self,val=0,next = None):
self.val = val
self.next = next
class MyLinkedList:
def __init__(self):
self.dummy_head=ListNode()
self.size = 0
(3)定义新节点时,应该写定义函数,而不是直接写newnode = listnode(...),在这道题目中直接调用ListNode类即可。self.dummy_head.next =ListNode(val,self.dummy_head.next)。
(4)本题目中使用虚拟头节点时,应该调用对象self才可以,因为虚拟头节点被定义在了初始化函数中。self.dummy_head.next。
(5)在进行插入节点后,需要最后size加1;删除节点就size-1。
题目链接/文章讲解/视频讲解: 代码随想录
四、206.反转链表(面试高频)
1. 看到这道题的第一想法
首先,定义一个current指针用于遍历链表;然后定义一个临时指针用于存放current指向的下一个节点,也就是即将改变方向的节点;接下来反转指针由指向下一个节点转变为指向上一个节点;注意结束循环的条件及边界点。
2. 看完代码随想录后的想法
(1)双指针法
A. 定义一个current用于遍历链表的每一个节点;定义一个pre指针,代表current反转后的next,用于反转链表,初始化为None,因为反转后的链表pre是尾节点。
B. 进入循环遍历,以current不为NULL为循环条件;
C. 定义一个临时指针temp用于存放原来的current.next,因为方向一旦改变就无法查找到这个节点了,也就无法进行反转链表;然后反转链表操作,将current.next指向pre,然后移动pre和current进行下一轮循环。
D.当current指向NULL时停止遍历,此时pre是链表的头节点,所以返回pre即可。
(2)递归法
A. 主函数直接调用我们即将定义的反转函数reverse;
B.定义reverse函数:同双指针法
3. 实现过程中遇到的困难及解决
(1)双指针法:pre指针的初始化为None,而不是NULL。
(2)递归法:注意定义方式和调用方式。