3 链表
3.0 链表题目总结
-
虚拟头结点
- 特点:链表通常会有一个头结点,指向第一个数据,这样的话,对于链表所有的数据操作都是一致的。无需对第一个数据坐特别判断。
-
遍历指针
- 特点:每次移动一步,直到当前节点或下一个节点为空时结束。
- 解决问题:遍历链表。
-
双指针
- 快慢指针
- 特点:这里快指针是每次移动2步,慢指针每次移动1步。
- 解决问题:
- 求解一个链表的中间节点。
- 求解环形链表时的第一步。
- 区域指针
- 特点:用来指定区域,比如:已遍历区域和未遍历区域,可以分别使用一个指针用于区分。
- 解决问题:通常结合其他方法解决具体问题:比如排序链表。
- 遍历指针+区域指针
- 一个指针为遍历指针,一个指针为区域指针,或者有多个遍历指针/区域指针,用于多个链表的遍历和划分。
- 快慢指针
-
递归算法
- 递归三要素:
- 递归终值(最后的返回值)
- 递归式(递归关系,下一次如何调用递归函数)
- 递归体(当递归不为终值时应该干的事情,包括一些操作和返回值)
- 解决问题:
- 反转链表
- 递归三要素:
3.1 链表的中间节点 (LC-876)
- 问题描述:给定一个头结点为head的非空单列表,放回链表的中间结点。如果有两个中间节点,则返回第二个。
- 解题思路:双指针-快慢指针
- 双指针初始指向头结点。
- 慢指针每次移动一步,快指针每次移动两步,直到快指针指向null,这个时候的慢指针就指向中间位置。
- 时间复杂度O(n),空间复杂度O(1)
3.2 合并两个升序链表 (LC-21)
- 问题描述:将两个升序链表合并成一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
- 解题思路:三遍历指针(新链表指针,老链表指针1,老链表指针2)
- 初始时生成一个新的虚拟头结点,并使得新链表指针cur指向头节点。
- 老链表指针1指向链表1的头结点,老链表指针2指向链表2的头结点。
- 不断对指针1和指针2指向的值进行比较
- 谁小则谁连接到新链表并往后移动一步,直到某个链表被遍历完。
- 最后将没遍历完的列表连接到新链表后面即可。
- 时间复杂度O(n),空间复杂度O(1)
3.3 反转链表 (LC-206)
- 问题描述:给你单链表的头结点head,请你反转链表。
- 解题思路:递归求解。
- 通过递归函数,一直递归到链表的最后一个节点为止,放回反转成功的头结点。
- 递归终值:判断当前节点是否是尾节点,并返回这个节点作为头节点。
- 递归式:对下一个节点进行递归判断。
- 递归体:将下一个节点指向当前节点,并将当前节点的指向清空。
- 通过递归函数,一直递归到链表的最后一个节点为止,放回反转成功的头结点。
- 时间复杂度O(n),空间复杂度O(n)
- 空间复杂度主要取决于递归调用的栈空间,最多为 n 层。
3.4 排序链表 (LC-148)
- 问题描述:给一个链表的头结点head,请按升序排列并返回排序后的链表。
- 解题思路:归并排序+三指针(参考合并两个升序链表)
- 归并排序
- 大循环对每个分区的大小sub_length进行循环,以2的倍数增长
- 初始化prev指针指向虚拟头结点dummyHead
- 使用cur遍历next及以后的数据,即遍历还没有进行排序的区块4。
- cur指针遍历一次sub_length长度,记录头指针head1,并断开后续连接形成区块2,cur指向新的后继。
- cur指针再遍历一次sub_length长度,记录头指针head2,并断开后续连接形参区块3,将后继交给next,作为区块4。
- 对head1和head2进行升序列表合并。返回头节点并接入虚拟头指针prev。可以记为区块1.
- 大循环对每个分区的大小sub_length进行循环,以2的倍数增长
- 归并排序
- 时间复杂度O(nlogn),空间复杂度O(1)
3.5 重排列表 (LC-143)
- 问题描述:给定一个链表:L0→L1→…→Ln-1→Ln,重排后为L0→Ln→L1→Ln-1→L2→Ln-2→…
- 解题思路:分拆+反转+拼接
- 首先寻找到中间节点,然后拆分成前后两个区域。
- 将后面的区域进行反转。
- 将前后两个区域从第一个位置一个个拼接。
- 时间复杂度O(n),空间复杂度O(1)
3.6 旋转链表 (LC-61)
- 问题描述:给定一个链表的头结点head,旋转链表,将链表的每个节点向右移动k个位置。
- 解题思路:遍历指针*2(先后遍历)
- 首先获取遍历一次链表,获取链表长度L。
- 然后前指针和后指针初始位置都为头指针
- 首先将前指针向前移动k%L次。
- 然后同时移动前指针和后指针,直到后指针指向最后一个头结点。
- 将前指针指向头节点,后指针的下一个节点为新的头结点,后指针指向的就是尾节点,将尾节点指向null。
- 返回头结点。
- 时间复杂度O(n),空间复杂度O(1)
3.7 两两交换链表中的节点 (LC-24)
- 问题描述:给定一个链表,两两交换相邻的节点,并返回交换后的头结点。
- 解题思路:递归求解/迭代解法
- 以递归为例子,主要思想是:每次返回交换好的子链表的头结点。
- 递归终值:因为是两个进行交换,所以终值条件是当前节点或者当前节点的下一个节点为空时,将当前节点返回作为头结点。
- 递归式: 将当前节点的下一个节点的下一个节点作为头结点递归,获取该节点的后续链表头结点。
- 递归体:
- 通过递归式,获取下下一个节点的后续排序好的子链表。
- 获取当前节点的下一个节点。
- 将下一个节点指向自己。
- 将当前节点指向后续链表头结点。
- 返回下一个节点。(即本次交换后的链表头结点)
- 以递归为例子,主要思想是:每次返回交换好的子链表的头结点。
- 时间复杂度O(n),空间复杂度O(n)
3.8 合并k个升序链表 (LC-23)
- 问题描述:给定一个链表数组,每个链表都已经升序排列,请合并在同一个列表并保存升序。
- 解题思路:递归归并两两列表/迭代使用两个升序列表归并(LC-21)
- 递归归并:
- 递归终值:
- 剩下的子数组只含有一个链表,则直接放回该链表。
- 如果含有两个链表,则合并后返回。
- 递归式:递归子数组。
- 递归体:
- 将整个链表从中间拆分成前后两个子数组
- 递归求解左右子数组
- 合并递归求解出的左右子数组链表,并返回。
- 递归终值:
- 递归归并:
- 时间复杂度O(kn*logk),空间复杂度O(logk)
3.9 删除排序链表中的重复元素 (LC-83)
- 问题描述:给定一个排序好的链表,删除所有重复元素,重复元素只保留一个,并返回链表。
- 解题思路:单指针循环遍历
- 初始化指针指向头结点
- 当指针指向节点不为空且下一个节点也不为空时
- 判断下一个节点的值和当前节点是否相同,相同则修改当前节点指向下下一个节点。
- 不相同,则将指针前进一步。
- 时间复杂度O(n),空间复杂度O(1)
3.10 删除排序链表中的重复元素2 (LC-82)
- 问题描述:给定一个已经排序的链表,删除所有重复元素,不保留重复元素,返回链表。
- 解题思路:递归/迭代
-
- 递归解法
- 递归终值:
- 当前头节点或者下一个节点为空,则返回当前头节点。
- 递归体:
- 当前头节点和下一个节点是否相同:(即跳过相同的节点,只递归不同的节点)
- 相同,不断循环遍历,找到不同的节点,返回该不同节点的 递归求解值。
- 不相同,求解下一个节点的 递归求解值,将当前头节点指向该 递归求解值,返回当前头节点。
- 当前头节点和下一个节点是否相同:(即跳过相同的节点,只递归不同的节点)
- 递归终值:
- 递归解法
-
- 迭代求解
- 设置一个虚拟头结点,和一个循环遍历指针指向它。
- 判断遍历指针指向的节点的 “下一个节点” 和 “下下个节点” 是否相同。
- 相同,则记录下相同的值,使用新的遍历指针循环删除值相同的节点,直到新的节点。将遍历指针的下一个节点变为新节点,然后再指向该新节点。
- 不相同,则遍历指针直接指向该节点。
- 返回虚拟头结点的下一个节点。
- 迭代求解
-
- 时间复杂度O(n),空间复杂度O(1),对于遍历空间复杂度是O(n)。
3.11 环形链表2 (LC-142)
- 问题描述:给定一个单链表,判断是否有环,如果有环返回环开始的位置,没有则放回null。
- 解题思路:快慢指针(相遇)+同步指针(相遇)
- 首先使用快慢指针从头结点开始不断遍历链表,快指针每次移动两步,慢指针每次移动一步。
- 如果遍历的时候发现有节点指向null,则说明无环。
- 如果快慢指针相遇了,那么记下相遇位置,作为同步起始点2。
- 如果有环,则同步指针1从头结点,同步指针2从同步起始点,每次移动一步,直到他们相遇,那么相遇点就是环的起始点,放回相遇点。
- 首先使用快慢指针从头结点开始不断遍历链表,快指针每次移动两步,慢指针每次移动一步。
- 时间复杂度O(n),空间复杂度O(1)
3.12 回文链表 (LC-234)
- 问题描述:给一个链表,判断是否它是否为回文链表,即顺序输出和逆序输出一样。
- 解题思路:快慢指针(折中拆分)+反转(后半链表)+双指针遍历
- 首先初始化快慢指针在头结点位置,快指针每次移动两步,慢指针每次移动一步,将整个链表拆成前半和后半链表。
- 将后半链表进行翻转。
- 再用两个指针分别遍历前半链表和后半链表对比。
- 时间复杂度O(n),空间复杂度O(1)
3.13 相交链表 (LC-160)
- 问题描述:给定两个单链表,找到他们相交的位置。
- 解题思路:双指针交叉遍历
- 两个指针分别指向两个链表的头结点,不断遍历,直到两个指针指向的值相同,返回相同值。
- 如果其中一个指针遍历完了,则从另一个链表的头结点开始重新遍历。
- 最后要么两个指针相遇在相交处,要么都分别遍历完了两个链表,值都等于null,即到达链表尾节点。
- 两个指针分别指向两个链表的头结点,不断遍历,直到两个指针指向的值相同,返回相同值。
- 时间复杂度O(m+n),空间复杂度O(1)
- 最差的情况就是两个链表没有交点,最后两个指针都遍历了m+n次(也就是都遍历完了一次两个链表),分别到达尾节点。
3.14 奇偶链表 (LC-328)
- 问题描述:给定一个单链表,奇数位置的结点放在前半,偶数位置的节点放在后半。只能进行原地操作。
- 解题思路:双指针记尾+双指针记头(也可以叫区域指针)
- 首先判断边界情况,是否为空或只有一个节点。
- 初始一个奇头指针和奇数尾指针指向当前头结点,初始化一个偶数头指针和偶数尾指针指向当前节点的下一个节点。
- 当偶数指针指向的节点不为空且下一个节点也不为空时(即还有奇数节点要排序时)
- 奇数尾指针指向的节点的下一个节点设为:偶数尾节点的下一个节点。奇数尾指针也指向该节点。
- 偶数指针指向奇数尾指针指向节点的下一个节点。偶数尾指针也指向该节点。
- 最后将偶数头指针拼接到奇数尾指针上。
- 时间复杂度O(n),空间复杂度O(1)
3.15 移除链表元素 (LC-203)
- 问题描述:给定一个单链表,删除链表中值为val的节点并返回新的头节点。
- 解题思路:虚拟头节点+双指针
- 初始化两个指针,一个指针为遍历指针,从头节点开始,一个是有效链表尾指针,指向虚拟头节点。
- 不断使用遍历指针遍历链表(每次循环结束时,遍历指针指向下一个节点),看当前的值是否为val
- 不是,将有效链表尾指针=遍历指针。
- 是,则将有效链表尾指针指向的节点的下一个节点指向遍历指针的下一个节点(即跳过当前要删除的节点)。
- 时间复杂度O(n),空间复杂度O(1)
3.16 k个一组翻转链表 (LC-25)
- 问题描述:给定一个链表,每k个节点一组进行翻转,返回新的链表。
- 解题思路:虚拟头节点+三指针(区域指针)
- 初始化一个虚拟头节点指向原头节点,一个待翻转区域头节点head,一个待翻转区域尾节点end,一个未翻转区域头节点next,区域节点初始化都指向虚拟头节点。
- 循环翻转链表,直到end指向的节点为实际尾节点。
- 首先循环移动end,直到移动了k个节点,或者当前节点为空时结束。
- 如果end不为空,next指向end的下一个节点。如果end为空则说明不足k个,直接跳出循环。
- 将head节点的下一个节点作为start头节点,end作为尾节点的区域进行翻转。
- 然后再将end拼接为head指向节点的下一个节点,start的下一个节点拼接为next。
- head和end指向start指向的节点。
- 时间复杂度O(n),空间复杂度O(1)