算法学习之链表篇


前言

这是算法学习链表篇的第二篇Blog,由于个人的懒癌+开学琐事,所以拖延了超级久才发布第二篇学习博客,学习是一个持续、长久的过程,希望以后自己能战胜拖延hhh,话不多说,我们继续深究链表,深入学习叭~

本博客参考了代码随想录,特此声明


一、链表设计大师

我们在链表操作时往往会使用他人写好的函数,我们直接调用即可,这样操作固然方便,但是这不利于我们理解、学习链表,所以,我们不妨来写5个常见的链表操作,你准备成为大师了么?

Leetcode 707设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0~index 的。
你需要实现:

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

时间和能力有限,仅仅手撕理解单链表的5大操作

单链表的定义:

class ListNode{
    int val;
    ListNode next;
    public ListNode(){
    }
    //创建一个有参构造,方便直接构建一个值为val的结点
    public ListNode(int val){
        this.val=val;
    }
}

操作细节定义:

class MyLinkedList {
    int size;
    ListNode head;  //虚构一个头结点
    /** Initialize your data structure here. */
    public MyLinkedList() {
        size=0;
        head=new ListNode();
    }
    
    /** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
    public int get(int index) {
        //非法位置,返回-1
        if(index<0||index>=size)
            return -1;
        
        //因为包含了一个虚拟的头结点,所以需要找到的结点位置值需要到index+1
        ListNode currentNode=head;
        for(int i=0;i<=index;i++){
            currentNode=currentNode.next;   //寻找链表的下一结点
        }

        return currentNode.val;
    }
    
    /** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
    public void addAtHead(int val) {
        addAtIndex(0,val);
    }
    
    /** Append a node of value val to the last element of the linked list. */
    public void addAtTail(int val) {
        addAtIndex(size,val);
    }
    
    /** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
    public void addAtIndex(int index, int val) {
        //若返回位置越界,那么终止
        if(index>size)
            return;
        if(index<0)
            index=0;
        size++;         //插入后的长度+1

        //找到插入点的前驱
        ListNode prev=head;
        for(int i=0;i<index;i++){
            prev=prev.next;
        }

        //创建新结点
        ListNode temp=new ListNode(val);
        temp.next=prev.next;
        prev.next=temp;     //不能写反:前驱下一结点是新结点
    }
    
    /** Delete the index-th node in the linked list, if the index is valid. */
    public void deleteAtIndex(int index) {
        if(index<0||index>=size){
            return;
        }

        size--;
        ListNode prev=head;
        for(int i=0;i<index;i++){
            prev=prev.next;
        }
        prev.next=prev.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);
 */

发现
其实大多数人学习链表时,多多少少都会被指针弄的天花乱坠,我也不例外,那么如何理解代码呢?

for(int i=0;i<=index;i++){
	currentNode=currentNode.next;
}

如何理解currentNode=currentNode.next?
我们从右往左解读:意为将当前结点的下一结点赋为当前结点

ListNode temp=new ListNode(val);
temp.next=prev.next;
prev.next=temp;

如何理解temp.next=prev.next?(temp为新建结点,prev为前驱结点)
我们从左往右解读:当前结点的下一结点指向的是前驱结点的下一结点,即如图所示:

在这里插入图片描述

如何理解prev.next=temp?
我们依旧从左往右解读:prev前驱结点的下一结点是新结点,就是平移了一条线:

在这里插入图片描述
这样,我们就完成了一个结点的新增,也就是说,在左端没有.next操作下我们从右往左解读会更好理解,而在左端存在.next操作下我们从左往右解读编写也许会更加容易理解,当然啦,这只是小编的经验,如果存在错误或者有更好的方法欢迎指正!

二、小试牛刀

1.反转链表

leetcode 206题:
题目:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述
参考Carl哥的动图,以及上面个人的理解,关键代码终于能自己写了!
参考于代码随想录

/**
 * 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) {    //无需重新构建头结点
        
        ListNode prev=null;                 //前一个结点
        ListNode current=head;              //当前结点

        ListNode temp=null;
        while(current!=null){
            temp=current.next;	//当前结点的下一结点为(中转结点)
            current.next=prev;	//当前结点指向前一个结点
            prev=current;		//前一个结点作当前结点
            current=temp;		//中转结点作当前结点
        }

        return prev;
    }
}

我们不妨参考一下上面的理解,发现可以理解通,并加以训练

2.两两交换链表中的结点

leetcode 24题
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:
在这里插入图片描述

使用虚拟头结点两两交换的方案:

/**
 * 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) {
        ListNode dummyNode=new ListNode();

        dummyNode.next=head;    //虚拟一个头结点dummyNode,方便对所有链表进行统一操作
        ListNode prev=dummyNode;
        while(prev.next!=null&&prev.next.next!=null){
            //当上一结点后面存在两个结点不为空时,可以交换
            ListNode temp=head.next.next;       //缓存记录点
            //step1:将原来prev的next改为head的next
            prev.next=head.next;                
            //step2:将head.next(prev.next)的next指向head
            /**关于这里为什么是head.next.next=head
            因为上一步已经将前驱结点对准了头结点的下一结点
            而这一步是要把头结点变为其后继结点,故也可以写成
            prev.next.next=head --->方便理解
            */
            head.next.next=head;
            //step3:将head的next接上缓存temp
            head.next=temp;

