03.数据结构-链表

链表理论基础

基本说明

由于数组的长度都是固定的,如果数组已被数据填满,再要加入新的元素是非常困难的。而且,对于数组的删除和添加操作,通常需要将数组中的其他元素向前或者向后平移,这些操作也十分繁琐。

链表是一种通过指针串联在一起的存储非连续线性结构,每个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(也就是空指针)。链表的入口节点称为链表的头结点也就是head

链表在Java中有两种:单向链表、双向链表。至于循环列表,可用这两种基础的链表结构实现。Java自带LinkedList类,其底层为双向列表,其构造方法分为有参/无参,需要导入泛型。相关的API方法在题目涉及到时进行详解。

操作方式

删除节点:将前一节点指针指向后一节点即可。Java不需要内存释放。

添加节点:前一节点指针指向新节点,新节点指针指向后一节点。

关键原理:如果要对某个节点进行增删,那么操作的一定是此节点位置的前一个节点

简单题

[203]移除链表元素、[206]反转链表(双指针法)

采用虚拟头节点法,方便进行增删操作。在结束时,返回处理后的头节点(即虚拟头节点的下一跳)。在刷题前,设置一个util包,专门存放自定义的数据结构,重写后的ListNode如下:

public class ListNode {
    public int val;
    public ListNode next;

    public ListNode() {
    }

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

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

中等题

[707]设计链表

题设:你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

思路:手撕链表看似是中等题,实际上通过率比困难题更低,因为其中需要注意非常多的细节。本题要求的五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目。还是采用虚拟头节点的方式,基于单链表根据要求一步步实现。

class MyLinkedList {
    //元素个数
    int size;
    //虚拟头节点
    ListNode head;

    //链表初始化
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取链表中下标为 index 的节点值
    public int get(int index) {
        if (index >= size) return -1;
        ListNode cur = head;
        for (int i = 0; i <= index; i++) cur = cur.next; //由于包含虚拟头节点,所以查找第 index+1 个
        return cur.val;
    }

    //将一个值为 val 的节点插入到链表中第一个元素之前
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //将一个值为 val 的节点追加到链表中作为链表的最后一个元素
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    //将一个值为 val 的节点插入到链表中下标为 index 的节点之前
    //如果 index 等于链表的长度,那么该节点会被追加到链表的末尾
    //如果 index 比长度更大,该节点将不会插入到链表中
    public void addAtIndex(int index, int val) {
        if (index > size) return;
        size++;
        ListNode pre = head;
        for (int i = 0; i < index; i++) pre = pre.next;
        ListNode toAdd = new ListNode(val);
        toAdd.next = pre.next;
        pre.next = toAdd;
    }

    //如果下标有效,则删除链表中下标为 index 的节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        size--;
        if (index == 0) {
            head = head.next;
            return; //处理完特殊情况后直接返回
        }
        ListNode pre = head;
        for (int i = 0; i < index; i++) pre = pre.next;
        pre.next = pre.next.next;
    }
}

[206]反转链表(递归法)

题设:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

思路:双指针的解法是简单题的难度,从前往后将每条路翻转。递归法其实和双指针是同一个逻辑,只是理解起来稍难一点。

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    public ListNode reverse(ListNode pre, ListNode cur) {
        if (cur == null) return pre;
        ListNode temp = cur.next;
        cur.next = pre;
        return reverse(cur, temp);
    }
}

其中,return reverse(cur, temp);这一步,实际上完成了pre=cur;cur=temp;两步。

时间复杂度:O(n),每一次递归都处理链表中的一个节点。

空间复杂度:O(n),递归调用了n层栈,其实要比双指针法占用更多空间。

[24]两两交换链表中的节点

题设:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

思路:虚拟头节点+画图解决。交换两个相邻节点时的操作顺序需要格外注意。

24图解.png

根据以上操作顺序(实际写代码时需要反着写),可得:

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(-1, head);
        ListNode cur = dummy;
        ListNode first;
        ListNode second;
        while (cur.next != null && cur.next.next != null) {
            first = cur.next;
            second = first.next;
            first.next = second.next;
            second.next = first;
            cur.next = second;
            cur = first;
        }
        return dummy.next;
    }
}

时间复杂度:O(n),需要遍历链表中的所有节点。

空间复杂度:O(1),在遍历过程中,并未开辟新的内存空间。

[19]删除链表的倒数第N个节点

题设:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

思路:双指针法的经典应用。具体操作为,两个指针都先指向头节点,快指针先走n步,再同时移动快慢指针,当快指针指向最后一个节点时,慢指针就会指向要删除的目标节点的头节点。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1, head);
        ListNode slow = dummy;
        ListNode fast = dummy;
        for (int i = 0; i < n; i++) fast = fast.next;
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }
}

时间复杂度:O(n),需要遍历链表中的所有节点,用双指针法只需要遍历一遍。

空间复杂度:O(1),在遍历过程中,并未开辟新的内存空间。

[160]链表相交

题设:给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交

160图解.png

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案

思路:此题的含义是指针指向同一个节点,而不是节点值相同。方法一:长度长的链表截掉2个链表的差值后,再和长度短的链表一一进行比较,这种方法被称为先行移动长列表。方法二:合并链表实现同步移动,也是本次使用的方法。两个链表同时移动,当短链表达到末尾时,长链表正好达到比短链表多余的部分。此时,让短链表指针从长链表开始遍历,当长链表遍历结束时从短链表开始遍历,就可以达到和方法一一样的效果。同时可以发现,无论短链还是长链,遍历后都从另一个链表开始,所以其实根本不需要判断孰长孰短。

class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p1 = headA;
        ListNode p2 = headB;
        while (p1 != p2) {
            if (p1 == null) p1 = headB;
            else p1 = p1.next;
            if (p2 == null) p2 = headA;
            else p2 = p2.next;
        }
        return p1;
    }
}

时间复杂度:O(n+m),需要同时遍历长链表和短链表。

空间复杂度:O(1),在遍历时不需要创建新的空间。

[142]环形链表II

题设:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

思路:本题分为2个步骤:1.确定是否有环;2.确定环开始的位置。学过奥数的应该知道,设置快慢两个指针,慢指针每次走一格,快指针每次走两格,若路径含环,则二者终相遇。确定入环位置时,有如下图:

142图解.png

首先,慢指针在入环的第一圈二者就会相遇。(慢指针走一圈的时间内,快指针一定走了两圈)此时慢指针走过的路程为x+y,快指针走过的路程为x+z+2y,乂,快指针移动速度正好为慢指针的两倍,所以有:2*(x+y)=x+z+2y,消去重复项后可得x=z!从快慢指针相遇节点和头节点同时开始,其相遇的地方便是环的入口。

class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                ListNode p1 = slow;
                ListNode p2 = head;
                while (p1 != p2) {
                    p1 = p1.next;
                    p2 = p2.next;
                }
                return p1;
            }
        }
        return null;
    }
}

时间复杂度:O(n),因为快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个p指针走的次数也小于链表长度,总体走的次数小于链表长度的两倍。

空间复杂度:O(1),满足题目的要求,并没有修改或创建链表。

总结

链表总结.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容