代码随想录-暑假算法第二天(链表篇)

代码随想录-暑假算法第二天(链表篇)

关于链表,你该了解这些!

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

如图所示: 链表1

#链表的类型

接下来说一下链表的几种类型:

#单链表

刚刚说的就是单链表。

#双链表

单链表中的指针域只能指向节点的下一个节点。

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

双链表 既可以向前查询也可以向后查询。

如图所示: 链表2

#循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决约瑟夫环问题。

链表4

#链表的存储方式

了解完链表的类型,再来说一说链表在内存中的存储方式。

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

如图所示:

链表3

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

#链表的定义

接下来说一说链表的定义。

链表节点的定义,很多同学在面试的时候都写不好。

这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。

而在面试的时候,一旦要自己手写链表,就写的错漏百出。

这里我给出C/C++的定义链表节点方式,如下所示:

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

1
2
3
4
5
6

有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。

但是这个构造函数不会初始化任何成员变量,下面我来举两个例子:

通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

1

使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

1
2

所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!

#链表的操作

#删除节点

删除D节点,如图所示:

链表删除节点

只要将C节点的next指针 指向E节点就可以了。

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

#添加节点

如图所示:

链表添加节点

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

#性能分析

再把链表的特性和数组的特性进行一个对比,如图所示:

链表链表与数据性能对比

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

相信大家已经对链表足够的了解

1.移除链表元素

力扣题目链接(opens new window)

题意:删除链表中等于给定值 val 的所有节点。

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]

示例 2: 输入:head = [], val = 1 输出:[]

示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

方法一:使用虚拟头节点

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {

        //方法一 使用虚拟头节点
        ListNode dummy = new ListNode(-1,head);
        //前一个节点
        ListNode pre = dummy;
        //临时节点 用于遍历每一个节点
        ListNode cur = head;
        while(cur!=null){

            if(cur.val == val){
                //数值相等
                pre.next = cur.next;
            }else{
                //数值不相等
                pre = cur;
            }
            //到下一个节点 因为是遍历
            cur = cur.next;
        }
        //返回头节点
        return dummy.next;
    }
}

方法二:原链表移除元素

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //方法二:原链表移除元素的方法

        //分两种情况
        //1; 头节点位置移除元素
        while(head!=null && head.val == val){
            head = head.next;
        }
        //2. 除了头节点的位置的其他位置移除元素
        //前一个节点  当前这个节点不可能是val相等的节点
        if(head == null){
            return head;
        }
        ListNode pre = head;
        //临时指针
        ListNode cur = head.next;
        while(cur != null){
            if(cur.val == val){
                //数值相等
                pre.next = cur.next;
            }else{
                //数值不相等
                pre = pre.next;      //666666666
            }
            //依次遍历
            cur = cur.next;
        }
        //返回头节点
        return head;

    }
}

总结

  1. 就是一个遍历操作,看不懂的时候可以画一个链表

2.设计链表

力扣题目链接(opens new window)

题意:

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

707示例

方法1:使用虚拟头节点

class ListNode {
    int val;
    ListNode next;

    ListNode() {

    }

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

class MyLinkedList {
    int size;
    // 虚拟头节点
    ListNode head;

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

    public int get(int index) {
        if (index > size - 1 || index < 0) {
            return -1;
        }
        // 遍历链表
        // 临时变量
        ListNode cur = head;
        for (int i = 0; i <= index; i++) {
            cur = cur.next;
        }
        return cur.val;
    }

    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        // 节点加加
        size++;

        // 临时变量
        ListNode cur = head;
        while (index > 0) {
            cur = cur.next;
            index--;
        }
        // 创建一个节点
        ListNode newListNode = new ListNode(val);
        newListNode.next = cur.next;
        cur.next = newListNode;

    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        //个数减减
        size--;
        //如果是头节点要判断,特殊处理
        if(index == 0){
            head = head.next;
            return;
        }
        // 遍历找到index前一个节点
        ListNode cur = head;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

3.反转链表

力扣题目链接(opens new window)

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

#算法公开课

《代码随想录》算法视频公开课 (opens new window)帮你拿下反转链表 | LeetCode:206.反转链表 (opens new window),相信结合视频再看本篇题解,更有助于大家对本题的理解

#思路

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

206反转链表

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

那么接下来看一看是如何反转的呢?

我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点

解法1: 双指针法

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        // 解法1: 双指针法
        // 当前指针
        ListNode cur = head;
        // 前一个指针
        ListNode pre = null;
        //临时指针 用于保存cur后面的指针
        ListNode temp = null;
        // 开始传向
        while (cur != null) {
            temp = cur.next;
            cur.next = pre;
            //往前移动
            pre = cur;
            cur = temp;

        }
        //返回头节点
        return pre;
    }
}

