基础知识
链表的分类
链表的存储特点
数组:在内存中连续分布
链表:在内存中散乱地分布在各处,通过指针串联
链表的定义
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; }
}
203. 移除链表元素
方法一:迭代
不使用虚拟头节点
如果不使用虚拟头节点,则在删除节点时要对“要删除的节点是否为头节点”进行分类讨论:
- 删除某个节点的做法是:令该节点的前一节点的next指针指向该节点的后一节点。如果该节点是链表头节点(没有前一节点),则直接令head指针指向该节点的后一节点。
- 由于删除中需要知道要删除节点的前一节点,因此在遍历时使用两个指针一起遍历,快指针指向当前节点,慢指针指向当前节点的前一节点。
注意:在更新pre
和cur
指针时,如果在本轮迭代中删除了cur
节点,就只需要将cur
向后移动一位,不需要移动pre
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode pre = null, cur = head;
while(cur != null){
if(cur.val == val){
// 如果cur是要删除的节点
if(cur == head){
// 如果cur是头节点
head = cur.next;
}else{
// 如果cur不是头节点
pre.next = cur.next;
}
// 只需将cur指针向后移动一位
cur = cur.next;
}else{
// 如果cur不是要删除的节点
// 将两个指针一起向后移动一位
pre = cur;
cur = cur.next;
}
}
return head;
}
}
使用虚拟头节点
使用虚拟头节点可以不用对“要删除的节点是否为链表头节点”进行分类讨论,简化代码编写:
- 开始遍历链表前创建一个虚拟头节点
dummyHead
,其next
指针指向链表头节点 - 最终返回
dummyHead.next
即可
注意:在更新pre
和cur
指针时,如果在本轮迭代中删除了cur
节点,就只需要将cur
向后移动一位,不需要移动pre
。
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 添加一个虚拟头节点
ListNode dummyHead = new ListNode(0, head);
ListNode pre = dummyHead, cur = head;
while(cur != null){
if(cur.val == val){
// 如果cur是要删除的节点
pre.next = cur.next;
// 只需将cur指针向后移动一位
cur = cur.next;
}else{
// 如果cur不是要删除的节点
// 将两个指针一起向后移动一位
pre = cur;
cur = cur.next;
}
}
return dummyHead.next;
}
}
方法二:递归
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 递归终点:空链表
if(head == null){
return head;
}
// 对除了头节点head以外的节点进行删除操作
ListNode node = removeElements(head.next, val);
// 判断头节点head是否需要删除(判断head的节点值是否等于给定的val)
if(head.val == val){
// 如果head.val等于val,则需要删除头节点
head = node;
}else{
// 如果head.val不等于val,则不需要删除头节点
head.next = node;
}
return head;
}
}
707. 设计链表
class MyLinkedList {
ListNode head;
public MyLinkedList() {
head = null;
}
// 获取指定下标位置节点的值
public int get(int index) {
ListNode node = getNodeByIndex(index);
if(node == null){
return -1;
}else{
return node.val;
}
}
// 获取指定下标的节点
private ListNode getNodeByIndex(int index){
// 如果当前链表为空
if(head == null){
return null;
}
// 如果指定下标不合法
if(index < 0){
return null;
}
// 如果链表不为空、且指定下标合法
// 从head开始遍历链表,index值代表还需遍历的元素个数
ListNode curNode = head;
while(true){
// 如果index已经减少到0了,说明找到指定下标的节点了
if(index == 0){
return curNode;
}
// 如果index还没有减少到0,但已经抵达链表结尾了,说明链表中不存在该下标的节点
if(curNode == null){
return null;
}
// 将遍历指针后移一位,并将index值减少1
curNode = curNode.next;
index--;
}
}
public void addAtHead(int val) {
ListNode newNode = new ListNode(val, head);
head = newNode;
}
public void addAtTail(int val) {
// 如果当前数组为空
if(head == null){
addAtHead(val);
return;
}
// 找到当前数组中的最后一个元素
ListNode lastNode = head;
while(lastNode.next != null){
lastNode = lastNode.next;
}
ListNode newNode = new ListNode(val);
lastNode.next = newNode;
}
public void addAtIndex(int index, int val) {
if(index < 0){
return;
}
if(index == 0){
addAtHead(val);
}
// 找到要插入位置的前一个位置的节点
ListNode preNode = getNodeByIndex(index - 1);
if(preNode == null){
return;
}
ListNode newNode = new ListNode(val, preNode.next);
preNode.next = newNode;
}
public void deleteAtIndex(int index) {
if(head == null){
return;
}
if(index < 0){
return;
}
if(index == 0){
head = head.next;
return;
}
// 找到要插入位置的前一个位置的节点
ListNode preNode = getNodeByIndex(index - 1);
if(preNode == null){
return;
}
if(preNode.next == null){
return;
}
preNode.next = preNode.next.next;
}
// 链表节点
class ListNode{
int val;
ListNode next;
ListNode(){}
ListNode(int val){this.val = val;}
ListNode(int val, ListNode next){this.val = val; this.next = next;}
}
}
19. 删除链表的倒数第N个节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 为了删除倒数第N个节点,需要找到链表的倒数第N+1个节点(即倒数第N个节点的前一节点)
/* 寻找链表倒数第N+1个节点的思路:
* 用快慢指针同时遍历链表,fast指针永远比slow指针快n步(即当slow指向第i个节点时,fast指向第i+n个节点)
* 当fast指针指向链表最后一个节点时,slow指针刚好指向倒数第N个节点的前一节点
*/
// 添加一个虚拟头节点
ListNode dummyHead = new ListNode(0, head);
ListNode slow = dummyHead; // 初始化slow指针
// 初始化fast指针,令其指向第n个节点
ListNode fast = dummyHead;
for(int i=1; i<=n; i++){
fast = fast.next;
if(fast == null){
// 链表中节点个数少于n
return head;
}
}
// 向后移动指针,直到fast指针指向链表最后一个节点
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
// 删除slow指向节点的下一节点
slow.next = slow.next.next;
return dummyHead.next;
}
}
面试题 02.07. 链表相交
解法一:哈希表
解法二:双指针
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 如果A和B中有空链表,则两个链表必然没有交点
if(headA == null || headB == null){
return null;
}
// 指针A先遍历链表A,再遍历链表B
// 指针B先遍历链表B,再遍历链表A
ListNode A = headA, B = headB;
while(A != B){
A = (A == null ? headB : A.next);
B = (B == null ? headA : B.next);
}
// 两指针相遇
return A;
}
}
环形链表
141. 环形链表 Ⅰ
解法一:哈希表
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
解法二:双指针 - 快慢指针
- 快慢指针:fast每次走两步,slow每次走一步
- 如果无环:fast指针能够抵达链表末尾的null
- 如果有环:fast和slow会在环中相遇
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null){
return false;
}
// fast每次走两步,slow每次走一步
ListNode slow = head, fast = head.next;
while(true){
if(slow == fast){
// 快慢指针相遇 => 链表中有环
return true;
}
// fast向后走两步
if(fast == null){
// fast指针抵达链表末尾的null => 链表中无环
return false;
}else{
fast = fast.next;
}
if(fast == null){
// fast指针抵达链表末尾的null => 链表中无环
return false;
}else{
fast = fast.next;
}
// slow指针向后走一步
slow = slow.next;
}
}
}
142. 环形链表 II
如果链表中有环,当二者第一次相遇时:
具体做法:
需要注意的是,这里的第一步其实应该是让fast指向头节点的前一个位置,这样当fast再走a步就可以到达环的入口。
也可以先让fast指向头节点,让slow指向slow.next,然后两个指针再一起走a-1步,但是这时会忽略“第一次相遇时slow指向的正好就是环的入口”这种情况,需要分类讨论。
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null){
return null;
}
// fast每次走两步,slow每次走一步
ListNode slow = head, fast = head.next;
while(true){
if(slow == fast){
// 快慢指针相遇 => 链表中有环
if(slow != head){
fast = head;
slow = slow.next;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
}
return slow;
}
// fast向后走两步
if(fast == null){
// fast指针抵达链表末尾的null => 链表中无环
return null;
}else{
fast = fast.next;
}
if(fast == null){
// fast指针抵达链表末尾的null => 链表中无环
return null;
}else{
fast = fast.next;
}
// slow指针向后走一步
slow = slow.next;
}
}
}