链表具有以下缺点:
1、无法高效获取长度
2、无法根据偏移快速访问元素
面试的时候经常碰见诸如获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这类的问题基本都可以使用双指针来解决。
1、倒数第k个元素的问题
2、获取中间元素的问题
3、是否存在环的问题
4、如果存在环,如何判断环的长度呢
5、链表的反转
6、两个递增排序的链表的合并
倒数第k个元素的问题
设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 q 即指向倒数第 k 个结点。可以参考下图来理解:
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *p = head, *q = head; //初始化
while(k--) { //将 p指针移动 k 次
p = p->next;
}
while(p != nullptr) {//同时移动,直到 p == nullptr
p = p->next;
q = q->next;
}
return q;
}
};
获取中间元素的问题
设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast向后走两次,slow向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠前一个
是否存在环的问题
当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针
如果存在环,如何判断环的长度呢?
快慢指针相遇后继续移动,直到第二次相遇。两次相遇间的移动次数即为环的长度。
链表的反转
方式1 遍历
private static Node reverseHead(Node head) {
if (head == null) {
return head;
}
// 这里借助两个指针pre指向header,cur指向header下一个节点
Node pre = head;
Node cur = head.nextNode;
while (cur != null) {
// 暂存cur的下一个节点
Node temp = cur.nextNode;
// 将当前的cur的下一个节点指向pre 也就是指向他原本的头节点
cur.nextNode = pre;
// pre 和 cur 同时向后移动一位
pre = cur;
cur = temp;
}
head.nextNode = null;
head = pre;
return head;
}
方式2 递归
private static Node reverseByRecur(Node current) {
if (current == null || current.nextNode == null) return current;
Node nextNode = current.nextNode;
// 将原本的指针指向断掉 走到最后是没有链表结构了
current.nextNode = null;
// 创建头节点 这里递归到最后 reverseRest 就是末尾节点
Node reverseRest = reverseByRecur(nextNode);
// 这里开始反向构建链表
nextNode.nextNode = current;
return reverseRest;
}
两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的
方法1 遍历合并
public static ListNode merge(ListNode head1, ListNode head2) {
// 如果第一个链表为空,返回第二个链表头结点
if (head1 == null) {
return head2;
}
// 如果第二个链表为空,返回第一个链表头结点
if (head2 == null) {
return head1;
}
// 创建一个临时结点,用于添加元素时方便
ListNode root = new ListNode();
// 用于指向合并后的新链的尾结点
ListNode pointer = root;
// 当两个链表都不为空就进行合并操作
while (head1 != null && head2 != null) {
// 下面的操作合并较小的元素
if (head1.value < head2.value) {
pointer.next = head1;
head1 = head1.next;
} else {
pointer.next = head2;
head2 = head2.next;
}
// 将指针移动到合并后的链表的末尾
pointer = pointer.next;
}
// 如果第一个链表的元素未处理完将其,接到合并链表的最后一个结点之后
if (head1 != null) {
pointer.next = head1;
}
// 如果第二个链表的元素未处理完将其,接到合并链表的最后一个结点之后
if (head2 != null) {
pointer.next = head2;
}
return root.next;
}
方法二 递归
public static ListNode merge2(ListNode head1, ListNode head2) {
// 如果第一个链表为空,返回第二个链表头结点
if (head1 == null) {
return head2;
}
// 如果第二个链表为空,返回第一个链表头结点
if (head2 == null) {
return head1;
}
// 记录两个链表中头部较小的结点
ListNode tmp = head1;
if (tmp.value < head2.value) {
// 如果第一个链表的头结点小,就递归处理第一个链表的下一个结点和第二个链表的头结点
tmp.next = merge2(head1.next, head2);
} else {
// 如果第二个链表的头结点小,就递归处理第一个链表的头结点和第二个链表的头结点的下一个结点
tmp = head2;
tmp.next = merge2(head1, head2.next);
}
return tmp;
}