代码随想录算法训练营第三天 | 203.移除链表元素,707.设计链表 ,206.反转链表

链表理论基础

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

单链表

单链表节点是由data和next组成,data储存数据,next储存的下一个节点的指针
在这里插入图片描述

双链表

双链表节点是由prev,data和next组成,prev储存的上一个节点的指针,data储存数据,next储存的下一个节点的指针
在这里插入图片描述

循环链表

特点就是收尾相连,最后一个节点的next指向开始节点
在这里插入图片描述

删除节点

以单链表为例(target是要删除的节点)
将要target的上一个节点的next指向target下一个节点的指针
在这里插入图片描述

添加节点

以单链表为例(target是要添加的节点)
将index的上一个节点的next指向target,将target的next指向index节点

性能分析

在这里插入图片描述

解题的注意事项

关于链表的题我们需要自己定义链表,如果自己定义的话,使用默认构造函数,无法直接对链表进行赋值
链表定义的代码实现:

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; }
}

LeetCode 203.移除链表元素

题目链接 :203.移除链表元素

解题思路

思路一(暴力解法)

根据链表的性质,将val的上一个节点的指针赋予下一个节点。
在这里插入图片描述

但是,头节点如果是目的节点的话,我们直接将头节点赋予个下一个节点
在这里插入图片描述
在这里插入图片描述

/**
 * 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) {
        while(head != null && head.val == val){
            head = head.next;
        }
        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 = cur;
            }
            cur = cur.next;
        }
        return head;
    }
}

注意:

  1. cur是指的节点并不代表节点的值,我们如果要将指针被赋予为下下一个节点:cur.next = pre.next;。
  2. 别忘了head为空时,记得直接输出head。
  3. 我们将前一个节点赋予个后一个节点,代码的实现head->next = head->next->next(C++),java需要分开写设两个指针(cur,pre)pre.next = cur.next;
  4. 这种解法是pre作为进行遍历比较的节点,而cur是pre前一个节点,不管cur.val等不等于val,cur的值都是往后移动一位,所以cur = cur.next;,而cur = 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 removeElements(ListNode head, int val) {
        if(head == null){
            return head;
        }
        ListNode Humhead = new ListNode(-1,head);
        ListNode pre = Humhead;
        ListNode cur = Humhead.next;
        while(cur != null){
            if(cur.val == val){
                pre.next = cur.next;
            }else{
                pre = cur;
            }
            cur = cur.next;
        }
        return Humhead.next;
    }
}

注意:
1.输出链表时,我们不能直接输出head,head可能已经改变了(头节点就是删除节点)。
2.上面的说法给以换一个角度去理解,上述代码中,我们从始至终都在对Humhead(虚拟头节点)的链表进行修改,除了虚拟头节点都会进行修改,所以真正的头结点可能已经改变。

LeetCode 707.设计链表

题目链接 :707.设计链表

解题思路

思路一(单链表—虚拟头节点)

  1. 需要先定义一个链表LinkList
  2. 初始化引入LinkList(0)虚拟头节点,并且定义一个长度size
  3. get函数我们需要判断index是否有意义,因为index是从0开始,我们遍历时我们需要从0开始遍历,到index停止,然后输出val。
  4. addAtIndex函数我们先判断插入index与链表的长度的大小,然后需要说明index<0,算作index=0,需要增加链表的长度,然后从0开始遍历到index-1的位置,在进行赋值
  5. deleteAtIndex函数我们先判断index与size的大小,然后缩小长度,如果index=0,就直接将头节点后移,需要增加链表的长度,然后从0开始遍历到index-1的位置,在进行赋值
  6. addAtHead和addAtTail可以通过addAtIndex实现
class LinkList {
    int val;
    LinkList next;
    LinkList(){}
    LinkList(int val){this.val = val;}
}
class MyLinkedList {
    LinkList head;
    int size;
    public MyLinkedList() {
    	//初始化虚拟头节点
        head = new LinkList(0);
        //链表的长度
        size = 0;
    }
    
    public int get(int index) {
    	//下标是否有效
        if(index < 0 || index >= size){
            return -1;
        }
        LinkList 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++;
        LinkList cur = head;
        //遍历至index-1
        for(int i = 0; i < index; i++){
            cur = cur.next;
        }
        LinkList pre = new LinkList(val);
        pre.next = cur.next;
        cur.next = pre;
        
        
    }
    
    public void deleteAtIndex(int index) {
        if(index >= size||index < 0){
            return;
        }
        size--;
        if(index == 0){
            head = head.next; 
            //将虚拟头节点变成了head,输出链表是从虚拟头节点的下一个节点开始,所以第一个数就被省略了
            return;
        }
        LinkList cur = head;
        //遍历至index-1
        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);
 */