            //step4:步进1位
            prev=head;
            head=head.next;
        }
        return dummyNode.next;
    }
}

在这里插入图片描述

3.删除链表的倒数第N个结点

leetcode 19:删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗

示例:
在这里插入图片描述

如何解决此链表问题:
我们通常可以想到一种解决方案,那就是两次扫描链表,第一次记录长度n,然后将需要删除的结点的位置标明:n-N+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 dummyNode=new ListNode();
        dummyNode.next=head;

        int length=0;
        //一次定长度
        ListNode prev=dummyNode;
        ListNode currentNode=head;
        while(currentNode!=null){
            length++;
            currentNode=currentNode.next;
        }

        currentNode=head;   //归位
        //二次定操作
        int index=length-n+1;
        int i=0;
        while(currentNode!=null){
            if(i==index-1){
                //删除操作
                prev.next=currentNode.next;
                currentNode.next=null;  //先释放
            }else{
                i++;
            }
            //步1位
            prev=currentNode;
            currentNode=currentNode.next;
        }
        return dummyNode.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 removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode=new ListNode();
        dummyNode.next=head;

        //快慢指针
        ListNode slow=dummyNode;
        ListNode fast=dummyNode;

        while(n-->0){
            //先移动快指针
            fast=fast.next;
        }

        //一起移动快慢指针,然后在快指针为空时截断操作
        ListNode prev=null;

        while(fast!=null){
            prev=slow;
            slow=slow.next;
            fast=fast.next;
        }

        //到达符合条件点
        prev.next=slow.next;
        slow.next=null;     //释放内存

        return dummyNode.next;
    }
}

4.链表相交

leetcode面试题:02.07
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

示例:
在这里插入图片描述

题目理解:题目中要求的相等并不是简单的数值相等,而是结点,指针的相等,即链表相交后它们后面的结点都相等,那么,我们需要将链表较长的一端先走完多余的部分,因为这样才能同步接下来的一些步骤:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        /***摘选自代码随想录中题目理解:
        简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。
        为了方便举例,假设节点元素数值相等,则节点指针相等。
        */

        int len1=0,len2=0;
        ListNode tempA=headA;
        ListNode tempB=headB;
        while(tempA!=null){
            len1++;
            tempA=tempA.next;
        }
        while(tempB!=null){
            len2++;
            tempB=tempB.next;
        }

        //归位
        tempA=headA;
        tempB=headB;

        //差值:
        int subNum=len1-len2;
        if(subNum<0){
            subNum=-subNum;
            //交换tempA和tempB,保证tempA始终长于tempB
            ListNode temp=tempA;
            tempA=tempB;
            tempB=temp;
        }

        //A先走
        while(subNum-->0){
            tempA=tempA.next;
        }

        //一起走,并开始比较结点,指针
        while(tempA!=null){
            if(tempA==tempB){
                return tempA;
            }
            tempA=tempA.next;
            tempB=tempB.next;
        }
        //未找到
        return null;
    }
}

总结

链表篇到这里基本就已经完结了,后续或许有新的心得或者好玩的题目会继续补充,本文中若有错误也欢迎大家指正!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Astronaut_001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值