数据结构与算法之链表篇

122 阅读3分钟

链表,是一种物理存储单元上非连续的存储结构,结点的逻辑顺序是通过链表中的指针链接实现。

image-20220409160908546

写链表代码的技巧

1.理解指针或引用的含义

将某个引用类型赋值给指针,实际上就是将这个引用类型在内存中的地址赋值给指针。即指针存储着引用类型的内存地址,通过指针就能找到引用类型在内存中的位置,从而进行修改操作。

2.插入结点时,注意操作的顺序

如下图,在 a,b 结点插入结点 x 时,一定要先将 x 结点的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x。

image.png

3.插入删除操作时设置哑节点

插入时:

newNode.next = p.next;
p.next = newNode;

但如果链表是空链表,需要进行特殊的处理


if (head == null) {
  head = newNode;
}

删除时:p.next = p.next.next;

但对于链表的最后一个结点,这个代码就不 work 了。需要进行特殊的处理


if (head.next == null) {
   head = null;
}

而设置了哑结点head = null之后,就可以统一代码了。

image.png

4.重点留意边界条件处理

软件开发中,代码在一些边界或者异常情况下,最容易产生 Bug。链表代码也不例外。要实现没有 Bug 的链表代码,一定要在编写的过程中以及编写完成之后,检查边界条件是否考虑全面,以及代码在边界条件下是否能正确运行。可以参考下列边界条件:

  • 如果链表为空时,代码是否能正常工作?
  • 如果链表只包含一个结点时,代码是否能正常工作?
  • 如果链表只包含两个结点时,代码是否能正常工作?
  • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

当写完链表代码之后,除了看下你写的代码在正常的情况下能否工作,还要看下在上面列举的几个边界条件下,代码仍然能否正确工作。

如果这些边界条件下都没有问题,那基本上可以认为没有问题了。

实际上,不光光是写链表代码,在写任何代码时,也千万不要只是实现业务正常情况下的功能就好了,一定要多想想,你的代码在运行的时候,可能会遇到哪些边界情况或者异常情况。遇到了应该如何应对,这样写出来的代码才够健壮!

leetcode题目

21. 合并两个有序链表

var mergeTwoLists = function(list1, list2) {
  let dump = new ListNode();
  let temp = dump;
  let pointer1 = list1;
  let pointer2 = list2;
  while (pointer1 !== null && pointer2 !== null) {
    if (pointer1.val <= pointer2.val) {
      temp.next = pointer1;
      temp = temp.next;
      pointer1 = pointer1.next;
    } else {
      temp.next = pointer2;
      temp = temp.next;
      pointer2 = pointer2.next;
    }
  }
  if (pointer1 !== null) {
    temp.next = pointer1;
  }
  if (pointer2 !== null) {
    temp.next = pointer2;
  }
  return dump.next;
};

206. 反转链表

通过双指针来遍历链表,依次翻转结点指向。

var reverseList = function(head) {
  let prev = null;
  let curr = head;
  while (curr !== null) {
    let temp = curr.next;
    curr.next = prev;
    prev = curr;
    curr = temp;
  }
  return prev;
};

203. 移除链表元素

删除结点操作,注意设置哑节点。

var removeElements = function(head, val) {
  let dump = new ListNode(0, head);
  let cur = dump;
  while(cur.next !== null) {
    if (cur.next.val === val) {
      cur.next = cur.next.next;
    } else {
      cur = cur.next;
    }
  }
  return dump.next;
};

19. 删除链表的倒数第 N 个结点

删除结点很容易,只需a.next = a.next.next,关键是如何定位到目标结点的前驱结点?

为此,采用快慢指针的技巧进行定位。这道题是快慢指针的经典应用。

var removeNthFromEnd = function(head, n) {
  let dump = new ListNode(0, head);
  let slow = fast = dump;
  for (let i = n; i >= 0; i--) {
    fast = fast.next;
  }
  while (fast !== null) {
    slow = slow.next;
    fast = fast.next;
  }
  slow.next = slow.next.next;
  return dump.next;
};

876. 链表的中间结点

快慢指针定位某个结点。

注意边界情况,循环条件不能写成 (fast.next !== null),这样处理尾部结点时会出错。

var middleNode = function(head) {
  let fast = slow = head;
  while (fast !== null && fast.next !== null) {
    fast = fast.next.next;
    slow = slow.next;
  }
  return slow;
};

24. 两两交换链表中的节点

var swapPairs = function(head) {
  let dump = new ListNode(0, head);
  let p1 = dump;
  while (p1.next && p1.next.next) {
    let p2 = p1.next;
    let p3 = p1.next.next;
    p2.next = p3.next;
    p3.next = p2;
    p1.next = p3;
    p1 = p2;
 }
  return dump.next;
};