让链表题目不再复杂

前言

我们接着上部分的二分查找,再继续链表相关的题目

换一个角度来理解链表

我相信大家对链表的数据结构已经很熟悉了。什么单链表,循环链表,双向链表,双向循环链表。

我们这里以java的层面来理解指针或引用的含义:

实际上,链表其实并不复杂,复杂的是我们很容易将它和指针混淆在一起。就会让人产生疑惑。所以想要写好链表的代码。就得抛开旧识,重新理解指针。

我们知道,有些语言有“指针”的概念,比如 C 语言;有些语言没有指针,取而代之的是“引用”,比如 Java、Python。不管是“指针”还是“引用”,实际上,它们的意思都是一样的,都是存储所指对象的内存地址。

所以作为java程序员,我们要牢牢记住,什么是引用:

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

如下面这段代码,就是一个链表结构的对象。当我们在调用引用移来移去的时候。你要牢牢记住,自己操作的是这个对象。其他引用该对象的都会收到影响。

public class ListNode {
      int val;
      ListNode next;
      ListNode() {}
      ListNode(int val) { this.val = val; }
      ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 }

解题技巧

警惕指针丢失

初学者写链表的时候,最容易犯的错误就是,常常把指针给搞丢了。

我们以向链表(a->b->c-null)插入节点来实例:


a.next = x;  
x.next = a.next;  

我一开始写链表代码的时候也经常犯这种错误。值得注意的是,当我们调用第一行代码的时候,a的next节点已经指向了x,此时内存中存在两个链表

a->x->null
b->c->null

当调用第二行代码的时候,x的next节点又指向了自己。

所以,我们插入结点时,一定要注意操作的顺序,要先将结点 x 的 next 引用指向结点 b,再把结点 a 的 next 引用指向结点 x,这样才不会丢失指针。

利用哨兵简化实现难度

我给出一段代码,就是链表的插入操作

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

但是,如果我们要插入链表中的第一个结点,前面的代码就不 work 了。我们需要对于这种情况特殊处理。写成代码是这样子的:

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

针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。这样代码实现起来就会很繁琐,不简洁,而且也容易因为考虑不全而出错。如何来解决这个问题呢?

“哨兵”节点不存储数据,无论链表是否为空,head指针都会指向它,作为链表的头结点始终存在。这样,插入第一个节点和插入其他节点,删除最后一个节点和删除其他节点都可以统一为相同的代码实现逻辑了。

重点留意边界条件处理

我每次写链表相关问题的时候,都会优先判断几个条件,看我的代码是否能正常work

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

当你写完链表代码之后,除了看下你写的代码在正常的情况下能否工作,还要看下在上面我列举的几个边界条件下,代码仍然能否正确工作。如果这些边界条件下都没有问题,那基本上可以认为没有问题了。

用递归来解决问题

有些时候,我们可以用递归来解决相关的题目可能效果会更好。当然后面会有一篇专门来讲解递归相关的题目。

我们这里用一道很基础的题目来示例。链表反转

public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }       

        ListNode lastNode = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return lastNode;
    }

看起来是不是感觉不知所云,完全不能理解这样为什么能够反转链表?我们下面来详细解释一下这段代码。

对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 reverseList 函数定义是这样的:

输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点。

明白了函数的定义,在来看这个问题。比如说我们想反转这个链表:

1->2->3->4 (head指向1)

那么输入 reverse(head) 后,会在这里进行递归():

ListNode last = reverse(head.next);

不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代<码产<生么结果:

1->2<-3<-4 (head指向1,lastNode指向4)

然后执行:

    head.next.next = head;
    head.next = null;

此时链表将变成:

1<-2<-3<-4

神奇不,链表在此时就成功的反转了。所以掌握递归解法也是很重要的。很多很复杂的指针解法,换个思路说不定也会非常的简单。

总结

写链表代码是最考验逻辑思维能力的。写的时候一定要考虑全面。最好将我刚才说的容易错的情况都在脑子里过一遍。

对于部分题解,换个思路使用递归。可能会简单很多。

多看,多练,多思考

posted @ 2020-11-21 15:10  正号先生  阅读(288)  评论(0编辑  收藏  举报