前言
链表理论知识点总结:
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个元素,直接返回
- 两个以上:[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;
}
}
归纳
- 前两题都比较快想到了解决方法,但实现起来出错很多。206思考了一会儿,找到迭代规律后,实现起来挺快的,没有太多出错点。
- 要点:①头节点、无节点等的特殊情况的处理 ②dummyhead的应用 ③循环结束条件格外要小心