文章目录
一、203.移除链表元素
题目描述: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
1.递归
对除了头节点
head
以外的节点进行删除操作,然后判断head
的节点值是否等于val
。
1.head == val
:head
需要被删除,新的头节点为head.next
;
2.head != val
:head
保留,新的头节点仍为head
;
3.递归的终止条件是head == null
, 此时直接返回head
。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(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 removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
head.next = removeElements(head.next, val);
return head.val == val ? head.next : head;
}
}
2.直接使用原来的链表来进行删除操作
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 头节点是需要删除的节点时
while (head != null && head.val == val) {
head = head.next;
}
// 已经是null,提前退出
if (head == null) {
return head;
}
// 已经确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
}
3.设置一个虚拟头结点在进行删除操作
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
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;
}
}
二、707.设计链表
题目描述: 使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
1.单向链表
实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个哨兵(sentinel)节点作为头节点,和一个
size
参数保存有效节点数。
- 时间复杂度:初始化消耗 O ( 1 ) O(1) O(1),
get
消耗 O ( i n d e x ) O(index) O(index),addAtHead
消耗 O ( 1 ) O(1) O(1),addAtTail
消耗 O ( n ) O(n) O(n),其中 n n n为链表当前长度,即addAtHead
、addAtTail
、addAtIndex
已调用次数之和,addAtIndex
消耗 O ( i n d e x ) O(index) O(index)。- 空间复杂度:所有函数单次调用均为 O ( 1 ) O(1) O(1),总体为 O ( n ) O(n) O(n),其中 n n n为
addAtHead
、addAtTail
、addAtIndex
已调用次数之和。
实现get(index)
时,先判断有效性,再通过循环来找到对应的节点的值。
addAtIndex(index, val)
实现过程如下图所示:
- 找前驱节点
pred
; - 创建新节点
toAdd
; - 将
toAdd.next
的设置为pred.next
, 将pred.next
设置为toAdd
(注意: 这里的更新操作要先更新toAdd.next
后更新pred.next
,简单理解为:先后-后前); - 更新
size
。
实现deleteAtIndex(index)
,实现过程:
- 判断参数有效性;
- 找前驱节点
pred
; - 更新
pred.next == pred.next.next
删除节点; - 更新
size
。
//单链表
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个节点的数值
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;
}
//在链表最前面插入一个节点
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点
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--;
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
pred.next = pred.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);
*/
2.双向链表
实现双向链表,即每个节点要存储本身的值,后继节点和前驱节点。除此之外,需要一个哨兵节点作为头节点
head
和一个哨兵节点作为尾节点tail
,还需要一个size
参数保存有效节点数。
- 时间复杂度:初始化消耗 O ( 1 ) O(1) O(1),
get
消耗 O ( i n d e x ) O(index) O(index),addAtHead
消耗 O ( 1 ) O(1) O(1),addAtTail
消耗 O ( 1 ) O(1) O(1),其中 n n n为链表当前长度,即addAtHead
、addAtTail
、addAtIndex
已调用次数之和,addAtIndex
消耗 O ( i n d e x ) O(index) O(index)。- 空间复杂度:所有函数单次调用均为 O ( 1 ) O(1) O(1),总体为 O ( n ) O(n) O(n),其中 n n n为
addAtHead
、addAtTail
、addAtIndex
已调用次数之和。
实现get(index)
时,先判断有限性,然后再比较从head
还是tail
来遍历会比较快找到目标,然后进行遍历。
addAtIndex(index, val)
实现过程如下图所示:
- 找原来下标为
index
的节点succ
- 找前驱节点
pred
; - 创建新节点
toAdd
; - 通过各自
prev
和next
变量的更新来增加toAdd
; - 更新
size
。
实现deleteAtIndex(index)
,实现过程: - 判断参数有效性;
- 找下标为
index
节点的前驱节点pred
和后继节点succ
; - 通过更新各自
pred
和next
变量的更新来删除节点; - 更新
size
。
//双链表
class MyLinkedList {
class ListNode {
int val;
ListNode next, pred;
ListNode(int x) { val = x; }
}
int size;
ListNode head, tail;
public MyLinkedList() {
size = 0;
head = new ListNode(0);
tail = new ListNode(0);
head.next = tail;
tail.pred = head;
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = head;
// 通过判断 index < (size-1)/2 来决定是从head还是tail遍历,提高效率
if (index < (size - 1) / 2) {
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
} else {
cur = tail;
for (int i = 0; i <= size - index - 1; i++) {
cur = cur.pred;
}
}
return cur.val;
}
public void addAtHead(int val) {
ListNode cur = head;
ListNode newNode = new ListNode(val);
newNode.next = cur.next;
cur.next.pred = newNode;
cur.next = newNode;
newNode.pred = cur;
size++;
}
public void addAtTail(int val) {
ListNode cur = tail;
ListNode newNode = new ListNode(val);
newNode.next = tail;
newNode.pred = cur.pred;
cur.pred.next = newNode;
cur.pred = newNode;
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 newNode = new ListNode(val);
newNode.next = cur.next;
cur.next.pred = newNode;
newNode.pred = cur;
cur.next = newNode;
size++;
}
public void deleteAtIndex(int index) {
if(index >= size || index < 0){return;}
ListNode cur = head;
for(int i = 0; i < index; i++){
cur = cur.next;
}
cur.next.next.pred = cur;
cur.next = cur.next.next;
size--;
}
}
三、206.反转链表
题目描述: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
1.双指针法
- 定义一个
cur
指针,指向头节点,再定义一个pred
指针,初始化为null
;- 反转:将
cur.next
节点用temp
保存一下,即保存一下当前节点;- 改变
cur.next
指向,将cur.next
指向pred
,即反转第一个节点;- 循环走代码逻辑,继续移动
pred
和cur
指针。
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(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; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pred = null;
ListNode cur = head;
ListNode temp = null;
while(cur != null) {
temp = cur.next; // 保存下一个节点
cur.next = pred;
pred = cur;
cur = temp;
}
return pred;
}
}
2.递归法
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
// 递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode pred, ListNode cur) {
if (cur == null) {
return pred;
}
ListNode temp = null;
temp = cur.next; // 先保存下一个节点
cur.next = pred; // 反转
return reverse(cur, temp);
}
}
// 从后向前递归
class Solution {
public ListNode reverseList(ListNode head) {
// 边缘条件判断
if (head == null) return null;
if (head.next == null) return head;
// 递归调用,反转第二个节点往后的链表
ListNode last = reverseList(head.next);
// 反转头节点与第二个节点的指向
head.next.next = head;
// 此时的head节点为尾节点,next需要指向null
head.next = null;
return last;
}
}
总结
以上就是今天要学习的内容,设计链表是重点,把这道题目做了,链表基本操作就OK了。