面试时链表解题的方法论
1)对于笔试,不需要太在乎空间复杂度,一切为了时间复杂度
2)对于面试,时间复杂度依然在第一位,一定要找到空间最省的方法
重要技巧
1)额外数据结构记录(哈希表)
2)快慢指针
反转单向链表(时间复杂度O(N),额外空间复杂度O(1))
反转链表,链表变成D→C→B-→A
思路:
链表反转,A的next是B经过反转,B的next变成A,即A.next.next =A
定义两个变量一个是next 一个是pre。pre记录上一个节点信息,next记录下一个节点信息,
当前元素A向右走, next = A.next,A.next = pre, pre设置为A,当前元素进下一个。
非递归形式
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
private static ListNode reverseSingleList(ListNode head) {
ListNode pre = null;
ListNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
head.prev = next;
head = next;
}
return head;
}
递归形式
一直遍历走到最右端节点C,发现右节点C到头了,回到B,设置B节点next节点的next为B,将B的next设置为空等待下一个人帮他填上,到A节点了,将A节点的next的next设置为A,A的next设置为空等待下一个人帮他,但是A是原链表的头节点没人帮他了,遍历结束。
private static ListNode reverseSingleList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode node = reverseSingleList(head.next);
head.next.next = head;
head.next = null;
return node;
}
反转单向链表(时间复杂度O(N),额外空间复杂度O(1))
反转链表,链表变成D→C→B-→A
思路:
链表反转,A的next是B经过反转,B的next变成A,即A.next.next =A
定义两个变量一个是next 一个是pre。pre记录上一个节点信息,next记录下一个节点信息,
当前元素A向右走, next = A.next,A.next = pre, A.pre设置为next, pre设置为A,当前元素进下一个。
非递归形式
private static ListNode reverseDoubleList(ListNode head) {
ListNode pre = null;
ListNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
head.prev = next;
pre = head;
head = next;
}
return head;
}
递归形式
private static ListNode reverseDoubleList(ListNode head) {
while (head == null || head.next == null) {
return head;
}
ListNode node = reverseDoubleList(head.next);
ListNode next = head.next;
next.next = head;
head.prev = next;
head.next = null;
return node;
}
打印两个有序链表的公共部分
【题目】 给定两个有序链表的头指针head1和head2,打印两个链表公共部分。
【要求】 如果两个链表长度之和为N,时间复杂度要求O(N),额外空间复杂度要求O(1)
有序链表,如果有公共部分,那么就需要比较两个链表的起始元素。需要A链表头元素与B链表元素大小,谁小谁向右移,如果一样大,一起移动,直到某个链表越界。
private static void intersectionList(ListNode first, ListNode second) {
while (first.next != null && second.next != null) {
if (first.val == second.val) {
first = first.next;
second = second.next;
System.out.println(first.val);
} else if (first.val < second.val) {
first = first.next;
} else {
second = second.next;
}
}
}
#判断一个链表是否为回文结构
【题目】 给定一个单链表的头节点head,请判断链表是否为回文结构。
【例子】1→2→1,返回true;1→2→2→1,返回true;15→6→15返回true,1→2→3返回false
【要求】 如果链表长度为N,时间复杂度要求O(N),额外空间复杂度要求O(1)
思路:
笔试:
定义一个栈将元素都入栈,pop元素和链表从头节点到尾节点是否一致。
面试:
- 使用快慢指针,快指针走两步,慢指针走2步,快指针越界,慢指针走到中间位置。
- 将慢指针后next进行逆序
private static boolean palindrome(ListNode node) {
ListNode quick = node;
ListNode slow = node;
//快慢指针,快指针走两步,慢指针走一步,快指针走到头了,慢指针到中间
/**
* 1→2→3→2→1
* 快指针走到1 、慢指针走到3 ;将3的next进行逆转变成1→2,比较
*
* 1→2→3→3→2→1
* 快指针走到1 、慢指针走到3 将3的next进行逆转变成1→2→3,比较
*/
while (quick.next != null && quick.next.next != null) {
quick = quick.next.next;
slow = slow.next;
}
//逆序
ListNode reverseNode = reverse(slow.next);
while (reverseNode != null) {
if (node.val != reverseNode.val) {
return false;
}
reverseNode = reverseNode.next;
node = node.next;
}
return true;
}
将单向链表按某值划分成左边小、中间相等、右边大的形式
【题目】给定一个单链表的头节点head,节点的值类型是整型,再给定一个整 数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的 节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。 。
【进阶】在实现原问题功能的基础上增加如下的要求 【要求】调整后所有小于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有等于pivot的节点之间的相对顺序和调整前一样
【要求】调整后所有大于pivot的节点之间的相对顺序和调整前一样
【要求】时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
思路:
笔试的时候,将链表变成数组,对数组进行荷兰国旗问题。
面试的时候:
借助6个元素
LH 、LT 小于链表头节点 小于链表尾节点
EH 、ET 小于链表头节点 小于链表尾节点
MH 、MT 小于链表头节点 小于链表尾节点
private static ListNode partitionList(ListNode node,
int partition) {
ListNode LH = null;
ListNode LT = null;
ListNode EH = null;
ListNode ET = null;
ListNode MH = null;
ListNode MT = null;
ListNode next = null;
//将之前的next清除,链表逆序、排序、设计到顺序修改的问题一定要把next值出来,并将其next设置为null
//非递归形式的修改链表顺序的,需要一个外部next节点作为游标,防止走乱套了
while (node != null) {
next = node.next;
node.next = null;
if (node.val < partition) {
if (LH == null) {
LH = node;
LT = node;
} else {
LT.next = node;
LT = node;
}
} else if (node.val > partition) {
if (MH == null) {
MH = node;
MT = node;
} else {
MT.next = node;
MT = node;
}
} else {
if (EH == null) {
EH = node;
ET = node;
} else {
ET.next = node;
ET = node;
}
}
node = next;
}
//现在要做的是把三块区域拼接起来,可能三块某一块是空的
//正常来说LT.next = EH
//ET.next= MH
if (LT != null) {
LT.next = EH;
//但是如果ET不存在,ST存在就需要ST直接和MH连,所以ST代替了ET位置。
ET = ET == null ? LT : ET;
}
if (ET != null) {
ET.next = MH;
}
return LH == null ? (EH == null ? MH : EH ): LH;
}
复制含有随机指针节点的链表
【题目】一种特殊的单链表节点类描述如下
class ListNode {
int val;
ListNode next;
ListNod rand;
ListNode(int x) {
val = x;
next = null;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节 点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度O(N),额外空间复杂度O(1)
思路:
笔试 使用hash,key为链表节点 value为new出来的node,然后从map中获取random
面试:节点拷贝,当前节点向下拷贝一个,random就是正常的random的下一个节点,然后将两条链表分开。
public static Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
//记住一个事情,while循环遍历节点时增加一个cursor,
//可以在外面和里面定义一个next,注意一定要有next
Node cursor = head;
while (cursor != null) {
Node next = cursor.next;
cursor.next = new Node(cursor.value);
cursor.next.next = next;
cursor = next;
}
//完成复制后进行完成复制random
cursor = head;
while (cursor != null) {
Node copy = cursor.next;
Node next = cursor.next.next;
//需要判断next是有random
copy.rand = cursor.rand == null ? null : cursor.rand.next;
cursor = next;
}
//进行剥离两个链表
cursor = head;
Node copyHead = cursor.next;
while (cursor != null) {
Node next = cursor.next.next;
Node copy = cursor.next;
cursor.next = next;
copy.next = next == null ? null : next.next;
cursor = next;
}
return copyHead;
}
两个单链表相交的一系列问题
【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。请实 现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返 回null
【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度 请达到O(1)。
无环单链表
-
两个单链表不相交
-
两个单链表相交
-
两个单链表肯定不会出现X型,因为对于链表来说,他不会有两个指针指向两个方向。
有环单链表
两个有环单链表相交,但是相交节点不一样
两个有环单链表不相交
两个有环单链表相交,但是相交节点一样
所以现在做的就是来确定他是否为有环链表,有环链表怎么区分:
快慢指针法
快指针走2步、慢指针走1步,如果快指针走到头了,next为null,那么他就是无环单链表。
public static Node acyclicSingleStrand(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node fast = head.next.next;
Node slow = head.next;
//如果有环快指针肯定会与慢指针有交集
while (fast != slow) {
//无环就是快指针后面没有节点了
if (fast.next == null || fast.next.next ==null) {
return null;
}
fast = fast.next.next;
slow = head.next;
}
//如slow等于fast,如果快慢指针有交集,快指针回到第一个节点,慢指针不动,然后继续走
//他俩第一个交集,就是第一个入环节点
fast = head;
while (fast != slow) {
fast = fast.next;
slow = fast.next;
}
return fast;
}
如果两个链表都是无环单链表,判断二者是否有交集
public static Node bothSingle(Node head1, Node head2) {
int cnt = 0;
Node cursor = head1;
//判断最后一个节点是否一样,如果一样则有环,不一样就是两个单独的单链表
while (cursor.next != null) {
cnt++;
cursor = cursor.next;
}
cursor = head2;
while (cursor.next != null) {
cnt -- ;
cursor = cursor.next;
}
//谁长谁就是node1
Node node1 = cnt > 0 ? head1 : head2;
//看看长的是不是head1,如果长的是head1,短的就是head2
Node node2 = node1 == head1 ? head1 : head2;
cnt = Math.abs(cnt);
while (cnt > 0) {
node1 = node1.next;
cnt --;
}
//因为上面已经判断是否为孤零零的两个链表,答案是否,所以肯定有交集
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
}
return node1;
}
如果是有环单链表,则需要判断两个链表入环节点是否一致 loop1 == loop2,如果一致的话,那么就是Y字型,与单链表求交集一致,如果不一致需要判断是否有交集,如果loop1 ,一直向后走,直到回到他本身都没有遇到loop2就说明这两个链表没有交集。
// 两个有环链表,返回第一个相交节点,如果不想交返回null
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int n = 0;
while (cur1 != loop1) {
n++;
cur1 = cur1.next;
}
while (cur2 != loop2) {
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}