链表问题总结(一)

570 阅读8分钟

1、链表

链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)

由于不必须按顺序存储,链表在插入的时候可以达到 O(1)O(1) 的复杂度,比另一种线性表 —— 顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n)O(n) 的时间,而顺序表相应的时间复杂度分别是 O(log\ n)O(log n) 和 O(1)O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。

链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。

涉及到链表的操作,一定要在纸上把过程先画出来,再写程序。
由于一个节点只通过next指针保存了它的下一个节点,失去了它的前驱节点信息,而且单链表数据结构通常长度未知,因此几乎所有单链表题目都是在前驱节点和长度上做文章。
常见链表面试算法,可以使用双指针迭代的方式以及递归的方式。

2、单链表

链表中最简单的一种是单向链表,它包含两个域,一个信息域和一个指针域。这个指针域指向列表中的下一个节点,而最后一个节点则指向一个空值。

3、反转单链表

3.1、迭代

如何迭代?双指针迭代。

使用双指针迭代方式反转单链表的中心思想:

1、双指针分别指向链表中的相邻节点,初始状态为当前指针指向头节点,前指针指向NULL; 2、修改当前指针指向的节点的指向为前一个节点,然后当前指针和前指针同时指向下一个节点; 3、循环往复,直到当前指针指向的节点为NULL,返回前指针指向的节点。

具体步骤: 1.申请两个指针,第一个指针叫 pre,最初是指向 null 的,意指 cur 指针的前一个指针。 2.第二个指针 cur 指向 head,然后不断遍历 cur。 3.每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。 4.都迭代完了,cur 变成 null 了,pre 就是最后一个节点了。

class Solution {
 public ListNode reverseList(ListNode head) {
  //申请节点,pre和 cur,pre指向null
  ListNode pre = null;
  ListNode cur = head;
  ListNode tmp = null;
  while(cur!=null) {
   //记录当前节点的下一个节点
   tmp = cur.next;
   //然后将当前节点指向pre
   cur.next = pre;
   //pre和cur节点都前进一位
   pre = cur;
   cur = tmp;
  }
  return pre;
 }
}

3.2、递归

  1. 终止条件是当前节点或者下一个节点==null。
  2. 在函数内部,改变节点的指向,也就是 head 的下一个节点指向 head。
class Solution {
 public ListNode reverseList(ListNode head) {
  //递归终止条件是当前为空,或者下一个节点为空
  if(head==null || head.next==null) {
   return head;
  }
  //这里的cur就是最后一个节点
  ListNode cur = reverseList(head.next);
  //这里请配合动画演示理解
  //如果链表是 1->2->3->4->5,那么此时的cur就是5
  //而head是4,head的下一个是5,下下一个是空
  //所以head.next.next 就是5->4
  head.next.next = head;
  //防止链表循环,需要将head.next设置为空
  head.next = null;
  //每层递归函数都返回cur,也就是最后一个节点
  return cur;
 }
}

4、删除单链表的倒数第N个节点,给定的 n 保证是有效的。

第一个指针从列表的开头向前移动 n+1n+1 步,而第二个指针将从列表的开头出发。现在,这两个指针被 nn 个结点分开。我们通过同时移动两个指针向前来保持这个恒定的间隔,直到第一个指针到达最后一个结点。此时第二个指针将指向从最后一个结点数起的第 nn 个结点。我们重新链接第二个指针所引用的结点的 next 指针指向该结点的下下个结点。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode first = dummy;
    ListNode second = dummy;
    // Advances first pointer so that the gap between first and second is n nodes apart
    for (int i = 1; i <= n + 1; i++) {
        first = first.next;
    }
    // Move first to the end, maintaining the gap
    while (first != null) {
        first = first.next;
        second = second.next;
    }
    second.next = second.next.next;
    return dummy.next;
    }
}

5、合并两个排好序的链表,作为新链表。

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

5.1、虚拟头节点迭代

首先,设定一个哨兵节点 dummyHead ,这可以在最后比较容易地返回合并后的链表。我们维护一个 curr 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 curr 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 curr 向后移一位。

在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            //虚拟头节点
            ListNode dummyHead = new ListNode(0);   
            ListNode curr = dummyHead;
            while(l1!=null&&l2!=null){
                if(l1.val<l2.val){
                    curr.next = l1;
                    l1 = l1.next;
                }else{
                    curr.next = l2;
                    l2 = l2.next;
                }
                curr = curr.next;
            }
            curr.next = l1==null?l2:l1;
            return dummyHead.next;
    }
}

5.2、递归

这道题可以使用递归实现,新链表也不需要构造新节点,列举递归三个要素 终止条件:两条链表分别名为 l1 和 l2,当 l1 为空或 l2 为空时结束 返回值:每一层调用都返回排序好的链表头 本级递归内容:如果 l1 的 val 值更小,则将 l1.next 与排序好的链表头相接,l2 同理 O(m+n)O(m+n),mm 为 l1的长度,nn 为 l2 的长度

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null) {
            return l2;
        }
        if(l2 == null) {
            return l1;
        }

        if(l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

6、判断是否是环形链表,并返回入口节点。

解法:双指针,快指针和满指针

设链表共有 a+b 个节点,其中 链表头部到链表入口 有a 个节点(不计链表入口节点), 链表环 有 b 个节点。 设两指针分别走了 f,s 步,则有: fast 走的步数是slow步数的 2 倍,即 f=2s;(解析: fast 每轮走 22 步) fast 比 slow多走了 n 个环的长度,即 f=s+nb;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )

解法重点思想:

  1. 走a+nb步一定是在环入口
  2. 第一次相遇时慢指针已经走了nb步
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;//快指针走两步
            slow = slow.next;//满指针走一步
            if (fast == slow) break;//相遇时暂停
        }
        fast = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

7、后续……

链表涉及题型太多,剩下的题型后续再总结,不过很多都可以双指针和递归思想解决,但是看了会,动手会不会就两说了,还是要学会动手写,最好尝试用笔在白纸上写,先画画链表图,再写下算法代码……

欢迎关注微信公众号,Android技术堆栈: