链表(下):如何轻松写出正确的链表代码

技巧一:理解指针或者引用的意义

指针或者引用都是存储对象的内存地址。
将某个变量赋值给指针,就是将这个变量的地址赋值给指针,或者反过来说指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能够找到这个变量
以下面这句代码为例:

p ->next = q

p ->next就是一个指针,把q的内存地址赋值给它。

技巧二:警惕指针丢失和内存泄漏

插入节点

如图所示。
正确的代码应该是

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

但是有时候写成如下代码的时候就会发生指针丢失:

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

技巧三:利用哨兵简化实现难度

一般我们插入和删除一个节点是通过如下代码来实现的:

//在a、b节点之间插入c节点
b = a.next;
a.next = c;
c.next = b;
//删除a之后的b节点
a.next = a.next.next;

但是上述代码对于空链表来说并不适用,因为空链表不存在a.next。这种情况下,我们就需要特殊处理:

//在空链表中插入节点a
if(head == null){
  head = a;
}
//在只有头结点的链表中删除节点
if(head.next == null){
  head = null;
}

针对链表的插入和删除操作,需要对第一个节点和删除最后一个节点的情况进行特殊处理。这样代码实现起来就会很繁琐,不简洁。
这个时候就需要哨兵来帮助解决了。
我们观察上面针对非空链表实现的代码:

  • 插入节点时,因为空链表不存在head,所以head.next会造成空指针异常。
  • 删除节点的时候,head.next.next会造成空指针异常。

所以我们针对这个情况,在头指针之前加一个哨兵指针,哨兵指针没有存储的数据,它指向头结点,它的功能就是在空链表插入或者只有一个节点删除时作为头结点来使用。这样不管什么情况下,链表都不是空链表了。此时拥有哨兵的链表叫做带头链表。


带头链表

技巧四:重点留意边界条件处理

我们在写好一个链表之后应该考虑下面四种情况:

  1. 如果链表为空,链表是否能够正常工作。
  2. 如果链表只有一个节点,能够正常工作。
  3. 如果链表只有两个节点,能够正常工作。
  4. 在处理头结点和尾节点的时候,能够正常工作。

如果我们的代码在上述四种情况下都能够正常运行,说明一般是没有问题的。

技巧五:使用举例和画图方法

有些特殊情况,如果仅仅是凭着脑内思考,可能不会想的很清楚。这个时候可以把这些情况分别列举出来,然后通过图示展示出来,更有利于逻辑的理顺。


举例画图

技巧六:多写多练

把下面几种链表操作都手动实现一边就不怕了。

  1. 链表反转
  2. 环的检测
  3. 两个有序的链表的合并
  4. 删除链表倒数第n个节点
  5. 求链表的中间节点
1. 链表反转
import java.util.ArrayList;

public class Reverse {

    /**
     * 第一种方法,通过将链表中的节点存到数组中,然后从后往前读取.
     *
     * @param listNode 头结点
     * @return false即为失败,true成功
     */
    boolean reverse1(ListNode listNode) {

        ListNode initial = listNode;
        //新建一个数组
        ArrayList<ListNode> arrayNodes = new ArrayList<>();
        if (listNode == null || listNode.next == null) {
            return true;
        }
        while (listNode.next != null) {
            arrayNodes.add(listNode);
            listNode = listNode.next;
        }
        for (int i = 0; i < arrayNodes.size(); i++) {
            initial.val = arrayNodes.get(i).val;
            initial = initial.next;
        }
        return true;
    }

    /**
     * 第二种方法,采用递归的思想来解决
     *
     * @param listNode 头节点
     * @return 返回已经反转的节点
     */
    ListNode reverse2(ListNode listNode) {
        if (listNode == null || listNode.next == null) {
            return listNode;
        }
        ListNode nextCode = listNode.next;
        nextCode = reverse2(nextCode);
        nextCode.next = listNode;
        listNode.next = null;
        return listNode;
    }

    /**
     * 普通的非递归的方法来解决
     *
     * @param node the node
     * @return false即为失败,true成功
     */
    boolean reverse3(ListNode node) {
        if (node == null || node.next == null) {
            return true;
        }

        ListNode pre = null;

        while (node.next != null) {
            ListNode next = node.next;
            node.next = pre;
            node = next;
            pre = node;
        }
        //最后一个节点反转
        node.next = pre;
        return true;
    }
}
2. 环的检测
import java.util.HashSet;

