前言:链表部分之前刷过一些题,掌握的还可以,希望可以顺利把这部分题刷完。
203.移除链表元素
思路:自己创建一个头节点,使新的头节点指向旧的头节点。
错误尝试:一开始考虑的比较复杂,设置了指针pre,能够想到直接比较cur.next.val和val的值会使代码更加简洁,但也要注意想清楚如果删除元素后cur是否需要向前走一步,答案是不需要的。因为删除之后cur.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) {
ListNode newHead=new ListNode();
newHead.next=head;
ListNode cur=newHead;
while(cur.next!=null){
if(cur.next.val==val){
cur.next=cur.next.next;
}else{
cur=cur.next;
}
}
return newHead.next;
}
}
707. 设计链表
前言:确实很少训练到链表设计的基础,这个题让我掌握如何从节点开始建立链表,并实现各种接口。
思路:该题存在一个接口需要在头节点前面插入节点,所以最好设置一个虚拟头节点。
1、需要先设计一个节点类。有以下好处:
- 封装性: 将节点定义为一个独立的类可以更好地隐藏内部实现细节。这样外部代码不需要知道节点的具体结构,只需要通过链表类提供的方法来操作链表。
- 重用性: Node 类可以在多个不同的数据结构中被复用,比如不仅可以用于单向链表,还可以用于双向链表、循环链表等。这种设计使得 Node类更加通用。
- 清晰性: 将节点逻辑和链表逻辑分离可以使代码更加清晰易懂。每个类都有明确的责任,这有助于维护和扩展。
节点类的实现代码如下:
class ListNode{
int val;
ListNode next;
ListNode(){};
ListNode(int val){
this.val=val;
}
}
2、单链表
class MyLinkedList {
//设置链表的节点数
int size;
//设置一个虚拟头节点
ListNode head;
public MyLinkedList() {
//初始化节点数和头节点
size=0;
head= new ListNode(0);
}
public int get(int index) {
if(index<0 || index>=size){
return -1;
}
ListNode cur=head;
for(int i=0;i<=index;i++){
cur=cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
ListNode headNode=new ListNode(val);
headNode.next=head.next;
head.next=headNode;
size++;
}
public void addAtTail(int val) {
ListNode cur=head;
while(cur.next!=null){
cur=cur.next;
}
ListNode tailNode=new ListNode(val);
tailNode.next=cur.next;
cur.next=tailNode;
size++;
}
public void addAtIndex(int index, int val) {
if(index>size){
return;
}
if(index<0){
index=0;
}
ListNode cur=head;
for(int i=0;i<index;i++){
cur=cur.next;
}
ListNode indexNode=new ListNode(val);
indexNode.next=cur.next;
cur.next=indexNode;
size++;
}
public void deleteAtIndex(int index) {
if(index<0 || index>=size){
return;
}
ListNode pre=head;
for(int i=0;i<index;i++){
pre=pre.next;
}
pre.next=pre.next.next;
size--;
}
}
总结:写的时候一定注意虚拟头节点的存在以及size的变化。还是挺不好写的,有很多细节,之后要多练习。注意size和index的关系,index>=size都是无效的,总是会忘记相等的情况。
206.反转链表
前言:反转链表之前写过,印象很深刻是头插+递归。但自己写的时候递归的条件和函数想不起来了。感觉之前其实是死记硬背的反转链表的递归写法,对原理掌握的并不清楚。这次通过先写双指针的写法,再写递归,整个逻辑更清晰了。
- 双指针写法的代码
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;
}
}
代码优化一下,把temp的定义写在循环外面就不用买次重新定义了:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null;
ListNode cur=head;
ListNode temp=null;
while(cur!=null){
temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
}
- 递归写法的代码
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、边缘条件判断,也就是链表为空或者只有一个节点的情况,这种情况直接返回head。
2、进行递归调用,返回值为后面的一个结点last。传入头节点的下一个节点,因为运行到递归这一行的时候没有达到边缘条件所以会多次调用递归,直到最后一个节点,因为最后一个节点的next为null,此时在这一层递归中last就是最后一个节点。
3、回到上一层递归继续执行,此时的head为倒数第二个节点,进行反转给head节点的next节点(也就是最后一个节点)的next赋值为head,完成head和head.next的反转。并将head的next域赋值为null,这一层的反转结束,回到上一层继续完成反转。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null){
return head;
}
ListNode last=reverseList(head.next);
head.next.next=head;
head.next=null;
return last;
}
}
总结:反向反转链表理解起来还是比较困难的。但是双指针和正向反转链表还可以,可以先掌握这两种方法。