今日任务
● 链表理论基础
● 203.移除链表元素
● 707.设计链表
● 206.反转链表
链表理论基础
建议:了解一下链表基础,以及链表和数组的区别
文章链接:代码随想录
203.移除链表元素
建议: 本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解/视频讲解::代码随想录
BFS实现
public ListNode removeElements(ListNode head, int val) {
ListNode vir = new ListNode(-1);
vir.next = head;
ListNode cur = vir;
while(cur != null && cur.next != null){
ListNode next = cur.next;
if(next.val == val){
cur.next = next.next;
}else{
cur = cur.next;
}
}
return vir.next;
}
DFS实现
public ListNode removeElements(ListNode head, int val) {
ListNode vir = new ListNode(-1);
vir.next = head;
dfs(vir,val);
return vir.next;
}
public void dfs(ListNode cur,int target){
//递归出口
if(cur == null || cur.next == null){
return;
}
ListNode next = cur.next;
if(next.val == target){
cur.next = next.next;
dfs(cur,target);
}else{
dfs(next,target);
}
}
核心点:
- 使用虚拟头结点可以迅速定位到最小循环体
- 确定循环终止条件
- 注意易错点:
- next的val如果相等移除后,cur指针不变无需后移
707.设计链表
建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解/视频讲解:代码随想录
lass ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
int size;
//记录链表的虚拟头结点和尾结点
ListNode head,tail;
public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
head.next=tail;
tail.prev=head;
}
public int get(int index) {
//判断index是否有效
if(index>=size){
return -1;
}
ListNode cur = this.head;
//判断是哪一边遍历时间更短
if(index >= size / 2){
//tail开始
cur = tail;
for(int i=0; i< size-index; i++){
cur = cur.prev;
}
}else{
for(int i=0; i<= index; i++){
cur = cur.next;
}
}
return cur.val;
}
public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}
public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
//index大于链表长度
if(index>size){
return;
}
size++;
//找到前驱
ListNode pre = this.head;
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;
}
public void deleteAtIndex(int index) {
//判断索引是否有效
if(index>=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
重点:
- 理解ListNode链表特点:
- 动态性强,可灵活增删节点,节点操作复杂度为O(1)
- 查询复杂度为O(n)
- 必须掌握链表有哪些核心方法
-
// 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); //在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 obj.addAtIndex(index,val); //删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的 obj.deleteAtIndex(index);
-
206.反转链表
建议先看我的视频讲解,视频讲解中对 反转链表需要注意的点讲的很清晰了,看完之后大家的疑惑基本都解决了。
题目链接:. - 力扣(LeetCode)
题目链接/文章讲解/视频讲解:代码随想录
/**
* 迭代(双指针):
* 遍历链表: 将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点
* 因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。
* 不要忘记在最后返回新的头引用!
* 时间复杂度:O(n),假设 nn 是列表的长度,时间复杂度是 O(n)。
* 空间复杂度:O(1)。
* @param head
* @return
*/
public static ListNode reverseList(ListNode head) {
//当前节点的前面一个元素(头结点前一个元素为null)
ListNode prev = null;
//迭代的当前元素
ListNode curr = head;
while (curr != null){
//获取当前节点的下一个节点
ListNode nextTmp = curr.next;
//将指针指向前一个元素
curr.next = prev;
//将当前元素置为下一个元素的前一个元素
prev = curr;
//将下一个节点设置为当前节点,移动指针
curr = nextTmp;
}
return prev;
}
/**
* 递归方式:
*
* 假设链表是[1, 2, 3, 4, 5]从最底层最后一个reverseList(5)来看
* 返回了5这个节点
* reverseList(4)中
* p为5
* head.next.next = head 相当于 5 -> 4
* 现在节点情况为 4 -> 5 -> 4
* head.next = null,切断4 -> 5 这一条,现在只有 5 -> 4
* 返回(return)p为5,5 -> 4
* 返回上一层reverseList(3)
* 处理完后返回的是4 -> 3
* 依次向上
*
* 理解:
* 假设: (1->2)-(2->3)-(3->4)-(4->5)-(5-null)
* 递归情况下直接到最后一个节点:(5->null)
* 反转之后是:节点5指向节点4, (5 -> 4) ,但是实际节点4又指向的5,所以,必须将节点4的next 设置位null(避免循环链表)
* 依次向上:
* 0. (1->2)-(2->3)-(3->4)-(4->5)-(5-null)
* 1. (1->2)-(2->3)-(3->4)-(4->null)-(5-4)
* 2. (1->2)-(2->3)-(3->null)-(4->3)-(5-4)
* 3. (1->2)-(2->null)-(3->4)-(4->5)-(5-4)
* 4. (1->null)-(2->3)-(3->4)-(4->5)-(5-4)
* 5. 递归完成返回
* 一眼就能看出,这里其实就是单向链表的末尾的next元素,不停的与前一个元素的val替换的过程
* @param head
* @return
*/
public static ListNode reverseList(ListNode head) {
//递归终止条件
if (head == null || head.next == null) {
return head;
}
//注意这里的递归位置,如果涉及到从末位开始处理的,那么递归的位置通常都是紧跟着终止条件之后的
ListNode p = reverseList2(head.next);
//翻转指向箭头 head -> nextA -> nextB 翻转为: head <- nextA X nextB
head.next.next = head;
//原head的指向需要置空
head.next = null;
return p;
}
总结:
- 初步理解递归的概念
- 递归: 将一个大问题分解为一个或多个与原问题相似但规模较小的子问题,直到子问题可以直接解决为止。然后通过逐步解决这些子问题并将结果合并,最终得到原问题的解。
- 要素:
- 终止条件:问题的最最小规模的解,比如是null或者一个元素的时候的解等
- 递归调用:需要理解在什么节点需要执行递归 --> 什么节点入栈
- 结果处理:出栈的时候,根据题目的需求做什么 --> 例如 翻转 合并 求和等
- 不需要考虑太多,概念引入的越多越容易麻爪,链表完全用数组的方式去理解,只不过多了一个指向
- 指针差异i++ --> cur = cur.next 的区别而已
- 双指针(快慢指针,对向指针(双向链表),滑动窗口同样都可以用在链表上)
- 指针差异i++ --> cur = cur.next 的区别而已