注意:
1.for(int i = 0; i < index; i++){ pred = pred.next; }
图解如下:
在这里插入图片描述
2.在进行添加和删除时,长度也许小改变
3.我们使用了虚拟头节点后,我们对链表头部进行修改时,可以通过虚拟头节点覆盖掉头节点,头节点就会被删除,因为我们输出链表是从虚拟头节点的下一个节点进行输出。

思路二(双链表—虚拟头节点)

与以不同之处在与对前指针的赋值,还有虚拟尾结点的设定

class ListNode{
    int val;
    ListNode prev;
    ListNode next;
    ListNode(){}
    ListNode(int val){this.val = val;}   
}
class MyLinkedList {
    ListNode head,tail;
    int size;
    public MyLinkedList() {
        size = 0;
        //初始化虚拟头节点
        head = new ListNode(0);
        //初始化虚拟尾节点
        tail = new ListNode(0);
        //为了防止addAtIndex中currentNode.next.prev = pred;出现空指针报错
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int index) {
    	//index是从0开始算,所以index=size不在链表范围内
        if(index < 0 || index >= size){
            return -1;
        }
        ListNode currentNode = head;
        //从虚拟头节点开始遍历,i <= index,是遍历至index 
        for(int i = 0; i <= index; i++){
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    // 卡哥的解法更好
    //  public int get(int index) {
    //     //判断index是否有效
    //     if(index<0 || index>=size){
    //         return -1;
    //     }
    //     ListNode cur = this.head;
    //     //判断是哪一边遍历时间更短
    //     if(index >= size / 2){
    //         //tail开始
    //         cur = tail;
    //         for(int i=0; i< size-index; i++){
    //             cur = cur.prev;
    //         }
    //     }else{
    //         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(size < index){
            return;
        }
        if(index < 0){
          index = 0;
        }
        size++;
        ListNode currentNode = head;
        //遍历至index前一个节点
        for(int i = 0; i < index; i++){
            currentNode = currentNode.next;
        }
        ListNode pred = new ListNode(val);
        pred.prev = currentNode;
        pred.next = currentNode.next;
        //设置尾节点是防止下面代码出现空指针报错
        currentNode.next.prev = pred;
        currentNode.next = pred; 
    }
    
    public void deleteAtIndex(int index) {
        if(index < 0 || index >= size){
            return;
        }
        size--;
        ListNode currentNode = head;
        //遍历至index前一个节点
        for(int i = 0; i < index; i++){
            currentNode = currentNode.next;
        }
        currentNode.next.next.prev = currentNode.next.next;
        currentNode.next = currentNode.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);
 */

注意:
1.虚拟头节点一般使用在对链表进行修改时,主要作用,可以防止head为空时出现空指针报错。
2.在代码中出现的return;,我们可以理解成链表从head.next输出。

LeetCode 206.反转链表

题目链接 :206.反转链表

解题思路

思路一(双指针算法)

解题思路如下图:
在这里插入图片描述
需要一个临时指针来存放cur.next,为了后面cur向后移
在这里插入图片描述

在这里插入图片描述

  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) {
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
}

2.迭代双指针解法

/**
 * 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 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);
    }
}

注意:
1.在输出链表时,需要注意cur是等于null才结束循环,所以pre变成头节点,输出时应该从pre开始输出。
建议: 不建议直接写迭代双指针法,可以先写双指针法,然后按照双指针法写迭代双指针法给容易理解

总结

1.虚拟头节点主要应用于对链表的修改。
2.使用虚拟头节点是为了使我们更好的对全链表进行修改,所以我们使用虚拟头节点时,我们需要将代码中所以头节点都看作为虚拟头节点。
3.在对链表节点的顺序问题上,我们一般会采用双指针方法,因为单链表是单向性的,且节点分布不一定连续,无法通过一个指针去遍历,实现两个不相邻的节点进行连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值