7.面试算法-链表之高频面试题(二)

高频面试题(二)

1.重点+热点:链表反转以及5道变形题

链表反转是一个出现频率特别高的算法题,笔者过去这些年面试,至少遇到过七八次。其中更夸张的是曾经两天写了三次,上午YY,下午金山云,第二天快手。链表反转在各大高频题排名网站也长期占领前三。所以链表反转是我们学习链表最重要的问题,没有之一。

那为什么反转这么重要呢?因为反转链表涉及结点的增加、删除等多种操作,能非常有效考察对指针的驾驭能力和思维能力。

另外很多题目也都要用它来做基础, 例如指定区间反转、链表K个一组翻转。还有一些在内部的某个过程用到了反转,例如两个链表生成相加链表。还有一种是链表排序的,也是需要移动元素之间的指针,难度与此差不多。接下来我们就具体看一下每个题目。

1.1 反转一个链表

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
示例1:
输入:head = [1, 2,3, 4, 5]
输出: [5, 4, 3, 2, 1]

在这里插入图片描述
分析
这个题同样有至少三种方法,我们都应该会,因为都很重要,面试的时候可以根据需要写。

1.1.1 建立虚拟头结点辅助反转

对于链表问题,如何处理头结点是个比较麻烦的问题。很多场景下可以先建立一个虚拟的结点ans ,使得 ans.next=head ,这样可以很好的简化我们的操作。如下图所示。
在这里插入图片描述

首先我们可以将1接到ans的后面之后,后面每个元素,例如 2, 3 ,4, 5,我们都将其接到ans后面,这样已经组成链的1 2 3 4 将被逐渐甩到后面去了,所以当5成功插入到ans之后,整个链表的反转就完成了。这时候只要返回ans.next就得到反转的链表了。

当我们插入元素的时候,可以创建新的结点然后接到ans后面,也可以复用已有的结点,只是调整指针,相对来说,前面一种思维难度稍微低一些,但是往往会被面试官禁止,我们提倡使用后者。直接复用已有结点,只是调整指针的代码:

/**
* 方法1:虚拟结点, ,并复用已有的结点
* @param head
* @return
*/
public static ListNode reverseList(ListNode head) {
   
   
	ListNode ans = new ListNode(-1);
	ListNode cur = head;
	while (cur != null) {
   
   
		ListNode next = cur.next;
		cur.next = ans.next;
		ans.next = cur;
		cur = next;
	}
	return ans.next;
}
1.1.2 直接操作链表实现反转

如果不使用虚拟结点,同样可以选择创建新结点或者只调整指针,但是如果再定义一个新的会浪费空间,所以我们只看如何将每个结点的指向都反过来的方法:
在这里插入图片描述
那这里的问题就是如何准确的记录并调整指针,我们看执行期间的过程示意图:
在这里插入图片描述

在上图中,我们用cur来表示旧链表被访问的位置,也就是本轮要调整的结点,pre表示已经调整好的新链表的表头,next是先一个要调整的。注意图中箭头方向,cur和pre都是两个表的表头,每移动完一个结点之后,我们必须准确知道两个链表的表头。

cur是需要接到pre的,那该怎么知道其下一个结点5呢?代码也不算很复杂:

public ListNode reverseList(ListNode head ) {
   
   
	ListNode prev = null;
	ListNode curr = head;
	while (curr != null) {
   
   
		ListNode next = curr.next;
		curr.next = prev;
		prev = curr;
		curr = next;
	}
	return prev;
}

将上面这段代码在理解的基础上背下来,是的,因为这个算法太重要

1.1.3 拓展通过递归来实现

这个问题其实还有个递归方式反转,我们在讲解递归的时候会再来看该部分,这里只做了解。

public ListNode reverseList(ListNode head) {
   
   
	if (head == null || head.next == null) {
   
   
		return head;
	}
	ListNode newHead = reverseList(head.next);
	head.next.next = head;
	head.next = null;
	return newHead;
}

除了上面的基础方式,还有几个典型的考题也是面试经常见到的,我们一个个来看。

1.2 指定区间反转

题目要求
LeetCode92 :给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。
示例 1:
输入:head = [1, 2, 3, 4, 5], left = 2, right = 4
输出: [1, 4, 3, 2, 5]

图示:
在这里插入图片描述

1.2.1 穿针引线法

我们以反转下图中蓝色区域的链表反转为例。
在这里插入图片描述

我们可以这么做:先反转 left 到 right 部分,然后再将三段链表拼接起来。为此,我们还需要记录 left 的前一个节点,和 right 的后一个节点。如图所示:
在这里插入图片描述
算法步骤:

  • 第 1 步:先将待反转的区域反转;
  • 第 2 步:把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的 next 指针指向 succ。
    在这里插入图片描述
    编码细节我们直接看下方代码。思路想明白以后,编码不是一件很难的事情。这里要提醒大家的是,链接什么时候切断,什么时候补上去,先后顺序一定要想清楚,如果想不清楚,可以在纸上模拟,让思路清晰。
class Solution {
   
   
	public ListNode reverseBetween(ListNode head, int left, int right) {
   
    
		// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
		ListNode dummyNode = new ListNode(-1);
		dummyNode.next = head;

		ListNode pre = dummyNode;
		// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点 
		// 建议写在 for 循环里,语义清晰
		for (int i = 0; i < left - 1; i++) {
   
   
			pre = pre.next;
		}

		// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点 
		ListNode rightNode = pre;
		for (int i = 0; i < right - left + 1; i++) {
   
   
			rightNode = rightNode.next;
		}

		// 第 3 步:切断出一个子链表(截取链表) 
		ListNode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值