第一题 移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
注意 :
在C和C++中,内存管理通常是手动进行的。这意味着开发者需要显式地分配和释放内存。当一个对象不再需要时,必须使用相应的方法(如C中的free()
函数或C++中的delete
操作符)来释放该对象占用的内存空间,否则会导致内存泄漏。
相比之下,Java和Python都采用了自动内存管理机制,即垃圾收集器(Garbage Collector,GC)。垃圾收集器会自动跟踪哪些内存块不再被使用,并自动回收这些内存,使得程序员不需要手动管理内存的分配和释放。
设置虚拟头结点
/**
* 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 null;
}
// 设置虚拟头结点,next指向真正的头结点,是为了处理要删除头结点这种情况
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可能被删
}
}
不添加虚拟节点方式(双指针)
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 移到第一个遇到的不需要删除的节点
// head为null,跳过
// 头结点不需要被删,跳过
while (head != null && head.val == val) {
head = head.next;
}
// head为null,返回
if (head == null) {
return head;
}
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur != val) {
pre = cur;
} else {
pre.next = cur.next;
}
cur = cur.next;
}
return head;
}
}
不添加虚拟节点方式(单指针)
class Solution {
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) {
head = head.next;
}
ListNode cur = head;
while (cur != null) {
// null.val 会报错
// 若cur的下一位等于val,直接跳过
while (cur.next != null && cur.next.val == val) {
cur.next = cur.next.next;
}
cur = cur.next;
}
return head;
}
}
第二题 设计链表
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
.val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
单链表
class MyLinkedList {
int size; // 链表长度,index + 1
ListNode dumyhead; // 虚拟头结点
// 初始化链表
// size = 0 表示虚拟头结点
public MyLinkedList() {
size = 0;
dumyhead = new ListNode(-1);
}
// 获取索引为index的值
// index = 0 表示头结点
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = dumyhead;
// 从虚拟头结点开始后移动,移一次到达index = 0 的头结点
// 再移动index次到达索引为index的节点
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);
}
// 在索引为index的节点前添加节点
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
// index == size时,相当于添加尾节点
ListNode pre = dumyhead;
for (int i = 0; i < index; i++) {
pre = pre.next;
} // 移动到index前一位
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next = newNode;
}
// 删除索引为index的节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode pre = dumyhead;
for (int i = 0; i < index; i++) {
pre = pre.next;
}
pre.next = pre.next.next;
}
}
双向链表
其中的添加和删除均尝试过改动,但是自己设想没问题,一运行就有问题
class MyLinkedList {
int size; // 链表长度,index + 1
ListNode dumyhead, dumytail; // 虚拟头,尾结点
class ListNode {
int val;
ListNode next, prev;
ListNode(){}
ListNode(int val) {
this.val = val;
}
}
// 初始化链表
// size = 0 表示虚拟头结点
public MyLinkedList() {
size = 0;
dumyhead = new ListNode(0);
dumytail = new ListNode(0);
// 这一步非常关键,否则在加入头结点的操作中会出现null.next的错误
dumyhead.next = dumytail;
dumytail.prev = dumyhead;
}
// 获取索引为index的值
// index = 0 表示头结点 index = size 表示虚拟尾节点
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
// 移到index处节点
ListNode cur = this.dumytail;
if (index >= size / 2) {
for (int i = size; i > index; i--) {
cur = cur.prev;
}
} else {
cur = this.dumyhead;
// 因为头结点索引是0,所以比倒序要多移一次
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);
}
// 在索引为index的节点前添加节点
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
ListNode pre = this.dumyhead;
for(int i = 0; i < index; i++){
pre = pre.next;
}
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
}
// 删除索引为index的节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode pre = this.dumyhead;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.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. 报错空指针异常
java.lang.NullPointerException: Cannot read field "next" because "<local3>" is null at line 78, MyLinkedList.addAtIndex
// 在索引为index的节点前添加节点
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
// 求index前一位
ListNode pre = this.dumytail;
if (index >= size / 2) {
for(int i = size; i >= index; i--){
pre = pre.prev;
}
} else {
for(int i = 0; i < index; i++){
pre = pre.next;
}
}
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
}
2. 无法正常删除元素
// 删除索引为index的节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode cur = this.dumytail;
if (index >= size / 2) {
for (int i = size; i > index; i--) {
cur = cur.prev;
}
} else {
cur = this.dumyhead;
// 因为头结点索引是0,所以比倒序要多移一次
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
第三题 翻转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
双指针法
/**
* 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; // cur前一位
ListNode cur = head;
ListNode temp = null; // cur后一位
// 后移并调换指向,交换后cur指向prev
while (cur != null) {
temp = cur.next;
cur.next = prev;
prev = cur;
cur = temp;
}
return prev; // cur为null,prev为最后一位,即新的头结点
}
}
递归法
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev; // cur到了原本最后一位指向的空,返回最后一位即新的头指针tprev
}
ListNode temp = cur.next; // 保存cur的下一位
cur.next = prev; // 指向变,cur指向prev
return reverse(cur, temp); // 递归,cur和prev一起往后移动一位
}
}
递归法(逆序)
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null)
return null;
if (head.next == null)
return head;
// 递归完成后,last一直等于尾节点
ListNode last = reverseList(head.next);
// 调换传入head和上一次递归的head的指向
head.next.next = head;
// 不写这句代码,head和head.next会形成环
head.next = null;
return last; // 返回尾节点,即新的头结点
}
}
栈
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null)
return null;
if (head.next == null)
return head;
Stack<ListNode> stack = new Stack<>();
ListNode cur = head;
// 入栈
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 创建一个虚拟头结点
ListNode dumyhead = new ListNode(0);
cur = dumyhead;
// 出栈
while (!stack.isEmpty()) {
cur.next = stack.pop();
cur = cur.next;
}
// cur此时是尾节点,应该指向null
cur.next = null;
return dumyhead.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 swapPairs(ListNode head) {
// 虚拟头结点
ListNode dumyhead = new ListNode(-1);
dumyhead.next = head;
ListNode pre = dumyhead; // 指向交换的两节点的前一位
ListNode temp; // 保存交换的两节点的后一位
ListNode first; // 交换的前一位节点
ListNode second; // 交换的后一位节点
while (pre.next != null && pre.next.next != null) {
first = pre.next;
second = pre.next.next;
temp = second.next;
pre.next = second;
second.next = first;
first.next = temp;
pre = first;
}
return dumyhead.next;
}
}
递归法
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode next = head.next;
//递归
ListNode newNode = swapPairs(next.next);
next.next = head; // 传入的后一位指向前一位
head.next = newNode; // 传入的前一位指向上次递归的next
// next即指向改变后,现在两节点的后一位
return next;
}
}
第五题 删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
普通解法
使用链表的基本操作先算出链表长度size,再根据索引size - N删去该元素
快慢指针法
可画图理解
/**
* 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 dumyhead = new ListNode(0);
dumyhead.next = head;
ListNode fast = dumyhead;
ListNode slow = dumyhead;
// 相差n个节点
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// fast到null时,slow位于要删除节点的前一位
slow.next = slow.next.next;
return dumyhead.next;
}
}
栈
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 虚拟头结点,因为可能会删去头结点
ListNode dumyhead = new ListNode(0, head);
Stack<ListNode> stack = new Stack<>();
ListNode cur = dumyhead;
// 入栈
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 出栈
for (int i = 0; i < n; i++) {
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return dumyhead.next;
}
}
第六题 链表相交
给你两个单链表的头节点 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) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
// 求出两链表长度
while (curA != null) {
lenA++;
curA = curA.next;
}
while (curB != null) {
lenB++;
curB = curB.next;
}
// 长链表和短链表 (名字不能起short和long)
// 特殊情况是两链表长度相等,必须加上等号
ListNode longList = lenA >= lenB ? headA : headB;
ListNode shortList = lenA < lenB ? headA : headB;
int max = lenA >= lenB ? lenA : lenB;
int min = lenA < lenB ? lenA : lenB;
int gap = max - min; // 长度差
while (gap-- > 0) {
longList = longList.next;
} // 长度相差几,长链表往后移动几位
while (shortList != null) {
if (longList == shortList) {
return shortList;
}
shortList = shortList.next;
longList = longList.next;
}
return null;
}
}
双指针法二
把两链表合并,在headA的后面加上headB,在headB的后面加上haedA。如果有相交点,那么总会相遇;如果没有相交点,那么遍历完都是null返回null。(可画图理解)
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
// 遍历完自己这边后,交换边遍历,使得如果有交点,则必定相遇
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
// 没交点,返回null
return pA;
}
}
哈希表
注意:Java中的Collection接口是单列集合的根接口,List和Set是它的两个子接口
List
(列表):一个有序的集合(元素可以重复)。它的主要实现类有ArrayList
、LinkedList
等。Set
(集合):一个不允许出现重复元素的集合。它的主要实现类有HashSet
、TreeSet
等。
它们都实现了根接口的contains方法,但底层效率却不相同。
List底层使用equals方法遍历整个列表,时间效率为O(n)。
Set基于hash表实现,底层使用hashCode方法计算对象的哈希值,并以此确定对象的存储位置,时间效率接近O(1)。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<>();
ListNode temp = headA;
// 将headA链表存入visited中
while (temp != null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
// 比较headB的各个节点headA是否包含
while (temp != null) {
if (visited.contains(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}
栈
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 提前排除,有备无患
if (headA == null || headB == null) {
return null;
}
ListNode curA = headA;
ListNode curB = headB;
Stack<ListNode> stackA = new Stack<>();
Stack<ListNode> stackB = new Stack<>();
while(curA != null) {
stackA.push(curA);
curA = curA.next;
}
while(curB != null) {
stackB.push(curB);
curB = curB.next;
}
// 记录弹出的节点,以便返回
ListNode temp = null;
while (!stackA.isEmpty() && !stackB.isEmpty()) {
ListNode nodeA = stackA.pop();
ListNode nodeB = stackB.pop();
if (nodeA == nodeB) {
temp = nodeB;
continue;
} else {
// 若无相等节点,返回null
// 若遇到不等节点,返回上个弹出的节点
return temp;
}
}
// 栈空且弹出的节点都相等,返回上个弹出节点
return temp;
}
}
第七题 环形链表
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,返回null。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
快慢指针法
(图片结合代码理解)
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
// 慢节点一次走一格,快捷点一次走两格
// 因为fast相对于slow每次多走一格,可列方程
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 有环
ListNode headNode = head; // 头结点
ListNode meetingNode = slow; // 相遇节点
while (headNode != meetingNode) {
headNode = headNode.next;
meetingNode = meetingNode.next;
}
return meetingNode;
}
}
// 没有环
return null;
}
}
哈希表
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head;
// visited记录遇到的所有节点
Set<ListNode> visited = new HashSet<>();
while (pos != null) {
// 一个节点遇到两次,即入口
if (visited.contains(pos)) {
return pos;
} else {
visited.add(pos);
pos = pos.next;
}
}
// 没环
return null;
}
}