【题目】
给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
【难度】
士 ★☆☆☆
【解答】
本题难度很低,因为是有序链表,所以从两个链表的头开始进行如下判断:
● 如果head1的值小于head2,则head1往下移动。
● 如果head2的值小于head1,则head2往下移动。
● 如果head1的值与head2的值相等,则打印这个值,然后head1与head2都往下移动。
● head1或head2有任何一个移动到null,整个过程停止。
具体过程参看如下代码中的printCommonPart方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public void printCommonPart(Node head1, Node head2) {
System.out.print("Common Part: ");
while (head1 ! = null && head2 ! = null) {
if (head1.value < head2.value) {
head1 = head1.next;
} else if (head1.value > head2.value) {
head2 = head2.next;
} else {
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
【题目】
分别实现两个函数,一个可以删除单链表中倒数第K 个节点,另一个可以删除双链表中倒数第K 个节点。
【要求】
如果链表长度为N ,时间复杂度达到O (N ),额外空间复杂度达到O (1)。
【难度】
士 ★☆☆☆
【解答】
本题较为简单,实现方式也是多种多样的,本书提供一种方法供读者参考。
先来看看单链表如何调整。如果链表为空或者K 值小于1,这种情况下,参数是无效的,直接返回即可。除此之外,让链表从头开始走到尾,每移动一步,就让K 的值减1。
链表:1->2->3,K = 4,链表根本不存在倒数第4个节点。
走到的节点:1 -> 2 -> 3
K 变化为:321
链表:1->2->3,K = 3,链表倒数第3个节点是1节点。
走到的节点:1 -> 2 -> 3
K 变化为:210
链表:1->2->3,K = 2,链表倒数第2个节点是2节点。
走到的节点:1 -> 2 -> 3
K 变化为:1 0 -1
由以上三种情况可知,让链表从头开始走到尾,每移动一步,就让K 值减1,当链表走到结尾时,如果K 值大于0,说明不用调整链表,因为链表根本没有倒数第K 个节点,此时将原链表直接返回即可;如果K 值等于0,说明链表倒数第K 个节点就是头节点,此时直接返回head.next,也就是原链表的第二个节点,让第二个节点作为链表的头返回即可,相当于删除头节点;接下来,说明一下如果K 值小于0,该如何处理。
先明确一点,如果要删除链表的头节点之后的某个节点,实际上需要找到要删除节点的前一个节点,比如:1->2->3,如果想删除节点2,则需要找到节点1,然后把节点1连到节点3上(1->3),以此来达到删除节点2的目的。
如果K 值小于0,如何找到要删除节点的前一个节点呢?方法如下:
1.重新从头节点开始走,每移动一步,就让K 的值加1。
2.当K 等于0时,移动停止,移动到的节点就是要删除节点的前一个节点。
这样做是非常好理解的,因为如果链表长度为N ,要删除倒数第K 个节点,很明显,倒数第K 个节点的前一个节点就是第N -K 个节点。在第一次遍历后,K 的值变为K -N 。第二次遍历时,K 的值不断加1,加到0就停止遍历,第二次遍历当然会停到第N -K 个节点的位置。
具体过程请参看如下代码中的removeLastKthNode方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node removeLastKthNode(Node head, int lastKth) {
if (head == null || lastKth < 1) {
return head;
}
Node cur = head;
while (cur ! = null) {
lastKth--;
cur = cur.next;
}
if (lastKth == 0) {
head = head.next;
}
if (lastKth < 0) {
cur = head;
while (++lastKth ! = 0) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return head;
}
对于双链表的调整,几乎与单链表的处理方式一样,注意last指针的重连即可。具体过程请参看如下代码中的removeLastKthNode方法。
public class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
this.value = data;
}
}
public DoubleNode removeLastKthNode(DoubleNode head, int lastKth) {
if (head == null || lastKth < 1) {
return head;
}
DoubleNode cur = head;
while (cur ! = null) {
lastKth--;
cur = cur.next;
}
if (lastKth == 0) {
head = head.next;
head.last = null;
}
if (lastKth < 0) {
cur = head;
while (++lastKth ! = 0) {
cur = cur.next;
}
DoubleNode newNext = cur.next.next;
cur.next = newNext;
if (newNext ! = null) {
newNext.last = cur;
}
}
return head;
}
【题目】
给定链表的头节点head,实现删除链表的中间节点的函数。
例如:
不删除任何节点;
1->2,删除节点1;
1->2->3,删除节点2;
1->2->3->4,删除节点2;
1->2->3->4->5,删除节点3;
进阶:
给定链表的头节点head、整数a和b,实现删除位于a/b处节点的函数。
例如:
链表:1->2->3->4->5,假设a/b的值为r。
如果r等于0,不删除任何节点;
如果r在区间(0,1/5]上,删除节点1;
如果r在区间(1/5,2/5]上,删除节点2;
如果r在区间(2/5,3/5]上,删除节点3;
如果r在区间(3/5,4/5]上,删除节点4;
如果r在区间(4/5,1]上,删除节点5;
如果r大于1,不删除任何节点。
【难度】
士 ★☆☆☆
【解答】
先来分析原问题,如果链表为空或者长度为1,不需要调整,则直接返回;如果链表的长度为2,将头节点删除即可;当链表长度到达3,应该删除第2个节点;当链表长度为4,应该删除第2个节点;当链表长度为5,应该删除第3个节点……也就是链表长度每增加2(3,5,7…),要删除的节点就后移一个节点。删除节点的问题在之前的题目中我们已经讨论过,如果要删除一个节点,则需要找到待删除节点的前一个节点。
具体过程请参看如下代码中的removeMidNode方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node removeMidNode(Node head) {
if (head == null || head.next == null) {
return head;
}
if (head.next.next == null) {
return head.next;
}
Node pre = head;
Node cur = head.next.next;
while (cur.next ! = null && cur.next.next ! = null) {
pre = pre.next;
cur = cur.next.next;
}
pre.next = pre.next.next;
return head;
}
接下来讨论进阶问题,首先需要解决的问题是,如何根据链表的长度n ,以及a与b的值决定该删除的节点是哪一个节点呢?根据如下方法:
先计算double r = ((double) (a * n)) / ((double) b)的值,然后r向上取整之后的整数值代表该删除的节点是第几个节点。
下面举几个例子来验证一下:
如果链表长度为7,a=5,b=7。
r = (7*5)/7 = 5.0,向上取整后为5,所以应该删除第5个节点。
如果链表长度为7,a=5,b=6。
r = (7*5)/6 = 5.8333…,向上取整后为6,所以应该删除第6个节点。
如果链表长度为7,a=1,b=6。
r = (7*1)/6 = 1.1666…,向上取整后为2,所以应该删除第2个节点。
知道该删除第几个节点之后,接下来找到需要删除节点的前一个节点即可。具体过程请参看如下代码中的removeByRatio方法。
public Node removeByRatio(Node head, int a, int b) {
if (a < 1 || a > b) {
return head;
}
int n = 0;
Node cur = head;
while (cur ! = null) {
n++;
cur = cur.next;
}
n = (int) Math.ceil(((double) (a * n)) / (double) b);
if (n == 1) {
head = head.next;
}
if (n > 1) {
cur = head;
while (--n ! = 1) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return head;
}
【题目】
分别实现反转单向链表和反转双向链表的函数。
【要求】
如果链表长度为N ,时间复杂度要求为O (N ),额外空间复杂度要求为O (1)。
【难度】
士 ★☆☆☆
【解答】
本题比较简单,读者做到代码一次成型,运行不出错即可。
反转单向链表的函数如下,函数返回反转之后链表新的头节点:
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node reverseList(Node head) {
Node pre = null;
Node next = null;
while (head ! = null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
反转双向链表的函数如下,函数返回反转之后链表新的头节点:
public DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
this.value = data;
}
}
public DoubleNode reverseList(DoubleNode head) {
DoubleNode pre = null;
DoubleNode next = null;
while (head ! = null) {
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return pre;
}
【题目】
给定一个单向链表的头节点head,以及两个整数from和to,在单向链表上把第from个节点到第to个节点这一部分进行反转。
例如:
1->2->3->4->5->null,from=2,to=4
调整结果为:1->4->3->2->5->null
再如:
1->2->3->null,from=1,to=3
调整结果为:3->2->1->null
【要求】
1.如果链表长度为N ,时间复杂度要求为O (N ),额外空间复杂度要求为O (1)。
2.如果不满足1<=from<=to<=N,则不用调整。
【难度】
士 ★☆☆☆
【解答】
本题有可能存在换头的问题,比如题目的第二个例子,所以函数应该返回调整后的新头节点,整个处理过程如下:
1.先判断是否满足1<=from<=to<=N,如果不满足,则直接返回原来的头节点。
2.找到第from-1个节点fPre和第to+1个节点tPos。fPre即是要反转部分的前一个节点,tPos是反转部分的后一个节点。把反转的部分先反转,然后正确地连接fPre和tPos。
例如:1->2->3->4->null,假设fPre为节点1,tPos为节点4,要反转部分为2->3。先反转成3->2,然后fPre连向节点3,节点2连向tPos,就变成了1->3->2->4->null。
3.如果fPre为null,说明反转部分是包含头节点的,则返回新的头节点,也就是没反转之前反转部分的最后一个节点,也是反转之后反转部分的第一个节点;如果fPre不为null,则返回旧的头节点。
全部过程请参看如下代码中的reversePart方法。
public Node reversePart(Node head, int from, int to) {
int len = 0;
Node node1 = head;
Node fPre = null;
Node tPos = null;
while (node1 ! = null) {
len++;
fPre = len == from - 1 ? node1 : fPre;
tPos = len == to + 1 ? node1 : tPos;
node1 = node1.next;
}
if (from > to || from < 1 || to > len) {
return head;
}
node1 = fPre == null ? head : fPre.next;
Node node2 = node1.next;
node1.next = tPos;
Node next = null;
while (node2 ! = tPos) {
next = node2.next;
node2.next = node1;
node1 = node2;
node2 = next;
}
if (fPre ! = null) {
fPre.next = node1;
return head;
}
return node1;
}
【题目】
据说著名犹太历史学家Josephus有过以下故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,报数到3的人就自杀,然后再由下一个人重新报1,报数到3的人再自杀,这样依次下去,直到剩下最后一个人时,那个人可以自由选择自己的命运。这就是著名的约瑟夫问题。现在请用单向环形链表描述该结构并呈现整个自杀过程。
输入:一个环形单向链表的头节点head和报数的值m 。
返回:最后生存下来的节点,且这个节点自己组成环形单向链表,其他节点都删掉。
进阶:
如果链表节点数为N ,想在时间复杂度为O (N )时完成原问题的要求,该怎么实现?
【难度】
原问题:士 ★☆☆☆
进阶:校 ★★★☆
【解答】
先来看看普通解法是如何实现的,其实非常简单,方法如下:
1.如果链表为空或者链表节点数为1,或者m 的值小于1,则不用调整就直接返回。
2.在环形链表中遍历每个节点,不断转圈,不断让每个节点报数。
3.当报数到达m 时,就删除当前报数的节点。
4.删除节点后,别忘了还要把剩下的节点继续连成环状,继续转圈报数,继续删除。
5.不停地删除,直到环形链表中只剩一个节点,过程结束。
普通的解法就像题目描述的过程一样,具体实现请参看如下代码中的josephusKill1方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node josephusKill1(Node head, int m) {
if (head == null || head.next == head || m < 1) {
return head;
}
Node last = head;
while (last.next ! = head) {
last = last.next;
}
int count = 0;
while (head ! = last) {
if (++count == m) {
last.next = head.next;
count = 0;
} else {
last = last.next;
}
head = last.next;
}
return head;
}
普通的解法在实现上不难,就是考查面试者基本的代码实现技巧,做到不出错即可。很明显的是,每删除掉一个节点,都需要遍历m 次,一共需要删除的节点数为n -1,所以普通解法的时间复杂度为O (n ×m ),这明显是不符合进阶要求的。
下面介绍进阶的解法。原问题之所以花费的时间多,是因为我们一开始不知道到底哪一个节点最后会活下来。所以依靠不断地删除来淘汰节点,当只剩下一个节点的时候,才知道是这个节点。如果不通过一直删除方式,有没有办法直接确定最后活下来的节点是哪一个呢?这就是进阶解法的实质。
举个例子,环形链表为:1->2->3->4->5->1,这个链表节点数为n =5,m =3。
通过不断删除的方式,最后节点4会活下来。但我们可以不用一直删除的方式,而是用进阶的方法,根据n 与m 的值,直接算出是第4个节点最终会活下来,接下来找到节点4即可。
那到底怎么直接算出来呢?首先,如果环形链表节点数为n ,我们做如下定义:从这个环形链表的头节点开始编号,头节点编号为1,头节点的下一个节点编号为2,……,最后一个节点编号为n 。然后考虑如下问题:
最后只剩下一个节点,这个幸存节点在只由自己组成的环中编号为1,记为Num(1) = 1;
在由两个节点组成的环中,这个幸存节点的编号是多少呢?假设编号是Num(2);
……
在由i -1个节点组成的环中,这个幸存节点的编号是多少呢?假设编号是Num(i-1);
在由i 个节点组成的环中,这个幸存节点的编号是多少呢?假设编号是Num(i);
……
在由n 个节点组成的环中,这个幸存节点的编号是多少呢?假设编号是Num(n)。
我们已经知道Num(1) = 1,如果再确定Num(i-1)和Num(i)到底是什么关系,就可以通过递归过程求出Num(n)。Num(i-1)和Num(i)的关系分析如下:
1.假设现在圈中一共有i 个节点,从头节点开始报数,报1的是编号1的节点,报2的是编号2的节点,假设报A的是编号B的节点,则A和B的对应关系如下。
A B
1 1
2 2
... ...
i i
i
+1 1
i
+2 2
... ...
2i i
2i
+1 1
2i
+2 2
… …
举个例子,环形链表有3个节点,报1的是编号1,报2的是编号2,报3的是编号3,报4的是编号1,报5的是编号2,报6的是编号3,报7的是编号1,报8的是编号2,报9的是编号3,报10的是编号1……
如上A和B的关系用数学表达式来表示可以写成:B=(A-1)%i+1。这个表达式不一定是唯一的,读者只要能写出准确概括A和B关系的式子就可以。总之,要找到报数(A)和编号节点(B)之间的关系。
2.如果编号为s的节点被删除,环的节点数自然从i 变成了i -1。那么原来在大小为i 的环中,每个节点的编号会发生什么变化呢?变化如下:
环大小为i的每个节点编号 删掉编号s的节点后,环大小为i-1的每个节点编号
... …
s-2 i
-2
s-1 i
-1
s —(无编号是因为被删掉了)
s+1 1
s+2 2
... …
新的环只有i -1个节点,因为有一个节点已经删掉。编号为s的节点往后,编号为s+1、s+2、s+3的节点就变成了新环中的编号为1、2、3的节点;编号为s的节点的前一个节点,也就是编号s-1的节点,就成了新环中的最后一个节点,也就是编号为i -1的节点。
假设环大小为i 的节点编号记为old,环大小为i -1的每个节点编号记为new,则old与new关系的数学表达式为:old=(new+s-1)%i+1。表达式同样不止一种,写出一种满足的即可。
3.因为每次都是报数到m 的节点被杀,所以根据步骤1的表达式B=(A-1)%i+1,A=m。被杀的节点编号为(m-1)%i+1,即s=(m-1)%i+1,带入到步骤2的表达式old=(new+s-1)%i+1中,经过化简为old=(new+m-1)%i+1。至此,我们终于得到了Num(i-1)—new和Num(i)—old的关系,且这个关系只和m 与i 的值有关。
整个进阶解法的过程总结为:
1.遍历链表,求链表的节点个数记为n ,时间复杂度为O (N )。
2.根据n 和m 的值,还有上文分析的Num(i-1)和Num(i)的关系,递归求生存节点的编号;这一步的具体过程请参看如下代码中的getLive方法,getLive方法为单决策的递归函数,且递归为N 层,所以时间复杂度为O (N )。
3.最后根据生存节点的编号,遍历链表找到该节点,时间复杂度为O (N )。
4.整个过程结束,总的时间复杂度为O (N )。
进阶解法的全部过程请参看如下代码中的josephusKill2方法。
public Node josephusKill2(Node head, int m) {
if (head == null || head.next == head || m < 1) {
return head;
}
Node cur = head.next;
int tmp = 1; // tmp -> list size
while (cur ! = head) {
tmp++;
cur = cur.next;
}
tmp = getLive(tmp, m); // tmp -> service node position
while (--tmp ! = 0) {
head = head.next;
}
head.next = head;
return head;
}
public int getLive(int i, int m) {
if (i == 1) {
return 1;
}
return (getLive(i - 1, m) + m - 1) % i + 1;
}
【题目】
给定一个链表的头节点head,请判断该链表是否为回文结构。
例如:
1->2->1,返回true。
1->2->2->1,返回true。
15->6->15,返回true。
1->2->3,返回false。
进阶:
如果链表长度为N ,时间复杂度达到O (N ),额外空间复杂度达到O (1)。
【难度】
普通解法 士 ★☆☆☆
进阶解法 尉 ★★☆☆
【解答】
方法一:
方法一是最容易实现的方法,利用栈结构即可。从左到右遍历链表,遍历的过程中把每个节点依次压入栈中。因为栈是先进后出的,所以在遍历完成后,从栈顶到栈底的节点值出现顺序会与原链表从左到右的值出现顺序反过来。那么,如果一个链表是回文结构,逆序之后,值出现的次序还是一样的,如果不是回文结构,顺序就肯定对不上。
例如:
链表1->2->3->4,从左到右依次压栈之后,从栈顶到栈底的节点值顺序为4,3,2,1。两者顺序对不上,所以这个链表不是回文结构。
链表1->2->2->1,从左到右依次压栈之后,从栈顶到栈底的节点值顺序为1,2,2,1。两者顺序一样,所以这个链表是回文结构。
方法一需要一个额外的栈结构,并且需要把所有的节点都压入栈中,所以这个额外的栈结构需要O (N )的空间。具体过程请参看如下代码中的isPalindrome1方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public boolean isPalindrome1(Node head) {
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while (cur ! = null) {
stack.push(cur);
cur = cur.next;
}
while (head ! = null) {
if (head.value ! = stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
方法二:
方法二对方法一进行了优化,虽然也是利用栈结构,但其实并不需要将所有的节点都压入栈中,只用压入一半的节点即可。首先假设链表的长度为N ,如果N 是偶数,前N /2的节点叫作左半区,后N /2的节点叫作右半区。如果N 是奇数,忽略处于最中间的节点,还是前N /2的节点叫作左半区,后N /2的节点叫作右半区。
例如:
链表1->2->2->1,左半区为:1,2;右半区为:2,1。
链表1->2->3->2->1,左半区为:1,2;右半区为:2,1。
方法二就是把整个链表的右半部分压入栈中,压入完成后,再检查栈顶到栈底值出现的顺序是否和链表左半部分的值相对应。
例如:
链表1->2->2->1,链表的右半部分压入栈中后,从栈顶到栈底为1,2。链表的左半部分也是1,2。所以这个链表是回文结构。
链表1->2->3->2->1,链表的右半部分压入栈中后,从栈顶到栈底为1,2。链表的左半部分也是1,2。所以这个链表是回文结构。
链表1->2->3->3->1,链表的右半部分压入栈中后,从栈顶到栈底为1,3。链表的左半部分也是1,2。所以这个链表不是回文结构。
方法二可以直观地理解为将链表的右半部分“折过去”,然后让它和左半部分比较,如图2-1所示。
图2-1
方法二的具体过程请参看如下代码中的isPalindrome2方法。
public boolean isPalindrome2(Node head) {
if (head == null || head.next == null) {
return true;
}
Node right = head.next;
Node cur = head;
while (cur.next ! = null && cur.next.next ! = null) {
right = right.next;
cur = cur.next.next;
}
Stack<Node> stack = new Stack<Node>();
while (right ! = null) {
stack.push(right);
right = right.next;
}
while (! stack.isEmpty()) {
if (head.value ! = stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
方法三:
方法三不需要栈和其他数据结构,只用有限几个变量,其额外空间复杂度为O (1),就可以在时间复杂度为O (N )内完成所有的过程,也就是满足进阶的要求。具体过程如下:
1.首先改变链表右半区的结构,使整个右半区反转,最后指向中间节点。
例如:
链表1->2->3->2->1,通过这一步将其调整之后的结构如图2-2所示。
图2-2
链表1->2->3->3->2->1,将其调整之后的结构如图2-3所示。
图2-3
我们将左半区的第一个节点(也就是原链表的头节点)记为leftStart,右半区反转之后最右边的节点(也就是原链表的最后一个节点)记为rightStart。
2.leftStart和rightStart同时向中间点移动,移动每一步都比较leftStart和rightStart节点的值,看是否一样。如果都一样,说明链表为回文结构,否则不是回文结构。
3.不管最后返回的是true还是false,在返回前都应该把链表恢复成原来的样子。
4.链表恢复成原来的结构之后,返回检查结果。
粗看起来,虽然方法三的整个过程也没有多少难度,但要想用有限几个变量完成以上所有的操作,在实现上还是比较考查代码实现能力的。方法三的全部过程请参看如下代码中的isPalindrome3方法,该方法只申请了三个Node类型的变量。
public boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
while (n2.next ! = null && n2.next.next ! = null) { // 查找中间节点
n1 = n1.next; // n1 -> 中部
n2 = n2.next.next; // n2 -> 结尾
}
n2 = n1.next; // n2 -> 右部分第一个节点
n1.next = null; // mid.next -> null
Node n3 = null;
while (n2 ! = null) { // 右半区反转
n3 = n2.next; // n3 -> 保存下一个节点
n2.next = n1; // 下一个反转节点
n1 = n2; // n1 移动
n2 = n3; // n2 移动
}
n3 = n1; // n3 -> 保存最后一个节点
n2 = head; // n2 -> 左边第一个节点
boolean res = true;
while (n1 ! = null && n2 ! = null) { // 检查回文
if (n1.value ! = n2.value) {
res = false;
break;
}
n1 = n1.next; // 从左到中部
n2 = n2.next; // 从右到中部
}
n1 = n3.next;
n3.next = null;
while (n1 ! = null) { // 恢复列表
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
【题目】
给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。除这个要求外,对调整后的节点顺序没有更多的要求。
例如:链表9->0->4->5->1,pivot=3。
调整后链表可以是1->0->4->9->5,也可以是0->1->9->5->4。总之,满足左部分都是小于3的节点,中间部分都是等于3的节点(本例中这个部分为空),右部分都是大于3的节点即可。对某部分内部的节点顺序不做要求。
进阶:
在原问题的要求之上再增加如下两个要求。
● 在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左到右的顺序与原链表中节点的先后次序一致。
例如:链表9->0->4->5->1,pivot=3。调整后的链表是0->1->9->4->5。在满足原问题要求的同时,左部分节点从左到右为0、1。在原链表中也是先出现0,后出现1;中间部分在本例中为空,不再讨论;右部分节点从左到右为9、4、5。在原链表中也是先出现9,然后出现4,最后出现5。
● 如果链表长度为N ,时间复杂度请达到O (N ),额外空间复杂度请达到O (1)。
【难度】
尉 ★★☆☆
【解答】
普通解法的时间复杂度为O (N ),额外空间复杂度为O (N ),就是把链表中的所有节点放入一个额外的数组中,然后统一调整位置的办法。具体过程如下:
1.先遍历一遍链表,为了得到链表的长度,假设长度为N 。
2.生成长度为N 的Node类型的数组nodeArr,然后遍历一次链表,将节点依次放进nodeArr中。本书在这里不用LinkedList或ArrayList等Java提供的结构,因为一个纯粹的数组结构比较利于步骤3的调整。
3.在nodeArr中把小于pivot的节点放在左边,把相等的放中间,把大于的放在右边。也就是改进了快速排序中partition的调整过程,即如下代码中的arrPartition方法。实现的具体解释请参看本书“数组类似partition的调整”问题,这里不再详述。
4.经过步骤3的调整后,nodeArr是满足题目要求的节点顺序,只要把nodeArr中的节点依次重连起来即可,整个过程结束。
全部过程请参看如下代码中的listPartition1方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node listPartition1(Node head, int pivot) {
if (head == null) {
return head;
}
Node cur = head;
int i = 0;
while (cur ! = null) {
i++;
cur = cur.next;
}
Node[] nodeArr = new Node[i];
i = 0;
cur = head;
for (i = 0; i ! = nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr, pivot);
for (i = 1; i ! = nodeArr.length; i++) {
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[i - 1].next = null;
return nodeArr[0];
}
public void arrPartition(Node[] nodeArr, int pivot) {
int small = -1;
int big = nodeArr.length;
int index = 0;
while (index ! = big) {
if (nodeArr[index].value < pivot) {
swap(nodeArr, ++small, index++);
} else if (nodeArr[index].value == pivot) {
index++;
} else {
swap(nodeArr, --big, index);
}
}
}
public void swap(Node[] nodeArr, int a, int b) {
Node tmp = nodeArr[a];
nodeArr[a] = nodeArr[b];
nodeArr[b] = tmp;
}
下面来看看增加要求之后的进阶解法。对每部分都增加了节点顺序要求,同时时间复杂度仍然为O (N ),额外空间复杂度为O (1)。既然额外空间复杂度为O (1),说明实现时只能使用有限的几个变量来完成所有的调整。
进阶解法的具体过程如下:
1.将原链表中的所有节点依次划分进三个链表,三个链表分别为small代表左部分,equal代表中间部分,big代表右部分。
例如,链表7->9->1->8->5->2->5,pivot=5。在划分之后,small、equal、big分别为:
small:1->2->null
equal:5->5->null
big:7->9->8->null
2.将small、equal和big三个链表重新串起来即可。
3.整个过程需要特别注意对null节点的判断和处理。
进阶解法还是主要考查面试者利用有限几个变量调整链表的代码实现能力,全部进阶解法请参看如下代码中的listPartition2方法。
public static Node listPartition2(Node head, int pivot) {
Node sH = null; // 小的头
Node sT = null; // 小的尾
Node eH = null; // 相等的头
Node eT = null; // 相等的尾
Node bH = null; // 大的头
Node bT = null; // 大的尾
Node next = null; // 保存下一个节点
// 所有的节点分进三个链表中
while (head ! = null) {
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (bH == null) {
bH = head;
bT = head;
} else {
bT.next = head;
bT = head;
}
}
head = next;
}
// 小的和相等的重新连接
if (sT ! = null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
// 所有的重新连接
if (eT ! = null) {
eT.next = bH;
}
return sH ! = null ? sH : eH ! = null ? eH : bH;
}
【题目】
一种特殊的链表节点类描述如下:
public class Node {
public int value;
public Node next;
public Node rand;
public Node(int data) {
this.value = data;
}
}
Node类中的value是节点值,next指针和正常单链表中next指针的意义一样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。
给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。例如:链表1->2->3->null,假设1的rand指针指向3,2的rand指针指向null,3的rand指针指向1。复制后的链表应该也是这种结构,比如,1′->2′->3′->null,1′的rand指针指向3′,2′的rand指针指向null,3′的rand指针指向1′,最后返回1′。
进阶:不使用额外的数据结构,只用有限几个变量,且在时间复杂度为O (N )内完成原问题要实现的函数。
【难度】
尉 ★★☆☆
【解答】
首先介绍普通解法,普通解法可以做到时间复杂度为O (N ),额外空间复杂度为O (N ),需要使用到哈希表(HashMap)结构。具体过程如下:
1.首先从左到右遍历链表,对每个节点都复制生成相应的副本节点,然后将对应关系放入哈希表map中。例如,链表1->2->3->null,遍历1、2、3时依次生成1′、2′、3′,最后将对应关系放入map中:
| key | value | 意义 |
| 1 | 1′ | 表示节点1复制了节点1′ |
| 2 | 2′ | 表示节点2复制了节点2′ |
| 3 | 3′ | 表示节点3复制了节点3′ |
步骤1完成后,原链表没有任何变化,每一个副本节点的next和rand指针都指向null。
2.再从左到右遍历链表,此时就可以设置每一个副本节点的next和rand指针。
例如:原链表1->2->3->null,假设1的rand指针指向3,2的rand指针指向null,3的rand指针指向1。遍历到节点1时,可以从map中得到节点1的副本节点1′,节点1的next指向节点2,所以从map中得到节点2的副本节点2′,然后令1′.next=2′,副本节点1′的next指针就设置好了。同时节点1的rand指向节点3,所以从map中得到节点3的副本节点3′,然后令1′.rand=3′,副本节点1′的rand指针也设置好了。以这种方式可以设置每一个副本节点的next与rand指针。
3.将1′节点作为结果返回即可。
哈希表增删改查的操作时间复杂度都是O (1),普通方法一共只遍历链表两遍,所以普通解法的时间复杂度为O (N ),因为使用了哈希表来保存原节点与副本节点的对应关系,所以额外空间复杂度为O (N )。
具体过程请参看如下代码中的copyListWithRand1方法。
public Node copyListWithRand1(Node head) {
HashMap<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
while (cur ! = null) {
map.put(cur, new Node(cur.value));
cur = cur.next;
}
cur = head;
while (cur ! = null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
接下来介绍进阶解法,进阶解法不使用哈希表来保存对应关系,而只用有限的几个变量完成所有的功能。具体过程如下:
1.首先从左到右遍历链表,对每个节点cur都复制生成相应的副本节点copy,然后把copy放在cur和下一个要遍历节点的中间。
例如:原链表1->2->3->null,在步骤1中完成后,原链表变成1->1′->2->2′->3->3′->null。
2.再从左到右遍历链表,在遍历时设置每一个副本节点的rand指针。还是举例来说明调整过程。
例如:此时链表为1->1′->2->2′->3->3′->null,假设1的rand指针指向3,2的rand指针指向null,3的rand指针指向1。遍历到节点1时,节点1的下一个节点1.next就是其副本节点1′。1的rand指针指向3,所以1′的rand指针应该指向3′。如何找到3′呢?因为每个节点的副本节点都在自己的后一个,所以此时通过3.next就可以找到3′,令1′.next=3′即可。以这种方式可以设置每一个副本节点的rand指针。
3.步骤2完成后,节点1,2,3,……之间的rand关系没有任何变化,节点1′,2′,3′……之间的rand关系也被正确设置了,此时所有的节点与副本节点串在一起,将其分离出来即可。
例如:此时链表为1->1′->2->2′->3->3′->null,分离成1->2->3->null和1′->2′->3′->null即可。并且在这一步中,每个节点的rand指针不用做任何调整,在步骤2中都已经设置好。
4.将1′节点作为结果返回即可。
进阶解法考查的依然是利用有限几个变量完成链表调整的代码实现能力。具体过程请参看如下代码中的copyListWithRand2方法。
public Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next = null;
// 复制并链接每一个节点
while (cur ! = null) {
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
// 设置复制节点的rand指针
while (cur ! = null) {
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand ! = null ? cur.rand.next : null;
cur = next;
}
Node res = head.next;
cur = head;
// 拆分
while (cur ! = null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next ! = null ? next.next : null;
cur = next;
}
return res;
}
【题目】
假设链表中每一个节点的值都在0~9之间,那么链表整体就可以代表一个整数。
例如:9->3->7,可以代表整数937。
给定两个这种链表的头节点head1和head2,请生成代表两个整数相加值的结果链表。
例如:链表1为9->3->7,链表2为6->3,最后生成新的结果链表为1->0->0->0。
【难度】
士 ★☆☆☆
【解答】
这道题难度较低,考查面试者基本的代码实现能力。一种实现方式是将两个链表先算出各自所代表的整数,然后求出两个整数的和,最后将这个和转换成链表的形式,但是这种方法有一个很大的问题,链表的长度可以很长,可以表达一个很大的整数,因此转成系统中的int类型时可能会溢出,所以不推荐这种方法。
方法一:利用栈结构求解。
1.将两个链表分别从左到右遍历,遍历过程中将值压栈,这样就生成了两个链表节点值的逆序栈,分别表示为s1和s2。
例如:链表9->3->7,s1从栈顶到栈底为7,3,9;链表6->3,s2从栈顶到栈底为3,6。
2.将s1和s2同步弹出,这样就相当于两个链表从低位到高位依次弹出,在这个过程中生成相加链表即可,同时需要关注每一步是否有进位,用ca表示。
例如:s1先弹出7,s2先弹出3,这一步相加结果为10,产生了进位,令ca=1,然后生成一个节点值为0的新节点,记为new1; s1再弹出3,s2再弹出6,这时进位为ca=1,所以这一步相加结果为10,继续产生进位,仍令ca=1,然后生成一个节点值为0的新节点记为new2,令new2.next=new1; s1再弹出9,s2为空,这时ca=1,这一步相加结果为10,仍令ca=1,然后生成一个节点值为0的新节点,记为new3,令new3.next=new2。这一步也是模拟简单的从低位到高位进位相加的过程。
3.当s1和s2都为空时,还要关注一下进位信息是否为1,如果为1,比如步骤2中的例子,表示还要生成一个节点值为1的新节点,记为new4,令new4.next=new3。
4.返回新生成的结果链表即可。
具体过程请参看如下代码中的addLists1方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node addLists1(Node head1, Node head2) {
Stack<Integer> s1 = new Stack<Integer>();
Stack<Integer> s2 = new Stack<Integer>();
while (head1 ! = null) {
s1.push(head1.value);
head1 = head1.next;
}
while (head2 ! = null) {
s2.push(head2.value);
head2 = head2.next;
}
int ca = 0;
int n1 = 0;
int n2 = 0;
int n = 0;
Node node = null;
Node pre = null;
while (! s1.isEmpty() || ! s2.isEmpty()) {
n1 = s1.isEmpty() ? 0 : s1.pop();
n2 = s2.isEmpty() ? 0 : s2.pop();
n = n1 + n2 + ca;
pre = node;
node = new Node(n % 10);
node.next = pre;
ca = n / 10;
}
if (ca == 1) {
pre = node;
node = new Node(1);
node.next = pre;
}
return node;
}
方法二:利用链表的逆序求解,可以省掉用栈的空间。
1.将两个链表逆序,这样就可以依次得到从低位到高位的数字。
例如:链表9->3->7,逆序后变为7->3->9;链表6->3,逆序后变为3->6。
2.同步遍历两个逆序后的链表,这样就依次得到两个链表从低位到高位的数字,在这个过程中生成相加链表即可,同时需要关注每一步是否有进位,用ca表示。具体过程与方法一的步骤2相同。
3.当两个链表都遍历完成后,还要关注进位信息是否为1,如果为1,还要生成一个节点值为1的新节点。
4.将两个逆序的链表再逆序一次,即调整成原来的样子。
5.返回新生成的结果链表。
具体过程请参看如下代码中的addLists2方法。
public Node addLists2(Node head1, Node head2) {
head1 = reverseList(head1);
head2 = reverseList(head2);
int ca = 0;
int n1 = 0;
int n2 = 0;
int n = 0;
Node c1 = head1;
Node c2 = head2;
Node node = null;
Node pre = null;
while (c1 ! = null || c2 ! = null) {
n1 = c1 ! = null ? c1.value : 0;
n2 = c2 ! = null ? c2.value : 0;
n = n1 + n2 + ca;
pre = node;
node = new Node(n % 10);
node.next = pre;
ca = n / 10;
c1 = c1 ! = null ? c1.next : null;
c2 = c2 ! = null ? c2.next : null;
}
if (ca == 1) {
pre = node;
node = new Node(1);
node.next = pre;
}
reverseList(head1);
reverseList(head2);
return node;
}
public Node reverseList(Node head) {
Node pre = null;
Node next = null;
while (head ! = null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
【题目】
在本题中,单链表可能有环,也可能无环。给定两个单链表的头节点head1和head2,这两个链表可能相交,也可能不相交。请实现一个函数,如果两个链表相交,请返回相交的第一个节点;如果不相交,返回null即可。
要求:如果链表1的长度为N ,链表2的长度为M ,时间复杂度请达到O (N +M ),额外空间复杂度请达到O (1)。
【难度】
将 ★★★★
【解答】
这道题需要分析的情况非常多,同时因为有额外空间复杂度为O (1)的限制,所以实现起来也比较困难。
本题可以拆分成三个子问题,每个问题都可以作为一道独立的算法题,具体如下。
问题一:如何判断一个链表是否有环,如果有,则返回第一个进入环的节点,没有则返回null。
问题二:如何判断两个无环链表是否相交,相交则返回第一个相交节点,不相交则返回null。
问题三:如何判断两个有环链表是否相交,相交则返回第一个相交节点,不相交则返回null。
注意:如果一个链表有环,另外一个链表无环,它们是不可能相交的,直接返回null。
下面逐一分析每个问题。
问题一:如何判断一个链表是否有环,如果有,则返回第一个进入环的节点,没有则返回null。
如果一个链表没有环,那么遍历链表一定可以遇到链表的终点;如果链表有环,那么遍历链表就永远在环里转下去了。如何找到第一个入环节点,具体过程如下:
1.设置一个慢指针slow和一个快指针fast。在开始时,slow和fast都指向链表的头节点head。然后slow每次移动一步,fast每次移动两步,在链表中遍历起来。
2.如果链表无环,那么fast指针在移动的过程中一定先遇到终点,一旦fast到达终点,说明链表是没有环的,直接返回null,表示该链表无环,当然也没有第一个入环的节点。
3.如果链表有环,那么fast指针和slow指针一定会在环中的某个位置相遇,当fast和slow相遇时,fast指针重新回到head的位置,slow指针不动。接下来,fast指针从每次移动两步改为每次移动一步,slow指针依然每次移动一步,然后继续遍历。
4.fast指针和slow指针一定会再次相遇,并且在第一个入环的节点处相遇。证明略。
注意:你也可以用哈希表完成问题一的判断,但是不符合题目关于空间复杂度的要求。
问题一的具体实现请参看如下代码中的getLoopNode方法。
public Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node n1 = head.next; // n1 -> slow
Node n2 = head.next.next; // n2 -> fast
while (n1 ! = n2) {
if (n2.next == null || n2.next.next == null) {
return null;
}
n2 = n2.next.next;
n1 = n1.next;
}
n2 = head; // n2 -> walk again from head
while (n1 ! = n2) {
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
如果解决了问题一,我们就知道了两个链表有环或者无环的情况。如果一个链表有环,另一个链表无环,那么这两个链表是无论如何也不可能相交的。能相交的情况就分为两种,一种是两个链表都无环,即问题二;另一种是两个链表都有环,即问题三。
问题二:如何判断两个无环链表是否相交,相交则返回第一个相交节点,不相交则返回null。
如果两个无环链表相交,那么从相交节点开始,一直到两个链表终止的这一段,是两个链表共享的。解决问题二的具体过程如下:
1.链表1从头节点开始,走到最后一个节点(不是结束),统计链表1的长度记为len1,同时记录链表1的最后一个节点记为end1。
2.链表2从头节点开始,走到最后一个节点(不是结束),统计链表2的长度记为len2,同时记录链表2的最后一个节点记为end2。
3.如果end1! =end2,说明两个链表不相交,返回null即可;如果end==end2,说明两个链表相交,进入步骤4来找寻第一个相交节点。
4.如果链表1比较长,链表1就先走len1-len2步;如果链表2比较长,链表2就先走len2-len1步。然后两个链表一起走,一起走的过程中,两个链表第一次走到一起的那个节点,就是第一个相交的节点。
例如:链表1长度为100,链表2长度为30,如果已经由步骤3确定了链表1和链表2一定相交,那么接下来,链表1先走70步,然后链表1和链表2一起走,它们一定会共同进入第一个相交的节点。
问题二的具体实现请参看如下代码中的noLoop方法。
public Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next ! = null) {
n++;
cur1 = cur1.next;
}
while (cur2.next ! = null) {
n--;
cur2 = cur2.next;
}
if (cur1 ! = cur2) {
return null;
}
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;
}
问题三:如何判断两个有环链表是否相交,相交则返回第一个相交节点,不相交则返回null。
考虑问题三的时候,我们已经得到了两个链表各自的第一个入环节点,假设链表1的第一个入环节点记为loop1,链表2的第一个入环节点记为loop2。以下是解决问题三的过程:
1.如果loop1==loop2,那么两个链表的拓扑结构如图2-4所示。
图2-4
这种情况下,我们只要考虑链表1从头开始到loop1这一段与链表2从头开始到loop2这一段,在那里第一次相交即可,而不用考虑进环该怎么处理,这就与问题二类似,只不过问题二是把null作为一个链表的终点,而这里是把loop1(loop2)作为链表的终点。但是判断的主要过程是相同的。
2.如果loop1! =loop2,两个链表不相交的拓扑结构如图2-5所示。两个链表相交的拓扑结构如图2-6所示。
图2-5
图2-6
如何分辨是这两种拓扑结构的哪一种呢?进入步骤3。
3.让链表1从loop1出发,因为loop1和之后的所有节点都在环上,所以将来一定能回到loop1。如果回到loop1之前并没有遇到loop2,说明两个链表的拓扑结构如图2-5所示,也就是不相交,直接返回null;如果回到loop1之前遇到了loop2,说明两个链表的拓扑结构如图2-6所示,也就是相交。因为loop1和loop2都在两条链表上,只不过loop1是离链表1较近的节点,loop2是离链表2较近的节点。所以,此时返回loop1或loop2都可以。
问题三的具体实现参看如下代码中的bothLoop方法。
public 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;
}
}
全部过程参看如下代码中的getIntersectNode方法,这也是整个题目的主方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
if (loop1 ! = null && loop2 ! = null) {
return bothLoop(head1, loop1, head2, loop2);
}
return null;
}
【题目】
给定一个单链表的头节点head,实现一个调整单链表的函数,使得每K 个节点之间逆序,如果最后不够K 个节点一组,则不调整最后几个节点。
例如:
链表:1->2->3->4->5->6->7->8->null,K =3。
调整后为:3->2->1->6->5->4->7->8->null。其中7、8不调整,因为不够一组。
【难度】
尉 ★★☆☆
【解答】
首先,如果K 的值小于2,不用进行任何调整。因为K <1没有意义,K==1时,代表每1个节点为1组进行逆序,原链表也没有任何变化。接下来介绍两种方法,如果链表长度为N ,方法一的时间复杂度为O (N ),额外空间复杂度为O (K )。方法二的时间复杂度为O (N ),额外空间复杂度为O (1)。本题考查面试者代码实现不出错的能力。
方法一:利用栈结构的解法。
1.从左到右遍历链表,如果栈的大小不等于K ,就将节点不断压入栈中。
2.当栈的大小第一次到达K 时,说明第一次凑齐了K 个节点进行逆序,从栈中依次弹出这些节点,并根据弹出的顺序重新连接,这一组逆序完成后,需要记录一下新的头部,同时第一组的最后一个节点(原来是头节点)应该连接下一个节点。
例如:链表1->2->3->4->5->6->7->8->null,K = 3。第一组节点进入栈,从栈顶到栈底依次为3,2,1。逆序重连之后为3->2->1->…,然后节点1去连接节点4,链表变为3->2->1->4->5->6->7->8->null,之后从节点4开始不断处理K 个节点为一组的后续情况,也就是步骤3,并且需要记录节点3,因为链表的头部已经改变,整个过程结束后需要返回这个新的头节点,记为newHead。
3.步骤2之后,当栈的大小每次到达K 时,说明又凑齐了一组应该进行逆序的节点,从栈中依次弹出这些节点,并根据弹出的顺序重新连接。这一组逆序完成后,该组的第一个节点(原来是该组最后一个节点)应该被上一组的最后一个节点连接上,这一组的最后一个节点(原来是该组第一个节点)应该连接下一个节点。然后继续去凑下一组,直到链表都被遍历完。
例如:链表3->2->1->4->5->6->7->8->null,K = 3,第一组已经处理完。第二组从栈顶到栈底依次为6,5,4。逆序重连之后为6->5->4,然后节点6应该被节点1连接,节点4应该连接节点7,链表变为3->2->1->6->5->4->7->8->null。然后继续从节点7往下遍历。
4.最后应该返回newHead,作为链表新的头节点。
方法一的具体实现请参看如下代码中的reverseKNodes1方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node reverseKNodes1(Node head, int K) {
if (K < 2) {
return head;
}
Stack<Node> stack = new Stack<Node>();
Node newHead = head;
Node cur = head;
Node pre = null;
Node next = null;
while (cur ! = null) {
next = cur.next;
stack.push(cur);
if (stack.size() == K) {
pre = resign1(stack, pre, next);
newHead = newHead == head ? cur : newHead;
}
cur = next;
}
return newHead;
}
public Node resign1(Stack<Node> stack, Node left, Node right) {
Node cur = stack.pop();
if (left ! = null) {
left.next = cur;
}
Node next = null;
while (! stack.isEmpty()) {
next = stack.pop();
cur.next = next;
cur = next;
}
cur.next = right;
return cur;
}
方法二:不需要栈结构,在原链表中直接调整。
用变量记录每一组开始的第一个节点和最后一个节点,然后直接逆序调整,把这一组的节点都逆序。和方法一一样,同样需要注意第一组节点的特殊处理,以及之后的每个组在逆序重连之后,需要让该组的第一个节点(原来是最后一个节点)被之前组的最后一个节点连接上,将该组的最后一个节点(原来是第一个节点)连接下一个节点。
方法二的具体实现请参看如下代码中的reverseKNodes2方法。
public Node reverseKNodes2(Node head, int K) {
if (K < 2) {
return head;
}
Node cur = head;
Node start = null;
Node pre = null;
Node next = null;
int count = 1;
while (cur ! = null) {
next = cur.next;
if (count == K) {
start = pre == null ? head : pre.next;
head = pre == null ? cur : head;
resign2(pre, start, cur, next);
pre = start;
count = 0;
}
count++;
cur = next;
}
return head;
}
public void resign2(Node left, Node start, Node end, Node right) {
Node pre = start;
Node cur = start.next;
Node next = null;
while (cur ! = right) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
if (left ! = null) {
left.next = end;
}
start.next = right;
}
【题目】
给定一个无序单链表的头节点head,删除其中值重复出现的节点。
例如:1->2->3->3->4->4->2->1->1->null,删除值重复的节点之后为1->2->3->4->null。
请按以下要求实现两种方法。
方法1:如果链表长度为N ,时间复杂度达到O (N )。
方法2:额外空间复杂度为O (1)。
【难度】
士 ★☆☆☆
【解答】
方法一:利用哈希表。时间复杂度为O (N ),额外空间复杂度为O (N )。
具体过程如下:
1.生成一个哈希表,因为头节点是不用删除的节点,所以首先将头节点的值放入哈希表。
2.从头节点的下一个节点开始往后遍历节点,假设当前遍历到cur节点,先检查cur的值是否在哈希表中,如果在,则说明cur节点的值是之前出现过的,就将cur节点删除,删除的方式是将最近一个没有被删除的节点pre连接到cur的下一个节点,即pre.next=cur.next。如果不在,将cur节点的值加入哈希表,同时令pre=cur,即更新最近一个没有被删除的节点。
方法一的具体实现请参看如下代码中的removeRep1方法。
public Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public void removeRep1(Node head) {
if (head == null) {
return;
}
HashSet<Integer> set = new HashSet<Integer>();
Node pre = head;
Node cur = head.next;
set.add(head.value);
while (cur ! = null) {
if (set.contains(cur.value)) {
pre.next = cur.next;
} else {
set.add(cur.value);
pre = cur;
}
cur = cur.next;
}
}
方法二:类似选择排序的过程,时间复杂度为O (N 2 ),额外空间复杂度为O (1)。
例如,链表1->2->3->3->4->4->2->1->1->null。
首先是头节点,节点值为1,往后检查所有值为1的节点,全部删除。链表变为:1->2->3->3->4->4->2->null。
然后是第二个节点,节点值为2,往后检查所有值为2的节点,全部删除。链表变为:1->2->3->3->4->4->null。
接着是第三个节点,节点值为3,往后检查所有值为3的节点,全部删除。链表变为:1->2->3->4->4->null。
最后是第四个节点,节点值为4,往后检查所有值为4的节点,全部删除。链表变为:1->2->3->4->null。
删除过程结束。
方法二的具体实现请参看如下代码中的removeRep2方法。
public void removeRep2(Node head) {
Node cur = head;
Node pre = null;
Node next = null;
while (cur ! = null) {
pre = cur;
next = cur.next;
while (next ! = null) {
if (cur.value == next.value) {
pre.next = next.next;
} else {
pre = next;
}
next = next.next;
}
cur = cur.next;
}
}
【题目】
给定一个链表的头节点head和一个整数num,请实现函数将值为num的节点全部删除。
例如,链表为1->2->3->4->null,num=3,链表调整后为:1->2->4->null。
【难度】
士 ★☆☆☆
【解答】
方法一:利用栈或者其他容器收集节点的方法。时间复杂度为O (N ),额外空间复杂度为O (N )。
将值不等于num的节点用栈收集起来,收集完成后重新连接即可。最后将栈底的节点作为新的头节点返回,具体过程请参看如下代码中的removeValue1方法。
public Node removeValue1(Node head, int num) {
Stack<Node> stack = new Stack<Node>();
while (head ! = null) {
if (head.value ! = num) {
stack.push(head);
}
head = head.next;
}
while (! stack.isEmpty()) {
stack.peek().next = head;
head = stack.pop();
}
return head;
}
方法二:不用任何容器而直接调整的方法。时间复杂度为O (N ),额外空间复杂度为O (1)。
首先从链表头开始,找到第一个值不等于num的节点,作为新的头节点,这个节点是肯定不用删除的,记为newHead。继续往后遍历,假设当前节点为cur,如果cur节点值等于num,就将cur节点删除,删除的方式是将之前最近一个值不等于num的节点pre连接到cur的下一个节点,即pre.next=cur.next;如果cur节点值不等于num,就令pre=cur,即更新最近一个值不等于num的节点。
具体实现过程请参看如下代码中的removeValue2方法。
public Node removeValue2(Node head, int num) {
while (head ! = null) {
if (head.value ! = num) {
break;
}
head = head.next;
}
Node pre = head;
Node cur = head;
while (cur ! = null) {
if (cur.value == num) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
【题目】
对二叉树的节点来说,有本身的值域,有指向左孩子和右孩子的两个指针;对双向链表的节点来说,有本身的值域,有指向上一个节点和下一个节点的指针。在结构上,两种结构有相似性,现在有一棵搜索二叉树,请将其转换为一个有序的双向链表。
例如,节点定义为:
public class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
一棵搜索二叉树如图2-7所示。
图2-7
这棵搜索二叉树转换后的双向链表从头到尾依次是1~9。对每一个节点来说,原来的right指针等价于转换后的next指针,原来的left指针等价于转换后的last指针,最后返回转换后的双向链表头节点。
【难度】
尉 ★★☆☆
【解答】
方法一:用队列等容器收集二叉树中序遍历结果的方法。时间复杂度为O (N ),额外空间复杂度为O (N ),具体过程如下:
1.生成一个队列,记为queue,按照二叉树中序遍历的顺序,将每个节点放入queue中。
2.从queue中依次弹出节点,并按照弹出的顺序重连所有的节点即可。
方法一的具体实现请参看如下代码中的convert1方法。
public Node convert1(Node head) {
Queue<Node> queue = new LinkedList<Node>();
inOrderToQueue(head, queue);
if (queue.isEmpty()) {
return head;
}
head = queue.poll();
Node pre = head;
pre.left = null;
Node cur = null;
while (! queue.isEmpty()) {
cur = queue.poll();
pre.right = cur;
cur.left = pre;
pre = cur;
}
pre.right = null;
return head;
}
public void inOrderToQueue(Node head, Queue<Node> queue) {
if (head == null) {
return;
}
inOrderToQueue(head.left, queue);
queue.offer(head);
inOrderToQueue(head.right, queue);
}
方法二:利用递归函数,除此之外不使用任何容器的方法。时间复杂度为O (N ),额外空间复杂度为O (h ),h 为二叉树的高度,具体过程如下:
1.实现递归函数process。process的功能是将一棵搜索二叉树转换为一个结构有点特殊的有序双向链表。结构特殊是指这个双向链表尾节点的right指针指向该双向链表的头节点。函数process最终返回这个链表的尾节点。
例如:搜索二叉树只有一个节点时,在经过process处理后,形成如图2-8所示的形式,最后返回节点1。
图2-8
搜索二叉树较为一般的情况,在经过process处理后,变为如图2-9所示的形式,最后返回节点3。
图2-9
总之,process函数的功能是将一棵搜索二叉树变成有序的双向链表,然后让最大值节点的right指针指向最小值节点,最后返回最大值节点。
那么递归函数process应该如何实现呢?
假设一棵搜索二叉树如图2-10所示。
图2-10
节点4为头节点,先用process函数处理左子树,就将左子树转换成了有序双向链表,同时返回尾节点,记为leftE;再用process函数处理右子树,就将右子树转换成了有序双向链表,同时返回尾节点,记为rightE,如图2-11所示。
图2-11
接下来,把节点3(左子树process处理后的返回节点)的right指针连向节点4,节点4的left指针连向节点3,节点4的right指针连向节点5(右子树process处理后的返回节点为节点7,通过节点7的right指针可以找到节点5),节点5的left指针连向节点4,就完成了整个棵树向有序双向链表的转换。最后根据process函数的要求,把节点7(右子树process处理后的返回节点)的right指针连向节点1(左子树process处理后的返回节点为节点3,通过节点3的right指针可以找到节点1),然后返回节点7即可,如图2-12所示。
图2-12
一开始时把整棵树的头节点作为参数传进process函数,然后每棵子树都会经历递归函数process的过程,具体过程请参看如下代码中的process方法。
为什么要将有序双向链表的尾节点连接头节点之后再返回尾节点呢?因为用这种方式可以快速找到双向链表的头尾两端,从而省去了通过遍历过程才能找到两端的麻烦。
2.通过process过程得到的双向链表是尾节点的right指针连向头节点的结构。所以,最终需要将尾节点的right指针设置为null来让双向链表变成正常的样子。
方法二的具体实现请参看如下代码中的convert2方法。
public Node convert2(Node head) {
if (head == null) {
return null;
}
Node last = process(head);
head = last.right;
last.right = null;
return head;
}
public Node process(Node head) {
if (head == null) {
return null;
}
Node leftE = process(head.left); // 左边结束
Node rightE = process(head.right); // 右边结束
Node leftS = leftE ! = null ? leftE.right : null; // 左边开始
Node rightS = rightE ! = null ? rightE.right : null; // 右边开始
if (leftE ! = null && rightE ! = null) {
leftE.right = head;
head.left = leftE;
head.right = rightS;
rightS.left = head;
rightE.right = leftS;
return rightE;
} else if (leftE ! = null) {
leftE.right = head;
head.left = leftE;
head.right = leftS;
return head;
} else if (rightE ! = null) {
head.right = rightS;
rightS.left = head;
rightE.right = head;
return rightE;
} else {
head.right = head;
return head;
}
}
关于方法二中时间复杂度与空间复杂度的解释,可以用process递归函数发生的次数来估算时间复杂度,process会处理所有的子树,子树的数量就是二叉树节点的个数。所以时间复杂度为O (N ),process递归函数最多占用二叉树高度为h 的栈空间,所以额外空间复杂度为O (h )。
【扩展】
相信读者已经注意到,本题在复杂度方面能够达到的程度完全取决于二叉树遍历的实现,如果一个二叉树遍历的实现在时间和空间复杂度上足够好,那么本题就可以做到在时间复杂度和空间复杂度上同样好。如果二叉树的节点数为N ,有没有时间复杂度为O (N )、额外空间复杂度为O (1)的遍历实现呢?如果有这样的实现,那本题也一定有时间复杂度为O (N )、额外空间复杂度为O (1)的方法。既不用栈,也不用递归函数,只用有限的几个变量就可以实现,这样的遍历实现是有的。欢迎有兴趣的读者阅读本书“遍历二叉树的神级方法”问题,然后结合神级的遍历方法再重新实现这道题。
【题目】
给定一个无序单链表的头节点head,实现单链表的选择排序。
要求:额外空间复杂度为O (1)。
【难度】
士 ★☆☆☆
【解答】
既然要求额外空间复杂度为O (1),就不能把链表装进数组等容器中排序,排好序之后再重新连接,而是要求面试者在原链表上利用有限几个变量完成选择排序的过程。选择排序是从未排序的部分中找到最小值,然后放在排好序部分的尾部,逐渐将未排序的部分缩小,最后全部变成排好序的部分。本书实现的方法模拟了这个过程。
1.开始时默认整个链表都是未排序的部分,对于找到的第一个最小值节点,肯定是整个链表的最小值节点,将其设置为新的头节点记为newHead。
2.每次在未排序的部分中找到最小值的节点,然后把这个节点从未排序的链表中删除,删除的过程当然要保证未排序部分的链表在结构上不至于断开,例如,2->1->3,删除节点1之后,链表应该变成2->3,这就要求我们应该找到要删除节点的前一个节点。
3.把删除的节点(也就是每次的最小值节点)连接到排好序部分的链表尾部。
4.全部过程处理完后,整个链表都已经有序,返回newHead。
和选择排序一样,如果链表的长度为N ,时间复杂度为O (N 2 ),额外空间复杂度为O (1)。
本题依然是考查调整链表的代码技巧,具体过程请参看如下代码中的selectionSort方法。
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public static Node selectionSort(Node head) {
Node tail = null; // 排序部分尾部
Node cur = head; // 未排序部分头部
Node smallPre = null; // 最小节点的前一个节点
Node small = null; // 最小的节点
while (cur ! = null) {
small = cur;
smallPre = getSmallestPreNode(cur);
if (smallPre ! = null) {
small = smallPre.next;
smallPre.next = small.next;
}
cur = cur == small ? cur.next : cur;
if (tail == null) {
head = small;
} else {
tail.next = small;
}
tail = small;
}
return head;
}
public Node getSmallestPreNode(Node head) {
Node smallPre = null;
Node small = head;
Node pre = head;
Node cur = head.next;
while (cur ! = null) {
if (cur.value < small.value) {
smallPre = pre;
small = cur;
}
pre = cur;
cur = cur.next;
}
return smallPre;
}
【题目】
链表节点值类型为int型,给定一个链表中的节点node,但不给定整个链表的头节点。如何在链表中删除node?请实现这个函数,并分析这么会出现哪些问题。
要求:时间复杂度为O (1)。
【难度】
士 ★☆☆☆
【解答】
本题的思路很简单,举例就能说明具体的做法。
例如,链表1->2->3->null,只知道要删除节点2,而不知道头节点。那么只需把节点2的值变成节点3的值,然后在链表中删除节点3即可。
这道题目出现的次数很多,这么做看起来非常方便,但其实是有很大问题的。
问题一:这样的删除方式无法删除最后一个节点。还是以原示例来说明,如果知道要删除节点3,而不知道头节点。但它是最后的节点,根本没有下一个节点来代替节点3被删除,那么只有让节点2的next指向null这一种办法,而我们又根本找不到节点2,所以根本没法正确删除节点3。读者可能会问,我们能不能把节点3在内存上的区域变成null呢?这样不就相当于让节点2的next指针指向了null,起到节点3被删除的效果了吗?不可以。null在系统中是一个特定的区域,如果想让节点2的next指针指向null,必须找到节点2。
问题二:这种删除方式在本质上根本就不是删除了node节点,而是把node节点的值改变,然后删除node的下一个节点,在实际的工程中可能会带来很大问题。比如,工程上的一个节点可能代表很复杂的结构,节点值的复制会相当复杂,或者可能改变节点值这个操作都是被禁止的;再如,工程上的一个节点代表提供服务的一个服务器,外界对每个节点都有很多依赖,比如,示例中删除节点2时,其实影响了节点3对外提供的服务。
这种删除方式的具体过程请参看如下代码中的removeNodeWired方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public void removeNodeWired(Node node) {
if (node == null) {
return;
}
Node next = node.next;
if (next == null) {
throw new RuntimeException("can not remove last node.");
}
node.value = next.value;
node.next = next.next;
}
【题目】
一个环形单链表从头节点head开始不降序,同时由最后的节点指回头节点。给定这样一个环形单链表的头节点head和一个整数num,请生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。
【难度】
士 ★☆☆☆
【解答】
直接给出时间复杂度为O (N )、额外空间复杂度为O (1)的方法。具体过程如下:
1.生成节点值为num的新节点,记为node。
2.如果链表为空,让node自己组成环形链表,然后直接返回node。
3.如果链表不为空,令变量pre=head,cur=head.next,然后令pre和cur同步移动下去,如果遇到pre的节点值小于或等于num,并且cur的节点值大于或等于num,说明node应该在pre节点和cur节点之间插入,插入node,然后返回head即可。例如,链表1->3->4->1->…,num=2。应该把节点值为2的节点插入到1和3之间,然后返回头节点。
4.如果pre和cur转了一圈,这期间都没有发现步骤3所说的情况,说明node应该插入到头节点的前面,这种情况之所以会发生,要么是因为node节点的值比链表中每个节点的值都大,要么是因为node的值比链表中每个节点的值都小。
分别举两个例子:示例1,链表1->3->4->1->…,num=5,应该把节点值为5的节点,插入到节点1的前面;示例2,链表1->3->4->1->…,num=0,也应该把节点值为0的节点,插入到节点1的前面。
5.如果node节点的值比链表中每个节点的值都大,返回原来的头节点即可;如果node节点的值比链表中每个节点的值都小,应该把node作为链表新的头节点返回。
具体过程请参看如下代码中的insertNum方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node insertNum(Node head, int num) {
Node node = new Node(num);
if (head == null) {
node.next = node;
return node;
}
Node pre = head;
Node cur = head.next;
while (cur ! = head) {
if (pre.value <= num && cur.value >= num) {
break;
}
pre = cur;
cur = cur.next;
}
pre.next = node;
node.next = cur;
return head.value < num ? head : node;
}
【题目】
给定两个有序单链表的头节点head1和head2,请合并两个有序链表,合并后的链表依然有序,并返回合并后链表的头节点。
例如:
0->2->3->7->null
1->3->5->7->9->null
合并后的链表为:0->1->2->3->3->5->7->7->9->null
【难度】
士 ★☆☆☆
【解答】
本题比较简单,假设两个链表的长度分别为M 和N ,直接给出时间复杂度为O (M +N )、额外空间复杂度为O (1)的方法。具体过程如下:
1.如果两个链表中有一个为空,说明无须合并过程,返回另一个链表的头节点即可。
2.比较head1和head2的值,小的节点也是合并后链表的最小节点,这个节点无疑应该是合并链表的头节点,记为head;在之后的步骤里,哪个链表的头节点的值更小,另一个链表的所有节点都会依次插入到这个链表中。
3.不妨设head节点所在的链表为链表1,另一个链表为链表2。链表1和链表2都从头部开始一起遍历,比较每次遍历到的两个节点的值,记为cur1和cur2,然后根据大小关系做出不同的调整,同时用一个变量pre表示上次比较时值较小的节点。
例如,链表1为1->5->6->null,链表2为2->3->7->null。
cur1=1,cur2=2,pre=null。cur1小于cur2,不做调整,因为此时cur1较小,所以令pre=cur1=1,然后继续遍历链表1的下一个节点,也就是节点5。
cur1=5,cur2=2,pre=1。cur2小于cur1,让pre的next指针指向cur2,cur2的next指针指向cur1,这样,cur2便插入到链表1中。因为此时cur2较小,所以令pre=cur2=2,然后继续遍历链表2的下一个节点,也就是节点3。这一步完成后,链表1变为1->2->5->6->null,链表2变为3->7->null,cur1=5,cur2=3,pre=2。
cur1=5,cur2=3,pre=2。此时又是cur2较小,与上一步调整类似,这一步完成后,链表1变为1->2->3->5->6->null,链表2为7->null,cur1=5,cur2=7,pre=3。
cur1=5,cur2=7,pre=3。cur1小于cur2,不做调整,因为此时cur1较小,所以令pre=cur1=5,然后继续遍历链表1的下一个节点,也就是节点6。
cur1=6,cur2=7,pre=5。cur1小于cur2,不做调整,因为此时cur1较小,所以令pre=cur1=6,此时已经走到链表1的最后一个节点,再往下就结束,如果链表1或链表2有任何一个走到了结束,就进入步骤4。
4.如果链表1先走完,此时cur1=null,pre为链表1的最后一个节点,那么就把pre的next指针指向链表2当前的节点(即cur2),表示把链表2没遍历到的有序部分直接拼接到最后,调整结束。如果链表2先走完,说明链表2的所有节点都已经插入到链表1中,调整结束。
5.返回合并后链表的头节点head。
全部过程请参看如下代码中的merge方法。
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
public Node merge(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return head1 ! = null ? head1 : head2;
}
Node head = head1.value < head2.value ? head1 : head2;
Node cur1 = head == head1 ? head1 : head2;
Node cur2 = head == head1 ? head2 : head1;
Node pre = null;
Node next = null;
while (cur1 ! = null && cur2 ! = null) {
if (cur1.value <= cur2.value) {
pre = cur1;
cur1 = cur1.next;
} else {
next = cur2.next;
pre.next = cur2;
cur2.next = cur1;
pre = cur2;
cur2 = next;
}
}
pre.next = cur1 == null ? cur2 : cur1;
return head;
}
【题目】
给定一个单链表的头部节点head,链表长度为N ,如果N 为偶数,那么前N /2个节点算作左半区,后N /2个节点算作右半区;如果N 为奇数,那么前N /2个节点算作左半区,后N /2+1个节点算作右半区。左半区从左到右依次记为L1->L2->…,右半区从左到右依次记为R1->R2->…,请将单链表调整成L1->R1->L2->R2->…的形式。
例如:
1->null,调整为1->null。
1->2->null,调整为1->2->null。
1->2->3->null,调整为1->2->3->null。
1->2->3->4->null,调整为1->3->2->4->null。
1->2->3->4->5->null,调整为1->3->2->4->5->null。
1->2->3->4->5->6->null,调整为1->4->2->5->3->6->null。
【难度】
士 ★☆☆☆
【解答】
假设链表的长度为N ,直接给出时间复杂度为O (N )、额外空间复杂度为O (1)的方法。具体过程如下:
1.如果链表为空或长度为1,不用调整,过程直接结束。
2.链表长度大于1时,遍历一遍找到左半区的最后一个节点,记为mid。
例如:1->2,mid为1;1->2->3,mid为1;1->2->3->4,mid为2;1->2->3->4->5,mid为2;1->2->3->4->5->6,mid为3。也就是说,从长度为2开始,长度每增加2,mid就往后移动一个节点。
3.遍历一遍找到mid之后,将左半区与右半区分离成两个链表(mid.next=null),分别记为left(head)和right(原来的mid.next)。
4.将两个链表按照题目要求合并起来。
具体过程请参看如下代码中的relocate方法,其中的mergeLR方法为步骤4的合并过程。
public class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
public void relocate(Node head) {
if (head == null || head.next == null) {
return;
}
Node mid = head;
Node right = head.next;
while (right.next ! = null && right.next.next ! = null) {
mid = mid.next;
right = right.next.next;
}
right = mid.next;
mid.next = null;
mergeLR(head, right);
}
public void mergeLR(Node left, Node right) {
Node next = null;
while (left.next ! = null) {
next = right.next;
right.next = left.next;
left.next = right;
left = right.next;
right = next;
}
left.next = right;
}