解法二: 递归

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        // 解法二 递归
        return resverse(null, head);
    }

    public ListNode resverse(ListNode pre,ListNode cur){
        //递归终止条件
        if(cur==null){
            return pre;
        }
        //改变指针的指向
        ListNode temp = cur.next;
        cur.next = pre;
        //往前移动
        return resverse(cur,temp);
    }
}

4. 两两交换链表中的节点

力扣题目链接(opens new window)

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

24两两交换链表中的节点题意

#算法公开课

《代码随想录》算法视频公开课 (opens new window)帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点 (opens new window),相信结合视频再看本篇题解,更有助于大家对本题的理解

#思路

这道题目正常模拟就可以了。

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。

对虚拟头结点的操作,还不熟悉的话,可以看这篇链表:听说用虚拟头节点会方便很多? (opens new window)

接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序

初始时,cur指向虚拟头结点,然后进行如下三步:

24两两交换链表中的节点1

操作之后,链表如下:

24两两交换链表中的节点2

看这个可能就更直观一些了:

24两两交换链表中的节点3

解法1: 使用虚拟头节点

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 解法1: 使用虚拟头节点
        // 虚拟头节点
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        // 定义操作指针
        ListNode cur = dummyHead;
        while (cur.next != null && cur.next.next != null) {
            //保存交换的第一个节点
            ListNode temp1 = cur.next;
            //保存要交换的两个节点的后一个节点  画个图就懂了
            ListNode temp3 = cur.next.next.next;
            //第一步交换
            cur.next = cur.next.next;
            //第二步交换
            cur.next.next = temp1;
            //第三步交换
            cur.next.next.next = temp3;
            //移动操作指针
            cur = cur.next.next;

        }
        return dummyHead.next;
    }
}

5.删除链表的倒数第N个节点

力扣题目链接(opens new window)

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

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

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

输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1 输出:[]

示例 3:

输入:head = [1,2], n = 1 输出:[1]

#算法公开课

《代码随想录》算法视频公开课 (opens new window)::链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点 (opens new window),相信结合视频再看本篇题解,更有助于大家对链表的理解。

#思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

思路是这样的,但要注意一些细节。

分为如下几步:

  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: 

  • fast和slow同时移动,直到fast指向末尾,如题: 

  • 删除slow指向的下一个节点,如图: 

解法1: 使用虚拟头节点

/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //使用虚拟头节点方法
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        //快指针
        ListNode fast = dummyHead;
        //慢指针
        ListNode slow = dummyHead;
        //先让快指针移动n步
        for(int i = 1;i<=n;i++){
            fast = fast.next;
        }
        //最后再移动一步 (为了让慢指针指向要删除的节点的前一个节点)
        if(fast!=null){
            fast = fast.next;
        }

        //最后让快慢指针同时移动 直到快指针指向null
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        //此时慢指针已经指向了要删除的节点的前一个节点
        //现在只需要改变方向即可
        if(slow.next != null){
            slow.next = slow.next.next;
        }
        //不能返回head的值 (因为如果只有一个节点,返回的并不是null,而是head)
        //要返回虚拟头节点的next节点
        return dummyHead.next;
    }

}

6.环形链表II

力扣题目链接(opens new window)

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

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

循环链表

#算法公开课

《代码随想录》算法视频公开课 (opens new window)把环形链表讲清楚!| LeetCode:142.环形链表II (opens new window),相信结合视频再看本篇题解,更有助于大家对链表的理解。

#思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

#判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

142环形链表1

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

141环形链表

#如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

142环形链表II(求入口)

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

题解

/**
 * Definition for singly-linked list.
 * class ListNode {
 * int val;
 * ListNode next;
 * ListNode(int x) {
 * val = x;
 * next = null;
 * }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        //快指针
        ListNode fast = head;
        //慢指针
        ListNode slow = head;
        //判断有环
        while(fast != null && fast.next != null){
            //快指针 一次移动2步
            fast = fast.next.next;
            //慢指针 一次移动一步
            slow = slow.next;
            //如果快指针和慢指针相等必定有环
            if(fast == slow){
                //有环
                //接下来判断入口
                //根据数学证明 入口必定是相遇点(x==z)
                ListNode index1 = head;
                ListNode index2 = fast;
                while(index1 != index2){
                    //一步一步走
                    index1 = index1.next;
                    index2 = index2.next;
                }
                //index1和index2以及相等 (即已经到了入口点)
                return index2;
            }
        }

        //没有环
        return null;
    }
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值