当使用 while
循环的时候,需要有判断结束的条件 while(condition)
,而 coditon 所包含的变量,通常都是需要在循环内变更的。
next
指针,以完成插入删除等操作哨兵(Sentinel)是个哑元结点(Dummy Node)。哑结点指数据域为空,指针域指向链表头结点的结点,它是为了简化边界条件而引入的。如果一个链表有哨兵结点的话,那么线性表的第一个元素应该是链表的第二个结点。
要对头结点进行操作时,考虑创建哨兵结点 dummy
,使用 dummy.next
表示真正的头结点。这样可以避免处理头结点为空的边界问题(例如:null
或单结点问题)。
缓存头结点:
var reorderList = function (head) {let dummy = new ListNode(-1);dummy.next = head;// ...// 指向头结点console.log(dummy.next);};
链表相关的题目一般都需要用到两个指针:prev
指针和 curr
指针
// Initialize slow & fast pointerslet slow = head,fast = head;/*** Change this condition to fit specific problem* Attention: remember to avoid null-pointer error*/while (slow != null && fast != null && fast.next != null) {slow = slow.next; // Move slow pointer one step each timefast = fast.next.next; // Move fast pointer two step each timeif (slow == fast) {// Change this condition to fit specific problemreturn true;}}return false; // Change return value to fit specific problem
提示
它与我们在数组中学到的内容类似。但它可能更棘手而且更容易出错。你应该注意以下几点:
获取空结点的下一个结点将导致空指针错误。例如,在我们运行 fast = fast.next.next
之前,需要检查 fast
和 fast.next
不为空
复杂度分析
空间复杂度分析容易。如果只使用指针,而不是用任何其他额外的空间,那么空间复杂度将是 O(1)
。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析 运行循环的次数
。
在前面的查找循环示例中,假设我们每次异动较快的指针 2 步,每次移动较慢的指针 1 步。
N/2
次才能到达链表的末尾,其中 N 是链表的长度M
次才能赶上慢指针,其中 M 是列表中循环的长度显然,M <= N
。所以我们将循环运行 N
次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)
。
自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。
快慢指针,就是定义两个指针,一个指针(慢指针)的移动速度为 v,另一个指针(快指针)速度为 2v,如此一来,经过相同的时间,快指针走过的路程是慢指针的两倍。因为链表无法得知长度,所以尝试用这种方法来达到某种效果(长度、检测环等)。
所以如果拆分链表,记得 slow.next = null
用于检测链表是否存在环
let slow = head,fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}
快指针每次移动两步,而慢指针每次移动一步。
var findMidNode = function (head) {let fast = head,slow = head;while (fast && fast.next) {slow = slow.next;fast = fast.next.next;}// 偶数 <-> 奇数const slow;};
快指针首先移动 K 步,随后与慢指针一起到达末尾。
var getKthFromEnd = function (head, k) {let slow = head,fast = head;for (let i = 1; i < k; i++) {if (fast.next) {fast = fast.next;continue;}return null;}while (fast != null) {fast = fast.next;slow = slow.next;}return slow;};
// 链表的遍历通常设定一个指针指向头部,然后遍历直至指针指的结点部位 NULLListNode cur = head;while (cur != null) {// ...cur = cur.next;}
构建两个指针,一个指向翻转后结点要指向的结点,另一个遍历原链表
var reverseList = function (head) {// 上一个结点和下一个结点let prev = null,cur = head;while (cur) {const next = cur.next;cur.next = pre;pre = cur;cur = next;}return prev;};
复杂度分析:
扩展:
如果只知道待翻转结点的前驱结点
一个指针用来指向待翻转结点的前一个结点(pre
),固定,一个指针用来指示待翻转结点(cur
),一个指针用来保持结点转移后原链表的次序(start
)。
// i 用来指示翻转的次数while (cur != null && i < n) {const next = cur.next;cur.next = pre.next;start.next = next;pre.next = cur;cur = next;i++;}
如果直到待翻转的第一个结点的前驱结点和尾结点
pre
之后的一个结点为删除结点,pre
直接指向待删除结点之后,将待删除结点插入 tail 之后,直到 pre.next != tail
cur = pre.next;if (i == k) {while (pre.next != tail) {temp = pre.next;pre.next = temp.next; // deletetemp.next = tail.next; // inserttail.next = temp;}pre = cur;}
采用递归一把梭穿到最后找到逆转后的头结点,然后从后往前挨个儿逆转,即后继结点指向前驱结点,前驱结点接着 NULL,顺序往前,直到完成整个逆转过程。
function reverseList(head) {if (head == null || head.next == null) return head;const newHead = reverseList(head.next);head.next.next = head;head.next = null;return newHead;}
先合并再拆分
var copyList = function (head) {let cur = head;while (cur) {cur.next = new Node(cur.val, cur.next);cur = cur.next;}cur = head.next;let pre = head,res = head.next;while (cur.next && pre.next) {pre.next = pre.next.next;cur.next = next.next.next;pre = pre.next;cur = cur.next;}pre.next = null;return res;};
var divideOdd = function (head) {};
var divideEven = function (head) {};
构建两个指针,一个指针遍历链表,另一个指针紧跟后面进行删除操作
let dummyHead = new ListNode(0);dummyHead.next = head;let pre = dummyHead,cur = head;while (cur != null && cur.next != null) {if (cur.val == val) {pre.next = cur.next;} else {pre = pre.next;}cur = cur.next;}return dummyHead.next;
记录上一个结点
let pre = slow;while (fast != null) {fast = fast.next;pre = slow;slow = slow.next;}pre.next = slow.next;
let cur = head;while (cur.next != null) {if (cur.val == cur.next.val) {// 前后值相同只需要更换下一个结点位置即可cur.next = cur.next.next;} else {cur = cur.next;}}