Leetcode移除链表元素203/设计链表707/反转链表206

前言

链表理论知识点总结:

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

2、List item

一、203题(移除链表元素)

题目描述:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。

题解1

题解1(mine):用时打败100%,内存打败31.95%(・∀・(・∀・(・∀・*)

/**
 * 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 node = head;
        //首先处理val在最开头的位置的情况
        while(node!=null&&node.val==val){
            node = node.next;
        }
        if(node==null){
            return null;
        }
        head=node;
        while(node.next!=null){
            //证明当前node非链表中最后一个元素
            if(node.next.val == val){
                //删除该节点
                node.next = node.next.next;
            }else{
                node = node.next;
            }
        }
        return head;
    }
}

算法思路:

  • 没啥思路,就是一个很基础的删除链表删除题。出错点在于while(node!=null&&node.val==val)要先判断node是否为null,才能用node.val。和数组要判断是否出界才能引用下标一样。
  • 要说有什么思考点,就这个吧:首先要处理1个到多个重复元素在最开头的情况。找到一个真正的head结点。

备注:mine解法也是一个正统的解法,解法2能让代码更加简洁,思想其实差不多😀

题解2(dummy head)

虚拟头节点解法

public ListNode removeElements(ListNode head, int val) {
    if (head == null) {
        return head;
    }
    // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
    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;
}

算法思想:
在head前添加一个虚拟头节点dummy head,从而统一链表操作(题解1中需要对头节点进行一个特殊处理)

二、707题(设计链表)

题目描述:题意:

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

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

我的辛勤劳动

这题我的评价是:纯折磨😩
以下只为记录我的辛苦劳作,不作为学习的备忘记录,太稀碎了。

class ListNode{
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val){
        this.val = val;
    }
}
class MyLinkedList {
    int size;
    ListNode head;
    public MyLinkedList() {
    }
    public void printList() {
    	if(size==0) {
    		System.out.println("没有元素");
    		return;
    	}
    	ListNode cur = head;
    	while(cur.next!=null) {
    		System.out.print(cur.val+",");
    		cur=cur.next;
    	}
    	System.out.println(cur.val);
    }
    //获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
    public int get(int index) {
        int i = 0 ;
        ListNode node = head;
        if(index>=size) return -1;
        while(i<index){
            node = node.next;
            i++;
        }
        return node.val;
    }
    //将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
    public void addAtHead(int val) {
    	ListNode node = new ListNode(val);
    	if(size==0) {
    		head = node;
    		size++;
    		return;
    	}
        node.next = head;
        head = node; 
        size++;
    }
    //将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
    public void addAtTail(int val) {
        ListNode node = new ListNode(val);
        if(size==0) {
        	addAtHead(val);
        	return;
        }
        ListNode cur = head;
        while(cur.next!=null){
            cur=cur.next;
        }
        //cur当前指向链表中最后一个元素
        cur.next = node;
        size++;
    }
    //将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
    public void addAtIndex(int index, int val) {
        if(index==size){
            addAtTail(val);
            return;
        }
        if(index>size){
            return;
        }
        if(index==0){
            addAtHead(val);
            return;
        }
        ListNode node = new ListNode(val);
        ListNode cur=head;;       
        int i = 0;
        //找到index的prev节点
        while(i<index-1){
            cur=cur.next;
            i++;
        }
        //执行插入操作
        ListNode cur2 = cur.next;
        cur.next = node;
        node.next = cur2;
        size++;
    }
    //如果下标有效,则删除链表中下标为 index 的节点。
    public void deleteAtIndex(int index) {
        if(index>=size){
            return;
        }
        if(size==1) {
        	head = null;
        	size--;
        	return;
        }
        if(index==0){
            head=head.next;
            size--;
            return;
        }
        //中间元素的删除
        //找到位于index-1的元素
        ListNode cur = head;
        int i = 0;
        while(i<index-1){
            cur=cur.next;
            i++;
        }
        cur.next = cur.next.next;
        size--;
    }
}

我的评价是很crazy,执行时间10ms打败了27.75%的用户,消耗内存击败49.56%的用户,整一个稀碎。🆒不是这里size++忘了,就是调用另一个函数size++了两次,i++也能忘。特殊情况大概是:一个元素没有,有一个元素,多个元素,总是会漏掉处理某一种情况。我真的是一个个对着用例找出来的错误,一把心酸老泪。

题解

讲解
其实思路都差不多,主要是实现方式的差异

虽然想到了可以一个方法里使用另外一种方法,但没想到下面代码这种利用:
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
    addAtIndex(0, val);
}

//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
    addAtIndex(size, val);
})
果然还是得先整体思考,而不是一上来就哐哐一顿写✍

补充:使用dummy head时,要在MyLinkedList类中加入成员变量ListNode head,用在构造器中直接初始化head = new ListNode(0);//初始化虚拟头节点

//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);//初始化虚拟头节点
    }

    //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head;
        //包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    //在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;
    }

    //删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        if (index == 0) {
            head = head.next;
	    return;
        }
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

思路说明:那三个Add都是一个套路,所以只用写一个AddIndex即可,其余的调用它。这么说来,就是写增删查函数。

三、206题(反转链表)

题目描述
反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

题解1(双指针)

mine

/**
 1. Definition for singly-linked list.
 2. public class ListNode {
 3.     int val;
 4.     ListNode next;
 5.     ListNode() {}
 6.     ListNode(int val) { this.val = val; }
 7.     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 8. }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        //处理0-1个结点
        if(head==null||head.next==null){
            return head;
        }
        //2个结点以上
        //思路:
        ListNode left=head,right=head.next;
        head.next = null;//斩断循环
        ListNode tmp;
        while(right!=null){
            tmp = right.next;
            right.next = left;
            left = right;
            right = tmp;
        }
        return left;
    }
}

算法思路:

  1. 特殊情况:空链表或者只有1个元素,直接返回
  2. 两个以上:[left,right]指向相邻的两个元素,执行代码"right.next->left",然后[left,right]往后移动。过程如下所示(如a->b->c->d):
· L指向a,R指向b:R.next = L; //则变成a<-b... c->d
· L指向b,R指向c:R.next = L; //则变成a<-b<-c ... d
会发现一旦执行了R.next = L,原来的R.next元素和R元素之间会断开连接,
这样无法往右移动了,所以需要拿一个tmp来在反转之前记录下R.next。

易错点:

  • 斩断循环点:如A->B->C的反转,反转时要把A.next置空,不然会变成C->B->A->B
  • 三组一处理时,如"a->b->c",要记录三个位置,a和b的位置毋庸置疑需要拿两个指针来记录,因为用于遍历的指针走到需要处理的b元素时,无法倒退回a,所以a要用一个指针记录。由于b.next=a,不再指向c了,所以要再拿一个变量tmp存储c,即b.next。

代码版本2

算法思路:①cur指针:当前要处理的节点。将指向下一个元素的指针next改为指向前一个元素。②pre指针:记录cur的前一个节点。③tmp指针:保存cur的next指针指向的元素(因为如果不记录,cur处理完之后就找不到下一个元素了)

重点:①初始化:cur=head,pre=null
②迭代:tmp=cur.next; cur.next = pre;pre = cur;
③循环退出条件:cur=null时,因为null节点无需处理。

更简洁的原因:在mine版本中pre=head,cur=head.next,所以要特殊处理head为空的情况。

// 双指针
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null) {
            temp = cur.next;// 保存下一个节点
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev;
    }
}

题解2(递归)

参照双指针版本很容易更改
算法思路:①很明显是[pre,cur]在递归,所以函数是reverse(pre,cur)
②递归跳出条件:cur=null时
③递归体:每轮当前递归完成pre和cur所指元素之间的反转操作。下一轮递归需要完成[cur,cur.next],所以是reverse(cur,cur.next)。这里又涉及之前所说的指针保存问题,所以在执行反转前要记录tmp = cur.next。故下一次递归为reverse(cur,tmp)。

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

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转
        // 更新prev、cur位置
        // prev = cur;
        // cur = temp;
        return reverse(cur, temp);
    }
}

题解3(递归)

mine版本:2024/8/25重做
算法思路:1->2->3->4->5,可以把2->3->4->5看作一个整体,过程是:先把1的next置空,然后reverse(2->3->4->5),再把2的next指向1。顺序不能颠倒,类似汉诺塔思想。

// 递归 
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head==null) return null;
        ListNode newhead=head;
        while(newhead.next!=null) newhead=newhead.next;
        reverse(head);
        return newhead;
    }

    public void reverse(ListNode head){
        ListNode node = head.next;
        if(node==null) return;
        head.next = null;
        reverse(node);
        node.next = head;
    }
}

归纳

  1. 前两题都比较快想到了解决方法,但实现起来出错很多。206思考了一会儿,找到迭代规律后,实现起来挺快的,没有太多出错点。
  2. 要点:①头节点、无节点等的特殊情况的处理 ②dummyhead的应用 ③循环结束条件格外要小心
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值