public class Cycle {

    /**
     * HasCycle boolean.
     * 使用快慢两个节点进行处理,如果快节点能够追上慢节点,说明有环
     * 时间复杂度O(n)
     * 空间复杂度O(1)
     *
     * @param head the head
     * @return the boolean
     */
    public boolean hasCycle1(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        //fast.next != null 是为了避免出现空指针异常
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                return true;
            }
        }
        return false;
    }

    /**
     * Has Cycle 2 boolean.
     * 使用一个Set来存储每个节点的内存地址,遍历链表,如果Set中不存在该节点,添加,存在,有环。
     * 当遍历遇到null时,说明不为环。
     * 空间复杂度:O(n)
     * 时间复杂度:O(n)
     * @param head the head
     * @return the boolean
     */
    public boolean hasCycle2(ListNode head) {

        HashSet<ListNode> listNodes = new HashSet<>();
        while (head != null) {
            if (!listNodes.contains(head)) {
                listNodes.add(head);
                head = head.next;
            } else {
                return true;
            }
        }
        return false;
    }
}
3. 合并两个有序的链表
public class merge {
    /**
     * Merge list boolean.
     * 使用遍历某一链表的方法来进行合并,合并的操作在某一固定的链表上进行,真的是落入了下乘。
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     *
     * @param a the a
     * @param b the b
     * @return the boolean
     */
    public boolean mergeList(Node a, Node b) {

        if (a == null || b == null) {
            return false;
        }
        Node pre = new Node(0);
        while (a.next != null && b != null) {
            if (a.val > b.val) {
                Node temp = b;
                b = b.next;
                pre.next = temp;
                temp.next = a;
            } else {
                pre = a;
                a = a.next;
            }
        }
        //此时a链表已经到头,只需要接上剩余的b链表即可。
        if (a.next == null){
            a.next = b;
            return true;
        }
        //此时b == null,说明已经完成合并了
        return true;
    }

    /**
     * Merge list 2 boolean.
     * LeetCode上一位大佬写的,应该是我目前见过的最好的解法了。
     * 其思路并没有像我局限在一定要在某一个链表上进行处理。
     * 其把两个链表打散,当成分割的节点进行处理。
     * 真的是难以望其项背。
     * @param a the a
     * @param b the b
     * @return the boolean
     */
    public Node mergeList2(Node a, Node b){
        if (a == null){
            return b;
        }
        if (b == null){
            return a;
        }
        //因为链表是从小到大排序的,所以我们必须获取两个链表中最小的下一个节点
        if (a.val > b.val){
            //当a节点的值比b节点的值大的时候,b节点的next节点还未确定,
            // 需要a和b的next节点进行比较,取其中较小的节点
            b.next = mergeList2(a, b.next);
            return b;
        } else {
            a.next = mergeList2(a.next, b);
            return a;
        }

    }
}

class Node {
    int val;
    Node next;

    Node(int num) {
        val = num;
    }
}
5. 删除链表倒数第n个节点
public class Reciprocal {

    /**
     * 使用快慢两个节点,快节点先比慢节点多走n步。
     * 注意:因为这是要删除倒数第n个节点,如果是返回第n个节点,那么就是多走n-1步了。
     *
     * @param head the head
     * @param n    the n
     * @return 链表删除节点后的head节点
     */
    public ListNode getReciprocalN(ListNode head, int n) {
        //使用哨兵节点来处理空节点或者节点只有一个的情况
        ListNode dummy = new ListNode("start");
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;
        for (int i = 1; i < n+1; i++) {
            fast = fast.next;
            if (fast == null){
                System.out.println("该链表并没有比n更多的节点,删除失败");
                return null;
            }
        }
        while (fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        //当fast节点到达末尾的时候,slow节点在倒数第n个节点之前的一个节点
        slow.next = slow.next.next;
        return dummy.next;
    }
}
6. 求链表的中间节点
/**
 * Definition for singly-linked list.
 * 简单地使用快慢速节点处理即可 
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        if(fast == null){
            return slow;
        }
        return slow.next;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容