上一篇文章中对剑指 offer
中数组相关的题目进行了归纳,这一篇文章是链表篇。同样地,如果各位大佬发现程序有什么 bug
或其他更巧妙的思路,欢迎交流学习。
6. 从尾到头打印链表
题目描述
输入一个链表的头节点,从尾到头打印链表的每个节点的值。
这里可以用显式栈,或者递归来实现,都比较简单,也就不多做解释了。
递归实现
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if(listNode == null){
return new ArrayList<>();
}
ArrayList<Integer> list = printListFromTailToHead(listNode.next);
list.add(listNode.val);
return list;
}
栈实现
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(listNode == null){
return list;
}
Deque<Integer> stack = new LinkedList<>();
ListNode node = listNode;
while(node != null) {
stack.push(node.val);
node = node.next;
}
while(!stack.isEmpty()) {
list.add(stack.pop());
}
return list;
}
18. 删除链表的节点
题目一描述
在 O(1) 时间内删除链表指定节点。给定单链表的头节点引用和一个节点引用,要求在 O(1) 时间内删除该节点。
解题思路
一般来说,要在单向链表中删除指定节点,需要得到被删除节点的前驱节点。但这需要从头节点开始顺序查找,时间复杂度肯定不是 O(1)
了,所以需要换一种思路。
我们可以将后继节点的值赋值给要删除的指定节点,再删除下一个节点,如此也同样实现了删除指定节点的功能。但是还需要注意两种特殊情况:
- 第一种是要删除的节点是头节点,这时还需要对链表的头结点进行更新;
- 第二种是要删除的节点是尾节点,它没有下一个节点,这时就只能从头节点开始顺序查找要删除节点的前驱节点了。
代码实现
public Node deleteNode(Node head, Node node) {
if (head == null || node == null) {
return head;
}
if (head == node) {
// 要删除的节点是头节点
return head.next;
} else if (node.next == null) {
// 要删除的节点是尾节点
Node cur = head;
while (cur.next != node) {
cur = cur.next;
}
cur.next = null;
} else {
// 要删除的节点在链表中间
ListNode nextNode = node.next;
node.val = nextNode.val;
node.next = nextNode.next;
}
return head;
}
这里除了最后一个节点,其他节点都可以在 O(1)
时间内删除,只有要删除的节点是尾节点时,才需要对链表进行遍历,所以,总体的时间复杂度还是 O(1)
。
题目二描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
解题思路
这里要删除排序链表中的重复节点,由于头节点也可能被删除,所以需要对头节点特殊处理,或者添加一个虚拟节点。这里选择使用虚拟节点。
由于这里需要判断当前节点和下一个节点的值,所以循环中条件就是要判断当前节点和下一个节点均不能为空。如果这两个值不相等,则继续遍历。
如果不相等,则循环判断跳过连续重复的数个节点,最后 cur
指向这些重复节点的最后一个。由于重复节点不保留,所以需要让 pre.next
指向 cur.next
,再更新 cur
为下一个节点 pre.next
,进而继续判断。
代码实现
public Node deleteDuplication(Node head) {
Node dummyHead = new Node(-1);
dummyHead.next = head;
Node pre = dummyHead;
Node cur = head;
while (cur != null && cur.next != null) {
if (cur.value != cur.next.value) {
pre = cur;
cur = cur.next;
} else {
while (cur.next != null && cur.value == cur.next.value) {
cur = cur.next;
}
pre.next = cur.next;
cur = pre.next;
}
}
return dummyHead.next;
}
这里虽然有两层嵌套循环,但实际上只对链表遍历了一遍,所以其时间复杂度为 O(n)
。另外只申请了一个虚拟节点,所以空间复杂度为 O(1)
。
22. 链表中倒数第 k 个节点
题目描述
输入一个链表,输出该链表中倒数第 k 个结点。(k 从 1 开始)
解题思路
这里可以定义两个指针。第一个指针从链表头开始遍历,向前移动 k - 1
步。然后从 k
步开始,第二个指针也开始从链表头开始遍历。
由于两个指针的距离为 k - 1
,所有当第一个指针移动到链表的尾节点时,第二个指针正好移动到倒数第 k
个节点。
代码实现
public static ListNode findKthToTail(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
for (int i = 0; i < k - 1; i++) {
if (fast.next == null) {
return null;
}
fast = fast.next;
}
ListNode slow = head;
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
23. 链表中环的入口节点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
解题思路
首先需要判断链表是否有环,可以使用两个指针,同时从链表的头部开始遍历,一个指针一次走一步,一个指针一次走两步。如果快指针能追上慢指针,则表示链表有环;否则如果快指针走到了链表的末尾,表示没有环。
在找到环之后,定义一个指针指向链表的头节点,再选择刚才的慢指针从快慢指针的相遇节点开始,两个指针同时以每次一步向前移动,它们相遇的节点就是链表的入口节点。
代码实现
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead == null || pHead.next == null) {
return null;
}
ListNode slow = pHead.next;
ListNode fast = slow.next;
while(slow != fast) {
if(fast == null || fast.next == null) {
return null;
}
slow = slow.next;
fast = fast.next.next;
}
ListNode p = pHead;
while(slow != p) {
slow = slow.next;
p = p.next;
}
return slow;
}
24. 反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
循环解决
思路如下图:
循环代码
public ListNode reverseList1(ListNode head) {
ListNode newHead = null;
ListNode cur = head;
ListNode nex;
while (cur != null) {
nex = cur.next;
cur.next = newHead;
newHead = cur;
// 记录
cur = nex;
}
return newHead;
}
递归解决
递归代码
public ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList2(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
25. 合并两个有序的链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
循环解题
在使用循环时,首先需要确定新链表的头节点,如果链表 first
的头节点的值小于链表 second
的头节点的值,那么链表 first
的头节点便是新链表的头节点。
然后循环处理两个链表中剩余的节点,如果链表 first
中的节点的值小于链表 second
中的节点的值,则将链表 first
中的节点添加到新链表的尾部,否则添加链表 second
中的节点。然后继续循环判断,直到某一条链表为空。
当其中一条链表为空后,只需要将另一条链表全部链接到新链表的尾部。
思路图如下:
循环代码
public ListNode merge1(ListNode first, ListNode second) {
if (first == null) {
return second;
}
if (second == null) {
return first;
}
ListNode p = first;
ListNode q = second;
ListNode newHead;
if (p.val < q.val) {
newHead = p;
p = p.next;
} else {
newHead = q;
q = q.next;
}
ListNode r = newHead;
while (p != null && q != null) {
if (p.val < q.val) {
r.next = p;
p = p.next;
} else {
r.next = q;
q = q.next;
}
r = r.next;
}
if (p == null) {
r.next = q;
} else {
r.next = p;
}
return newHead;
}
递归解题
使用递归解决,比较简单。首先判断两条链表是否为空,如果 first
为空,则直接返回 second
;如果 second
为空,则直接返回 first
。
接着判断链表 first
中节点的值和链表 second
中节点的值,如果 first
中节点的值较小,则递归地求 first.next
和 second
的合并链表,让 first.next
指向新的链表头节点,然后返回 first
即可。
另一种情况类似,这里就不再赘述了。
递归代码
public ListNode merge2(ListNode first, ListNode second) {
if (first == null) {
return second;
}
if (second == null) {
return first;
}
if (first.val < second.val) {
first.next = merge2(first.next, second);
return first;
} else {
second.next = merge2(first, second.next);
return second;
}
}
35. 复杂链表的复制
题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。
解题思路
这可以分为三步来解决。第一步是根据原始链表的所有节点,将每一节点的复制节点链接到它的后面。
第二步设置复制出来的节点的特殊指针。如果原始链表的节点 p
的特殊指针指向节点 s
,则复制出来的节点 cloned
的特殊指针就指向节点 s
的下一个节点。
第三部是将长链表拆分成两个链表,把所有偶数位置的节点连接起来就是新的复制出来的链表。
代码实现
public RandomListNode Clone(RandomListNode head) {
cloneNodes(head);
connectSiblingNode(head);
return reconnectNodes(head);
}
private void cloneNodes(RandomListNode head) {
RandomListNode p = head;
while(p != null) {
RandomListNode newNode = new RandomListNode(p.label);
newNode.next = p.next;
p.next = newNode;
p = newNode.next;
}
}
private void connectSiblingNode(RandomListNode head) {
RandomListNode p = head;
while(p != null) {
RandomListNode cloned = p.next;
if(p.random != null) {
cloned.random = p.random.next;
}
p = cloned.next;
}
}
private RandomListNode reconnectNodes(RandomListNode head) {
RandomListNode p = head;
RandomListNode newHead = null;
RandomListNode tail = null;
if(p != null) {
tail = newHead = p.next;
p.next = tail.next;
p = p.next;
}
while(p != null) {
tail.next = p.next;
tail = tail.next;
p.next = tail.next;
p = p.next;
}
return newHead;
}
36. 二叉搜索树与双向链表
题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
解题思路
这里将二叉搜索树转换为一个排序的双向链表,可以采用使用递归算法。
首先递归地转换左子树,返回其链表头节点,然后需要遍历该链表,找到链表的尾节点,这是为了和根节点相连接。需要让链表的尾节点的 right
指向根节点,让根节点的 left
指向链表的尾节点。
然后递归地转换右子树,返回其链表头节点,然后需要让根节点的 right
指向链表头节点,让链表的头节点指向根节点。
最后判断如果左子树转换的链表为空,则返回以 root
根节点为头节点的链表,否则返回以左子树最小值为头节点的链表。
代码实现
public TreeNode Convert(TreeNode root) {
if(root == null) {
return null;
}
TreeNode leftHead = Convert(root.left);
TreeNode leftEnd = leftHead;
while(leftEnd != null && leftEnd.right != null) {
leftEnd = leftEnd.right;
}
if(leftEnd != null) {
leftEnd.right = root;
root.left = leftEnd;
}
TreeNode rightHead = Convert(root.right);
if(rightHead != null) {
root.right = rightHead;
rightHead.left = root;
}
return leftHead == null ? root : leftHead;
}
52. 两个链表的第一个公共节点
题目描述
输入两个链表,找出它们的第一个公共结点。
解题思路
对于两个链表,如果有公共节点,要不它们就是同一条链表,要不它们的公共节点一定在公共链表的尾部。
可以遍历两个链表得到它们的长度,然后在较长的链表上,先走它们的长度差的步数,接着同时在两个链表上遍历,如此找到的第一个节点就是它们的第一个公共节点。
代码实现
public ListNode findFirstCommonNode(ListNode first, ListNode second) {
int length1 = getListLength(first);
int length2 = getListLength(second);
ListNode headLongList = first;
ListNode headShortList = second;
int diff = length1 - length2;
if (length1 < length2) {
headLongList = second;
headShortList = first;
diff = length2 - length1;
}
for (int i = 0; i < diff; i++) {
headLongList = headLongList.next;
}
while (headLongList != null && headShortList != null) {
if (headLongList == headShortList) {
return headLongList;
}
headLongList = headLongList.next;
headShortList = headShortList.next;
}
return null;
}
public int getListLength(ListNode head) {
int length = 0;
ListNode cur = head;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}