提示:努力生活,开心、快乐的一天
文章目录
链表的基础知识
一、 什么是链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思),链表的入口节点称为链表的头结点也就是head
二、链表的几种类型:
单链表
:如上解释
双链表
:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询
循环链表
:顾名思义,就是链表首尾相连
三、链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
四、链表的操作
删除链表
添加链表
五、性能对比
203.移除链表元素
💡解题思路
- 移除元素可能为链表的头节点,需要特殊处理
- 在原链表进行操作分两种情况:
删除的元素是头节点
:将头节点向后移动一位即可
删除的元素非头节点
:记住一个原则,删除的元素一定是当前元素的下一个元素,所以,当前指针与下一个指针都不能为空
,定义一个当前元素cur
,当cur.next.val===val
时,将cur.next
指向cur.next.next
;否则,当前指针后移一位。最后return head
- 设置虚拟节点进行操作:该方法是在头节点之前再添加一个节点,将头节点当成普通节点对待,无需特殊处理。该方法的重点在于设置虚拟节点,
let newHead = new ListNode(0, head)
,然后从该虚拟节点开始遍历。最后return newHead.next
,为什么此处不return出head,因为,有可能head是被删除的那一个,而newHead.next却是一定存在的。
🤔遇到的问题
- 在原链表进行操作时,没有结束循环的操作
💻代码实现
在原链表操作
var removeElements = function (head, val) {
//原链表进行操作
//删除的是头节点
while (head !== null && head.val === val) {
head = head.next
}
//删除非头节点
let cur = head
while (cur !== null && cur.next !== null) {
if (cur.next.val === val) {
cur.next = cur.next.next
continue //错误点,忘记结束单次循环
} else {
cur = cur.next
}
}
return head
};
设置虚拟节点
var removeElements = function (head, val) {
//设置虚拟节点
let newHead = new ListNode(0, head);
let cur = newHead;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next
continue
}
cur = cur.next
}
return newHead.next
};
🎯题目总结
- 无论哪种方式,最终删除的都是cur.next,然后cur.next = cur.next.next
707.设计链表
💡解题思路
- 初始化列表对象:定义三个值,分别为
头节点head
、尾节点tail
、列表长度size
- 创建节点用类,节点中包含2个参数:
val
和next
- getNode(index)方法:
获取输入的值在链表中的节点
index非法(小于0或者大于列表长度)时,return null;index合法时,创建一个虚拟节点,指向head
,然后0到index循环,获取index在列表中的节点 - get(index)方法:考虑边界index无效的情况返回-1;其他情况,可掉getNode(index)方法,获取index在列表中的节点后,取该节点的数据(val)
- addAtHead(val)方法:定义新节点,
新节点指向head,将head移到该节点,size++,
边界情况,原链表为空链表
,在原来操作的基础上添加尾节点的赋值
,赋值为新节点 - addAtTail(val)方法:创建新添加的节点后,处理边界,如果
原链表为空链表
,新添加的节点也会是头节点
,否则原链表的尾节点就是新添加的节点
。另外新的尾节点是新添加的节点,size++
- addAtIndex(index,val)方法:先处理边界,边界一:
index > size
,直接return;边界二:index <= 0
,相当于添加头节点;边界三:index === size
,相当于添加尾节点;其他正常情况:获取需要插入位置的前一个节点,创建需要插入的新节点,保存cur的下一个节点,cur的下个节点指向新节点,新节点的下一个节点指向之前保存的原来的cur的下一个节点,size++
- deleteAtIndex(index)方法:先处理边界,边界一:
index合法性
,如上;边界二:列表只有一个节点,头尾节点均为null
;边界三:index为0,head指向head.next
;边界四:index为size-1(尾节点),获取期望删除节点的上一个节点并指向null,修改尾节点
;其他情况(index>0&&index<size-1):获取删除节点的上一个节点后,将其指向下一个节点的下一个节点。最后size--
🤔遇到的问题
- 边界值处理问题,基本都是针对空列表的情况或者只有1个节点的列表的增加及删除操作的边界处理,调试很麻烦
💻代码实现
class LinkNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
var MyLinkedList = function () {
this.size = 0;//节点数量
this.tail = null;//尾节点
this.head = null;//头节点
};
/**
* @param {number} index
* @return {number} 获取输入的值在链表中的节点
*/
MyLinkedList.prototype.getNode = function (index) {
if (index < 0 || index >= this.size) return null
//创建虚拟节点
let cur = new LinkNode(0, this.head)
//从0(虚拟节点)-index遍历
while (index >= 0) {
cur = cur.next;
index--
}
return cur
};
/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function (index) {
if (index < 0 || index >= this.size) return -1;
return this.getNode(index).val
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
//创建新添加的节点
let node = new LinkNode(val, this.head)
this.head = node;
//原链表为空时
if (!this.size) {
this.tail = node;
}
this.size++
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
//创建新添加的节点
let node = new LinkNode(val)
if (!this.size) {
//原链表为空链表,新添加的节点也会是头节点
this.head = node
} else {
//原链表的尾节点就是新添加的节点
this.tail.next = node
}
//新的尾节点是新添加的节点
this.tail = node
this.size++
};
/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
if (index > this.size) return;
if (index <= 0) {
this.addAtHead(val)
return
}
if (index === this.size) {
this.addAtTail(val)
return
}
//获取需要插入位置的前一个节点
let cur = this.getNode(index - 1)
//创建需要插入的新节点
const node = new LinkNode(val)
//保存cur的下一个节点
let temp = cur.next;
//cur的下个节点指向新节点
cur.next = node
//新节点的下一个节点指向之前保存的原来的cur的下一个节点
node.next = temp
this.size++
};
/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function (index) {
if (index < 0 || index >= this.size) return
if (this.size === 1) {
this.head = null
this.tail = null
this.size--
return
}
//如果删除的是头节点
if (index === 0) {
this.head = this.head.next
}
//如果删除的是尾节点
if (index === this.size - 1) {
//获取删除节点的上一个节点
const node = this.getNode(index - 1)
//将该节点指向null
node.next = null
//修改尾节点
this.tail = node
}
//正常
if (index > 0 && index < this.size - 1) {
//获取删除节点的上一个节点
const node = this.getNode(index - 1)
//将该节点指向下一个节点的下一个节点
const temp = node.next.next
node.next = temp
}
this.size--;
};
🎯题目总结
- 设置虚拟节点能解决很多问题,非常有用
- 边界处理需要认真小心,尤其对于空链表或者只有1个节点的列表
- 这道题值得反复刷,不断回味
206.反转链表
💡解题思路
- 双指针解法:反转链表是将每个指针的指向调转一下,这样原本最后一个元素指向null的,变成第一个元素指向null。
定义两个指针,一个pre为null
,另一个cur(当前节点)为head
,结束条件是cur=null
,也就是原链表的最后一个元素的指向。接下来的4个反转步骤不可调换顺序,否则就会报错
1、将当前节点的下一个节点指向先保存:temp = cur.next
2、反转,将当前节点的指向下一个节点改为指向pre(此时原本的cur.next已断开,这就是为什么先保存的原因):cur.next = pre
3、pre前移一位,来到cur所在的位置:pre = cur
4、cur前移一位,来到原本cur.next的位置,即保存的temp的位置:cur = temp
- 递归解法:与双指针的逻辑一致,只是在pre与cur移动时采用递归的方式
🤔遇到的问题
- 反转步骤会搞错,需要画图慢慢来
- 递归解法的跳出递归的条件判断错误
💻代码实现
双指针解法
var reverseList = function (head) {
//双指针
if (!head || !head.next) return head;
let cur = head//当前节点
let pre = null//前节点
let temp = null//当前节点的后一个节点
while (cur) {
//下面的顺序不可更改
temp = cur.next;//将当前节点的下一个节点先保存
cur.next = pre;//反转,将当前节点的指向下一个节点改为指向pre(此时原本的cur.next已断开,这就是为什么先保存的原因)
pre = cur;//pre前移一位,来到cur所在的位置
cur = temp;//cur前移一位,来到原本cur.next的位置,即保存的temp的位置
}
return pre;
};
递归解法
var reverse = function (pre, cur) {
if (!cur) return pre;
const temp = cur.next;
cur.next = pre;
return reverse(cur, temp)
}
var reverseList = function (head) {
//递归的写法
return reverse(null, head)
};
🎯题目总结
链表的双指针:分为pre
和cur
两个,在移动的时候也需要注意,需要先将cur.next保存下来,将指向反转后,先移动pre,再移动cur
。
递归很容易进入死循环,需要谨慎写明跳出循环的条件
🎈今日心得
链表基本忘光啦,现在重现捡起来,有种熟悉又陌生的感觉,今天很充实,加油