剑指Offer(专项突破版):链表
作者:互联网
剑指Offer(专项突破版):链表
目录
面试题21:删除倒数第k个节点
题目:如果给定一个链表,请问如何删除链表中的倒数第k个节点?假设链表中节点的总数为n,那么1≤k≤n。要求只能遍历链表一次。
例如,输入图4.11(a)中的链表,删除倒数第2个节点之后的链表如图4.1(b)所示。
面试题22:链表中环的入口节点
题目:如果一个链表中包含环,那么应该如何找出环的入口节点?从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点。例如,在如图4.3所示的链表中,环的入口节点是节点3。
面试题23:两个链表的第1个重合节点
题目:输入两个单向链表,请问如何找出它们的第1个重合节点。例如,图4.5中的两个链表的第1个重合节点的值是4。
面试题24:反转链表
题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。例如,把图4.8(a)中的链表反转之后得到的链表如图4.8(b)所示。
面试题25:链表中的数字相加
题目:给定两个表示非负整数的单向链表,请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示?链表中的每个节点表示整数十进制的一位,并且头节点对应整数的最高位数而尾节点对应整数的个位数。例如,在图4.10(a)和图4.10(b)中,两个链表分别表示整数123和531,它们的和为654,对应的链表如图4.10(c)所示。
面试题26:重排链表
问题:给定一个链表,链表中节点的顺序是L0→L1→L2→…→Ln-1→Ln,请问如何重排链表使节点的顺序变成L0→Ln→L1→Ln-1→L2→Ln-2→…?例如,输入图4.12(a)中的链表,重排之后的链表如图4.12(b)所示。
面试题27:回文链表
问题:如何判断一个链表是不是回文?要求解法的时间复杂度是O(n),并且不得使用超过O(1)的辅助空间。如果一个链表是回文,那么链表的节点序列从前往后看和从后往前看是相同的。例如,图4.13中的链表的节点序列从前往后看和从后往前看都是1、2、3、3、2、1,因此这是一个回文链表。
面试题28:展平多级双向链表
问题:在一个多级双向链表中,节点除了有两个指针分别指向前后两个节点,还有一个指针指向它的子链表,并且子链表也是一个双向链表,它的节点也有指向子链表的指针。请将这样的多级双向链表展平成普通的双向链表,即所有节点都没有子链表。例如,图4.14(a)所示是一个多级双向链表,它展平之后如图4.14(b)所示。
面试题29:排序的循环链表
问题:在一个循环链表中节点的值递增排序,请设计一个算法在该循环链表中插入节点,并保证插入节点之后的循环链表仍然是排序的。例如,图4.152(a)所示是一个排序的循环链表,插入一个值为4的节点之后的链表如图4.15(b)所示。
划线笔记
本文由 简悦 SimpRead 转码, 原文地址 n.yuhanliu.com:8887
链表是一种常见的基础数据结构。在链表中,每个节点包含指向下一个节点的指针,这些指针把节点连接成链状结构。
在链表中,每个节点包含指向下一个节点的指针,这些指针把节点连接成链状结构。
单向链表的节点包含指向下一个节点的指针
哨兵节点是为了简化处理链表边界条件而引入的附加链表节点。
当输入的链表头节点为 null 时,输入的链表为空。此时新添加的节点成为链表中唯一的节点,也就是链表的头节点。在这种情况下,我们改变了输入链表的头节点,因此在上述代码中有一条用来处理这种情况的 if 语句。
首先创建一个哨兵节点,并把该节点当作链表的头节点,然后把原始的链表添加在哨兵节点的后面。当完成添加操作之后,再返回链表真正的头节点,也就是哨兵节点的下一个节点。
由于将新创建的一个哨兵节点当作链表的头节点,链表无论如何也不会为空,因此不需要使用 if 语句来单独处理输入头节点 head 为 null 的情形。
通常为了删除一个节点,应该找到被删除节点的前一个节点,然后把该节点的 next 指针指向它下一个节点的下一个节点,这样下一个节点没有被其他节点引用,也就相当于被删除了。
在上述代码中有两条 if 语句,分别用于处理两个特殊情况:输入的链表为空;被删除的节点是原始链表的头节点。
如果在链表的最前面添加一个哨兵节点作为头节点,那么链表就不为空,并且链表的头节点无论如何都不会被删除。
输入的链表为空,或者操作可能会产生新的头节点,这些都是应聘者在面试时特别容易忽视的测试用例。如果合理应用哨兵节点,就不再需要单独处理这些特殊的输入,从而杜绝由于忘记处理这些特殊输入而出现 Bug 的可能性。
第 1 种方法是前后双指针,即一个指针在链表中提前朝着指向下一个节点的指针移动若干步,然后移动第 2 个指针。前后双指针的经典应用是查找链表的倒数第_k_个节点。先让第 1 个指针从链表的头节点开始朝着指向下一个节点的指针先移动_k_-1 步,然后让第 2 个指针指向链表的头节点,再让两个指针以相同的速度一起移动,当第 1 个指针到达链表的尾节点时第 2 个指针正好指向倒数第_k_个节点。
第 2 种方法是快慢双指针,即两个指针在链表中移动的速度不一样,通常是快的指针朝着指向下一个节点的指针一次移动两步,慢的指针一次只移动一步。采用这种方法,在一个没有环的链表中,当快的指针到达链表尾节点的时候慢的指针正好指向链表的中间节点。
如果可以遍历链表两次,那么这个问题就会变得简单一些。在第 1 次遍历链表时,可以得出链表的节点总数_n_。在第 2 次遍历链表时,可以找出链表的第_n-k_个节点(即倒数第_k_+1 个节点)。然后把倒数第_k_+1 个节点的 next 指针指向倒数第_k_-1 个节点,这样就可以把倒数第_k_个节点从链表中删除。
为了实现只遍历链表一次就能找到倒数第_k_+1 个节点,可以定义两个指针。第 1 个指针 P1 从链表的头节点开始向前走_k_步,第 2 个指针 P2 保持不动;从第_k_+1 步开始指针 P2 也从链表的头节点开始和指针 P1 以相同的速度遍历。由于两个指针的距离始终保持为_k_,当指针 P1 指向链表的尾节点时指针 P2 正好指向倒数第_k_+1 个节点。
可以根据一快一慢两个指针是否能够相遇来判断链表中是否包含环。
先定义两个指针 P1 和 P2,指向链表的头节点。如果链表中的环有_n_个节点,第 1 个指针 P1 先在链表中向前移动_n_步,然后两个指针以相同的速度向前移动。当第 2 个指针 P2 指向环的入口节点时,指针 P1 已经围绕环走了一圈又回到了入口节点。
前面在判断链表中是否有环时用到了一快一慢两个指针。如果两个指针相遇,则表明链表中存在环。两个指针之所以会相遇是因为快的指针绕环一圈追上慢的指针,因此它们相遇的节点一定是在环中。可以从这个相遇的节点出发一边继续向前移动一边计数,当再次回到这个节点时就可以得到环中节点的数目。
当后面的指针到达环的入口节点时,前面的指针比它多走了_k_步,而_k_是环中节点的数目的整数倍,相当于前面的指针在环中转了_k_圈后也到达环的入口节点,两个指针正好相遇。也就是说,两个指针相遇的节点正好是环的入口节点。
可以在重合的两个链表的基础上构造一个包含环的链表。
如果两个链表有重合节点,那么这些重合节点一定只出现在链表的尾部。
在单向链表中,每个节点只有一个 next 指针,因此在第 1 个重合节点开始之后它们的所有节点都是重合的,不可能再出现分叉。
可以用栈来解决这个问题:分别把两个链表的节点放入两个栈,这样两个链表的尾节点就位于两个栈的栈顶。接下来比较两个栈的栈顶节点是否相同。如果相同,则把栈顶节点弹出,然后比较下一个栈顶节点,直到找到最后一个相同的节点。
有些面试题只有从链表尾节点开始遍历到头节点才容易解决。这个时候可以先将链表反转,然后在反转的链表中从头到尾遍历,这就相当于在原来的链表中从尾到头遍历。
在调整节点_j_的 next 指针时,除了需要知道节点_j_本身,还需要知道节点_j_的前一个节点_i_,这是因为需要把节点_j_的 next 指针指向节点_i_。同时,还需要事先保存节点_j_的下一个节点_k_,以防止链表断开。因此,在遍历链表逐个反转每个节点的 next 指针时需要用到 3 个指针,分别指向当前遍历到的节点、它的前一个节点及后一个节点。
这是一个看起来很简单的题目。很多应聘者的第一反应是根据链表求出整数,然后直接将两个整数相加,最后把结果用链表表示。这种思路的最大的问题是没有考虑到整数有可能会溢出。当链表较长时,表示的整数很大,可能会超出 int 甚至 long 的范围,如果根据链表求出整数就有可能会溢出。
把表示整数的链表反转。反转之后的链表的头节点表示个位数,尾节点表示最高位数。此时从两个链表的头节点开始相加,就相当于从整数的个位数开始相加。
在做加法时还需要注意的是进位。如果两个整数的个位数相加的和超过 10,就会往十位数产生一个进位。在下一步做十位数相加时就要把这个进位考虑进去。
一个值得注意的问题是,链表的节点总数既可能是奇数也可能是偶数。当链表的节点总数是奇数时,就要确保链表的前半段比后半段多一个节点。
反转链表是面试中经常出现的操作,所以能熟练、正确地编写出反转链表的代码非常重要。
由于单向链表只能从头节点开始遍历到尾节点,遍历的顺序受到限制,在很多场景下使用起来不是很方便,因此双向链表应运而生。双向链表在单向链表节点的基础上增加了指向前一个节点的指针,这样一来,既可以从头节点开始从前往后遍历到尾节点,也可以从尾节点开始从后往前遍历到头节点。
由于双向链表的每个节点多了一个指针,因此在双向链表中添加、删除节点等操作要稍微复杂一点。
如果把链表尾节点指向下一个节点的指针指向链表的头节点,那么此时链表就变成一个循环链表,相当于循环链表的所有节点都位于一个环中。循环链表既可以是单向链表也可以是双向链表。即使一个循环链表是单向链表,也可以从任意节点出发到达另一个任意节点,因此,在循环链表中任意节点都可以当作链表的头节点。
在面试时如果遇到这种类型的题目,应聘者需要先弄清楚展平的规则。
展平的规则是一个节点的子链展平之后将插入该节点和它的下一个节点之间。
由于子链表中的节点也可能有子链表,因此这里的链表是一个递归的结构。
递归函数 flattenGetTail 在展平以 head 为头节点的链表之后返回链表的尾节点。
在该函数中需要逐一扫描链表中的节点。如果一个节点 node 有子链表,由于子链表也可能有嵌套的子链表,因此先递归调用 flattenGetTail 函数展平子链表,子链表展平之后的头节点是 child,尾节点是 childTail。最后将展平的子链表插入节点 node 和它的下一个节点 next 之间,即把展平的子链表的头节点 child 插入节点 node 之后,并将尾节点 childTail 插入节点 next 之前。
这种解法每个节点都会遍历一次,如果链表总共有_n_个节点,那么时间复杂度是_O_(n)。函数 flattenGetTail 的递归调用次数取决于链表嵌套的层数,因此,如果链表的层数为_k_,那么该节点的空间复杂度是_O_(k)。
首先分析在排序的循环链表中插入节点的规律。当在图 4.15(a)的链表中插入值为 4 的节点时,新的节点位于值为 3 的节点和值为 5 的节点之间。这很容易理解,为了使插入新节点的循环链表仍然是排序的,新节点的前一个节点的值应该比新节点的值小,后一个节点的值应该比新节点的值大。
但是特殊情况需要特殊处理。如果新节点的值比链表中已有的最大值还要大,那么新的节点将被插入最大值和最小值之间。
新节点的值比链表中已有的最小值还要小的情形和前面类似,新的节点也将被插入最大值和最小值之间。
先试图在链表中找到相邻的两个节点,如果这两个节点的前一个节点的值比待插入的值小并且后一个节点的值比待插入的值大,那么就将新节点插入这两个节点之间。如果找不到符合条件的两个节点,即待插入的值大于链表中已有的最大值或小于已有的最小值,那么新的节点将被插入值最大的节点和值最小的节点之间。
如果开始的时候链表中的节点数小于 2,那么应该有两种可能。第 1 种可能是开始的时候链表是空的,一个节点都没有。此时插入一个新的节点,该节点成为循环链表中的唯一节点,那么 next 指针指向节点自己,如图 4.17(a)所示。第 2 种可能是开始的时候链表中只有一个节点,插入一个新的节点之后,两个节点的 next 指针互相指向对方,如图 4.17(b)所示。
将插入规则和各种边界条件都考虑清楚之后就可以开始编写代码。
在函数 insert 中先处理链表是空的或链表中只有一个节点的情况,然后调用函数 insertCore 处理链表中的节点数超过一个的情况。在函数 insertCore 中试图找到相邻的两个节点 cur 和 next,使 cur 的值小于或等于待插入的值且 next 的值大于或等于待插入的值。如果找到了就将新节点插入它们之间。如果没有找到符合条件的两个节点,就将新的节点插入值最大的节点 biggest 的后面。
如果一个操作可能产生新的头节点,则可以尝试在链表的最前面添加一个哨兵节点来简化代码逻辑,降低代码出现问题的可能性。
双指针是解决与链表相关的面试题的一种常用技术。前后双指针思路让一个指针提前走若干步,然后将第 2 个指针指向头节点,两个指针以相同的速度一起走。快慢双指针让快的指针每次走两步而慢的指针每次只走一步。
大部分与链表相关的面试题都是考查单向链表的操作。单向链表的特点是只能从前往后遍历而不能从后往前遍历。如果不得不从后往前遍历链表,则可以把链表反转之后再遍历。
如果链表中的节点除了有指向下一个节点的指针,还有指向前一个节点的指针,那么该链表就是双向链表。由于双向链表的操作牵涉到的指针比较多,因此应聘者在解决面试题的时候要格外小心,确保每个指针都指向了正确的位置。
循环链表是一种特殊形态的链表,它的所有节点都在一个环中。在解决与循环链表相关的面试题时需要特别注意避免死循环,遍历链表时等所有节点都遍历完就要停止,不能一直在里面绕圈子出不来。
标签:专项,遍历,指向,Offer,面试题,链表,节点,指针 来源: https://blog.csdn.net/lyh1106741606/article/details/120497855