3.1 数据结构与算法 —— 写链表代码技巧

335 阅读3分钟

技巧一,理解指针或引用的含义

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针存储了这个变量的内存地址,指向了这个变量将,通过指针就能找到这个变量。

警惕指针丢失和内存泄漏

插入结点

在插入结点时,一定要注意操作的顺序,要先将结点 x 的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x,也可以认为是要将 a 的 next 指针保存下来。这样才不会丢失指针,导致内存泄漏。

在删除结点时,也一定要记得手动释放内存空间,否则会有内存泄漏的问题。

如果使用的是 Java 这种虚拟机自动管理内存的语言来说,内存泄漏不需要我们操心。

技巧二,利用哨兵简化实现难度

具体的例子看 链表(下):如何轻松写出正确的链表代码? 的「技巧三:利用哨兵简化实现难度」

技巧三,重点留意边界条件处理

几个经常用来检查链表代码是否正确的边界条件有这样几个:

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

技巧四,举例画图,辅助思考

找一个具体的例子,把它画在纸上,释放一些脑容量。把各种情况都举一个例子,画出插入前和插入后的链表变化。

技巧五,多写多练,没有捷径

熟能生巧,将下面 5 个常见的链表操作都写熟练。

单链表翻转

/**
 * 单链表翻转
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 *
 * @param list 目标链表
 * @return 返回翻转后的链表
 */
public static Node reverse(Node list) {
    Node prev = null;
    Node curr = list;

    while (curr != null) {
        Node next = curr.next;  // 先保存下面的数据
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

链表中环的检测

/**
 * 检测环
 * 时间复杂度 O(n)  取决于环的大小
 * 空间复杂度 O(1)
 *
 * @param list 目标链表
 * @return 是否存在环
 */
public static boolean checkCircle(Node list) {
    if (list == null) return false;

    Node slow = list;
    Node fast = list;

    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (fast == slow) return true;
    }
    return false;
}

两个有序的链表合并

/**
 * 有序链表合并
 *
 * @param la
 * @param lb
 * @return
 */
public static Node mergeSortedList(Node la, Node lb) {
    if (la == null) return lb;
    if (lb == null) return la;

    Node head = null;
    if (la.data < lb.data) {
        head = la;
        la = la.next;
    } else {
        head = lb;
        lb = lb.next;
    }
    Node tmp = head;    // 用一个指针来进行合并操作

    while (la != null && lb != null) {
        if (la.data < lb.data) {
            tmp.next = la;
            la = la.next;
        } else {
            tmp.next = lb;
            lb = lb.next;
        }
        tmp = tmp.next; // 指针始终指向尾结点
    }

    if (la != null) {
        tmp.next = la;
    } else {
        tmp.next = lb;
    }
    return head;
}

删除链表倒数第 n 个结点

/**
 * 删除倒数第 k 个元素
 *
 * @param list
 * @param k
 * @return
 */
public static Node deleteLastKth(Node list, int k) {
    if (list == null) return null;

    Node fast = list;
    int i = 1;
    while (fast != null && i < k) { // 找到顺数第 k 个元素
        fast = fast.next;
        i++;
    }
    if (fast == null) return list;  // 不够 k 个元素

    Node slow = list;
    Node slowPre = null;
    while (fast.next != null) { // fast 指针遍历完剩下的链表
        fast = fast.next;
        slowPre = slow;
        slow = slow.next;
    }
    // slowPre == null 说明 k == list 的 length
    if (slowPre == null) {
        list = list.next;   // 所以就是删除头结点

    } else {
        slowPre.next = slowPre.next.next;
    }
    return list;
}

求链表的中间结点

/**
 * 找到中间元素
 *
 * @param list
 * @return
 */
public static Node findMiddleNode(Node list) {
    if (list == null) return null;

    Node fast = list;
    Node slow = list;

    while (fast.next != null && fast.next.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    return slow;
}